@qoretechnologies/reqraft 0.10.64 → 0.10.65-pr.137.g20c3bec

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 (834) hide show
  1. package/README.md +53 -1
  2. package/dist/components/Description/AllowedValuesHelp.d.ts +20 -0
  3. package/dist/components/Description/AllowedValuesHelp.d.ts.map +1 -0
  4. package/dist/components/Description/AllowedValuesHelp.js +86 -0
  5. package/dist/components/Description/AllowedValuesHelp.js.map +1 -0
  6. package/dist/components/Description/describedControl.d.ts +54 -0
  7. package/dist/components/Description/describedControl.d.ts.map +1 -0
  8. package/dist/components/Description/describedControl.js +81 -0
  9. package/dist/components/Description/describedControl.js.map +1 -0
  10. package/dist/components/Description/index.d.ts.map +1 -1
  11. package/dist/components/Description/index.js +3 -2
  12. package/dist/components/Description/index.js.map +1 -1
  13. package/dist/components/colorValue/colorValue.d.ts +59 -0
  14. package/dist/components/colorValue/colorValue.d.ts.map +1 -0
  15. package/dist/components/colorValue/colorValue.js +245 -0
  16. package/dist/components/colorValue/colorValue.js.map +1 -0
  17. package/dist/components/colorValue/index.d.ts +34 -0
  18. package/dist/components/colorValue/index.d.ts.map +1 -0
  19. package/dist/components/colorValue/index.js +136 -0
  20. package/dist/components/colorValue/index.js.map +1 -0
  21. package/dist/components/colorValue/labels.d.ts +12 -0
  22. package/dist/components/colorValue/labels.d.ts.map +1 -0
  23. package/dist/components/colorValue/labels.js +11 -0
  24. package/dist/components/colorValue/labels.js.map +1 -0
  25. package/dist/components/composer/Composer.d.ts +1 -1
  26. package/dist/components/composer/Composer.d.ts.map +1 -1
  27. package/dist/components/composer/Composer.js +2 -2
  28. package/dist/components/composer/Composer.js.map +1 -1
  29. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  30. package/dist/components/dpqlEditor/DpqlEditor.js +41 -8
  31. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  32. package/dist/components/dpqlEditor/dpqlCompletions.d.ts +35 -0
  33. package/dist/components/dpqlEditor/dpqlCompletions.d.ts.map +1 -0
  34. package/dist/components/dpqlEditor/dpqlCompletions.js +122 -0
  35. package/dist/components/dpqlEditor/dpqlCompletions.js.map +1 -0
  36. package/dist/components/dpqlEditor/dpqlHelpers.d.ts +47 -5
  37. package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
  38. package/dist/components/dpqlEditor/dpqlHelpers.js +390 -62
  39. package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
  40. package/dist/components/dpqlEditor/dpqlInserter.d.ts.map +1 -1
  41. package/dist/components/dpqlEditor/dpqlInserter.js +59 -29
  42. package/dist/components/dpqlEditor/dpqlInserter.js.map +1 -1
  43. package/dist/components/dpqlEditor/dpqlTags.d.ts +17 -7
  44. package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
  45. package/dist/components/dpqlEditor/dpqlTags.js +44 -17
  46. package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
  47. package/dist/components/dpqlEditor/types.d.ts +61 -8
  48. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  49. package/dist/components/dpqlEditor/useDpqlSession.d.ts +2 -2
  50. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  51. package/dist/components/dpqlEditor/useDpqlSession.js +16 -7
  52. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  53. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  54. package/dist/components/form/engine/CompactRow.js +327 -123
  55. package/dist/components/form/engine/CompactRow.js.map +1 -1
  56. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  57. package/dist/components/form/engine/CompactToolbar.js +29 -3
  58. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  59. package/dist/components/form/engine/FormEngine.d.ts +66 -11
  60. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  61. package/dist/components/form/engine/FormEngine.js +597 -173
  62. package/dist/components/form/engine/FormEngine.js.map +1 -1
  63. package/dist/components/form/engine/HiddenFieldsNote.d.ts +28 -0
  64. package/dist/components/form/engine/HiddenFieldsNote.d.ts.map +1 -0
  65. package/dist/components/form/engine/HiddenFieldsNote.js +32 -0
  66. package/dist/components/form/engine/HiddenFieldsNote.js.map +1 -0
  67. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  68. package/dist/components/form/engine/OptionFieldMessages.js +25 -3
  69. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  70. package/dist/components/form/engine/OptionsHelpDialog.d.ts.map +1 -1
  71. package/dist/components/form/engine/OptionsHelpDialog.js +19 -2
  72. package/dist/components/form/engine/OptionsHelpDialog.js.map +1 -1
  73. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts +2 -2
  74. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  75. package/dist/components/form/engine/_structuredData/SchemaDataView.js +12 -6
  76. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  77. package/dist/components/form/engine/compactRowContext.d.ts +32 -1
  78. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  79. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  80. package/dist/components/form/engine/compactRowStyles.d.ts +22 -10
  81. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  82. package/dist/components/form/engine/compactRowStyles.js +53 -39
  83. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  84. package/dist/components/form/engine/compactToolbarColors.d.ts +16 -0
  85. package/dist/components/form/engine/compactToolbarColors.d.ts.map +1 -0
  86. package/dist/components/form/engine/compactToolbarColors.js +21 -0
  87. package/dist/components/form/engine/compactToolbarColors.js.map +1 -0
  88. package/dist/components/form/engine/inheritProps.d.ts +29 -0
  89. package/dist/components/form/engine/inheritProps.d.ts.map +1 -0
  90. package/dist/components/form/engine/inheritProps.js +43 -0
  91. package/dist/components/form/engine/inheritProps.js.map +1 -0
  92. package/dist/components/form/engine/operatorSummary.d.ts +26 -0
  93. package/dist/components/form/engine/operatorSummary.d.ts.map +1 -0
  94. package/dist/components/form/engine/operatorSummary.js +35 -0
  95. package/dist/components/form/engine/operatorSummary.js.map +1 -0
  96. package/dist/components/form/engine/readFirst.d.ts +24 -8
  97. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  98. package/dist/components/form/engine/readFirst.js +77 -109
  99. package/dist/components/form/engine/readFirst.js.map +1 -1
  100. package/dist/components/form/engine/readFirstColors.d.ts +30 -0
  101. package/dist/components/form/engine/readFirstColors.d.ts.map +1 -0
  102. package/dist/components/form/engine/readFirstColors.js +51 -0
  103. package/dist/components/form/engine/readFirstColors.js.map +1 -0
  104. package/dist/components/form/engine/rowFocus.d.ts +2 -0
  105. package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
  106. package/dist/components/form/engine/rowFocus.js +26 -3
  107. package/dist/components/form/engine/rowFocus.js.map +1 -1
  108. package/dist/components/form/engine/typeChoices.d.ts +1 -1
  109. package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -1
  110. package/dist/components/form/engine/variants/VariantCalmTable.js +2 -1
  111. package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -1
  112. package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -1
  113. package/dist/components/form/engine/variants/VariantCards.js +2 -1
  114. package/dist/components/form/engine/variants/VariantCards.js.map +1 -1
  115. package/dist/components/form/engine/variants/VariantFocus.js +1 -1
  116. package/dist/components/form/engine/variants/VariantFocus.js.map +1 -1
  117. package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -1
  118. package/dist/components/form/engine/variants/VariantMinimal.js +2 -1
  119. package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -1
  120. package/dist/components/form/engine/variants/variantParts.d.ts +1 -1
  121. package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -1
  122. package/dist/components/form/engine/variants/variantParts.js +25 -17
  123. package/dist/components/form/engine/variants/variantParts.js.map +1 -1
  124. package/dist/components/form/expressions/DpqlRendering.d.ts +1 -1
  125. package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -1
  126. package/dist/components/form/expressions/DpqlRendering.js +7 -6
  127. package/dist/components/form/expressions/DpqlRendering.js.map +1 -1
  128. package/dist/components/form/expressions/ExpressionField.d.ts +66 -3
  129. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  130. package/dist/components/form/expressions/ExpressionField.js +475 -100
  131. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  132. package/dist/components/form/expressions/argumentPresence.d.ts +14 -0
  133. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -1
  134. package/dist/components/form/expressions/argumentPresence.js +16 -1
  135. package/dist/components/form/expressions/argumentPresence.js.map +1 -1
  136. package/dist/components/form/expressions/builder/argumentDetail.d.ts.map +1 -1
  137. package/dist/components/form/expressions/builder/argumentDetail.js +4 -1
  138. package/dist/components/form/expressions/builder/argumentDetail.js.map +1 -1
  139. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  140. package/dist/components/form/expressions/builder/argumentLabel.js +27 -9
  141. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  142. package/dist/components/form/expressions/builder/argumentWrapper.d.ts.map +1 -1
  143. package/dist/components/form/expressions/builder/argumentWrapper.js +3 -1
  144. package/dist/components/form/expressions/builder/argumentWrapper.js.map +1 -1
  145. package/dist/components/form/expressions/builder/confirmMismatchedTypesModal.d.ts.map +1 -1
  146. package/dist/components/form/expressions/builder/confirmMismatchedTypesModal.js +5 -4
  147. package/dist/components/form/expressions/builder/confirmMismatchedTypesModal.js.map +1 -1
  148. package/dist/components/form/expressions/builder/confirmUnsupportedTypeModal.d.ts +1 -1
  149. package/dist/components/form/expressions/builder/confirmUnsupportedTypeModal.d.ts.map +1 -1
  150. package/dist/components/form/expressions/builder/confirmUnsupportedTypeModal.js +6 -3
  151. package/dist/components/form/expressions/builder/confirmUnsupportedTypeModal.js.map +1 -1
  152. package/dist/components/form/expressions/builder/index.d.ts +29 -2
  153. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  154. package/dist/components/form/expressions/builder/index.js +161 -78
  155. package/dist/components/form/expressions/builder/index.js.map +1 -1
  156. package/dist/components/form/expressions/builder/item.d.ts +6 -2
  157. package/dist/components/form/expressions/builder/item.d.ts.map +1 -1
  158. package/dist/components/form/expressions/builder/item.js +19 -5
  159. package/dist/components/form/expressions/builder/item.js.map +1 -1
  160. package/dist/components/form/expressions/builder/renderTemplate.js +1 -1
  161. package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
  162. package/dist/components/form/expressions/dpqlMockRecordPath.d.ts +11 -0
  163. package/dist/components/form/expressions/dpqlMockRecordPath.d.ts.map +1 -0
  164. package/dist/components/form/expressions/dpqlMockRecordPath.js +254 -0
  165. package/dist/components/form/expressions/dpqlMockRecordPath.js.map +1 -0
  166. package/dist/components/form/expressions/expressionOperands.d.ts +16 -0
  167. package/dist/components/form/expressions/expressionOperands.d.ts.map +1 -0
  168. package/dist/components/form/expressions/expressionOperands.js +97 -0
  169. package/dist/components/form/expressions/expressionOperands.js.map +1 -0
  170. package/dist/components/form/expressions/fieldPath.d.ts +24 -0
  171. package/dist/components/form/expressions/fieldPath.d.ts.map +1 -0
  172. package/dist/components/form/expressions/fieldPath.js +152 -0
  173. package/dist/components/form/expressions/fieldPath.js.map +1 -0
  174. package/dist/components/form/expressions/loneValue.d.ts +17 -0
  175. package/dist/components/form/expressions/loneValue.d.ts.map +1 -0
  176. package/dist/components/form/expressions/loneValue.js +33 -0
  177. package/dist/components/form/expressions/loneValue.js.map +1 -0
  178. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  179. package/dist/components/form/expressions/mockExpressions.js +22 -5
  180. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  181. package/dist/components/form/expressions/renderExpressionToText.d.ts +2 -0
  182. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  183. package/dist/components/form/expressions/renderExpressionToText.js +43 -10
  184. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  185. package/dist/components/form/expressions/textOfExpression.d.ts +18 -0
  186. package/dist/components/form/expressions/textOfExpression.d.ts.map +1 -0
  187. package/dist/components/form/expressions/textOfExpression.js +49 -0
  188. package/dist/components/form/expressions/textOfExpression.js.map +1 -0
  189. package/dist/components/form/expressions/types.d.ts +7 -0
  190. package/dist/components/form/expressions/types.d.ts.map +1 -1
  191. package/dist/components/form/expressions/types.js +6 -1
  192. package/dist/components/form/expressions/types.js.map +1 -1
  193. package/dist/components/form/expressions/useExpressionType.d.ts +26 -0
  194. package/dist/components/form/expressions/useExpressionType.d.ts.map +1 -0
  195. package/dist/components/form/expressions/useExpressionType.js +181 -0
  196. package/dist/components/form/expressions/useExpressionType.js.map +1 -0
  197. package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
  198. package/dist/components/form/expressions/useRenderExpression.js +13 -9
  199. package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
  200. package/dist/components/form/expressions/wildcardExpressions.d.ts +27 -0
  201. package/dist/components/form/expressions/wildcardExpressions.d.ts.map +1 -0
  202. package/dist/components/form/expressions/wildcardExpressions.js +101 -0
  203. package/dist/components/form/expressions/wildcardExpressions.js.map +1 -0
  204. package/dist/components/form/fields/Field.d.ts.map +1 -1
  205. package/dist/components/form/fields/Field.js +43 -20
  206. package/dist/components/form/fields/Field.js.map +1 -1
  207. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  208. package/dist/components/form/fields/allowed-values/AllowedValues.js +39 -6
  209. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  210. package/dist/components/form/fields/allowed-values/choicePicture.d.ts +37 -0
  211. package/dist/components/form/fields/allowed-values/choicePicture.d.ts.map +1 -0
  212. package/dist/components/form/fields/allowed-values/choicePicture.js +64 -0
  213. package/dist/components/form/fields/allowed-values/choicePicture.js.map +1 -0
  214. package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
  215. package/dist/components/form/fields/array/ArrayAuto.js +24 -0
  216. package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
  217. package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
  218. package/dist/components/form/fields/array/ArrayAutoField.js +36 -13
  219. package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
  220. package/dist/components/form/fields/auto/AutoFormField.d.ts +20 -1
  221. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  222. package/dist/components/form/fields/auto/AutoFormField.js +91 -53
  223. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  224. package/dist/components/form/fields/auto/hostEditorValue.d.ts +30 -0
  225. package/dist/components/form/fields/auto/hostEditorValue.d.ts.map +1 -0
  226. package/dist/components/form/fields/auto/hostEditorValue.js +53 -0
  227. package/dist/components/form/fields/auto/hostEditorValue.js.map +1 -0
  228. package/dist/components/form/fields/binary/Binary.d.ts.map +1 -1
  229. package/dist/components/form/fields/binary/Binary.js +23 -3
  230. package/dist/components/form/fields/binary/Binary.js.map +1 -1
  231. package/dist/components/form/fields/boolean/Boolean.d.ts +5 -0
  232. package/dist/components/form/fields/boolean/Boolean.d.ts.map +1 -1
  233. package/dist/components/form/fields/boolean/Boolean.js +9 -1
  234. package/dist/components/form/fields/boolean/Boolean.js.map +1 -1
  235. package/dist/components/form/fields/color/Color.d.ts +103 -16
  236. package/dist/components/form/fields/color/Color.d.ts.map +1 -1
  237. package/dist/components/form/fields/color/Color.js +616 -16
  238. package/dist/components/form/fields/color/Color.js.map +1 -1
  239. package/dist/components/form/fields/emittedValues.d.ts +34 -3
  240. package/dist/components/form/fields/emittedValues.d.ts.map +1 -1
  241. package/dist/components/form/fields/emittedValues.js +24 -1
  242. package/dist/components/form/fields/emittedValues.js.map +1 -1
  243. package/dist/components/form/fields/file/File.d.ts +1 -1
  244. package/dist/components/form/fields/file/File.d.ts.map +1 -1
  245. package/dist/components/form/fields/file/File.js +4 -2
  246. package/dist/components/form/fields/file/File.js.map +1 -1
  247. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  248. package/dist/components/form/fields/long-string/LongString.js +29 -8
  249. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  250. package/dist/components/form/fields/markdown/MarkdownView.js +1 -1
  251. package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
  252. package/dist/components/form/fields/multi-select/MultiSelectFormField.js +13 -9
  253. package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
  254. package/dist/components/form/fields/number/Number.d.ts +6 -0
  255. package/dist/components/form/fields/number/Number.d.ts.map +1 -1
  256. package/dist/components/form/fields/number/Number.js +41 -6
  257. package/dist/components/form/fields/number/Number.js.map +1 -1
  258. package/dist/components/form/fields/object/Object.d.ts +1 -1
  259. package/dist/components/form/fields/object/Object.d.ts.map +1 -1
  260. package/dist/components/form/fields/object/Object.js +4 -2
  261. package/dist/components/form/fields/object/Object.js.map +1 -1
  262. package/dist/components/form/fields/pendingEdit.d.ts +52 -0
  263. package/dist/components/form/fields/pendingEdit.d.ts.map +1 -0
  264. package/dist/components/form/fields/pendingEdit.js +79 -0
  265. package/dist/components/form/fields/pendingEdit.js.map +1 -0
  266. package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
  267. package/dist/components/form/fields/radio-group/RadioGroup.js +4 -3
  268. package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
  269. package/dist/components/form/fields/rich-text/RichText.d.ts +8 -1
  270. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  271. package/dist/components/form/fields/rich-text/RichText.js +211 -85
  272. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  273. package/dist/components/form/fields/rich-text/useTemplateTags.d.ts +18 -0
  274. package/dist/components/form/fields/rich-text/useTemplateTags.d.ts.map +1 -0
  275. package/dist/components/form/fields/rich-text/useTemplateTags.js +77 -0
  276. package/dist/components/form/fields/rich-text/useTemplateTags.js.map +1 -0
  277. package/dist/components/form/fields/schema-definition/CatalogLeafForm.d.ts.map +1 -1
  278. package/dist/components/form/fields/schema-definition/CatalogLeafForm.js +4 -1
  279. package/dist/components/form/fields/schema-definition/CatalogLeafForm.js.map +1 -1
  280. package/dist/components/form/fields/schema-definition/CatalogNodeEditor.d.ts.map +1 -1
  281. package/dist/components/form/fields/schema-definition/CatalogNodeEditor.js +20 -5
  282. package/dist/components/form/fields/schema-definition/CatalogNodeEditor.js.map +1 -1
  283. package/dist/components/form/fields/schema-definition/TableEditor.d.ts.map +1 -1
  284. package/dist/components/form/fields/schema-definition/TableEditor.js +10 -2
  285. package/dist/components/form/fields/schema-definition/TableEditor.js.map +1 -1
  286. package/dist/components/form/fields/schema-definition/index.d.ts.map +1 -1
  287. package/dist/components/form/fields/schema-definition/index.js +8 -4
  288. package/dist/components/form/fields/schema-definition/index.js.map +1 -1
  289. package/dist/components/form/fields/select/Select.d.ts +10 -1
  290. package/dist/components/form/fields/select/Select.d.ts.map +1 -1
  291. package/dist/components/form/fields/select/Select.js +28 -14
  292. package/dist/components/form/fields/select/Select.js.map +1 -1
  293. package/dist/components/form/fields/select/SelectCollection.d.ts +5 -0
  294. package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
  295. package/dist/components/form/fields/select/SelectCollection.js +25 -12
  296. package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
  297. package/dist/components/form/fields/select/unavailableRow.d.ts +23 -0
  298. package/dist/components/form/fields/select/unavailableRow.d.ts.map +1 -0
  299. package/dist/components/form/fields/select/unavailableRow.js +55 -0
  300. package/dist/components/form/fields/select/unavailableRow.js.map +1 -0
  301. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +11 -5
  302. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  303. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +21 -5
  304. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  305. package/dist/components/form/fields/template/TemplateBrowser.d.ts +22 -0
  306. package/dist/components/form/fields/template/TemplateBrowser.d.ts.map +1 -0
  307. package/dist/components/form/fields/template/TemplateBrowser.js +54 -0
  308. package/dist/components/form/fields/template/TemplateBrowser.js.map +1 -0
  309. package/dist/components/form/fields/template/TemplateField.d.ts +103 -1
  310. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  311. package/dist/components/form/fields/template/TemplateField.js +652 -104
  312. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  313. package/dist/components/form/fields/template/TemplateText.d.ts +0 -17
  314. package/dist/components/form/fields/template/TemplateText.d.ts.map +1 -1
  315. package/dist/components/form/fields/template/TemplateText.js +22 -16
  316. package/dist/components/form/fields/template/TemplateText.js.map +1 -1
  317. package/dist/components/form/fields/template/referenceNames.d.ts +63 -0
  318. package/dist/components/form/fields/template/referenceNames.d.ts.map +1 -0
  319. package/dist/components/form/fields/template/referenceNames.js +69 -0
  320. package/dist/components/form/fields/template/referenceNames.js.map +1 -0
  321. package/dist/components/form/fields/template/writtenAsText.d.ts +61 -0
  322. package/dist/components/form/fields/template/writtenAsText.d.ts.map +1 -0
  323. package/dist/components/form/fields/template/writtenAsText.js +204 -0
  324. package/dist/components/form/fields/template/writtenAsText.js.map +1 -0
  325. package/dist/components/form/index.d.ts +11 -0
  326. package/dist/components/form/index.d.ts.map +1 -1
  327. package/dist/components/form/index.js +24 -1
  328. package/dist/components/form/index.js.map +1 -1
  329. package/dist/components/form/typeSentenceForm.d.ts +11 -0
  330. package/dist/components/form/typeSentenceForm.d.ts.map +1 -0
  331. package/dist/components/form/typeSentenceForm.js +40 -0
  332. package/dist/components/form/typeSentenceForm.js.map +1 -0
  333. package/dist/components/imageLightbox/ImageLightbox.d.ts.map +1 -1
  334. package/dist/components/imageLightbox/ImageLightbox.js +19 -7
  335. package/dist/components/imageLightbox/ImageLightbox.js.map +1 -1
  336. package/dist/components/log/Log.d.ts.map +1 -1
  337. package/dist/components/log/Log.js +16 -8
  338. package/dist/components/log/Log.js.map +1 -1
  339. package/dist/components/menu/Menu.d.ts.map +1 -1
  340. package/dist/components/menu/Menu.js +19 -10
  341. package/dist/components/menu/Menu.js.map +1 -1
  342. package/dist/components/smartEditor/CompletionMenu.d.ts +45 -0
  343. package/dist/components/smartEditor/CompletionMenu.d.ts.map +1 -0
  344. package/dist/components/smartEditor/CompletionMenu.js +207 -0
  345. package/dist/components/smartEditor/CompletionMenu.js.map +1 -0
  346. package/dist/components/smartEditor/MarkdownDoc.d.ts +1 -1
  347. package/dist/components/smartEditor/MarkdownDoc.js +3 -3
  348. package/dist/components/smartEditor/MarkdownDoc.js.map +1 -1
  349. package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
  350. package/dist/components/smartEditor/SmartEditor.js +366 -187
  351. package/dist/components/smartEditor/SmartEditor.js.map +1 -1
  352. package/dist/components/smartEditor/completionStyle.d.ts +27 -0
  353. package/dist/components/smartEditor/completionStyle.d.ts.map +1 -0
  354. package/dist/components/smartEditor/completionStyle.js +68 -0
  355. package/dist/components/smartEditor/completionStyle.js.map +1 -0
  356. package/dist/components/smartEditor/index.d.ts +5 -1
  357. package/dist/components/smartEditor/index.d.ts.map +1 -1
  358. package/dist/components/smartEditor/index.js +19 -1
  359. package/dist/components/smartEditor/index.js.map +1 -1
  360. package/dist/components/smartEditor/templateCompletions.d.ts +38 -0
  361. package/dist/components/smartEditor/templateCompletions.d.ts.map +1 -0
  362. package/dist/components/smartEditor/templateCompletions.js +126 -0
  363. package/dist/components/smartEditor/templateCompletions.js.map +1 -0
  364. package/dist/components/smartEditor/typedReferences.d.ts +21 -0
  365. package/dist/components/smartEditor/typedReferences.d.ts.map +1 -0
  366. package/dist/components/smartEditor/typedReferences.js +82 -0
  367. package/dist/components/smartEditor/typedReferences.js.map +1 -0
  368. package/dist/components/smartEditor/types.d.ts +66 -2
  369. package/dist/components/smartEditor/types.d.ts.map +1 -1
  370. package/dist/components/smartEditor/useLspAutocomplete.d.ts +62 -2
  371. package/dist/components/smartEditor/useLspAutocomplete.d.ts.map +1 -1
  372. package/dist/components/smartEditor/useLspAutocomplete.js +345 -146
  373. package/dist/components/smartEditor/useLspAutocomplete.js.map +1 -1
  374. package/dist/components/smartEditor/useLspHover.d.ts +33 -7
  375. package/dist/components/smartEditor/useLspHover.d.ts.map +1 -1
  376. package/dist/components/smartEditor/useLspHover.js +351 -35
  377. package/dist/components/smartEditor/useLspHover.js.map +1 -1
  378. package/dist/components/smartEditor/useLspSemanticTokens.js +1 -1
  379. package/dist/components/smartEditor/useLspSemanticTokens.js.map +1 -1
  380. package/dist/components/smartEditor/useLspSession.d.ts +6 -0
  381. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  382. package/dist/components/smartEditor/useLspSession.js +7 -6
  383. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  384. package/dist/components/smartEditor/useLspSignatureHelp.d.ts.map +1 -1
  385. package/dist/components/smartEditor/useLspSignatureHelp.js +6 -11
  386. package/dist/components/smartEditor/useLspSignatureHelp.js.map +1 -1
  387. package/dist/components/supportTicket/AttachmentChips.d.ts.map +1 -1
  388. package/dist/components/supportTicket/AttachmentChips.js +15 -5
  389. package/dist/components/supportTicket/AttachmentChips.js.map +1 -1
  390. package/dist/components/supportTicket/ReferencePicker.d.ts +9 -2
  391. package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
  392. package/dist/components/supportTicket/ReferencePicker.js +27 -6
  393. package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
  394. package/dist/components/supportTicket/TicketHeader.d.ts +3 -4
  395. package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
  396. package/dist/components/supportTicket/TicketHeader.js +20 -2
  397. package/dist/components/supportTicket/TicketHeader.js.map +1 -1
  398. package/dist/components/supportTicket/TicketMetaTags.d.ts +1 -1
  399. package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -1
  400. package/dist/components/ticketReferences/TicketReferences.js +9 -4
  401. package/dist/components/ticketReferences/TicketReferences.js.map +1 -1
  402. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  403. package/dist/components/ticketThread/TicketThread.js +38 -13
  404. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  405. package/dist/helpers/allowedValues.d.ts +21 -0
  406. package/dist/helpers/allowedValues.d.ts.map +1 -1
  407. package/dist/helpers/allowedValues.js +40 -1
  408. package/dist/helpers/allowedValues.js.map +1 -1
  409. package/dist/helpers/descriptionTooltip.d.ts +46 -0
  410. package/dist/helpers/descriptionTooltip.d.ts.map +1 -0
  411. package/dist/helpers/descriptionTooltip.js +76 -0
  412. package/dist/helpers/descriptionTooltip.js.map +1 -0
  413. package/dist/helpers/domProps.d.ts +5 -0
  414. package/dist/helpers/domProps.d.ts.map +1 -0
  415. package/dist/helpers/domProps.js +47 -0
  416. package/dist/helpers/domProps.js.map +1 -0
  417. package/dist/helpers/expressions.d.ts +107 -2
  418. package/dist/helpers/expressions.d.ts.map +1 -1
  419. package/dist/helpers/expressions.js +223 -8
  420. package/dist/helpers/expressions.js.map +1 -1
  421. package/dist/helpers/hoveringPointer.d.ts +26 -0
  422. package/dist/helpers/hoveringPointer.d.ts.map +1 -0
  423. package/dist/helpers/hoveringPointer.js +29 -0
  424. package/dist/helpers/hoveringPointer.js.map +1 -0
  425. package/dist/helpers/markdownText.d.ts +14 -0
  426. package/dist/helpers/markdownText.d.ts.map +1 -0
  427. package/dist/helpers/markdownText.js +160 -0
  428. package/dist/helpers/markdownText.js.map +1 -0
  429. package/dist/helpers/noDescription.d.ts +9 -0
  430. package/dist/helpers/noDescription.d.ts.map +1 -0
  431. package/dist/helpers/noDescription.js +13 -0
  432. package/dist/helpers/noDescription.js.map +1 -0
  433. package/dist/helpers/optionUiTypes.d.ts +2 -2
  434. package/dist/helpers/qorusTypeCatalogue.d.ts +85 -0
  435. package/dist/helpers/qorusTypeCatalogue.d.ts.map +1 -0
  436. package/dist/helpers/qorusTypeCatalogue.js +301 -0
  437. package/dist/helpers/qorusTypeCatalogue.js.map +1 -0
  438. package/dist/helpers/readableColors.d.ts +36 -0
  439. package/dist/helpers/readableColors.d.ts.map +1 -0
  440. package/dist/helpers/readableColors.js +116 -0
  441. package/dist/helpers/readableColors.js.map +1 -0
  442. package/dist/helpers/recordFieldReference.d.ts +15 -0
  443. package/dist/helpers/recordFieldReference.d.ts.map +1 -0
  444. package/dist/helpers/recordFieldReference.js +89 -0
  445. package/dist/helpers/recordFieldReference.js.map +1 -0
  446. package/dist/helpers/templateItems.d.ts +2 -2
  447. package/dist/helpers/templateItems.d.ts.map +1 -1
  448. package/dist/helpers/templateItems.js +6 -4
  449. package/dist/helpers/templateItems.js.map +1 -1
  450. package/dist/helpers/templateText.d.ts +1 -0
  451. package/dist/helpers/templateText.d.ts.map +1 -1
  452. package/dist/helpers/templateText.js +7 -3
  453. package/dist/helpers/templateText.js.map +1 -1
  454. package/dist/helpers/templates.d.ts +68 -14
  455. package/dist/helpers/templates.d.ts.map +1 -1
  456. package/dist/helpers/templates.js +210 -24
  457. package/dist/helpers/templates.js.map +1 -1
  458. package/dist/helpers/validations.d.ts +28 -1
  459. package/dist/helpers/validations.d.ts.map +1 -1
  460. package/dist/helpers/validations.js +166 -37
  461. package/dist/helpers/validations.js.map +1 -1
  462. package/dist/helpers/webLink.d.ts +8 -0
  463. package/dist/helpers/webLink.d.ts.map +1 -0
  464. package/dist/helpers/webLink.js +29 -0
  465. package/dist/helpers/webLink.js.map +1 -0
  466. package/dist/hooks/useQorusTypes.d.ts +7 -15
  467. package/dist/hooks/useQorusTypes.d.ts.map +1 -1
  468. package/dist/hooks/useQorusTypes.js +25 -166
  469. package/dist/hooks/useQorusTypes.js.map +1 -1
  470. package/dist/hooks/useReadableIntentShade.d.ts +9 -0
  471. package/dist/hooks/useReadableIntentShade.d.ts.map +1 -0
  472. package/dist/hooks/useReadableIntentShade.js +23 -0
  473. package/dist/hooks/useReadableIntentShade.js.map +1 -0
  474. package/dist/hooks/useReadableOpacity.d.ts +6 -0
  475. package/dist/hooks/useReadableOpacity.d.ts.map +1 -0
  476. package/dist/hooks/useReadableOpacity.js +17 -0
  477. package/dist/hooks/useReadableOpacity.js.map +1 -0
  478. package/dist/index.d.ts +1 -0
  479. package/dist/index.d.ts.map +1 -1
  480. package/dist/index.js +1 -0
  481. package/dist/index.js.map +1 -1
  482. package/dist/tracking/ui/ExperimentCard.d.ts +1 -1
  483. package/dist/tracking/ui/ExperimentCard.d.ts.map +1 -1
  484. package/dist/tracking/ui/ExperimentCard.js +26 -10
  485. package/dist/tracking/ui/ExperimentCard.js.map +1 -1
  486. package/dist/tracking/ui/card.d.ts +2 -2
  487. package/dist/tracking/ui/card.d.ts.map +1 -1
  488. package/dist/tracking/ui/card.js +2 -2
  489. package/dist/tracking/ui/card.js.map +1 -1
  490. package/dist/tracking/ui/readable.d.ts +14 -0
  491. package/dist/tracking/ui/readable.d.ts.map +1 -0
  492. package/dist/tracking/ui/readable.js +60 -0
  493. package/dist/tracking/ui/readable.js.map +1 -0
  494. package/dist/utils/fetch.d.ts +9 -2
  495. package/dist/utils/fetch.d.ts.map +1 -1
  496. package/dist/utils/fetch.js +6 -5
  497. package/dist/utils/fetch.js.map +1 -1
  498. package/dist/utils/websocket.d.ts +65 -2
  499. package/dist/utils/websocket.d.ts.map +1 -1
  500. package/dist/utils/websocket.js +276 -44
  501. package/dist/utils/websocket.js.map +1 -1
  502. package/package.json +14 -5
  503. package/.claude/CLAUDE.md +0 -317
  504. package/.tasks/EXPORT_EXPRESSION_BUILDER.md +0 -38
  505. package/.tasks/EXPRESSION_BUILDER_REPORT.md +0 -149
  506. package/.tasks/EXPRESSION_FIELD_SEAMS.md +0 -66
  507. package/.tasks/FIELD_STACK_REPORT.md +0 -234
  508. package/.tasks/FORM_ENGINE_COMPACT_READ_FIRST.md +0 -558
  509. package/.tasks/FORM_ENGINE_EXPRESSIONS.md +0 -197
  510. package/.tasks/FORM_ENGINE_OPTIONS_SYNC.md +0 -95
  511. package/.tasks/FORM_FIELD_EXTRAS.md +0 -72
  512. package/.tasks/INDEX.md +0 -111
  513. package/.tasks/LSP_SHARED_CONNECTION.md +0 -75
  514. package/.tasks/QONSOLE_ASSIST_FEATURES.md +0 -306
  515. package/.tasks/RENDER_EXPRESSION_LSP.md +0 -159
  516. package/.tasks/REQRAFT_AUTO_FIELD.md +0 -176
  517. package/.tasks/SCHEMA_DEFINITION_FIELD.md +0 -250
  518. package/.tasks/SMART_EDITOR_CONTEXT_AND_POLISH.md +0 -340
  519. package/.tasks/SMART_EDITOR_LSP_FEATURES.md +0 -299
  520. package/.tasks/SMART_EDITOR_REVIEW_FOLLOWUPS.md +0 -280
  521. package/.tasks/SMART_EDITOR_UX_POLISH.md +0 -383
  522. package/.tasks/SMART_EDITOR_VISUAL_POLISH.md +0 -246
  523. package/.tasks/STORY_NETWORK_ISOLATION.md +0 -98
  524. package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +0 -72
  525. package/CODE_OF_CONDUCT.md +0 -105
  526. package/CONTRIBUTING.MD +0 -78
  527. package/SECURITY.md +0 -3
  528. package/chromatic.config.json +0 -5
  529. package/design/COMPACT_ENGINE_REDESIGN.md +0 -156
  530. package/design/COMPACT_INFO_DISPLAY.md +0 -87
  531. package/design/COMPACT_PARITY.md +0 -85
  532. package/design/EXPRESSION_BUILDER_REPORT_STRATEGY.md +0 -92
  533. package/design/FORM_ENGINE_COMPACT.md +0 -300
  534. package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +0 -353
  535. package/design/FORM_ENGINE_EXPRESSIONS.md +0 -257
  536. package/design/FORM_ENGINE_FIELD_MIGRATION.md +0 -419
  537. package/design/IDE_INTEGRATION.md +0 -64
  538. package/design/RENDER_EXPRESSION_TRANSPORT.md +0 -156
  539. package/design/SELECT_DIALOG_FLOOR.md +0 -86
  540. package/design/SMART_EDITOR_UX.md +0 -438
  541. package/design/TRACKING.md +0 -290
  542. package/design/UNAVAILABLE_ALLOWED_VALUES.md +0 -226
  543. package/src/components/Description/index.tsx +0 -254
  544. package/src/components/Description/markdownRendererContext.tsx +0 -29
  545. package/src/components/FocusedEditing/index.tsx +0 -50
  546. package/src/components/codeSize/index.tsx +0 -79
  547. package/src/components/composer/Composer.stories.tsx +0 -238
  548. package/src/components/composer/Composer.tsx +0 -554
  549. package/src/components/composer/helpers.ts +0 -74
  550. package/src/components/composer/index.ts +0 -2
  551. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +0 -1183
  552. package/src/components/dpqlEditor/DpqlEditor.tsx +0 -219
  553. package/src/components/dpqlEditor/dpqlHelpers.ts +0 -287
  554. package/src/components/dpqlEditor/dpqlInserter.ts +0 -121
  555. package/src/components/dpqlEditor/dpqlProbe.ts +0 -165
  556. package/src/components/dpqlEditor/dpqlTags.ts +0 -114
  557. package/src/components/dpqlEditor/index.ts +0 -41
  558. package/src/components/dpqlEditor/types.ts +0 -203
  559. package/src/components/dpqlEditor/useDpqlProbe.ts +0 -33
  560. package/src/components/dpqlEditor/useDpqlSession.ts +0 -411
  561. package/src/components/form/engine/CompactRow.tsx +0 -2620
  562. package/src/components/form/engine/CompactToolbar.tsx +0 -330
  563. package/src/components/form/engine/FormEngine.stories.tsx +0 -9920
  564. package/src/components/form/engine/FormEngine.tsx +0 -4569
  565. package/src/components/form/engine/FormEngineRemote.stories.tsx +0 -679
  566. package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +0 -331
  567. package/src/components/form/engine/FormFieldsSkeleton.tsx +0 -82
  568. package/src/components/form/engine/OptionFieldMessages.tsx +0 -477
  569. package/src/components/form/engine/OptionsHelpDialog.tsx +0 -29
  570. package/src/components/form/engine/__fixtures__/basicFormOptions.ts +0 -309
  571. package/src/components/form/engine/__fixtures__/fieldChromeOptions.ts +0 -45
  572. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +0 -233
  573. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +0 -742
  574. package/src/components/form/engine/_structuredData/StructuredDataView.stories.tsx +0 -85
  575. package/src/components/form/engine/_structuredData/StructuredDataView.tsx +0 -75
  576. package/src/components/form/engine/_structuredData/structuredData.ts +0 -145
  577. package/src/components/form/engine/compactRowContext.ts +0 -148
  578. package/src/components/form/engine/compactRowStyles.ts +0 -898
  579. package/src/components/form/engine/compactToolbarContext.ts +0 -69
  580. package/src/components/form/engine/optionActions.ts +0 -128
  581. package/src/components/form/engine/optionsContext.ts +0 -24
  582. package/src/components/form/engine/readFirst.ts +0 -1077
  583. package/src/components/form/engine/rendererTypes.ts +0 -107
  584. package/src/components/form/engine/revealRow.ts +0 -123
  585. package/src/components/form/engine/rowFocus.ts +0 -101
  586. package/src/components/form/engine/rowMenuContext.ts +0 -159
  587. package/src/components/form/engine/rowOpenPicker.ts +0 -27
  588. package/src/components/form/engine/typeChoices.ts +0 -63
  589. package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +0 -159
  590. package/src/components/form/engine/variants/VariantCalmTable.tsx +0 -244
  591. package/src/components/form/engine/variants/VariantCards.tsx +0 -212
  592. package/src/components/form/engine/variants/VariantFocus.tsx +0 -382
  593. package/src/components/form/engine/variants/VariantMinimal.tsx +0 -170
  594. package/src/components/form/engine/variants/focusDemo.ts +0 -145
  595. package/src/components/form/engine/variants/variantModel.ts +0 -223
  596. package/src/components/form/engine/variants/variantParts.tsx +0 -313
  597. package/src/components/form/expressions/DpqlRendering.tsx +0 -85
  598. package/src/components/form/expressions/ExpressionField.stories.tsx +0 -1395
  599. package/src/components/form/expressions/ExpressionField.tsx +0 -503
  600. package/src/components/form/expressions/argumentPresence.ts +0 -213
  601. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +0 -1683
  602. package/src/components/form/expressions/builder/argumentDetail.tsx +0 -17
  603. package/src/components/form/expressions/builder/argumentLabel.tsx +0 -146
  604. package/src/components/form/expressions/builder/argumentSlot.tsx +0 -101
  605. package/src/components/form/expressions/builder/argumentWrapper.tsx +0 -243
  606. package/src/components/form/expressions/builder/confirmMismatchedTypesModal.tsx +0 -120
  607. package/src/components/form/expressions/builder/confirmUnsupportedTypeModal.tsx +0 -69
  608. package/src/components/form/expressions/builder/index.tsx +0 -1545
  609. package/src/components/form/expressions/builder/item.tsx +0 -89
  610. package/src/components/form/expressions/builder/moveToPositionStrip.tsx +0 -156
  611. package/src/components/form/expressions/builder/renderTemplate.tsx +0 -42
  612. package/src/components/form/expressions/dpqlMockLanguage.ts +0 -566
  613. package/src/components/form/expressions/dpqlMockLsp.ts +0 -126
  614. package/src/components/form/expressions/mockExpressions.ts +0 -317
  615. package/src/components/form/expressions/renderExpressionToText.ts +0 -137
  616. package/src/components/form/expressions/types.ts +0 -110
  617. package/src/components/form/expressions/useExpressions.ts +0 -101
  618. package/src/components/form/expressions/useRenderExpression.ts +0 -165
  619. package/src/components/form/fields/Field.stories.tsx +0 -143
  620. package/src/components/form/fields/Field.tsx +0 -732
  621. package/src/components/form/fields/allowed-values/AllowedValues.tsx +0 -297
  622. package/src/components/form/fields/array/ArrayAuto.tsx +0 -237
  623. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +0 -475
  624. package/src/components/form/fields/array/ArrayAutoField.tsx +0 -367
  625. package/src/components/form/fields/auto/AutoFormField.stories.tsx +0 -825
  626. package/src/components/form/fields/auto/AutoFormField.tsx +0 -1164
  627. package/src/components/form/fields/binary/Binary.stories.tsx +0 -122
  628. package/src/components/form/fields/binary/Binary.tsx +0 -91
  629. package/src/components/form/fields/boolean/Boolean.stories.tsx +0 -125
  630. package/src/components/form/fields/boolean/Boolean.tsx +0 -34
  631. package/src/components/form/fields/byte-size/ByteSize.stories.tsx +0 -87
  632. package/src/components/form/fields/byte-size/ByteSize.tsx +0 -58
  633. package/src/components/form/fields/color/Color.stories.tsx +0 -99
  634. package/src/components/form/fields/color/Color.tsx +0 -46
  635. package/src/components/form/fields/cron/Cron.stories.tsx +0 -68
  636. package/src/components/form/fields/cron/Cron.tsx +0 -80
  637. package/src/components/form/fields/date/Date.stories.tsx +0 -77
  638. package/src/components/form/fields/date/Date.tsx +0 -55
  639. package/src/components/form/fields/emittedValues.ts +0 -70
  640. package/src/components/form/fields/file/File.stories.tsx +0 -161
  641. package/src/components/form/fields/file/File.tsx +0 -227
  642. package/src/components/form/fields/long-string/LongString.stories.tsx +0 -120
  643. package/src/components/form/fields/long-string/LongString.tsx +0 -130
  644. package/src/components/form/fields/markdown/Markdown.stories.tsx +0 -76
  645. package/src/components/form/fields/markdown/Markdown.tsx +0 -81
  646. package/src/components/form/fields/markdown/MarkdownView.tsx +0 -222
  647. package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +0 -139
  648. package/src/components/form/fields/number/Number.stories.tsx +0 -159
  649. package/src/components/form/fields/number/Number.tsx +0 -160
  650. package/src/components/form/fields/object/Object.stories.tsx +0 -398
  651. package/src/components/form/fields/object/Object.tsx +0 -255
  652. package/src/components/form/fields/radio-group/RadioGroup.stories.tsx +0 -95
  653. package/src/components/form/fields/radio-group/RadioGroup.tsx +0 -46
  654. package/src/components/form/fields/radio-group/images/java-96x128.png +0 -0
  655. package/src/components/form/fields/radio-group/images/python-129x128.png +0 -0
  656. package/src/components/form/fields/radio-group/images/qore-106x128.png +0 -0
  657. package/src/components/form/fields/rich-text/RichText.stories.tsx +0 -260
  658. package/src/components/form/fields/rich-text/RichText.tsx +0 -537
  659. package/src/components/form/fields/schema-definition/CatalogLeafForm.tsx +0 -242
  660. package/src/components/form/fields/schema-definition/CatalogNodeEditor.tsx +0 -758
  661. package/src/components/form/fields/schema-definition/Hint.tsx +0 -22
  662. package/src/components/form/fields/schema-definition/InlineValidationBanner.tsx +0 -138
  663. package/src/components/form/fields/schema-definition/Loader.tsx +0 -20
  664. package/src/components/form/fields/schema-definition/MigrationsTab.tsx +0 -187
  665. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +0 -286
  666. package/src/components/form/fields/schema-definition/TableEditor.tsx +0 -155
  667. package/src/components/form/fields/schema-definition/TablesTab.tsx +0 -138
  668. package/src/components/form/fields/schema-definition/catalog.ts +0 -237
  669. package/src/components/form/fields/schema-definition/helpers.ts +0 -90
  670. package/src/components/form/fields/schema-definition/index.tsx +0 -418
  671. package/src/components/form/fields/schema-definition/mockCatalog.ts +0 -325
  672. package/src/components/form/fields/schema-definition/mockDefinition.ts +0 -111
  673. package/src/components/form/fields/schema-definition/referenceSuggestions.tsx +0 -152
  674. package/src/components/form/fields/schema-definition/starterTemplate.ts +0 -68
  675. package/src/components/form/fields/schema-definition/types.ts +0 -185
  676. package/src/components/form/fields/schema-definition/useSchemaOptionCatalog.ts +0 -49
  677. package/src/components/form/fields/select/Select.stories.tsx +0 -872
  678. package/src/components/form/fields/select/Select.tsx +0 -624
  679. package/src/components/form/fields/select/SelectCollection.tsx +0 -594
  680. package/src/components/form/fields/string/String.stories.tsx +0 -86
  681. package/src/components/form/fields/string/String.tsx +0 -43
  682. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +0 -114
  683. package/src/components/form/fields/template/ExampleValueModal.tsx +0 -99
  684. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +0 -47
  685. package/src/components/form/fields/template/TemplateField.stories.tsx +0 -1121
  686. package/src/components/form/fields/template/TemplateField.tsx +0 -1717
  687. package/src/components/form/fields/template/TemplateText.tsx +0 -115
  688. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +0 -7
  689. package/src/components/form/fields/template/__fixtures__/multiLevelTemplates.json +0 -147
  690. package/src/components/form/fields/template/__fixtures__/templates.json +0 -83
  691. package/src/components/form/fields/timeout/Timeout.stories.tsx +0 -228
  692. package/src/components/form/fields/timeout/Timeout.tsx +0 -87
  693. package/src/components/form/fields/url/Url.stories.tsx +0 -94
  694. package/src/components/form/fields/url/Url.tsx +0 -85
  695. package/src/components/form/index.tsx +0 -47
  696. package/src/components/imageLightbox/ImageLightbox.stories.tsx +0 -238
  697. package/src/components/imageLightbox/ImageLightbox.tsx +0 -236
  698. package/src/components/imageLightbox/index.ts +0 -1
  699. package/src/components/log/Log.stories.tsx +0 -333
  700. package/src/components/log/Log.tsx +0 -285
  701. package/src/components/menu/Menu.stories.tsx +0 -155
  702. package/src/components/menu/Menu.tsx +0 -312
  703. package/src/components/qonsoleSmartInput/QonsoleSmartInput.stories.tsx +0 -797
  704. package/src/components/qonsoleSmartInput/QonsoleSmartInput.tsx +0 -105
  705. package/src/components/qonsoleSmartInput/index.ts +0 -16
  706. package/src/components/qonsoleSmartInput/qonsoleInserter.ts +0 -77
  707. package/src/components/qonsoleSmartInput/types.ts +0 -133
  708. package/src/components/qonsoleSmartInput/useQonsoleSession.ts +0 -124
  709. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +0 -62
  710. package/src/components/searchFilterBar/SearchFilterBar.tsx +0 -144
  711. package/src/components/searchFilterBar/index.ts +0 -1
  712. package/src/components/smartEditor/MarkdownDoc.tsx +0 -190
  713. package/src/components/smartEditor/SmartEditor.stories.tsx +0 -475
  714. package/src/components/smartEditor/SmartEditor.tsx +0 -808
  715. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +0 -205
  716. package/src/components/smartEditor/helpers.ts +0 -217
  717. package/src/components/smartEditor/index.ts +0 -35
  718. package/src/components/smartEditor/styling.ts +0 -45
  719. package/src/components/smartEditor/types.ts +0 -211
  720. package/src/components/smartEditor/useLspAutocomplete.ts +0 -758
  721. package/src/components/smartEditor/useLspDiagnosticDecorations.ts +0 -125
  722. package/src/components/smartEditor/useLspHover.ts +0 -230
  723. package/src/components/smartEditor/useLspSemanticTokens.ts +0 -298
  724. package/src/components/smartEditor/useLspSession.ts +0 -231
  725. package/src/components/smartEditor/useLspSignatureHelp.ts +0 -198
  726. package/src/components/supportTicket/AttachmentChips.stories.tsx +0 -171
  727. package/src/components/supportTicket/AttachmentChips.tsx +0 -147
  728. package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +0 -257
  729. package/src/components/supportTicket/InterfaceReferenceTags.tsx +0 -129
  730. package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -584
  731. package/src/components/supportTicket/ReferencePicker.tsx +0 -563
  732. package/src/components/supportTicket/TicketConversation.stories.tsx +0 -121
  733. package/src/components/supportTicket/TicketHeader.stories.tsx +0 -183
  734. package/src/components/supportTicket/TicketHeader.tsx +0 -247
  735. package/src/components/supportTicket/TicketMetaTags.stories.tsx +0 -103
  736. package/src/components/supportTicket/TicketMetaTags.tsx +0 -85
  737. package/src/components/supportTicket/TicketReplyBox.stories.tsx +0 -354
  738. package/src/components/supportTicket/TicketReplyBox.tsx +0 -150
  739. package/src/components/supportTicket/__fixtures__/kindMarks.ts +0 -28
  740. package/src/components/supportTicket/attachmentAccess.ts +0 -97
  741. package/src/components/supportTicket/fileToAttachment.ts +0 -24
  742. package/src/components/supportTicket/index.ts +0 -10
  743. package/src/components/supportTicket/meta.ts +0 -206
  744. package/src/components/supportTicket/ticketModel.ts +0 -131
  745. package/src/components/supportTicket/useLoadedImages.ts +0 -73
  746. package/src/components/ticketReferences/TicketReferences.stories.tsx +0 -315
  747. package/src/components/ticketReferences/TicketReferences.tsx +0 -906
  748. package/src/components/ticketReferences/index.ts +0 -2
  749. package/src/components/ticketThread/TicketThread.stories.tsx +0 -829
  750. package/src/components/ticketThread/TicketThread.tsx +0 -719
  751. package/src/components/ticketThread/index.ts +0 -6
  752. package/src/contexts/FetchContext.tsx +0 -25
  753. package/src/contexts/ReqraftContext.tsx +0 -9
  754. package/src/contexts/StorageContext.tsx +0 -33
  755. package/src/global.d.ts +0 -4
  756. package/src/helpers/allowedValues.ts +0 -249
  757. package/src/helpers/choiceChip.ts +0 -227
  758. package/src/helpers/common.ts +0 -326
  759. package/src/helpers/dpqlDetection.ts +0 -209
  760. package/src/helpers/expressions.ts +0 -140
  761. package/src/helpers/optionUiTypes.ts +0 -199
  762. package/src/helpers/options.ts +0 -150
  763. package/src/helpers/providerValue.ts +0 -125
  764. package/src/helpers/scrollbar.ts +0 -32
  765. package/src/helpers/singleLineString.ts +0 -61
  766. package/src/helpers/templateItems.ts +0 -81
  767. package/src/helpers/templateText.ts +0 -350
  768. package/src/helpers/templates.ts +0 -602
  769. package/src/helpers/validations.ts +0 -2085
  770. package/src/hooks/useArgSchema.ts +0 -37
  771. package/src/hooks/useCoarsePointer.ts +0 -30
  772. package/src/hooks/useFetch/useFetch.stories.tsx +0 -379
  773. package/src/hooks/useFetch/useFetch.tsx +0 -154
  774. package/src/hooks/usePhoneViewport.ts +0 -34
  775. package/src/hooks/useQorusTypes.ts +0 -233
  776. package/src/hooks/useReqraftProperty.ts +0 -16
  777. package/src/hooks/useStorage/useStorage.stories.tsx +0 -168
  778. package/src/hooks/useStorage/useStorage.ts +0 -35
  779. package/src/hooks/useTemplates.ts +0 -123
  780. package/src/hooks/useValidation.ts +0 -9
  781. package/src/hooks/useWebSocket/useWebSocket.ts +0 -224
  782. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +0 -640
  783. package/src/hooks/useWhyDidYouUpdate.ts +0 -38
  784. package/src/index.tsx +0 -86
  785. package/src/providers/FetchProvider.tsx +0 -40
  786. package/src/providers/ReqraftProvider.tsx +0 -52
  787. package/src/providers/StorageProvider.tsx +0 -131
  788. package/src/stores/currentUser/currentUser.stories.tsx +0 -100
  789. package/src/stores/currentUser/currentUser.tsx +0 -94
  790. package/src/stories/Data/fileFieldArgSchema.json +0 -86
  791. package/src/stories/Tests/utils.ts +0 -320
  792. package/src/stories/storyNetwork.ts +0 -92
  793. package/src/stories/tracking.tsx +0 -206
  794. package/src/tracking/browser/attach.ts +0 -186
  795. package/src/tracking/browser/env.ts +0 -78
  796. package/src/tracking/core/consent.ts +0 -37
  797. package/src/tracking/core/engagement.ts +0 -47
  798. package/src/tracking/core/experiments.ts +0 -258
  799. package/src/tracking/core/hash.ts +0 -26
  800. package/src/tracking/core/ids.ts +0 -25
  801. package/src/tracking/core/observable.ts +0 -23
  802. package/src/tracking/core/queue.ts +0 -131
  803. package/src/tracking/core/sections.ts +0 -166
  804. package/src/tracking/core/storage.ts +0 -69
  805. package/src/tracking/core/tracker.ts +0 -417
  806. package/src/tracking/core/types.ts +0 -162
  807. package/src/tracking/core/utm.ts +0 -23
  808. package/src/tracking/index.ts +0 -72
  809. package/src/tracking/markup.ts +0 -17
  810. package/src/tracking/react/Experiment.tsx +0 -44
  811. package/src/tracking/react/TrackingProvider.tsx +0 -56
  812. package/src/tracking/react/hooks.ts +0 -106
  813. package/src/tracking/react/index.ts +0 -13
  814. package/src/tracking/testing.ts +0 -108
  815. package/src/tracking/ui/ExperimentCard.stories.tsx +0 -296
  816. package/src/tracking/ui/ExperimentCard.tsx +0 -667
  817. package/src/tracking/ui/ExperimentsOverlay.stories.tsx +0 -155
  818. package/src/tracking/ui/ExperimentsOverlay.tsx +0 -437
  819. package/src/tracking/ui/anchor.ts +0 -180
  820. package/src/tracking/ui/card.ts +0 -464
  821. package/src/tracking/ui/client.ts +0 -117
  822. package/src/tracking/ui/index.ts +0 -77
  823. package/src/tracking/ui/types.ts +0 -110
  824. package/src/tracking/ui/useExperimentsAdmin.ts +0 -103
  825. package/src/types/Form.ts +0 -29
  826. package/src/types.ts +0 -14
  827. package/src/utils/datetime.ts +0 -10
  828. package/src/utils/fetch.ts +0 -457
  829. package/src/utils/lspClient.ts +0 -711
  830. package/src/utils/lspClient.types.ts +0 -211
  831. package/src/utils/queryUnlessCancelled.ts +0 -28
  832. package/src/utils/websocket.ts +0 -293
  833. package/vite.config.ts +0 -58
  834. package/vitest.config.ts +0 -69
@@ -1,4569 +0,0 @@
1
- import {
2
- ReqoreButton,
3
- ReqoreCollection,
4
- ReqoreControlGroup,
5
- ReqoreErrorBoundary,
6
- ReqoreIcon,
7
- ReqoreMessage,
8
- ReqoreP,
9
- ReqoreTag,
10
- ReqoreTagGroup,
11
- ReqoreVerticalSpacer,
12
- useReqoreProperty,
13
- useReqoreTheme,
14
- } from '@qoretechnologies/reqore';
15
- import { IReqoreCollectionProps } from '@qoretechnologies/reqore/dist/components/Collection';
16
- import { IReqoreCollectionItemProps } from '@qoretechnologies/reqore/dist/components/Collection/item';
17
- import { IReqorePanelProps } from '@qoretechnologies/reqore/dist/components/Panel';
18
- import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
19
- import { TFieldWithOwnTemplates } from './rendererTypes';
20
- import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
21
- import {
22
- changeDarkness,
23
- getMainBackgroundColor,
24
- getReadableColor,
25
- percentToHexAlpha,
26
- } from '@qoretechnologies/reqore/dist/helpers/colors';
27
- import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
28
- import {
29
- IQorusFormField,
30
- IQorusFormFieldMessage,
31
- IQorusFormFieldOnChangeMeta,
32
- IQorusFormOperator,
33
- IQorusFormOperatorsSchema,
34
- IQorusFormSchema,
35
- TQorusFlatForm,
36
- TQorusForm,
37
- TQorusFormFieldSchema,
38
- TQorusFormOperatorValue,
39
- TQorusType,
40
- } from '@qoretechnologies/ts-toolkit';
41
- import { shouldMarkAsExpression } from '../expressions/argumentPresence';
42
- import { offersTypeChoices } from './typeChoices';
43
- import { optionRowActions, resolveOptionActions, TOptionActions } from './optionActions';
44
- import { createRendererOnlyUiTypeCheck, isRendererOnlyUiType } from './rendererTypes';
45
- import { revealRow } from './revealRow';
46
- import { useEmittedValues } from '../fields/emittedValues';
47
- import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last, uniq } from 'lodash';
48
- import map from 'lodash/map';
49
- import reduce from 'lodash/reduce';
50
- import size from 'lodash/size';
51
- import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
52
- import { useMeasure, useMount, useUpdateEffect } from 'react-use';
53
- import styled, { css } from 'styled-components';
54
- import {
55
- fixOperatorValue,
56
- getDefaultValue,
57
- insertAtIndex,
58
- richtextToString,
59
- } from '../../../helpers/common';
60
- import {
61
- getRequiredOptionMessage,
62
- resolveDegenerateRequiredGroups,
63
- } from '../../../helpers/options';
64
- import {
65
- IValidationResult,
66
- hasAllDependenciesFullfilled,
67
- parseDependency,
68
- validateField,
69
- validateFieldWithResult,
70
- } from '../../../helpers/validations';
71
- import { useQorusTypes } from '../../../hooks/useQorusTypes';
72
- import { useTemplates } from '../../../hooks/useTemplates';
73
- import { isQueryCancelled } from '../../../utils/fetch';
74
- import { queryUnlessCancelled } from '../../../utils/queryUnlessCancelled';
75
- import { Description } from '../../Description';
76
- import { FocusedEditing } from '../../FocusedEditing';
77
- import { SelectFormField } from '../fields/select/Select';
78
- import {
79
- ITemplateFieldProps,
80
- TCustomTemplateItems,
81
- TemplateField,
82
- } from '../fields/template/TemplateField';
83
- import { CompactRow } from './CompactRow';
84
- import { FormFieldsSkeleton } from './FormFieldsSkeleton';
85
- import { CompactRowContext, ICompactRowContext, TCodePreviewRenderer } from './compactRowContext';
86
- import {
87
- GROUP_INDENT,
88
- LABEL_AFFORDANCE_WIDTH,
89
- LABEL_COL_MAX,
90
- LABEL_COL_MIN,
91
- LABEL_COL_VAR,
92
- StyledCompactPanel,
93
- StyledGroupBody,
94
- StyledGroupHeader,
95
- StyledRequiredClusterBox,
96
- StyledRequiredClusterHeader,
97
- StyledStatusBox,
98
- StyledStatusBoxGroupLabel,
99
- } from './compactRowStyles';
100
- import { CompactToolbar } from './CompactToolbar';
101
- import {
102
- CompactToolbarContext,
103
- ICompactToolbarContext,
104
- TCompactSort,
105
- } from './compactToolbarContext';
106
- import {
107
- OptionFieldMessages,
108
- getAllowedValueAvailability,
109
- getShownSchemaMessages,
110
- } from './OptionFieldMessages';
111
- import { OptionsContext } from './optionsContext';
112
- import {
113
- MarkdownRendererContext,
114
- TMarkdownRenderer,
115
- } from '../../Description/markdownRendererContext';
116
- import { OptionsHelpDialog } from './OptionsHelpDialog';
117
- import { firstDeclaredType, isUntypedOptionType } from '../../../helpers/optionUiTypes';
118
- import {
119
- TReadFirstStatus,
120
- getFirstAttentionOptionName,
121
- getOptionGroup,
122
- getOptionGroupLabel,
123
- getReadFirstBucket,
124
- getReadFirstCompletion,
125
- getReadFirstStatus,
126
- isOptionValueEmpty,
127
- isFixedCompactAllowedValueOption,
128
- shouldAutoCollapseCompactOption,
129
- isUnsetSchemaHash,
130
- } from './readFirst';
131
-
132
- // Re-export types for consumers
133
- /** The compact form's three status boxes. */
134
- export type TFormEngineBoxKey = 'attention' | 'set' | 'optional';
135
-
136
- /** Which parts of the compact toolbar to render — see `compactToolbar`. */
137
- export interface IFormEngineToolbarParts {
138
- /** The "Ready / 4 of 6 set / 67%" line and its meter. */
139
- completion?: boolean;
140
- /** The "Filter fields…" input. */
141
- search?: boolean;
142
- /** The field-picker dropdown (add optional fields, reset, revert). */
143
- fields?: boolean;
144
- /** The help button that opens the options dialog. */
145
- help?: boolean;
146
- }
147
-
148
- export type IOptions = TQorusForm;
149
- export type TFlatOptions = TQorusFlatForm;
150
- export type TOption = IQorusFormField;
151
- export type TOperatorValue = TQorusFormOperatorValue;
152
- /**
153
- * A field message, optionally conditional on the form's current values.
154
- *
155
- * `when` / `unless` use the `depends_on` grammar and are evaluated against the
156
- * same form the field belongs to — see `isConditionalMessageShown`. Without
157
- * either, the message is static and always shown, which is what every existing
158
- * consumer gets.
159
- */
160
- export type IOptionFieldMessage = IQorusFormFieldMessage;
161
- /** `T` without `K`, member by member — `Omit` on a union collapses the union. */
162
- type TDistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
163
- /**
164
- * One field of a form's schema: the shared descriptor, plus what this engine
165
- * reads that the shared type does not carry — conditional `messages`, and
166
- * `absorb_fields`. Declared, so a descriptor using them is checked rather than
167
- * cast; still assignable wherever the shared descriptor is expected.
168
- */
169
- export type IOptionsSchemaArg = TDistributiveOmit<TQorusFormFieldSchema, 'messages'> & {
170
- messages?: IOptionFieldMessage[];
171
- /**
172
- * Siblings this field renders inside its own editor instead of on rows of
173
- * their own — a code editor and its language, say.
174
- */
175
- absorb_fields?: string[];
176
- };
177
- export interface IOptionsSchema {
178
- [optionName: string]: IOptionsSchemaArg;
179
- }
180
- export interface IOperator extends IQorusFormOperator {}
181
- export interface IOperatorsSchema extends IQorusFormOperatorsSchema {}
182
- export interface IOptionsOnChangeMeta extends IQorusFormFieldOnChangeMeta {
183
- /** `commitMode='batched'`: the emitted value is staged (a draft), not committed. */
184
- draft?: boolean;
185
- }
186
-
187
- export interface IFormFieldValidityData {
188
- fieldName: string;
189
- type: TQorusType;
190
- value: any;
191
- validation: IValidationResult;
192
- }
193
-
194
- export interface IFormValidityData {
195
- isValid: boolean;
196
- fields: IFormFieldValidityData[];
197
- invalidFields: IFormFieldValidityData[];
198
- }
199
-
200
- /**
201
- * Resolve a field's `inherit_props` map against the current available
202
- * options (plus any values inherited from an outer FormEngine scope),
203
- * producing a `{ propName: siblingValue }` hash suitable for spreading
204
- * onto the field's renderer.
205
- *
206
- * For each entry `<prop-name-on-renderer> -> <sibling-field-name>`, the
207
- * sibling's current value is forwarded under the receiving prop name.
208
- * Lookup order:
209
- * 1. `availableOptions[siblingName]?.value` — the field's local scope.
210
- * 2. `inheritedFromParent[siblingName]` — the bag threaded in by an
211
- * outer FormEngine (see the `inheritedFromParent` prop). Used when
212
- * a composite field's `arg_schema` sub-form needs to reach a value
213
- * on an ancestor scope — e.g. a service-method row's `body` picking
214
- * up `language` from the parent service form.
215
- * Missing siblings emit `undefined`, which the renderer's prop type can
216
- * treat as "no hint".
217
- *
218
- * Designed to be JSON-pure: no closures, no transformations. Renderers
219
- * decide how to use the forwarded value (e.g. the consumer-injected
220
- * `code-editor` renderer maps a `language: "qore"` prop to its
221
- * highlighter mode).
222
- *
223
- * Mirrored in qorus-ide `src/components/Field/systemOptions.tsx`; keep
224
- * the two in sync (see qorus-ide's `.claude/CLAUDE.md`).
225
- */
226
- const resolveInheritProps = (
227
- inheritProps: Record<string, string> | undefined,
228
- availableOptions: TQorusForm | undefined,
229
- inheritedFromParent?: Record<string, unknown>
230
- ): Record<string, unknown> => {
231
- if (!inheritProps) return {};
232
- const out: Record<string, unknown> = {};
233
- for (const propName in inheritProps) {
234
- const siblingName = inheritProps[propName];
235
- const localValue = (availableOptions?.[siblingName] as IQorusFormField | undefined)?.value;
236
- out[propName] = localValue !== undefined ? localValue : inheritedFromParent?.[siblingName];
237
- }
238
- return out;
239
- };
240
-
241
- /** One shared "nothing came down from above" bag. See `inheritedScopeFor`. */
242
- const NO_INHERITED_SCOPE: Record<string, unknown> = Object.freeze({});
243
-
244
- /**
245
- * The inheritance bag ONE field forwards to any `arg_schema` sub-form it hosts:
246
- * whatever the ancestor chain passed down, plus whatever this field's own
247
- * `inherit_props` resolves to.
248
- *
249
- * Written as a function with a stable answer because it is a PROP, and a prop
250
- * that is a new object on every render re-renders the field that receives it
251
- * every time this form renders — for most fields, forever, over a value that
252
- * never changed. It showed up in the console as `Template field kind Updated
253
- * because: {inheritedFromParent}` repeating on a form nobody was touching:
254
- * `kind` declares no `inherit_props` at all, so the bag it was handed was
255
- * `{ ...(undefined ?? {}) }` — a fresh empty object each time, unequal to the
256
- * last one, and `TemplateField` is memoised precisely so it does not re-render
257
- * on a prop that did not change.
258
- *
259
- * So a field with nothing of its own to add forwards the bag it was HANDED,
260
- * unchanged and unwrapped, and a form with no inherited scope at all forwards
261
- * one shared frozen object rather than minting one per field per render.
262
- *
263
- * Mirrored in qorus-ide's `systemOptions.tsx` (which grew this treatment
264
- * first); per the CLAUDE.md rule there, the forwarding logic is kept identical
265
- * in both until FormEngine replaces that renderer.
266
- *
267
- * @param inheritProps the field's `inherit_props` map, if it declares one
268
- * @param availableOptions this form's fields, where a sibling's value is read
269
- * @param inheritedFromParent the bag threaded in from an outer form
270
- */
271
- export const inheritedScopeFor = (
272
- inheritProps: Record<string, string> | undefined,
273
- availableOptions: TQorusForm | undefined,
274
- inheritedFromParent: Record<string, unknown> | undefined
275
- ): Record<string, unknown> => {
276
- const resolved = resolveInheritProps(inheritProps, availableOptions, inheritedFromParent);
277
- if (!Object.keys(resolved).length) {
278
- return inheritedFromParent ?? NO_INHERITED_SCOPE;
279
- }
280
- return { ...(inheritedFromParent ?? {}), ...resolved };
281
- };
282
-
283
- const NegativeColorEffect: any = {
284
- gradient: {
285
- colors: { 0: 'danger', 100: 'danger:darken:2' },
286
- },
287
- color: '#ffffff',
288
- glow: 'danger',
289
- };
290
-
291
- const PositiveColorEffect: any = {
292
- gradient: {
293
- colors: { 0: 'success', 100: 'success:darken:2' },
294
- },
295
- color: '#ffffff',
296
- glow: 'success',
297
- };
298
-
299
- // Compact (read-first) layout: flat label | value | action rows in collapsible
300
- // group panels; colours come in as props so the layout follows the Reqore theme.
301
-
302
- const StyledCompactWrap = styled.div<{ $ownScroll?: boolean }>`
303
- display: flex;
304
- flex-flow: column;
305
- gap: 10px;
306
- width: 100%;
307
- /* Allow the wrap to shrink inside flex/grid parents so its rows' ellipsis can
308
- engage instead of overflowing the container horizontally. */
309
- min-width: 0;
310
- max-width: 100%;
311
-
312
- /* Scroll ownership — see \`compactScroll\`.
313
-
314
- 'host' (the default): the page or panel around us already scrolls, so we
315
- must not cap our height and scroll our own body, or the reader gets two
316
- scrollbars in two styles and cannot tell which moves what.
317
-
318
- \`overflow-x: clip\`, NOT \`hidden\`: CSS coerces an \`overflow-y: visible\`
319
- to \`auto\` whenever the other axis is neither visible nor clip, which would
320
- leave this box a scroll container — the element stops scrolling only
321
- because \`max-height: none\` means nothing overflows, while \`position:
322
- sticky\` inside it still resolves against US instead of the host, so the
323
- toolbar silently stops pinning. \`clip\` keeps the horizontal guard without
324
- that coercion. */
325
- ${({ $ownScroll }) =>
326
- $ownScroll ?
327
- css`
328
- /* 'own': we are the scroller. The sticky toolbar pins to whatever
329
- scrolls, so a host whose scrollport carries top padding would
330
- resolve sticky \`top: 0\` against its padding box and leave an
331
- unblurred strip above the toolbar. Scrolling in this unpadded box
332
- pins it flush. Cap at the host's available height, but do not grow
333
- to fill it — short forms still hug their content.
334
-
335
- \`min-height: 0\` is what lets a flex item shrink below its content
336
- so there is something to scroll; it belongs to this branch only. */
337
- min-height: 0;
338
- max-height: 100%;
339
- overflow-y: auto;
340
- overflow-x: hidden;
341
- `
342
- : css`
343
- /* \`min-height: auto\` is load-bearing, not a default: inside a column
344
- flex parent the shrink allowance above collapses this box to zero
345
- height while its rows paint outside it, so the form looks right but
346
- contributes nothing to layout — the host cannot size or scroll to
347
- it and anything below it stacks against nothing. */
348
- min-height: auto;
349
- max-height: none;
350
- overflow-y: visible;
351
- overflow-x: clip;
352
- `}
353
-
354
- /* Option logos (e.g. language images) render as <img> inside ReqoreIcon's
355
- square box; constrain them so portrait PNGs don't overflow the row. */
356
- .reqore-icon img {
357
- width: 100%;
358
- height: 100%;
359
- object-fit: contain;
360
- }
361
- `;
362
-
363
- // Batched-commit dock: a floating Save/Discard card pinned bottom-right. Sticky
364
- // (not fixed) so it stays within the form's own scroll bounds when the engine
365
- // renders inside a drawer/panel; align-self pushes it to the right edge, and it
366
- // elevates above the rows with an opaque surface + border + shadow.
367
- const StyledCommitDock = styled.div<{ $bg: string; $border: string }>`
368
- position: sticky;
369
- bottom: 8px;
370
- z-index: 6;
371
- align-self: flex-end;
372
- width: fit-content;
373
- max-width: 100%;
374
- padding: 8px 10px;
375
- background: ${({ $bg }) => $bg};
376
- border: 1px solid ${({ $border }) => $border};
377
- border-radius: 8px;
378
- box-shadow: 0 4px 16px ${({ $bg }) => `${changeDarkness($bg, 0.4)}${percentToHexAlpha(55)}`};
379
- `;
380
-
381
- export const getType = (
382
- type?: TQorusType | TQorusType[],
383
- operators?: IOperatorsSchema,
384
- operator?: TOperatorValue
385
- ): TQorusType => {
386
- const finalType = getTypeFromOperator(operators, fixOperatorValue(operator)) || type;
387
- const resolvedType = firstDeclaredType(finalType);
388
-
389
- // A value can be received before its server-driven option schema. Keep the
390
- // renderer type-safe during that transition without guessing a concrete type.
391
- return (resolvedType || 'any') as TQorusType;
392
- };
393
-
394
- const getTypeFromOperator = (
395
- operators?: IOperatorsSchema,
396
- operatorData?: (string | null | undefined)[]
397
- ): TQorusType | null => {
398
- if (!operators || !operatorData || !size(operatorData) || !last(operatorData)) {
399
- return null;
400
- }
401
- return (operators[last(operatorData) as string]?.type as TQorusType) || null;
402
- };
403
-
404
- /** Re-exported from `helpers/common` so existing importers keep working; it
405
- * lives there because `helpers/validations` needs it and cannot import this
406
- * module (this module imports the validators). */
407
- export { fixOperatorValue } from '../../../helpers/common';
408
-
409
- /**
410
- * The renderer-only predicate. Defaults to reqraft's built-ins; a FormEngine
411
- * instance passes its own (built-ins + the `rendererOnlyUiTypes` prop) so a
412
- * consumer's injected editors are recognised too. See `./rendererTypes`.
413
- */
414
- type TRendererOnlyCheck = (type?: TQorusType | TQorusType[]) => boolean;
415
-
416
- const getOptionSchemaStorageType = (
417
- option?: TQorusFormFieldSchema,
418
- isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
419
- ): TQorusType =>
420
- ((isFixedCompactAllowedValueOption(option) ? option?.type || option?.ui_type
421
- : isRendererOnly(option?.ui_type) ? option?.type || option?.ui_type
422
- : option?.ui_type || option?.type) || 'any') as TQorusType;
423
-
424
- /**
425
- * Whether an option currently holds an expression, in EITHER shape it comes in.
426
- *
427
- * At runtime the editor writes the flag onto the option itself
428
- * (`{ type, is_expression: true, value }`), but a value loaded from a saved
429
- * draft carries the envelope NESTED (`{ type, value: { is_expression: true,
430
- * value } }`). Same thing, two shapes, and only the flat one was ever checked —
431
- * so a freshly typed expression behaved while a reloaded one did not, which is
432
- * exactly how this survived several fixes: every test used the runtime shape.
433
- */
434
- const optionHoldsExpression = (option: unknown): boolean => {
435
- const o = option as { is_expression?: unknown; value?: { is_expression?: unknown } } | undefined;
436
- return !!(o?.is_expression || o?.value?.is_expression);
437
- };
438
-
439
- /**
440
- * Which type an option resolves to — the schema's, or the one stored on the
441
- * value.
442
- *
443
- * Exported because this single decision is what every option field is rendered
444
- * with, and what an expression's return type is checked against. It deserves
445
- * to be assertable without standing up a form.
446
- */
447
- export const getOptionFieldStorageType = (
448
- optionName: string,
449
- fieldType: TQorusType | TQorusType[] | undefined,
450
- schema?: IQorusFormSchema,
451
- operators?: IOperatorsSchema,
452
- operatorData?: TOperatorValue,
453
- isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType,
454
- /** Whether the stored value is an expression — see below. */
455
- isExpression?: boolean
456
- ): TQorusType => {
457
- const schemaOption = schema?.[optionName];
458
- /* A STORED type does not describe an expression.
459
-
460
- An expression value is `{ is_expression: true, value: {...} }`, so the type
461
- recorded beside it describes that envelope — `hash` — and not what the
462
- field holds. Letting it win means the SCHEMA's declared type is ignored and
463
- the wrong answer is handed to the expression editor as the return type to
464
- check against: `1 + 2` on a field declared `"type": "auto"` came back
465
- "The expression returns int, and this field holds hash". A literal `1` was
466
- fine, because a literal is stored as a plain value whose type matches.
467
-
468
- `auto`/`any` accept anything, which is exactly why nothing should be
469
- checked against them; a concrete declared type still is. The schema is the
470
- only party that knows what the field accepts. */
471
- const storedType =
472
- (
473
- !isExpression &&
474
- fieldType &&
475
- !(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType))
476
- ) ?
477
- fieldType
478
- : getOptionSchemaStorageType(schemaOption, isRendererOnly);
479
-
480
- return getType(storedType as TQorusType, operators, operatorData);
481
- };
482
-
483
- export const hasRequiredOptions = (options: IQorusFormSchema = {}) => {
484
- return !!findKey(options, (option) => option.required);
485
- };
486
-
487
- /* Re-exported from the module that owns it so the existing import path keeps
488
- working; see `optionsContext.ts` for why it is not declared here. */
489
- export { OptionsContext } from './optionsContext';
490
- export type { IOptionsContext } from './optionsContext';
491
-
492
- export const fixOptions = (
493
- value: TQorusForm | TQorusFlatForm = {},
494
- options?: IQorusFormSchema,
495
- operators?: IOperatorsSchema,
496
- isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
497
- ): TQorusForm => {
498
- const fixedValue = cloneDeep(value);
499
-
500
- // Server-driven schemas can arrive after the persisted value. Preserve that
501
- // value verbatim until its schema is available instead of manufacturing
502
- // partially typed form fields that can crash the following render.
503
- if (!size(options)) {
504
- return fixedValue as TQorusForm;
505
- }
506
-
507
- forEach(options, (option, name) => {
508
- if (
509
- option.value ||
510
- option.required_groups ||
511
- ((option.required || option.preselected) && !fixedValue[name]) ||
512
- ((option.required || option.preselected) && option.default_value && !option.value)
513
- ) {
514
- let obj: IQorusFormField;
515
- const type = getType(
516
- getOptionSchemaStorageType(option, isRendererOnly),
517
- operators,
518
- (fixedValue[name] as IQorusFormField)?.op
519
- );
520
-
521
- if (
522
- (option.default_value as IQorusFormField)?.is_expression &&
523
- (fixedValue[name] as IQorusFormField)?.value === undefined
524
- ) {
525
- obj = option.default_value as IQorusFormField;
526
- } else {
527
- // A field declared with `default_view: 'expression'` is NOT seeded with
528
- // an empty expression here. This runs on every load, so a seed written
529
- // here changed a saved object merely by opening it: an existing Qog
530
- // state gained `{args: []}` the moment its panel opened, and the editor
531
- // autosaved a draft nobody made. The renderer already opens such a
532
- // field in expression mode from the schema (`isDefaultFunction`), the
533
- // first edit stores `is_expression`, and a host that creates a NEW
534
- // object seeds the expression itself, at creation.
535
- obj = {
536
- type,
537
- value:
538
- (typeof fixedValue[name] === 'object' ?
539
- (fixedValue[name] as IQorusFormField)?.value
540
- : fixedValue[name]) ??
541
- option.value ??
542
- getDefaultValue(option),
543
- };
544
-
545
- if ((fixedValue[name] as IQorusFormField)?.is_expression) {
546
- obj.is_expression = true;
547
- }
548
- }
549
-
550
- fixedValue[name] = obj;
551
- }
552
- });
553
-
554
- const res = reduce(
555
- fixedValue,
556
- (newValue, option, optionName) => {
557
- let newOption = option as IQorusFormField;
558
-
559
- if (!isPlainObject(newOption) || !(newOption as IQorusFormField)?.type) {
560
- const isUntypedEnvelope = isPlainObject(newOption) && 'value' in newOption;
561
- const untypedFieldValue =
562
- isUntypedEnvelope ? (newOption as IQorusFormField).value : newOption;
563
- // Spread the original envelope first so every key it carried survives —
564
- // rebuilding from just {type, value} silently dropped `op`, `is_expression`
565
- // and anything else a field needs to render, which emptied the card.
566
- const fixedOption: IQorusFormField = {
567
- ...(isPlainObject(newOption) ? (newOption as IQorusFormField) : {}),
568
- type: getType(
569
- getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
570
- operators,
571
- (newOption as IQorusFormField)?.op
572
- ),
573
- value: untypedFieldValue,
574
- };
575
-
576
- newOption = fixedOption;
577
- } else if (
578
- newOption.type === options?.[optionName]?.ui_type &&
579
- isRendererOnly(newOption.type)
580
- ) {
581
- newOption = {
582
- ...newOption,
583
- type: getType(
584
- getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
585
- operators,
586
- newOption.op
587
- ),
588
- };
589
- }
590
-
591
- // A value that is not one of the declared choices is KEPT. It used to be
592
- // dropped here, on load, and the dropped value was three times a real
593
- // one — a bare-declared choice, a document naming a choice, a value whose
594
- // choices had not arrived yet — that the emptied form then autosaved, so
595
- // it was gone for good. Whether a value is one of the choices is now the
596
- // validator's question (`getUnlistedChoiceReason`, asked through
597
- // `validateFieldWithResult`): such a field is invalid, sits under "Needs
598
- // attention" saying which value is not a choice, and the form reports
599
- // itself invalid so the host blocks submitting it.
600
-
601
- if (
602
- newOption.value &&
603
- options?.[optionName]?.readonly &&
604
- options?.[optionName]?.default_value &&
605
- getDefaultValue(options?.[optionName]) !== newOption.value
606
- ) {
607
- newOption.value = getDefaultValue(options[optionName]);
608
- }
609
-
610
- return { ...newValue, [optionName]: newOption };
611
- },
612
- {} as TQorusForm
613
- );
614
-
615
- return res;
616
- };
617
-
618
- // A required field with no value can arrive as `{ value: '' }` OR with no `value`
619
- // key at all — fixOptions emits either depending on the field's default. They mean
620
- // the same "empty", so drop the key before comparing: a value that differs ONLY by
621
- // that representation must read as unchanged, otherwise the controlled value-sync
622
- // effect re-fixes → re-emits → re-fixes forever (the echo loop).
623
- const normalizeEmptyFieldValues = (fields: TQorusForm | TQorusFlatForm | undefined): TQorusForm =>
624
- reduce(
625
- (fields as TQorusForm) || {},
626
- (acc, field, name) => {
627
- const fieldValue = (field as IQorusFormField)?.value;
628
- if (field && typeof field === 'object' && (fieldValue === '' || fieldValue === undefined)) {
629
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
630
- const { value: _dropped, ...rest } = field as IQorusFormField;
631
- acc[name] = rest as IQorusFormField;
632
- } else {
633
- acc[name] = field as IQorusFormField;
634
- }
635
- return acc;
636
- },
637
- {} as TQorusForm
638
- );
639
-
640
- /**
641
- * Does this rebuild say anything the parent does not already hold?
642
- *
643
- * `false` means the rebuild is a MIRROR: `fixOptions` found no default to
644
- * materialise, so the result repeats the `value` the parent handed down. The
645
- * compare is empty-normalized because `fixOptions` round-trips a required-empty
646
- * field between `{ value: '' }` and no `value` key, and that cosmetic
647
- * difference would otherwise read as new information forever.
648
- */
649
- export const isMirrorOfValue = (
650
- fixedValue: TQorusForm | TQorusFlatForm | undefined,
651
- value: TQorusForm | TQorusFlatForm | undefined
652
- ): boolean => isEqual(normalizeEmptyFieldValues(fixedValue), normalizeEmptyFieldValues(value));
653
-
654
- /**
655
- * What this rebuild lets the form agree with the parent on, if anything.
656
- *
657
- * Returns the rebuild when it mirrors `value`, and `undefined` when it
658
- * contributed something the parent has yet to learn.
659
- *
660
- * Adopting the parent's value IS agreement, and saying so matters as much as
661
- * staying quiet about it. The sync-down skips only when the parent echoed what
662
- * this form last sent; if adopting a value never updates that record, it points
663
- * at an older state forever, the skip can never fire again, and the form
664
- * rebuilds on every single change. Holding the mirror back WITHOUT recording
665
- * the agreement was measured making the starvation worse, not better — 8 s
666
- * became a 45 s timeout — which is why the two belong together.
667
- */
668
- export const agreementFromRebuild = (
669
- fixedValue: TQorusForm | TQorusFlatForm | undefined,
670
- value: TQorusForm | TQorusFlatForm | undefined
671
- ): TQorusForm | TQorusFlatForm | undefined =>
672
- isMirrorOfValue(fixedValue, value) ? fixedValue : undefined;
673
-
674
- /** The state the emit decision is made against. */
675
- export interface IEmitDecision {
676
- /** The form's own copy of the value, as it stands now. */
677
- localValue: TQorusForm | TQorusFlatForm | undefined;
678
- /** The `value` prop, read at the moment the decision is made. */
679
- value: TQorusForm | TQorusFlatForm | undefined;
680
- /**
681
- * The last rebuild that mirrored `value` without adding anything, if the most
682
- * recent one did. `undefined` when the last rebuild contributed something.
683
- */
684
- mirroredValue: TQorusForm | TQorusFlatForm | undefined;
685
- }
686
-
687
- /**
688
- * Should the form report `localValue` to its parent as an answer?
689
- *
690
- * Extracted and pure because the situation it exists for cannot be reproduced
691
- * through a rendered form: it needs the emit to be decided against a `value`
692
- * NEWER than the `localValue` built from it, which only happens when two
693
- * components' effects land in one React commit — and a test renderer flushes
694
- * effects between updates, so the two never fall out of step. The states below
695
- * were recorded from a live instrumented run instead, and this is what makes
696
- * them assertable.
697
- *
698
- * Two ways the answer is no:
699
- *
700
- * 1. `localValue` is a mirror — a rebuild of the parent's own value that added
701
- * nothing. Repeating it back is not an answer. **Checked first**, because
702
- * the comparison in (2) reads whatever `value` has become by now, which,
703
- * when a nested engine shares the parent value, is already that engine's
704
- * newer write; comparing against it would find a difference this form never
705
- * made and emit the older mirror over the newer write. That is the loop.
706
- * 2. `localValue` already equals `value` — there is nothing to report.
707
- */
708
- export const shouldEmitLocalValue = ({
709
- localValue,
710
- value,
711
- mirroredValue,
712
- }: IEmitDecision): boolean => {
713
- if (mirroredValue !== undefined && isEqual(localValue, mirroredValue)) {
714
- return false;
715
- }
716
- return !isEqual(localValue, value);
717
- };
718
-
719
- export const flattenOptions = (options: TQorusForm): TQorusFlatForm => {
720
- return reduce(
721
- options,
722
- (newOptions, option, optionName) => {
723
- return {
724
- ...newOptions,
725
- [optionName]: typeof option === 'object' ? (option as IQorusFormField)?.value : option,
726
- };
727
- },
728
- {} as TQorusFlatForm
729
- );
730
- };
731
-
732
- export const getTypeAndCanBeNull = (
733
- type?: TQorusType | TQorusType[],
734
- allowed_values?: any[],
735
- operatorData?: TOperatorValue,
736
- operators?: IOperatorsSchema
737
- ) => {
738
- let canBeNull = false;
739
- let realType = getType(type, operators, operatorData);
740
-
741
- realType = realType.replace('soft', '') as TQorusType;
742
- realType = realType.replace(/<[^>]*>/g, '') as TQorusType;
743
-
744
- if (realType?.startsWith('*')) {
745
- realType = realType.replace('*', '') as TQorusType;
746
- canBeNull = true;
747
- }
748
-
749
- realType = (realType === 'string' && allowed_values ? 'string' : realType) as TQorusType;
750
-
751
- return {
752
- type: realType,
753
- defaultType: realType,
754
- defaultInternalType: isUntypedOptionType(realType) ? undefined : realType,
755
- canBeNull,
756
- };
757
- };
758
-
759
- /** Display metadata for a read-first group, keyed by the raw `group` string;
760
- * omissions fall back to a title-cased key, no icon, and schema order. */
761
- export interface IFormEngineGroup {
762
- label?: string;
763
- icon?: IReqoreIconName;
764
- subtitle?: string;
765
- sort?: number;
766
- }
767
-
768
- export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange'> {
769
- name: string;
770
- uniqueName?: string;
771
- value?: TQorusForm | TQorusFlatForm;
772
- options?: IOptionsSchema;
773
- onChange?: (name: string, value?: TQorusForm, meta?: IOptionsOnChangeMeta) => void;
774
- /**
775
- * `'immediate'` (default): edits flow out via the debounced `onChange`.
776
- * `'batched'`: edits stage locally (Draft chips + Save/Discard bar); Save
777
- * fires `onCommit`, gated on validity; staged `onChange` carries `meta.draft`.
778
- */
779
- commitMode?: 'immediate' | 'batched';
780
- /**
781
- * How many read-first rows can be expanded (editing) at once. `'single'`
782
- * (default) collapses the previously-open row when another is opened — the
783
- * accordion model that keeps the read-first list scannable. `'multi'` lets
784
- * several stay open (e.g. filling an empty form top-to-bottom).
785
- */
786
- expandMode?: 'single' | 'multi';
787
- /** `commitMode='batched'` only: the user applied the staged changes. */
788
- onCommit?: (name: string, value?: TQorusForm, meta?: IOptionsOnChangeMeta) => void;
789
- /**
790
- * Operator schema (`WHERE <field> IS <op>`): adds the operator selector rows;
791
- * operator-bearing fields always card-edit in compact mode.
792
- */
793
- operators?: IOperatorsSchema;
794
- onSingleOptionsChange?: (name: string, value: TOption) => void;
795
- onDependableOptionChange?: (
796
- name: string,
797
- value: TOption,
798
- options: TQorusForm,
799
- schema: IQorusFormSchema
800
- ) => void;
801
- placeholder?: string;
802
- noValueString?: string;
803
- isValid?: boolean;
804
- /** Fetch the options schema from `options/{url}` (IDE Options parity). */
805
- url?: string;
806
- /** Fetch the options schema from a custom endpoint; wins over `url`. */
807
- customUrl?: string;
808
- /** Fetch the search-operators schema (enables the per-option operator UI). */
809
- operatorsUrl?: string;
810
- /** Fired with the fetched schema after a successful `url`/`customUrl` load. */
811
- onOptionsLoaded?: (options: IQorusFormSchema) => void;
812
- recordRequiresSearchOptions?: boolean;
813
- readOnly?: boolean;
814
- /**
815
- * What a click on a READ-ONLY row does, when there is somewhere to send it.
816
- *
817
- * A read-only row opens nothing by default — it shows everything it has and
818
- * answers a click with silence, which is right when the form is the only
819
- * place the value exists. Where the same field IS editable somewhere else,
820
- * that silence is a dead end: the reader clicking a value they want to
821
- * change has said exactly what they want.
822
- *
823
- * Given this, each row becomes the way in to that place and announces it —
824
- * it takes a button's role, keyboard handling and cursor. Omit it and
825
- * nothing changes.
826
- */
827
- onReadOnlyActivate?: (optionName: string) => void;
828
- allowTemplates?: boolean;
829
- /**
830
- * The form's SHARED template vocabulary — config items, system properties —
831
- * offered to every field that supports templates.
832
- *
833
- * A field may also declare `templates` of its own in the schema, and that
834
- * narrower list wins for that field: see {@link TFieldWithOwnTemplates}.
835
- */
836
- stringTemplates?: IReqoreFormTemplates;
837
- /** Opt-in: fetch global templates from `system/getContextData` for this context. */
838
- interfaceContext?: string;
839
- templateFieldProps?: Partial<ITemplateFieldProps>;
840
- /**
841
- * Force single-value selectors to use inline dropdowns even when their
842
- * allowed values carry descriptions.
843
- */
844
- forceDropdown?: boolean;
845
- showTypeToggle?: boolean;
846
- /**
847
- * Compact (read-first) mode: each option renders as a row with its formatted
848
- * value; clicking expands the real editor inline, Done collapses it.
849
- */
850
- compact?: boolean;
851
- /**
852
- * Compact mode only: drop the read-first wrap's horizontal gutter (the 12px
853
- * breathing room) so the form sits flush to its container's edges. For embeds
854
- * that own their own gutters — e.g. the SchemaDefinition tab body, where the
855
- * form should line up with the section description above it. Default `false`.
856
- *
857
- * @deprecated No-op. The wrap has never had the gutter this prop claims to
858
- * drop — the value reached a styled template with no interpolation slot — so
859
- * every form already renders flush. Kept so existing call sites keep
860
- * compiling; do not add new ones.
861
- */
862
- compactFlush?: boolean;
863
- /**
864
- * Compact mode only: this form is an EMBEDDED sub-form (e.g. an arg_schema
865
- * field's nested form) rather than the top-level scroller. It doesn't own a
866
- * scroll context, so the toolbar isn't sticky and its header drops the dark
867
- * blurred backdrop (and the stacking context that goes with it) — it sits
868
- * transparently inside the parent's edit card. Default `false`.
869
- *
870
- * Implies `compactScroll: 'host'` — a nested sub-form never owns a scroller.
871
- */
872
- compactNested?: boolean;
873
- /**
874
- * Compact mode only: who owns the form's vertical scroll.
875
- *
876
- * - `'host'` (default) — the surrounding page, panel or drawer scrolls, and
877
- * the form grows to its content. This is right almost everywhere: a form
878
- * inside something that already scrolls must not cap its own height, or the
879
- * reader gets two scrollbars side by side in two different styles and no
880
- * way to tell which one moves what.
881
- * - `'own'` — the form caps at the host's available height and scrolls its
882
- * own body. Reach for this only when the host has a definite height and is
883
- * NOT itself a scroll container (it would clip the form instead of
884
- * scrolling it), or when the host's scrollport is padded and the sticky
885
- * toolbar must pin flush against an unpadded box.
886
- *
887
- * Default `'host'`.
888
- */
889
- compactScroll?: 'own' | 'host';
890
- /**
891
- * Compact mode only: props forwarded to the read-first form's outer
892
- * `ReqorePanel` — the panel that carries the sticky toolbar header and holds
893
- * the status boxes. Spread AFTER the engine's own defaults, so anything set
894
- * here wins (`flat`, `raised`, `minimal`, `stickyHeader`, `label`, `icon`,
895
- * `intent`, `padded`, `size`, …). Two props merge instead of replacing, so a
896
- * consumer can add to the panel without dismantling the form:
897
- * - `actions` — appended after the engine's toolbar action.
898
- * - `contentStyle` — merged over the engine's flex-column layout.
899
- * No-op in classic (non-compact) mode.
900
- */
901
- compactPanelProps?: IReqorePanelProps;
902
- /**
903
- * Compact mode only: which status boxes start collapsed. Defaults to
904
- * `['optional']` — a form opens on what is in use.
905
- *
906
- * Pass `['set', 'optional']` where the form is one item in a longer list and
907
- * the reader is choosing WHICH to open rather than reading this one, e.g. a
908
- * template's per-action option forms stacked in a drawer.
909
- *
910
- * One escape is not negotiable: a box always opens while a search query is
911
- * running (`ReqorePanel` unmounts collapsed content, so a match inside a
912
- * closed box would be unreachable).
913
- *
914
- * A second applies only by DEFAULT: the Optional box stays open when every
915
- * row is optional, because a form that collapsed its only box would render a
916
- * card that looks empty and broken. Passing this prop explicitly waives it —
917
- * a host that supplies its own heading and explanation around the form (a
918
- * run-options panel, say) has already told the reader what the card is, and
919
- * asked for the settings to start folded away.
920
- */
921
- compactCollapsedGroups?: TFormEngineBoxKey[];
922
- /**
923
- * Compact mode only: rename the status boxes.
924
- *
925
- * The three are `Needs attention`, `Set` and `Optional`, which read correctly
926
- * when the form is the page's subject. Inside a host that already named the
927
- * subject — a panel headed "Change a setting for this run" — a bare
928
- * `Optional` names a category rather than the things in it, and
929
- * `Optional settings` is what the reader is looking at. Only the labels
930
- * change; bucketing, icons and intents are untouched.
931
- */
932
- compactBoxLabels?: Partial<Record<TFormEngineBoxKey, string>>;
933
- /**
934
- * Compact mode only: the three status-box headers stay pinned to the top of
935
- * whatever scrolls while their own box is in view. Default `true`.
936
- *
937
- * A box header is the only thing that says which bucket a row is in — `Set`,
938
- * `Needs attention`, `Optional`. On a form long enough to scroll it leaves
939
- * the screen, and every row below it then reads as an undifferentiated list:
940
- * the reader deep inside `Optional` has no way to tell it from `Set` except
941
- * by scrolling back. Pinning the header is what keeps the answer on screen.
942
- *
943
- * ALL THREE behave identically, deliberately. They are one control rendered
944
- * three times, differing only in label, tint and icon; a reader who has
945
- * scrolled into the Optional box needs to know that exactly as much as one
946
- * who has scrolled into Set, and a rule that pinned only some of them would
947
- * make the absence of a pinned header mean two different things (this box has
948
- * no header vs. you are above every box).
949
- *
950
- * A header can never outlive its own box: `position: sticky` is bounded by
951
- * its containing block, so each header unpins as its box scrolls away and at
952
- * most ONE box header is ever pinned at a time. The pin costs one header's
953
- * height, not three.
954
- *
955
- * Off by construction for a nested (`arg_schema`) sub-form — it sits inside
956
- * the parent form's card and owns no scroll context to pin against.
957
- *
958
- * On a NARROW form this also takes the pin off the form's own toolbar, so the
959
- * two never cost a third of a phone's screen between them — see the note at
960
- * the panel's `stickyHeader`.
961
- *
962
- * Turn it off where the form is one item in a scrolling LIST of forms: there
963
- * the reader is choosing which form to read, and a box header pinned from the
964
- * item they have scrolled past is chrome belonging to something they have
965
- * left.
966
- */
967
- compactStickyBoxHeaders?: boolean;
968
- /**
969
- * Compact mode only: how many pixels of the HOST's own sticky chrome sit
970
- * above this form, inside the same scroll container.
971
- *
972
- * Sticky offsets do not compose by themselves — every pinned thing resolves
973
- * `top` against the same scrollport, so two of them with the same offset land
974
- * on top of each other. A host that pins a bar of its own (a page header
975
- * carrying a name and a Run control, say) measures it and passes the height
976
- * here; the form then pins its toolbar below that bar, and its box headers
977
- * below the toolbar, instead of underneath both.
978
- *
979
- * Default `0` — the form is the topmost pinned thing in its scroller.
980
- */
981
- compactStickyOffset?: number;
982
- /**
983
- * Compact mode only: which parts of the form's toolbar to show. `true`
984
- * (default) shows all of it; `false` hides the whole thing.
985
- *
986
- * The parts already hide themselves when they have nothing to say — the
987
- * completion meter needs at least one counted option, and the search and
988
- * field-picker need more than one option to pick between — so reach for this
989
- * only to hide something that WOULD have rendered. A per-action form asking
990
- * for one value, say, has a meter reading 0/1 that tells the reader nothing
991
- * they cannot see in the row below it.
992
- *
993
- * Hiding `search` removes the only way to run a query, which is what forces
994
- * a collapsed box open; `compactCollapsedGroups` then fully determines what
995
- * is open.
996
- */
997
- compactToolbar?: boolean | IFormEngineToolbarParts;
998
- /** Compact mode only: per-group display metadata (label / icon / subtitle /
999
- * order) — the server only sends the bare group key. */
1000
- groups?: Record<string, IFormEngineGroup>;
1001
- /**
1002
- * Async schema source (used when `options` is absent): transport-agnostic,
1003
- * called on mount and on identity change (memoize it); the engine owns the
1004
- * loading/error lifecycle and fires `onOptionsLoaded` on success.
1005
- */
1006
- optionsLoader?: () => Promise<IQorusFormSchema>;
1007
- onValidityChange?: (isValid: boolean, data: IFormValidityData) => void;
1008
- /**
1009
- * SEAM (reqraft): extra hover actions prepended to each option row — where
1010
- * the IDE renders its `allowAi` AI-assist button. A factory receives the
1011
- * option's name/schema/value (the context the IDE's `AiAssistanceAction`
1012
- * captures); the consumer injects the button, reqraft stays AI-free.
1013
- */
1014
- optionActions?: TOptionActions;
1015
- /**
1016
- * Whether per-option injected actions collapse into the row's overflow menu
1017
- * instead of rendering as inline buttons.
1018
- *
1019
- * `'auto'` (default) collapses when the device cannot hover or the viewport is
1020
- * narrow — on a phone a hover-gated action would otherwise be unreachable, and
1021
- * a row has no width for a button strip. `'always'` / `'never'` force it, which
1022
- * consumers can use for a known-mobile surface and stories use to capture the
1023
- * collapsed state deterministically.
1024
- *
1025
- * Actions beyond `MAX_INLINE_OPTION_ACTIONS` always overflow into the menu, so
1026
- * a consumer injecting many actions can never blow out the row.
1027
- */
1028
- optionActionsCollapse?: 'auto' | 'always' | 'never';
1029
- /**
1030
- * The HOST is still loading something this form needs.
1031
- *
1032
- * Declared because it was already read (`rest.skeleton`) and honoured by the
1033
- * loading gate, but undeclared props are invisible to the people who need
1034
- * them — and the reason to reach for this is not obvious.
1035
- *
1036
- * A host that resolves anything before it can render a form — an option list
1037
- * fetched by `get_message`, a schema id resolved over the wire — has a wait of
1038
- * its own that ends just as this component's begins. Drawing its own
1039
- * placeholder for that first half and then handing over produces TWO
1040
- * placeholders in series for one load, from two different components: the DOM
1041
- * node is replaced, the geometry can differ, and the page relayouts in the
1042
- * middle of waiting. Measured on the Qorus alert rule, whose three sections
1043
- * each did this — six placeholder mounts where three would do.
1044
- *
1045
- * Passing the host's own wait in here instead makes ONE component own the
1046
- * whole wait, from the host's first fetch to this form's last, with one
1047
- * placeholder and one DOM node.
1048
- */
1049
- skeleton?: boolean;
1050
- /**
1051
- * What the HOST is waiting for, when it hands its wait down via `skeleton`.
1052
- *
1053
- * Without it every host wait reads as the same word and the reading stops at
1054
- * the package boundary — measured on the live alert rule, three sections all
1055
- * reported `host` and the next step was guessing which of the host's own five
1056
- * conditions had fired. Surfaced as `data-wait` on the placeholder.
1057
- */
1058
- skeletonReason?: string;
1059
-
1060
- /**
1061
- * SEAM (reqraft): consumer-injected field editors for types reqraft doesn't
1062
- * ship (IDE domain fields). Keyed by field `type`/`ui_type`; forwarded through
1063
- * `TemplateField` to the `AutoFormField` override seam.
1064
- */
1065
- componentOverrides?: Record<string, React.FC<any>>;
1066
- /**
1067
- * Draws the read-first preview of a `code-editor` value.
1068
- *
1069
- * The built-in preview is a plain monospace block, because this package
1070
- * cannot ship a syntax highlighter -- a code editor is the very dependency it
1071
- * keeps out, which is why the *editor* for `code-editor` also arrives through
1072
- * `componentOverrides`. A host that already has a highlighter supplies one
1073
- * here; everyone else keeps the plain block.
1074
- */
1075
- codePreviewRenderer?: TCodePreviewRenderer;
1076
- /**
1077
- * Draws every markdown description this form shows -- the inline row
1078
- * description, the focused-editing header, and the field help dialog.
1079
- *
1080
- * A host with field descriptions has markdown elsewhere too, on its object
1081
- * pages and in its catalogues. Without this the same description renders one
1082
- * way inside a form and another way outside it, and the built-in dialect --
1083
- * no GFM, no host heading scale -- is the one nobody chose. Supplying a
1084
- * renderer here makes the form agree with the rest of the application.
1085
- */
1086
- markdownRenderer?: TMarkdownRenderer;
1087
- /**
1088
- * The `ui_type` names among `componentOverrides` that select a bespoke editor
1089
- * but store their value as the schema's plainer `type` (a `cron` editor stores
1090
- * a string). Declaring them here keeps the field's stored `type` correct —
1091
- * without it the renderer name is written into the value and validation and
1092
- * round-tripping both break. Merged with reqraft's own built-in list, so a
1093
- * consumer's new editor no longer needs a reqraft release to be handled.
1094
- */
1095
- rendererOnlyUiTypes?: string[];
1096
- /**
1097
- * The `ui_type` names whose own editor renders templates as chips INLINE.
1098
- *
1099
- * For these the template selector is suppressed and the field's editor is
1100
- * shown instead — the selector would replace a control the author can type in
1101
- * with one they can only pick from. Merged with reqraft's built-in list
1102
- * (`richtext`), the same way `rendererOnlyUiTypes` is.
1103
- */
1104
- templateAwareUiTypes?: string[];
1105
- /**
1106
- * Extra `ui_type` names that mean "a value of any type", for the purpose of
1107
- * offering the per-type choices in the row's menu.
1108
- *
1109
- * A field that pins no concrete type offers "enter a value of type X" for
1110
- * each type the form knows. That was gated on the schema saying
1111
- * `ui_type: 'any'` alone, which misses two cases: an option that declares its
1112
- * untypedness as `type: 'auto'` and carries no `ui_type` at all (the server
1113
- * spells an assertion's Expected Value exactly that way), and an option whose
1114
- * `ui_type` is a HOST's own untyped editor. Merged with reqraft's built-ins
1115
- * (`any`, `auto`), the same way `templateAwareUiTypes` is.
1116
- */
1117
- anyLikeUiTypes?: string[];
1118
-
1119
- /**
1120
- * Bag of values forwarded from an outer FormEngine scope, used as a
1121
- * fallback when a field's `inherit_props` names a sibling that isn't in
1122
- * this form's own `availableOptions`. Populated automatically when
1123
- * FormEngine renders a nested `arg_schema` sub-form (through
1124
- * AutoFormField's hash / list mount sites) so that each level accumulates
1125
- * its ancestors' inherited props. Consumers rarely set this by hand — it's
1126
- * plumbing for the inherit_props scope-forwarding contract.
1127
- */
1128
- inheritedFromParent?: Record<string, unknown>;
1129
-
1130
- /**
1131
- * Opt-in: on mount, drop the user straight into the first field they must
1132
- * fill — the first empty, focusable field (in schema/sort order) that is
1133
- * `required` or a member of a still-unsatisfied one-of `required_groups`
1134
- * group. Disabled, readonly, read-only-form, and dependency-locked fields are
1135
- * skipped. In compact (read-first) mode the target row is expanded through
1136
- * the engine's own `expandedOptions` state, so its editor gains focus with no
1137
- * DOM scraping or synthetic clicks. Strictly one-shot: it fires the first
1138
- * time the form has focusable content (so an async-loaded schema is covered)
1139
- * and then never again — a later value edit, server-driven field update, or
1140
- * in-place schema reload will not re-focus; only a remount re-arms it. It also
1141
- * never steals focus from a control the user has already moved into. No-op in
1142
- * classic (non-compact) mode. Default: off.
1143
- */
1144
- autoFocusFirstRequired?: boolean;
1145
-
1146
- /**
1147
- * Opt-in: on mount, OPEN the first row that needs attention — without taking
1148
- * focus.
1149
- *
1150
- * `autoFocusFirstRequired` fuses two decisions that are not the same: which
1151
- * row is open (layout) and where the caret is (focus). Focus is the half that
1152
- * can be stolen, so that flag waits for focus to be free — and a form mounted
1153
- * BY a click never sees free focus, because the button that mounted it still
1154
- * has it. Adding a list item is exactly that case: the row the author must
1155
- * fill stayed shut behind a second click, and the flag that exists to prevent
1156
- * it could not fire.
1157
- *
1158
- * Opening a row takes nothing from the user, so this half needs no guard. Use
1159
- * it where the mount is already the answer to a deliberate action; use
1160
- * `autoFocusFirstRequired` where the form is the destination and the caret
1161
- * should land in it. Setting both keeps the focusing behaviour. Same one-shot
1162
- * contract: it fires on the first render with focusable content and then never
1163
- * again for the life of the instance. No-op in classic mode. Default: off.
1164
- */
1165
- expandFirstRequired?: boolean;
1166
-
1167
- /**
1168
- * Names of read-first rows to open on mount, in addition to whatever the
1169
- * user opens afterwards.
1170
- *
1171
- * Which rows are expanded is otherwise private to the engine, which is
1172
- * fine while expansion is only ever a click. It stops being fine when the
1173
- * expanded row is part of an address: a consumer whose field renders its
1174
- * own routable surface (a table whose rows open panes with their own URLs)
1175
- * can restore the pane from the URL, but not the row that has to be open
1176
- * for the pane to exist at all — so a pasted or reloaded link lands on a
1177
- * collapsed form.
1178
- *
1179
- * Applied once, on the first render that actually CONTAINS one of the named
1180
- * rows — not merely the first render with any rows — so a schema arriving in
1181
- * pieces still gets its address applied rather than spending the one shot on
1182
- * a form the field had not reached yet.
1183
- *
1184
- * It never re-expands a row the user has since collapsed, and it does not
1185
- * participate in `expandMode: 'single'` accordion collapsing — the caller is
1186
- * naming a starting point, not driving the state. That holds against the
1187
- * engine's own openers too: `expandFirstRequired` / `autoFocusFirstRequired`
1188
- * open their target ALONGSIDE these rows in single mode instead of replacing
1189
- * them. Only a remount re-arms it. No-op in classic (non-compact) mode.
1190
- */
1191
- initialExpandedOptions?: string[];
1192
-
1193
- /**
1194
- * Cap how many rows a status box shows before the rest fold behind a
1195
- * "N more fields" control. Applies to every box — Needs attention, Set and
1196
- * Optional alike — so a long form stays scannable instead of making the
1197
- * reader page past one box to reach the next.
1198
- *
1199
- * Optional keeps its own rule on top of this: a box that would have
1200
- * collapsed but holds preselected rows shows those rows rather than an
1201
- * arbitrary first-N. The cap then applies to that set, so
1202
- * `maxFieldsShown: 5` over 10 preselected fields shows 5 — and opening the
1203
- * control reveals EVERY row in the box, not just the other 5 preselected
1204
- * ones, because once the reader has asked to see more, hiding the rest is
1205
- * the same defect one level down.
1206
- *
1207
- * Unset (the default) leaves every box uncapped.
1208
- */
1209
- maxFieldsShown?: number;
1210
- }
1211
-
1212
- // Option types rendered full-width (IDE Options parity, commit 8e6b7781).
1213
- const STRECHABLE_TYPES = new Set<TQorusType>(['tool-catalog' as TQorusType]);
1214
-
1215
- const FormEngineImpl = ({
1216
- name,
1217
- uniqueName,
1218
- value,
1219
- onChange,
1220
- onSingleOptionsChange,
1221
- onDependableOptionChange,
1222
- placeholder,
1223
- noValueString, // eslint-disable-line @typescript-eslint/no-unused-vars
1224
- isValid, // eslint-disable-line @typescript-eslint/no-unused-vars
1225
- url,
1226
- customUrl,
1227
- operatorsUrl,
1228
- onOptionsLoaded,
1229
- recordRequiresSearchOptions,
1230
- readOnly,
1231
- onReadOnlyActivate,
1232
- allowTemplates = true,
1233
- interfaceContext,
1234
- templateFieldProps,
1235
- forceDropdown,
1236
- showTypeToggle = true,
1237
- compact,
1238
- // Destructured only to keep it out of `rest` (and so off the DOM). The gutter
1239
- // this prop documents was never implemented: it was passed to the wrap as
1240
- // `$flush`, whose styled template had no interpolation slot, so it has always
1241
- // been a no-op. Removing it from the API is a separate, breaking change —
1242
- // every consumer passing it today already gets the flush layout by default.
1243
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1244
- compactFlush = false,
1245
- compactNested = false,
1246
- compactScroll = 'host',
1247
- compactPanelProps,
1248
- compactCollapsedGroups: compactCollapsedGroupsProp,
1249
- compactBoxLabels,
1250
- compactStickyBoxHeaders = true,
1251
- compactStickyOffset = 0,
1252
- compactToolbar = true,
1253
- commitMode = 'immediate',
1254
- expandMode = 'single',
1255
- onCommit,
1256
- operators: operatorsProp,
1257
- groups,
1258
- optionsLoader,
1259
- onValidityChange,
1260
- optionActions,
1261
- optionActionsCollapse = 'auto',
1262
- componentOverrides,
1263
- codePreviewRenderer,
1264
- // consumed by the wrapper below, which publishes it to every description this
1265
- // form draws; destructured here only so it cannot reach `rest` and be spread
1266
- // onto a DOM node
1267
- markdownRenderer: _markdownRenderer, // eslint-disable-line @typescript-eslint/no-unused-vars
1268
- rendererOnlyUiTypes,
1269
- templateAwareUiTypes,
1270
- anyLikeUiTypes,
1271
- inheritedFromParent,
1272
- autoFocusFirstRequired,
1273
- expandFirstRequired,
1274
- initialExpandedOptions,
1275
- maxFieldsShown,
1276
- ...rest
1277
- }: IFormEngineProps) => {
1278
- // Built-ins + whatever the consumer declared for its own injected editors.
1279
- const isRendererOnly = useMemo(
1280
- () => createRendererOnlyUiTypeCheck(rendererOnlyUiTypes),
1281
- [JSON.stringify(rendererOnlyUiTypes)]
1282
- );
1283
- // Pointer CAPABILITY and viewport WIDTH are different questions and both
1284
- // matter here: a hover-gated action is unreachable without a hover, and a
1285
- // phone-width row has no space for a button strip either way. Both now come
1286
- // from Reqore's context (one subscription for the whole app) rather than a
1287
- // reqraft-local matchMedia hook.
1288
- //
1289
- // Both are always set by `ReqoreProvider`, and the peer floor (>= 0.74.0) is
1290
- // the version that sets them. A defensive `?? true` here would be dead code:
1291
- // `useReqoreProperty` THROWS on a context that lacks the key, so an older
1292
- // Reqore fails at the hook rather than degrading to a default.
1293
- const isHoverCapable = useReqoreProperty('isHoverCapable');
1294
- const isMobile = useReqoreProperty('isMobile');
1295
- const collapseOptionActions =
1296
- optionActionsCollapse === 'always' ? true
1297
- : optionActionsCollapse === 'never' ? false
1298
- : !isHoverCapable || !!isMobile;
1299
- /* The schema, as the form should ASK it — not always as it was served.
1300
- `resolveDegenerateRequiredGroups` turns a one-of group that has a single
1301
- member in this schema back into a plain required field, because a choice
1302
- with one option is not a choice. Applied here, at the one place the schema
1303
- enters the component, so the header, the chips, the messages, the meter and
1304
- the read-first summary cannot disagree about it: every one of them reads
1305
- `options`, and none of them has to know the rule. */
1306
- const [servedOptions, setOptions] = useState<IQorusFormSchema | undefined>(
1307
- rest?.options || undefined
1308
- );
1309
- const options = useMemo(() => resolveDegenerateRequiredGroups(servedOptions), [servedOptions]);
1310
- // optionsLoader lifecycle: loading feeds the skeleton gate, error the banner.
1311
- const [optionsLoading, setOptionsLoading] = useState<boolean>(!!optionsLoader && !rest?.options);
1312
- const [optionsError, setOptionsError] = useState<string | undefined>();
1313
- // Operators: prop-provided (compact) or fetched via operatorsUrl (dpql,
1314
- // ported from IDE Options) — the fetch overrides the seeded prop value.
1315
- const [operators, setOperators] = useState<IOperatorsSchema | undefined>(operatorsProp);
1316
- // Remote-fetch loading (ported from IDE Options); only relevant when one of
1317
- // the fetch urls is set — schema-as-props consumers never see the skeleton.
1318
- const [loading, setLoading] = useState<boolean>(!!(url || customUrl || operatorsUrl));
1319
- const confirmAction = useReqoreProperty('confirmAction');
1320
- const theme = useReqoreTheme();
1321
- const [focusedEditing, setFocusedEditing] = useState<string>();
1322
- const [showFieldTypes, setShowFieldTypes] = useState<boolean>(false);
1323
- // Global toggle (toolbar ⓘ): reveal the short-description info panel on every
1324
- // field that has a short_desc. Per-row ⓘ overrides still win over it.
1325
- // Global field-info visibility, tri-state: `undefined` = default (critical
1326
- // messages auto-open, the rest closed); `true` = show all; `false` = hide all
1327
- // (even message fields). The toolbar ⓘ drives it.
1328
- const [showAllDescriptions, setShowAllDescriptions] = useState<boolean | undefined>(undefined);
1329
- const [showHelpForOption, setShowHelpForOption] = useState<string | undefined>();
1330
- const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
1331
- // Which options are expanded into their editor (several can be open at once).
1332
- const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
1333
- /** Status boxes whose "N more fields" control the reader has opened. Keyed by
1334
- box so opening Optional does not also unfold Set. */
1335
- const [expandedMoreBoxes, setExpandedMoreBoxes] = useState<TFormEngineBoxKey[]>([]);
1336
-
1337
- // --- Reveal a status box's content when it is opened ----------------------
1338
- //
1339
- // The status boxes stack, so the last one ("Optional", which holds every
1340
- // not-yet-added field) sits at the bottom of the form — frequently at the
1341
- // bottom of the scroll container too. Opening it mounts its rows BELOW the
1342
- // fold: the box's own header is all that stays on screen, the click appears to
1343
- // have done nothing, and the fields it just revealed have to be hunted for by
1344
- // scrolling.
1345
- //
1346
- // The box that was just opened, read by the layout effect below. Held as state
1347
- // rather than acted on inside the collapse handler because the handler runs
1348
- // BEFORE React re-renders — at that moment the rows do not exist yet, so the
1349
- // box has not grown and there is nothing to scroll to. A layout effect runs
1350
- // after the DOM is updated and before paint, which makes the reveal
1351
- // deterministic instead of a guessed delay.
1352
- const [openedStatusBox, setOpenedStatusBox] = useState<string | undefined>(undefined);
1353
- const statusBoxRefs = useRef<Record<string, HTMLDivElement | null>>({});
1354
- useLayoutEffect(() => {
1355
- if (!openedStatusBox) {
1356
- return;
1357
- }
1358
- const element = statusBoxRefs.current[openedStatusBox];
1359
- // Consume it either way: a box that has gone (a search narrowed it out of
1360
- // existence) must not leave a pending reveal armed for a later render.
1361
- setOpenedStatusBox(undefined);
1362
- // `scrollIntoView` is not implemented in jsdom, and a missing reveal must
1363
- // never break a form.
1364
- if (!element?.scrollIntoView) {
1365
- return;
1366
- }
1367
- // 'nearest' scrolls the MINIMUM needed: nothing at all when the box is
1368
- // already fully visible, and a top-alignment when it is taller than the
1369
- // viewport (header plus the first rows — the right answer for a long
1370
- // Optional list). Anything else moves the page when it did not need to.
1371
- element.scrollIntoView({
1372
- block: 'nearest',
1373
- behavior:
1374
- window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth',
1375
- });
1376
- }, [openedStatusBox]);
1377
- // Remembers each row's last settled status box, so an actively-edited field
1378
- // stays put when its status flips (e.g. becomes valid) instead of jumping to
1379
- // another box mid-edit and stealing focus. Keyed by option name.
1380
- const settledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
1381
- // Measured form width (not viewport — the form lives in drawers/panels of
1382
- // arbitrary width) drives the stacked narrow layout.
1383
- const [compactWrapRef, { width: compactWrapWidth }] = useMeasure<HTMLDivElement>();
1384
- // Own handle on the scroll wrap (useMeasure's ref is a callback, no `.current`)
1385
- // so the label-column measurement can publish its CSS var on the element. It's
1386
- // STATE, not a ref: the wrap mounts only after the loading-skeleton gate
1387
- // resolves, so the measurement effect must re-run when the node appears — a
1388
- // ref wouldn't retrigger it.
1389
- const [compactWrapNode, setCompactWrapNode] = useState<HTMLDivElement | null>(null);
1390
- // Ref mirror of the wrap node for callbacks that must not re-create when it
1391
- // mounts (flashOptions reads it inside a rAF).
1392
- const compactWrapNodeRef = useRef<HTMLDivElement | null>(null);
1393
- const setCompactWrap = useCallback(
1394
- (node: HTMLDivElement | null) => {
1395
- compactWrapRef(node);
1396
- compactWrapNodeRef.current = node;
1397
- setCompactWrapNode((prev) => (prev === node ? prev : node));
1398
- },
1399
- [compactWrapRef]
1400
- );
1401
- /* Declared here, not where it is first USED in the render: the sticky
1402
- measurement below takes it as a dependency, and a dependency array is
1403
- evaluated during render — a `const` declared further down would still be
1404
- in its temporal dead zone. */
1405
- const compactNarrow = !!compactWrapWidth && compactWrapWidth < 480;
1406
-
1407
- /* How much pinned chrome sits above a status-box header.
1408
-
1409
- Sticky offsets do not compose on their own: everything pinned in one
1410
- scroller resolves `top` against the same scrollport, so a box header and
1411
- this form's own toolbar both asking for 0 land on top of each other — and
1412
- the toolbar is the loser, because it renders first and the box header wins
1413
- on z-order. Covering it hides the search, which is the one control that
1414
- forces a collapsed box open.
1415
-
1416
- So the box headers pin BELOW the toolbar, at the host's own offset plus the
1417
- toolbar's height. Measured, not assumed: the toolbar is a completion meter,
1418
- a search row and a field picker that each hide themselves when they have
1419
- nothing to say, and what is left wraps differently at every form width.
1420
- Zero when this form draws no toolbar at all. */
1421
- const [compactToolbarHeight, setCompactToolbarHeight] = useState(0);
1422
- useLayoutEffect(() => {
1423
- const wrap = compactWrapNode;
1424
- if (!compact || !wrap || compactNested || !compactStickyBoxHeaders) {
1425
- setCompactToolbarHeight(0);
1426
- return undefined;
1427
- }
1428
- const header = wrap.querySelector<HTMLElement>(
1429
- ':scope > .reqore-panel > .reqore-panel-title'
1430
- );
1431
- if (!header) {
1432
- setCompactToolbarHeight(0);
1433
- return undefined;
1434
- }
1435
- const read = () =>
1436
- setCompactToolbarHeight((prev) => {
1437
- /* Only a toolbar that is ITSELF pinned takes room off the top. A host
1438
- can switch the pin off (`compactPanelProps.stickyHeader`), and it
1439
- does where the form is a couple of fields inside a longer page — and
1440
- a box header offset by a toolbar that scrolls away would float a
1441
- header's height below the top edge with nothing under it. Read from
1442
- the element rather than from the props that produced it, so every
1443
- route to that decision is accounted for. */
1444
- const pinned = getComputedStyle(header).position === 'sticky';
1445
- const next = pinned ? Math.round(header.getBoundingClientRect().height) : 0;
1446
- return prev === next ? prev : next;
1447
- });
1448
- read();
1449
- /* jsdom and older engines have no ResizeObserver; the one-shot read above
1450
- still gives the right answer for a form whose toolbar never reflows. */
1451
- if (typeof ResizeObserver === 'undefined') {
1452
- return undefined;
1453
- }
1454
- const observer = new ResizeObserver(read);
1455
- observer.observe(header);
1456
- return () => observer.disconnect();
1457
- /* `compactNarrow` is a dependency, not decoration: crossing the width
1458
- breakpoint takes the pin OFF the toolbar without changing its height, so
1459
- the ResizeObserver never fires and a stale 124px would go on offsetting
1460
- the box headers below a toolbar that is no longer there. */
1461
- }, [compact, compactNested, compactStickyBoxHeaders, compactNarrow, compactWrapNode, options]);
1462
-
1463
- // Global label-column sizing: size the label column to the WIDEST field label
1464
- // across the whole form, clamped to [MIN, MAX], and publish it as a CSS var on
1465
- // the scroll wrap. Both the grid column and the value-surface offsets read the
1466
- // var, so the surface stays glued to the (now variable) column edge. Measured
1467
- // off-DOM from the option labels — stable regardless of filtering/scroll, so it
1468
- // never reflows as rows come and go.
1469
- useLayoutEffect(() => {
1470
- const wrap = compactWrapNode;
1471
- if (!compact || !wrap) {
1472
- return;
1473
- }
1474
- const family = getComputedStyle(wrap).fontFamily || 'sans-serif';
1475
- const measurer = document.createElement('span');
1476
- // Match StyledRowLabel's typography (font-weight 600, 13px) so the measured
1477
- // width matches what the row actually renders.
1478
- measurer.style.cssText = `position:absolute;left:-9999px;top:-9999px;visibility:hidden;white-space:nowrap;font:600 13px ${family};`;
1479
- document.body.appendChild(measurer);
1480
- let widest = 0;
1481
- forEach(options || {}, (schema, name) => {
1482
- measurer.textContent = (schema?.display_name as string) || name;
1483
- widest = Math.max(widest, measurer.offsetWidth);
1484
- });
1485
- document.body.removeChild(measurer);
1486
- // The affordances are added AFTER the clamp, not inside it.
1487
- //
1488
- // They used to be inside — `min(MAX, widest + 28)` — which quietly spends the
1489
- // allowance the moment the labels are long enough to reach the ceiling. Past
1490
- // that point the column is exactly MAX and the text is free to use all of it,
1491
- // so the trailing `?` has nowhere to sit on the last line and wraps onto a
1492
- // line of its own. That is what an auth profile's authorization block showed:
1493
- // "Require All Of These Permissions" filling the column with a lone `?`
1494
- // beneath it.
1495
- //
1496
- // Clamping the TEXT and then adding the affordance means the reserved space
1497
- // survives at every label length. MAX still bounds how much room the NAME may
1498
- // take, which is what it is for; the asterisk and the `?` are chrome that has
1499
- // to fit beside it either way.
1500
- const text = Math.max(LABEL_COL_MIN, Math.min(LABEL_COL_MAX, Math.round(widest)));
1501
- wrap.style.setProperty(LABEL_COL_VAR, `${text + LABEL_AFFORDANCE_WIDTH}px`);
1502
- }, [compact, options, theme, compactWrapNode]);
1503
- // Editing rows pin min-height to the measured read row they replace, so the
1504
- // toggle never shifts neighbours (height varies with chrome — measure it).
1505
- const readRowHeights = useRef<Record<string, number>>({});
1506
- // Required-groups linkage: hovering a group chip highlights every member row;
1507
- // clicking a sibling in the chip's popover scrolls to it and flashes it.
1508
- const [highlightedOptions, setHighlightedOptions] = useState<string[]>([]);
1509
- const [flashedOptions, setFlashedOptions] = useState<string[]>([]);
1510
- const flashTimeout = useRef<ReturnType<typeof setTimeout>>();
1511
- const flashOptions = useCallback((optionNames: string[], scrollToFirst = false) => {
1512
- if (scrollToFirst && optionNames[0]) {
1513
- // Defer to the next frame: when this fires for a field that just changed
1514
- // panels, its row has only just re-mounted in the new box — scrolling in the
1515
- // same tick targets the stale (pre-move) layout, so the page doesn't budge.
1516
- // A rAF lets the new position settle first.
1517
- requestAnimationFrame(() => {
1518
- // Scoped to THIS engine's wrap: several compact engines can be mounted at
1519
- // once with colliding field names (e.g. one form per state in the qog
1520
- // template drawer, each with `guild`/`channel`), and a document-wide
1521
- // lookup grabs the first match in the DOM — scrolling the container to a
1522
- // DIFFERENT form's row. No document fallback for the same reason.
1523
- const target = compactWrapNodeRef.current?.querySelector<HTMLElement>(
1524
- `.readfirst-row[data-field="${optionNames[0]}"]`
1525
- );
1526
- // Only when it is not already in view, and never under a pinned box
1527
- // header: `block: 'center'` scrolled a row that was in plain sight and
1528
- // centred it against the whole scrollport, so the header pinned at the
1529
- // top was drawn over it (see `revealRow`).
1530
- if (target) {
1531
- revealRow(
1532
- target,
1533
- window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth'
1534
- );
1535
- }
1536
- });
1537
- }
1538
- setFlashedOptions(optionNames);
1539
- clearTimeout(flashTimeout.current);
1540
- flashTimeout.current = setTimeout(() => setFlashedOptions([]), 1400);
1541
- }, []);
1542
- const flashOption = useCallback(
1543
- (optionName: string) => flashOptions([optionName], true),
1544
- [flashOptions]
1545
- );
1546
- useEffect(() => () => clearTimeout(flashTimeout.current), []);
1547
-
1548
- // Follow a field across panels: when its status bucket changes — e.g. you fill
1549
- // an optional field and it jumps to Set / Needs attention — scroll to its new
1550
- // row and flash it so it's easy to keep track of. `settledBucket` holds each
1551
- // field's current panel (frozen while the field is being edited, it re-buckets
1552
- // on collapse), so diffing it after every render catches the move the instant it
1553
- // lands in the new panel. Runs every render; the diff is cheap and only fires a
1554
- // scroll on an ACTUAL move of a non-expanded field.
1555
- const prevSettledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
1556
- useEffect(() => {
1557
- if (!compact) return;
1558
- const cur = settledBucket.current;
1559
- const prev = prevSettledBucket.current;
1560
- const moved = Object.keys(cur).find(
1561
- (name) => prev[name] && prev[name] !== cur[name] && !expandedOptions.includes(name)
1562
- );
1563
- prevSettledBucket.current = { ...cur };
1564
- if (moved) {
1565
- flashOptions([moved], true);
1566
- }
1567
- });
1568
-
1569
- // Info panels auto-open on Tier-1 content; the per-row user override sticks.
1570
- const [infoPanelOverrides, setInfoPanelOverrides] = useState<Record<string, boolean>>({});
1571
- // Toolbar filters affect the listed rows only — the meter reflects the full set.
1572
- const [requiredOnly, setRequiredOnly] = useState<boolean>(false);
1573
- const [compactQuery, setCompactQuery] = useState<string>('');
1574
- // Compact field sort (Fields menu → "Sort by"); 'schema' = declared order.
1575
- const [compactSort, setCompactSort] = useState<TCompactSort>('schema');
1576
- const [localValue, setLocalValue] = useState<{
1577
- fields: TQorusForm | TQorusFlatForm;
1578
- meta?: IOptionsOnChangeMeta;
1579
- }>(() => ({
1580
- fields: fixOptions(value, options || {}, undefined, isRendererOnly),
1581
- meta: undefined,
1582
- }));
1583
- const originalValue = useRef<any>();
1584
- // Track the last value we emitted via onChange so we can skip re-applying fixOptions
1585
- // when the parent echoes it back as the new value prop (controlled component loop prevention)
1586
- const lastEmittedValue = useRef<TQorusForm | TQorusFlatForm | undefined>(value);
1587
- /**
1588
- * A `localValue` that is only a MIRROR of the incoming `value`, and so must
1589
- * never be emitted back as though this form had answered something.
1590
- *
1591
- * `localValue` is derived state: the sync-down effect below rebuilds it from
1592
- * the `value` prop. When that rebuild adds nothing (`fixOptions` had no
1593
- * default to materialise), the result says exactly what the parent already
1594
- * holds, and emitting it is not a change — it is this form repeating the
1595
- * parent's own words back at it.
1596
- *
1597
- * That repetition is harmless in a form that owns its state outright and
1598
- * fatal in one that shares a parent with a NESTED engine. The two write to
1599
- * the same parent value, so each sees the other's write arrive as an external
1600
- * change; `lastEmittedValue` is then always one step behind and its guard
1601
- * never fires. The engines alternate between two states forever, several
1602
- * times a second, and the newer write is repeatedly overwritten by the older
1603
- * one. Measured in the test editor: the row's summary flipped between two
1604
- * readings indefinitely and `setTimeout` was starved past a 45-second
1605
- * deadline.
1606
- *
1607
- * Only a mirror that CHANGES nothing is held back. A rebuild that restores a
1608
- * required or preselected field's default still emits, because that default
1609
- * is genuinely new information the parent has to be told about — which is the
1610
- * whole reason the sync-down runs `fixOptions` at all.
1611
- */
1612
- const mirroredValue = useRef<TQorusForm | TQorusFlatForm | undefined>(undefined);
1613
- /**
1614
- * Every emit the parent has yet to feed back, not only the last one. A parent that takes the value back
1615
- * late (a store, a debounced owner, a slow render) echoes an older emit after this form has emitted a newer
1616
- * one. Compared with `lastEmittedValue` alone, that echo read as the parent's own change, and the sync-down
1617
- * reset the form to it: the field under the cursor went back a step and the keys typed since were lost
1618
- * ("I have value" came out as "I he value" in a story under load).
1619
- */
1620
- const inFlight = useEmittedValues<TQorusForm | TQorusFlatForm | undefined>((a, b) =>
1621
- isEqual(normalizeEmptyFieldValues(a), normalizeEmptyFieldValues(b))
1622
- );
1623
-
1624
- if (originalValue.current === undefined && size(value)) {
1625
- originalValue.current = localValue.fields;
1626
- }
1627
-
1628
- const unavailableOptionsCount = useRef(0);
1629
- const { compactValue, loading: typesLoading } = useQorusTypes();
1630
- const templates = useTemplates(allowTemplates, rest.stringTemplates, interfaceContext);
1631
-
1632
- useEffect(() => {
1633
- if (
1634
- !shouldEmitLocalValue({
1635
- localValue: localValue.fields,
1636
- value,
1637
- mirroredValue: mirroredValue.current,
1638
- })
1639
- ) {
1640
- return;
1641
- }
1642
-
1643
- const toEmit = size(localValue.fields) ? (localValue.fields as TQorusForm) : undefined;
1644
- lastEmittedValue.current = toEmit;
1645
- inFlight.record(toEmit);
1646
- const meta = size(localValue.meta) ? localValue.meta : undefined;
1647
- // Batched mode still emits every staged change (consumers may want to
1648
- // live-validate), but flags it as a draft — persistence waits for Save.
1649
- onChange?.(name, toEmit, commitMode === 'batched' ? { ...(meta || {}), draft: true } : meta);
1650
- }, [JSON.stringify(localValue)]);
1651
-
1652
- useUpdateEffect(() => {
1653
- // When a loader owns the schema, ignore controlled `options` syncs so a
1654
- // late/undefined `options` prop can't clobber the loaded schema.
1655
- if (optionsLoader) {
1656
- return;
1657
- }
1658
- setOptions(rest.options);
1659
- }, [JSON.stringify(rest.options)]);
1660
-
1661
- // Fetch the schema on mount and on loader identity change.
1662
- useEffect(() => {
1663
- if (!optionsLoader) {
1664
- return undefined;
1665
- }
1666
-
1667
- let cancelled = false;
1668
- setOptionsLoading(true);
1669
- setOptionsError(undefined);
1670
-
1671
- // `Promise.resolve().then(...)` so a synchronous throw in the loader is
1672
- // funnelled into the same rejection path as an async failure.
1673
- Promise.resolve()
1674
- .then(() => optionsLoader())
1675
- .then((loaded) => {
1676
- if (cancelled) {
1677
- return;
1678
- }
1679
- setOptions(loaded || {});
1680
- onOptionsLoaded?.(loaded || {});
1681
- setOptionsLoading(false);
1682
- })
1683
- .catch((error) => {
1684
- if (cancelled) {
1685
- return;
1686
- }
1687
- /* A loader built on `query` rejects with a cancel when its request is
1688
- abandoned (the query cache was cleared mid-flight). The schema was not
1689
- refused, so it is not an error to show: the form just stops waiting. */
1690
- if (isQueryCancelled(error)) {
1691
- setOptionsLoading(false);
1692
- return;
1693
- }
1694
- setOptionsError(
1695
- error instanceof Error ? error.message : String(error ?? 'Failed to load options')
1696
- );
1697
- setOptionsLoading(false);
1698
- });
1699
-
1700
- return () => {
1701
- cancelled = true;
1702
- };
1703
- }, [optionsLoader]);
1704
-
1705
- // --- Remote schema fetching, ported from IDE Options (systemOptions.tsx
1706
- // lines 394-462 + 504-522). reqraft's `query()` replaces the IDE's
1707
- // `fetchData` — same `{ data, ok, error }` shape and `api/latest/` prefix;
1708
- // the IDE's leading-slash URLs are normalised away.
1709
- const getFetchUrl = useCallback(() => customUrl || `options/${url}`, [customUrl, url]);
1710
-
1711
- useMount(() => {
1712
- if (url || customUrl) {
1713
- (async () => {
1714
- setOptions(undefined);
1715
- setLoading(true);
1716
- const data = await queryUnlessCancelled<IQorusFormSchema>({ url: getFetchUrl() });
1717
-
1718
- // abandoned: nobody is waiting for this schema any more
1719
- if (!data) {
1720
- return;
1721
- }
1722
-
1723
- if (!data.ok || data.data === null) {
1724
- setLoading(false);
1725
- setOptions({});
1726
- return;
1727
- }
1728
- setLocalValue({
1729
- fields: fixOptions(value, data.data, undefined, isRendererOnly),
1730
- meta: undefined,
1731
- });
1732
- if (!operatorsUrl) {
1733
- setLoading(false);
1734
- }
1735
- setOptions(data.data);
1736
- onOptionsLoaded?.(data.data);
1737
- })();
1738
- }
1739
-
1740
- if (operatorsUrl) {
1741
- (async () => {
1742
- setOperators(undefined);
1743
- setLoading(true);
1744
- const data = await queryUnlessCancelled<IOperatorsSchema>({
1745
- url: operatorsUrl.replace(/^\//, ''),
1746
- });
1747
-
1748
- if (!data) {
1749
- return;
1750
- }
1751
-
1752
- if (!data.ok) {
1753
- setLoading(false);
1754
- setOperators({});
1755
- return;
1756
- }
1757
- setOperators(data.data);
1758
- setLoading(false);
1759
- })();
1760
- }
1761
- });
1762
-
1763
- // Changing the source clears the current value and re-seeds the form from
1764
- // the freshly fetched schema (IDE semantics).
1765
- useUpdateEffect(() => {
1766
- if (url || customUrl) {
1767
- (async () => {
1768
- setOptions(undefined);
1769
- setLoading(true);
1770
- const data = await queryUnlessCancelled<IQorusFormSchema>({ url: getFetchUrl() });
1771
-
1772
- // abandoned: nobody is waiting for this schema any more
1773
- if (!data) {
1774
- return;
1775
- }
1776
-
1777
- if (!data.ok) {
1778
- setLoading(false);
1779
- setOptions({});
1780
- return;
1781
- }
1782
- if (!operatorsUrl) {
1783
- setLoading(false);
1784
- }
1785
- setOptions(data.data);
1786
- onOptionsLoaded?.(data.data);
1787
- setLocalValue({
1788
- fields: fixOptions({}, data.data, undefined, isRendererOnly),
1789
- meta: undefined,
1790
- });
1791
- })();
1792
- }
1793
- }, [url, customUrl]);
1794
-
1795
- useUpdateEffect(() => {
1796
- if (operatorsUrl) {
1797
- (async () => {
1798
- setOperators(undefined);
1799
- setLoading(true);
1800
- const data = await queryUnlessCancelled<IOperatorsSchema>({
1801
- url: operatorsUrl.replace(/^\//, ''),
1802
- });
1803
-
1804
- if (!data) {
1805
- return;
1806
- }
1807
-
1808
- if (!data.ok) {
1809
- setLoading(false);
1810
- setOperators({});
1811
- return;
1812
- }
1813
- setLoading(false);
1814
- setOperators(data.data);
1815
- })();
1816
- }
1817
- }, [operatorsUrl]);
1818
-
1819
- useUpdateEffect(() => {
1820
- const fixedValue = fixOptions(value, options || {}, undefined, isRendererOnly);
1821
-
1822
- // When the value we're receiving is the one we just emitted AND fixOptions has nothing to
1823
- // meaningfully add or change, skip the update. This breaks the controlled-component loop for
1824
- // arg_schema fields while still allowing required/preselected options to be restored (in that
1825
- // case fixedValue will differ from value, so we don't skip).
1826
- // The compare is empty-normalized: fixOptions can round-trip a required-empty field between
1827
- // `{ value: '' }` and no `value` key — without normalizing, that cosmetic difference reads as a
1828
- // change and re-fixes forever (e.g. byte-size / expression / ruled string fields).
1829
- // Note: compare fixedValue against value, not localValue.fields — localValue may have been
1830
- // updated by nested FormEngine emissions, so comparing against it would never skip.
1831
- const normalizedValue = normalizeEmptyFieldValues(value);
1832
- /* What this rebuild lets us agree with the parent on — `undefined` when it
1833
- contributed something the parent has yet to learn. */
1834
- const agreed = agreementFromRebuild(fixedValue, value);
1835
- // the echo of an emit still in flight - the last or an older one - is not the parent's change
1836
- const echo = inFlight.isEcho(value);
1837
- if (
1838
- (echo || isEqual(normalizedValue, normalizeEmptyFieldValues(lastEmittedValue.current))) &&
1839
- agreed !== undefined
1840
- ) {
1841
- return;
1842
- }
1843
-
1844
- if (!originalValue.current && size(fixedValue)) {
1845
- originalValue.current = fixedValue;
1846
- }
1847
-
1848
- /* Remember a mirror so the emit effect can tell one from an answer, and
1849
- record the agreement it represents. A rebuild that DID add something
1850
- leaves both alone — the parent has to learn about it. */
1851
- mirroredValue.current = agreed;
1852
- if (agreed !== undefined) {
1853
- lastEmittedValue.current = agreed;
1854
- }
1855
-
1856
- setLocalValue?.({ fields: fixedValue, meta: undefined });
1857
- }, [JSON.stringify(options), JSON.stringify(value), isRendererOnly]);
1858
-
1859
- const handleValueChange = useCallback(
1860
- (optionName: string, val?: any, _type?: string, isFunction?: boolean) => {
1861
- setLocalValue(({ fields = {} }) => {
1862
- const schemaType = getOptionSchemaStorageType(options?.[optionName], isRendererOnly);
1863
- const isAnyLike = isUntypedOptionType(schemaType);
1864
- // For any/auto schema types, preserve the user's chosen type stored in the field
1865
- const resolvedSchemaType =
1866
- isAnyLike && (fields[optionName] as IQorusFormField)?.type ?
1867
- ((fields[optionName] as IQorusFormField).type as TQorusType)
1868
- : (
1869
- (fields[optionName] as IQorusFormField)?.type === options?.[optionName]?.ui_type &&
1870
- isRendererOnly((fields[optionName] as IQorusFormField)?.type)
1871
- ) ?
1872
- schemaType
1873
- : schemaType || ((fields[optionName] as IQorusFormField)?.type as TQorusType);
1874
- // A field reports back the type it was drawn as, which for a renderer-only editor (markdown, cron,
1875
- // dpql, ...) is the editor's name rather than the type the value is stored as. Storing that name
1876
- // sent a description as `{type: "markdown"}`, which the server decodes by type and so parsed as
1877
- // YAML. A type picked for an untyped field is never renderer-only, so it still wins.
1878
- const emittedType = _type && !isRendererOnly(_type as TQorusType) ? _type : undefined;
1879
- const type =
1880
- emittedType ||
1881
- getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
1882
-
1883
- if (!(fields as TQorusForm)[optionName]) {
1884
- const defaultOperators: TOperatorValue = reduce(
1885
- operators || {},
1886
- (filteredOperators: TOperatorValue, operator, operatorKey) => {
1887
- if (operator.selected) {
1888
- return [...(filteredOperators as string[]), operatorKey];
1889
- }
1890
- return filteredOperators;
1891
- },
1892
- []
1893
- );
1894
- if ((defaultOperators as string[])?.length) {
1895
- return {
1896
- fields: {
1897
- ...fields,
1898
- [optionName]: {
1899
- type,
1900
- value: val,
1901
- op: defaultOperators,
1902
- },
1903
- },
1904
- };
1905
- }
1906
- }
1907
-
1908
- // IDE Options model: the 4th `onChange` argument (`isFunction`, sent
1909
- // by TemplateField/auto) drives the option's `is_expression` flag;
1910
- // the value itself is the raw AST `{ exp, args }`.
1911
- const updatedValue: TQorusForm = {
1912
- ...(fields as TQorusForm),
1913
- [optionName]: {
1914
- ...(fields as TQorusForm)[optionName],
1915
- type: type as TQorusType,
1916
- value: val,
1917
- },
1918
- };
1919
-
1920
- /* The flag belongs to the VALUE, so a change that said nothing about it
1921
- does not throw it away — `undefined` is no opinion, not a denial.
1922
- Deleting it unconditionally left the AST on the row with nothing
1923
- marking it as one: an assertion's Expected Value came back from a
1924
- saved draft drawn as a two-field hash of `exp` and `args`, because
1925
- merely opening the row and reverting had stripped the flag. */
1926
- if (
1927
- shouldMarkAsExpression(
1928
- isFunction,
1929
- val,
1930
- ((fields as TQorusForm)[optionName] as { is_expression?: boolean })?.is_expression
1931
- )
1932
- ) {
1933
- (updatedValue[optionName] as { is_expression?: boolean }).is_expression = true;
1934
- } else {
1935
- delete updatedValue[optionName].is_expression;
1936
- }
1937
-
1938
- const meta: IOptionsOnChangeMeta = {};
1939
-
1940
- if (
1941
- options?.[optionName]?.has_dependents &&
1942
- val !== undefined &&
1943
- val !== (fields as TQorusForm)[optionName]?.value
1944
- ) {
1945
- forEach(options, (option, depName) => {
1946
- /* A dependency is `name`, `name=value` or `name!=value`, so the
1947
- entry has to be PARSED to find which option it names. Compared as
1948
- a whole string, only the bare spelling ever matched — so a field
1949
- declared `depends_on: ['subject_iface_kind=workflow']` was never
1950
- cleared when that option changed.
1951
-
1952
- What that cost: a test whose subject was switched from a workflow
1953
- to a service kept the workflow's `Subject Interface Version`.
1954
- Services and jobs are versioned but only the latest is testable,
1955
- so the field no longer applied, yet it stayed on the form as a
1956
- locked control in front of a value the author could not clear —
1957
- the value being there is exactly what stops the field being
1958
- withheld outright. */
1959
- if (
1960
- option.depends_on &&
1961
- flatten(option.depends_on).some(
1962
- (dependency) => parseDependency(dependency as string).name === optionName
1963
- ) &&
1964
- updatedValue[depName]
1965
- ) {
1966
- updatedValue[depName].value = undefined;
1967
- }
1968
- });
1969
-
1970
- onDependableOptionChange?.(optionName, val, updatedValue, options);
1971
- }
1972
-
1973
- if (size(options?.[optionName]?.on_change)) {
1974
- meta.events = options?.[optionName]?.on_change;
1975
- }
1976
-
1977
- onSingleOptionsChange?.(optionName, updatedValue[optionName]);
1978
-
1979
- if (
1980
- compact &&
1981
- !readOnly &&
1982
- shouldAutoCollapseCompactOption(
1983
- options?.[optionName],
1984
- val,
1985
- !!(updatedValue[optionName] as { is_expression?: boolean } | undefined)?.is_expression
1986
- )
1987
- ) {
1988
- setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
1989
- }
1990
-
1991
- return {
1992
- fields: updatedValue,
1993
- meta,
1994
- };
1995
- });
1996
- },
1997
- [
1998
- onSingleOptionsChange,
1999
- onDependableOptionChange,
2000
- isRendererOnly,
2001
- compact,
2002
- readOnly,
2003
- JSON.stringify(options),
2004
- JSON.stringify(operators),
2005
- ]
2006
- );
2007
-
2008
- const handleOperatorChange = useCallback(
2009
- (optionName: string, currentValue: TQorusForm, operator: string, index: number) => {
2010
- setLocalValue?.({
2011
- fields: {
2012
- ...currentValue,
2013
- [optionName]: {
2014
- ...currentValue[optionName],
2015
- op: fixOperatorValue(currentValue[optionName].op).map((op, idx) => {
2016
- if (idx === index) {
2017
- return operator;
2018
- }
2019
- return op as string;
2020
- }),
2021
- },
2022
- },
2023
- meta: undefined,
2024
- });
2025
- },
2026
- []
2027
- );
2028
-
2029
- const handleAddOperator = useCallback(
2030
- (optionName: string, currentValue: TQorusForm, index: number) => {
2031
- setLocalValue?.({
2032
- fields: {
2033
- ...currentValue,
2034
- [optionName]: {
2035
- ...currentValue[optionName],
2036
- op: insertAtIndex(fixOperatorValue(currentValue[optionName].op), index, null),
2037
- },
2038
- },
2039
- meta: undefined,
2040
- });
2041
- },
2042
- []
2043
- );
2044
-
2045
- const handleRemoveOperator = useCallback(
2046
- (optionName: string, currentValue: TQorusForm, index: number) => {
2047
- setLocalValue?.({
2048
- fields: {
2049
- ...currentValue,
2050
- [optionName]: {
2051
- ...currentValue[optionName],
2052
- op: fixOperatorValue(currentValue[optionName].op).filter((_op, idx) => idx !== index),
2053
- },
2054
- },
2055
- meta: undefined,
2056
- });
2057
- },
2058
- []
2059
- );
2060
-
2061
- const buildBadges = useCallback(
2062
- (option: TQorusFormFieldSchema, optionName: string): IReqorePanelProps['badge'] => {
2063
- const badges: IReqorePanelProps['badge'] = [];
2064
-
2065
- if (option.required || option.required_groups) {
2066
- badges.push({
2067
- icon: 'Asterisk',
2068
- size: 'tiny',
2069
- leftIconProps: {
2070
- size: 'tiny',
2071
- },
2072
- color: 'transparent',
2073
- minimal: true,
2074
- iconColor: option.required_groups ? 'warning:lighten:7' : 'danger:lighten:7',
2075
- tooltip: {
2076
- delay: 300,
2077
- content: getRequiredOptionMessage(options || {}, option.required_groups, optionName),
2078
- },
2079
- });
2080
- }
2081
-
2082
- if (option.has_dependents) {
2083
- badges.push({
2084
- icon: 'LinkUnlink',
2085
- intent: 'info',
2086
- size: 'tiny',
2087
- tooltip: {
2088
- content:
2089
- 'Other options depend on this option, changing it may result in configuration changes.',
2090
- delay: 300,
2091
- },
2092
- });
2093
- }
2094
-
2095
- return badges;
2096
- },
2097
- [options]
2098
- );
2099
-
2100
- const fixedValue: TQorusForm = (localValue.fields || {}) as TQorusForm;
2101
-
2102
- const removeSelectedOption = useCallback((optionName: string) => {
2103
- setLocalValue?.(({ fields }) => {
2104
- const newFields = cloneDeep(fields as TQorusForm);
2105
- delete newFields[optionName];
2106
- return {
2107
- fields: newFields,
2108
- meta: undefined,
2109
- };
2110
- });
2111
- // Collapse it too: a removed field drops back to the (collapsed) Optional box
2112
- // as a quiet addable row — if it was being edited, that editor must close
2113
- // rather than linger as an open editor for a field that's no longer added.
2114
- setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
2115
- }, []);
2116
-
2117
- const handleAddOptionalFieldChange = useCallback(
2118
- (_name: string, optionName: unknown) => {
2119
- handleValueChange(
2120
- optionName as string,
2121
- getDefaultValue(options?.[optionName as string]),
2122
- getTypeAndCanBeNull(
2123
- getOptionSchemaStorageType(options?.[optionName as string], isRendererOnly),
2124
- options?.[optionName as string]?.allowed_values
2125
- ).type
2126
- );
2127
- },
2128
- [JSON.stringify(options), handleValueChange]
2129
- );
2130
-
2131
- const availableOptions: TQorusForm = useMemo(() => {
2132
- if (!options) {
2133
- return {};
2134
- }
2135
- unavailableOptionsCount.current = 0;
2136
-
2137
- return Object.keys(fixedValue)
2138
- .sort((a, b) => {
2139
- const aSort = (options[a] as any)?.sort || 0;
2140
- const bSort = (options[b] as any)?.sort || 0;
2141
- return aSort - bSort;
2142
- })
2143
- .reduce((newValue: TQorusForm, optionName) => {
2144
- const option = fixedValue[optionName];
2145
- if (!options?.[optionName]) {
2146
- unavailableOptionsCount.current += 1;
2147
- removeSelectedOption(optionName);
2148
- return newValue;
2149
- }
2150
-
2151
-
2152
- const rendererType = getType(
2153
- (options[optionName].ui_type || options[optionName].type) as TQorusType,
2154
- operators,
2155
- (option as IQorusFormField)?.op
2156
- );
2157
- const storageType = getOptionFieldStorageType(
2158
- optionName,
2159
- (option as IQorusFormField)?.type,
2160
- options,
2161
- operators,
2162
- (option as IQorusFormField)?.op,
2163
- isRendererOnly,
2164
- optionHoldsExpression(option)
2165
- );
2166
-
2167
- if (!isPlainObject(option)) {
2168
- return {
2169
- ...newValue,
2170
- [optionName]: {
2171
- type: storageType || rendererType,
2172
- value: option,
2173
- },
2174
- };
2175
- }
2176
-
2177
- return {
2178
- ...newValue,
2179
- [optionName]: { ...(option as IQorusFormField), type: storageType || rendererType },
2180
- };
2181
- }, {});
2182
- }, [
2183
- isRendererOnly,
2184
- JSON.stringify(fixedValue),
2185
- JSON.stringify(options),
2186
- unavailableOptionsCount.current,
2187
- JSON.stringify(operators),
2188
- showInvalidOptionsOnly,
2189
- ]);
2190
-
2191
- // Per required-group: its member options, and which member (if any) already
2192
- // satisfies it — drives the "One of" chips and the "covered by" notes.
2193
- const requiredGroupsInfo = useMemo(() => {
2194
- const members: Record<string, string[]> = {};
2195
- forEach(options || {}, (optionSchema, name) => {
2196
- (optionSchema?.required_groups || []).forEach((groupName: string) => {
2197
- (members[groupName] = members[groupName] || []).push(name);
2198
- });
2199
- });
2200
- const satisfiedBy: Record<string, string | undefined> = {};
2201
- Object.keys(members).forEach((groupName) => {
2202
- satisfiedBy[groupName] = members[groupName].find(
2203
- (name) => !isOptionValueEmpty((availableOptions as TQorusForm)?.[name]?.value)
2204
- );
2205
- });
2206
- return { members, satisfiedBy };
2207
- }, [JSON.stringify(options), JSON.stringify(availableOptions)]);
2208
-
2209
- /* One bag per field, rebuilt only when something it is made of changes — the
2210
- schema that declares the `inherit_props`, the sibling values they resolve
2211
- against, or the scope handed in from an outer form. Held per field rather
2212
- than per form because each field's bag is different: `inherit_props` is
2213
- declared on the field. See `inheritedScopeFor` for what a churning bag
2214
- costs the field that receives it. */
2215
- const inheritedScopes = useMemo(() => {
2216
- const scopes = new Map<string, Record<string, unknown>>();
2217
- for (const optionName of Object.keys(options ?? {})) {
2218
- scopes.set(
2219
- optionName,
2220
- inheritedScopeFor(
2221
- options?.[optionName]?.inherit_props,
2222
- availableOptions,
2223
- inheritedFromParent
2224
- )
2225
- );
2226
- }
2227
- return scopes;
2228
- }, [options, availableOptions, inheritedFromParent]);
2229
-
2230
- // Dependency linkage (3a): when filling a dependency unlocks rows, flash
2231
- // them once — the form visibly "opens up" instead of silently changing.
2232
- const dependencyLockedNames = useMemo(() => {
2233
- const names: string[] = [];
2234
- forEach(options || {}, (optionSchema, name) => {
2235
- if (
2236
- optionSchema?.depends_on &&
2237
- !hasAllDependenciesFullfilled(optionSchema.depends_on, availableOptions, options || {})
2238
- ) {
2239
- names.push(name);
2240
- }
2241
- });
2242
- return names;
2243
- }, [JSON.stringify(options), JSON.stringify(availableOptions)]);
2244
- const previousDependencyLocked = useRef<string[] | null>(null);
2245
- useEffect(() => {
2246
- const previous = previousDependencyLocked.current;
2247
- previousDependencyLocked.current = dependencyLockedNames;
2248
- if (!previous) {
2249
- return;
2250
- }
2251
- const unlocked = previous.filter(
2252
- (name) => !dependencyLockedNames.includes(name) && !options?.[name]?.disabled
2253
- );
2254
- if (unlocked.length) {
2255
- flashOptions(unlocked);
2256
- }
2257
- }, [dependencyLockedNames.join('|')]);
2258
-
2259
- /* The same thing one level down: a CHOICE that becomes available is as
2260
- invisible as a field that becomes editable, and it is worse, because it
2261
- opens up inside a control the reader may not have open. So the field that
2262
- offers it flashes, which is the affordance that says "look here" without
2263
- claiming the choice was made.
2264
-
2265
- Keyed by field and by the value's identity, so a value that merely moves in
2266
- the list is not mistaken for one that opened. Fields whose values gate on
2267
- nothing never enter the set, which is nearly all of them. */
2268
- const lockedAllowedValues = useMemo(() => {
2269
- const locked: string[] = [];
2270
- forEach(options || {}, (optionSchema, name) => {
2271
- forEach(optionSchema?.allowed_values || [], (allowedValue) => {
2272
- if (!getAllowedValueAvailability(allowedValue, availableOptions, options || {}).available) {
2273
- locked.push(`${name}\u0000${JSON.stringify(allowedValue?.value?.value)}`);
2274
- }
2275
- });
2276
- });
2277
- return locked;
2278
- }, [JSON.stringify(options), JSON.stringify(availableOptions)]);
2279
- const previousLockedAllowedValues = useRef<string[] | null>(null);
2280
- useEffect(() => {
2281
- const previous = previousLockedAllowedValues.current;
2282
- previousLockedAllowedValues.current = lockedAllowedValues;
2283
- if (!previous) {
2284
- return;
2285
- }
2286
- const opened = previous
2287
- .filter((key) => !lockedAllowedValues.includes(key))
2288
- .map((key) => key.split('\u0000')[0]);
2289
- const fields = uniq(opened).filter(
2290
- (name) =>
2291
- !!options?.[name] &&
2292
- !options?.[name]?.disabled &&
2293
- // A field that is itself still locked flashes for its OWN unlocking, in
2294
- // the effect above; two flashes for one change is a flicker, not a cue.
2295
- !dependencyLockedNames.includes(name)
2296
- );
2297
- if (fields.length) {
2298
- flashOptions(fields);
2299
- }
2300
- }, [lockedAllowedValues.join('|')]);
2301
-
2302
- // The not-yet-added optional fields: everything in the schema the form is not
2303
- // already showing a row for. This ONE list feeds all three ways a field gets
2304
- // added — the inline addable rows, the Fields menu, and its "Select all" — so
2305
- // whatever it leaves out is out of all three at once, and they cannot disagree.
2306
- //
2307
- // A field whose `depends_on` is not fulfilled is left out. It is not an option
2308
- // yet: `hasAllDependenciesFullfilled` is the same predicate that then refuses
2309
- // to let it be edited, so offering it can only end at a row that opens and says
2310
- // it is disabled — the dead end an auth profile's cookie-only fields showed
2311
- // while the scheme was Permissive. Setting the dependency brings the row back
2312
- // (and flashes it — see `dependencyLockedNames`), which is the discoverable
2313
- // path: pick the scheme, and the fields that scheme has appear.
2314
- //
2315
- // Only the not-yet-added ones. A field that already HAS a value stays listed
2316
- // even when its dependency later stops holding — it renders disabled with the
2317
- // reason, so the value is visible and removable rather than silently orphaned
2318
- // in a form that no longer mentions it.
2319
- const filteredOptions: IQorusFormSchema = useMemo(
2320
- () =>
2321
- reduce(
2322
- options,
2323
- (newOptions, option, optName) => {
2324
- if (optName in fixedValue) {
2325
- return newOptions;
2326
- }
2327
- if (
2328
- option?.depends_on &&
2329
- !hasAllDependenciesFullfilled(option.depends_on, availableOptions, options || {})
2330
- ) {
2331
- return newOptions;
2332
- }
2333
- return { ...newOptions, [optName]: option };
2334
- },
2335
- {} as IQorusFormSchema
2336
- ),
2337
- [JSON.stringify(options), JSON.stringify(fixedValue), JSON.stringify(availableOptions)]
2338
- );
2339
-
2340
- const isOptionValid = useCallback(
2341
- (optionName: string, type: TQorusType, optionValue: any) => {
2342
- const isEmpty = optionValue === undefined || optionValue === '';
2343
- // An empty field locked by an unmet `depends_on` does not apply, so it is
2344
- // not missing — the same rule the read-first status follows.
2345
- if (
2346
- isEmpty &&
2347
- ((!options?.[optionName]?.required && !options?.[optionName]?.required_groups) ||
2348
- dependencyLockedNames.includes(optionName))
2349
- ) {
2350
- return true;
2351
- }
2352
-
2353
- return validateField(getType(type), optionValue, {
2354
- has_to_have_value: true,
2355
- optionSchema: options,
2356
- options: availableOptions,
2357
- ...options?.[optionName],
2358
- // The expression flag lives on the field value, not the schema — so an
2359
- // expression value is validated as an expression, not the base type.
2360
- isFunction: (availableOptions?.[optionName] as { is_expression?: boolean })?.is_expression,
2361
- // A field drawn by a bespoke editor (the consumer's own included) is
2362
- // not judged against `allowed_values` as fixed choices.
2363
- hasOwnEditor: isRendererOnly(options?.[optionName]?.ui_type as TQorusType),
2364
- } as any);
2365
- },
2366
- [
2367
- JSON.stringify(options),
2368
- JSON.stringify(availableOptions),
2369
- JSON.stringify(localValue.fields),
2370
- dependencyLockedNames,
2371
- isRendererOnly,
2372
- ]
2373
- );
2374
-
2375
- const getValidityData = useCallback((): IFormValidityData => {
2376
- const fields: IFormFieldValidityData[] = reduce(
2377
- availableOptions,
2378
- (result, option, optionName) => {
2379
- const type = getOptionFieldStorageType(
2380
- optionName,
2381
- (option as IQorusFormField).type,
2382
- options,
2383
- operators,
2384
- (option as IQorusFormField).op,
2385
- isRendererOnly,
2386
- optionHoldsExpression(option)
2387
- );
2388
- const optionValue = (option as IQorusFormField).value;
2389
-
2390
- const isRequired = options?.[optionName]?.required;
2391
- const hasRequiredGroups = !!options?.[optionName]?.required_groups;
2392
- const isEmpty = optionValue === undefined || optionValue === '';
2393
-
2394
- let validation: IValidationResult;
2395
-
2396
- /* A required field locked by an unmet `depends_on` does not apply
2397
- while it is locked: it is not in "Needs attention", and an empty one
2398
- does not make the form incomplete either. Otherwise a check whose
2399
- kind is Equals could never be saved, because Minimum — required for
2400
- Between alone — is empty; and a form whose only gaps are fields
2401
- nobody can fill in would say Incomplete with nothing to act on. A
2402
- locked field that still HOLDS a value is validated as before. */
2403
- if (
2404
- isEmpty &&
2405
- ((!isRequired && !hasRequiredGroups) || dependencyLockedNames.includes(optionName))
2406
- ) {
2407
- validation = { isValid: true, reasons: [] };
2408
- } else {
2409
- validation = validateFieldWithResult(getType(type), optionValue, {
2410
- has_to_have_value: true,
2411
- optionSchema: options,
2412
- options: availableOptions,
2413
- ...options?.[optionName],
2414
- // The expression flag lives on the field value, not the schema.
2415
- isFunction: (option as { is_expression?: boolean }).is_expression,
2416
- hasOwnEditor: isRendererOnly(options?.[optionName]?.ui_type as TQorusType),
2417
- } as any);
2418
- }
2419
-
2420
- result.push({
2421
- fieldName: optionName,
2422
- type: getType(type),
2423
- value: optionValue,
2424
- validation,
2425
- });
2426
-
2427
- return result;
2428
- },
2429
- [] as IFormFieldValidityData[]
2430
- );
2431
-
2432
- const invalidFields = fields.filter((f) => !f.validation.isValid);
2433
-
2434
- return {
2435
- isValid: invalidFields.length === 0,
2436
- fields,
2437
- invalidFields,
2438
- };
2439
- }, [
2440
- isRendererOnly,
2441
- JSON.stringify(availableOptions),
2442
- JSON.stringify(options),
2443
- JSON.stringify(operators),
2444
- JSON.stringify(localValue.fields),
2445
- dependencyLockedNames,
2446
- ]);
2447
-
2448
- const validityData = useMemo(() => getValidityData(), [getValidityData]);
2449
- const optionsAreValid = validityData.isValid;
2450
-
2451
- useEffect(() => {
2452
- onValidityChange?.(optionsAreValid, validityData);
2453
- }, [JSON.stringify(validityData)]);
2454
-
2455
- const shownOptions = useMemo((): TQorusForm => {
2456
- return reduce(
2457
- availableOptions,
2458
- (newValue, option, optionName) => {
2459
- if (
2460
- showInvalidOptionsOnly &&
2461
- isOptionValid(
2462
- optionName,
2463
- getOptionFieldStorageType(
2464
- optionName,
2465
- (option as IQorusFormField).type,
2466
- options,
2467
- operators,
2468
- (option as IQorusFormField).op,
2469
- isRendererOnly,
2470
- optionHoldsExpression(option)
2471
- ),
2472
- (option as IQorusFormField).value
2473
- )
2474
- ) {
2475
- return newValue;
2476
- }
2477
- return { ...newValue, [optionName]: option };
2478
- },
2479
- {}
2480
- );
2481
- }, [
2482
- showInvalidOptionsOnly,
2483
- isRendererOnly,
2484
- JSON.stringify(availableOptions),
2485
- JSON.stringify(options),
2486
- JSON.stringify(operators),
2487
- ]);
2488
-
2489
- // Read-first STATUS / BOX for one option — lifted to component scope so the
2490
- // status boxes (renderCompact) and the header's "needs attention" count share
2491
- // exactly one definition. One-of group members travel together (bucket by the
2492
- // group's satisfaction); everything else by its own status.
2493
- const schemaMsgIntent = useCallback(
2494
- (name: string): 'danger' | 'warning' | undefined => {
2495
- // Filtered, not raw: a message hidden by its own `when`/`unless` must not
2496
- // colour the row or push the field into "Needs attention" — the whole point
2497
- // of a conditional warning is that it is absent while it does not apply.
2498
- const msgs = getShownSchemaMessages(
2499
- (options?.[name] as { messages?: Array<{ intent?: string }> } | undefined)?.messages,
2500
- availableOptions,
2501
- options
2502
- );
2503
- if (msgs.some((m) => m.intent === 'danger')) return 'danger';
2504
- if (msgs.some((m) => m.intent === 'warning')) return 'warning';
2505
- return undefined;
2506
- },
2507
- [JSON.stringify(options), JSON.stringify(availableOptions)]
2508
- );
2509
- const getOptionStatus = useCallback(
2510
- (name: string, hidden = false): TReadFirstStatus => {
2511
- if (hidden) return 'optional';
2512
- const schema = options?.[name];
2513
- const type = getOptionFieldStorageType(
2514
- name,
2515
- (availableOptions as TQorusForm)?.[name]?.type,
2516
- options,
2517
- operators,
2518
- (availableOptions as TQorusForm)?.[name]?.op,
2519
- isRendererOnly,
2520
- optionHoldsExpression((availableOptions as TQorusForm)?.[name])
2521
- );
2522
- const value = (availableOptions as TQorusForm)?.[name]?.value;
2523
- // A schema-declared hash whose every entry is a materialised-but-unset
2524
- // field holds nothing, so it is not "set" — without this the row showed a
2525
- // green set-dot and counted toward the completion meter while the
2526
- // sub-form it summarises said 0/3 set.
2527
- const empty = isOptionValueEmpty(value) || isUnsetSchemaHash(value, schema);
2528
- const reqGroups = (schema?.required_groups as string[] | undefined) || [];
2529
- // Required only while it applies. A field locked by an unmet `depends_on`
2530
- // renders disabled with what unlocks it; nothing the author can do to it
2531
- // is a next step — the next step is the field it depends on, which carries
2532
- // its own status. Counting it here put a guided check's locked Path /
2533
- // Minimum / Maximum under "Needs attention" before its kind was even
2534
- // picked. The moment the dependency holds, it is required again.
2535
- const required =
2536
- !dependencyLockedNames.includes(name) && !!(schema?.required || reqGroups.length);
2537
- const covered =
2538
- empty &&
2539
- reqGroups.some((g) => {
2540
- const by = requiredGroupsInfo.satisfiedBy[g];
2541
- return !!by && by !== name;
2542
- });
2543
- const msgIntent = schemaMsgIntent(name);
2544
- const invalid = (!empty && !isOptionValid(name, type, value)) || msgIntent === 'danger';
2545
- return getReadFirstStatus({
2546
- empty,
2547
- required,
2548
- covered,
2549
- invalid,
2550
- warned: msgIntent === 'warning',
2551
- });
2552
- },
2553
- [
2554
- isRendererOnly,
2555
- JSON.stringify(options),
2556
- JSON.stringify(availableOptions),
2557
- JSON.stringify(operators),
2558
- isOptionValid,
2559
- requiredGroupsInfo,
2560
- schemaMsgIntent,
2561
- dependencyLockedNames,
2562
- ]
2563
- );
2564
- const getOptionBucket = useCallback(
2565
- (name: string, hidden = false): 'attention' | 'set' | 'optional' => {
2566
- // A locked one-of member cannot be the one that satisfies its group, so it
2567
- // does not travel with the group into "Needs attention"; its own status
2568
- // (not required while locked) decides its box.
2569
- if (!hidden && !dependencyLockedNames.includes(name)) {
2570
- const reqGroups = (options?.[name]?.required_groups as string[] | undefined) || [];
2571
- if (reqGroups.length) {
2572
- return reqGroups.some((g) => !requiredGroupsInfo.satisfiedBy[g]) ? 'attention' : 'set';
2573
- }
2574
- }
2575
- return getReadFirstBucket(getOptionStatus(name, hidden));
2576
- },
2577
- [JSON.stringify(options), requiredGroupsInfo, getOptionStatus, dependencyLockedNames]
2578
- );
2579
- // How many fields are in the "Needs attention" box — drives the header link.
2580
- const readFirstAttentionCount = useMemo(
2581
- () =>
2582
- Object.keys(availableOptions || {}).filter((name) => getOptionBucket(name) === 'attention')
2583
- .length,
2584
- [JSON.stringify(availableOptions), getOptionBucket]
2585
- );
2586
-
2587
- const getIntent = useCallback(
2588
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2589
- (optName: string, type: TQorusType, optValue: any, _op: any): TReqoreIntent => {
2590
- const intent =
2591
- isOptionValid(optName, type, optValue) ? undefined
2592
- : recordRequiresSearchOptions ? 'info'
2593
- : 'danger';
2594
-
2595
- return intent || (options?.[optName] as any)?.intent;
2596
- },
2597
- [isOptionValid, recordRequiresSearchOptions, JSON.stringify(options)]
2598
- );
2599
-
2600
- const handleShowFieldTypesClick = useCallback(() => setShowFieldTypes((prev) => !prev), []);
2601
- const handleToggleAllDescriptions = useCallback(() => {
2602
- setInfoPanelOverrides({});
2603
- setShowAllDescriptions((prev) => prev !== true);
2604
- }, []);
2605
- const handleToggleInvalidOnly = useCallback(() => setShowInvalidOptionsOnly((prev) => !prev), []);
2606
- const handleRevertChangesClick = useCallback(() => {
2607
- setLocalValue({
2608
- fields: originalValue.current,
2609
- meta: undefined,
2610
- });
2611
- }, [JSON.stringify(options), JSON.stringify(originalValue.current)]);
2612
-
2613
- // `commitMode='batched'`: which options differ from the committed baseline —
2614
- // drives the per-row Draft chips and the Save/Discard bar.
2615
- const dirtyOptionNames = useMemo(() => {
2616
- if (commitMode !== 'batched') {
2617
- return [];
2618
- }
2619
- const keys = new Set([
2620
- ...Object.keys((localValue.fields as TQorusForm) || {}),
2621
- ...Object.keys((originalValue.current as TQorusForm) || {}),
2622
- ]);
2623
- return Array.from(keys).filter(
2624
- (key) =>
2625
- !isEqual(
2626
- (localValue.fields as TQorusForm)?.[key]?.value,
2627
- originalValue.current?.[key]?.value
2628
- )
2629
- );
2630
- }, [commitMode, JSON.stringify(localValue.fields), JSON.stringify(originalValue.current)]);
2631
-
2632
- // Save: emit the staged form via onCommit and make it the new baseline, so
2633
- // the Draft chips and the bar clear. Validation gates the button itself.
2634
- const handleCommitClick = useCallback(() => {
2635
- const toEmit = size(localValue.fields) ? (localValue.fields as TQorusForm) : undefined;
2636
- onCommit?.(name, toEmit, size(localValue.meta) ? localValue.meta : undefined);
2637
- originalValue.current = localValue.fields;
2638
- setLocalValue((current) => ({ ...current }));
2639
- }, [JSON.stringify(localValue), name, onCommit]);
2640
-
2641
- const hasOptionChanged = useCallback(
2642
- (optionValue: unknown, optionName: string): boolean => {
2643
- return !isEqual(optionValue, originalValue.current?.[optionName]?.value);
2644
- },
2645
- [JSON.stringify(originalValue.current)]
2646
- );
2647
-
2648
- const handleOptionLabelClick = useCallback((optionName: string) => {
2649
- setShowHelpForOption(optionName);
2650
- }, []);
2651
-
2652
- const toggleExpandedOption = useCallback(
2653
- (optionName: string) => {
2654
- setExpandedOptions((prev) =>
2655
- prev.includes(optionName) ? prev.filter((name) => name !== optionName)
2656
- // single (default): opening a row collapses any other open one.
2657
- : expandMode === 'single' ? [optionName]
2658
- : [...prev, optionName]
2659
- );
2660
- },
2661
- [expandMode]
2662
- );
2663
-
2664
- // --- Caller-named starting rows (opt-in) ----------------------------------
2665
- // `initialExpandedOptions` lets a consumer name the rows that must already
2666
- // be open when the form first paints — the case where the expanded row is
2667
- // part of an address rather than a click (see the prop's docs).
2668
- //
2669
- // One-shot for the same reason autofocus is: it fires on the first render
2670
- // that actually has rows (so an async-loaded schema is covered) and then
2671
- // never again, so a row the user collapses afterwards stays collapsed and a
2672
- // later schema reload does not reopen it.
2673
- const hasAppliedInitialExpansionRef = useRef(false);
2674
- useEffect(() => {
2675
- if (!initialExpandedOptions?.length || !compact || hasAppliedInitialExpansionRef.current) {
2676
- return;
2677
- }
2678
- const names = Object.keys(availableOptions);
2679
- if (!names.length) {
2680
- return;
2681
- }
2682
- const toExpand = initialExpandedOptions.filter((name) => names.includes(name));
2683
- if (!toExpand.length) {
2684
- // Not "there is no such row" — "not YET". A server-driven schema arrives
2685
- // in pieces, and the row the caller named can be in a later one (a test's
2686
- // `cases` field materialises once its step-kind catalogue lands). Latching
2687
- // here would spend the one shot on a form that did not yet contain the
2688
- // field, and the address would never be applied at all.
2689
- return;
2690
- }
2691
- hasAppliedInitialExpansionRef.current = true;
2692
- setExpandedOptions((prev) => [...prev, ...toExpand.filter((name) => !prev.includes(name))]);
2693
- }, [initialExpandedOptions, compact, availableOptions]);
2694
-
2695
- // --- First-attention-field autofocus (opt-in) -----------------------------
2696
- // With `autoFocusFirstRequired`, drop the user straight into the first field
2697
- // they must fix. We reuse the engine's own ordering (`availableOptions`), the
2698
- // very same `getOptionBucket` the status boxes use to decide "needs attention"
2699
- // (so we cover empty-required, unsatisfied one-of, AND filled-but-invalid rows
2700
- // without ever drifting from what the user sees), and dependency gating
2701
- // (`dependencyLockedNames`), then expand the target row through
2702
- // `expandedOptions` — the row's editor-focus effect does the rest. No DOM
2703
- // scraping, no synthetic clicks, no polling.
2704
- //
2705
- // Strictly one-shot: it fires the first time the form has focusable content
2706
- // (so an async-loaded schema is still covered — the empty first pass doesn't
2707
- // count), then never again for the life of this instance. A server-driven
2708
- // field update or an in-place schema reload will NOT re-grab focus; only a
2709
- // genuine remount (a fresh instance) auto-focuses again, which is the intended
2710
- // on-mount behaviour.
2711
- const hasAutoFocusedRef = useRef(false);
2712
- useEffect(() => {
2713
- // Two decisions, one scan. Both open the row that needs attention;
2714
- // `autoFocusFirstRequired` additionally promises the caret lands in it, and
2715
- // so refuses to run at all while the reader holds the caret somewhere else
2716
- // (the guard below). An opened row takes the caret either way — that is
2717
- // CompactRow's tap-to-edit rule, and it declines just as firmly to take one
2718
- // that is already somewhere outside the row.
2719
- const wantsFocus = !!autoFocusFirstRequired;
2720
- if ((!wantsFocus && !expandFirstRequired) || !compact || !options) {
2721
- return;
2722
- }
2723
- if (hasAutoFocusedRef.current) {
2724
- return;
2725
- }
2726
-
2727
- const orderedNames = Object.keys(availableOptions);
2728
- if (!orderedNames.length) {
2729
- return;
2730
- }
2731
-
2732
- // Never grab focus from a control the user is already in — including a field
2733
- // *inside* this form. If a slow/async schema lets the user start typing
2734
- // before this first-required scan runs, we must not steal their caret. On a
2735
- // clean mount focus rests on the body, so the intended "drop into the first
2736
- // field" still fires. Bailing here leaves `hasAutoFocusedRef` false, so it
2737
- // retries once focus is free.
2738
- //
2739
- // Only the FOCUSING variant waits: opening a row moves no caret, so there is
2740
- // nothing to steal and nothing to wait for. Making the expand-only variant
2741
- // wait here would make it a no-op in its main use — a sub-form mounted by a
2742
- // click, where the button that mounted it still holds focus and never gives
2743
- // it up.
2744
- const active = document.activeElement as HTMLElement | null;
2745
- if (wantsFocus && active && active !== document.body) {
2746
- return;
2747
- }
2748
-
2749
- const target = getFirstAttentionOptionName(orderedNames, (fieldName) => {
2750
- const schema = options[fieldName];
2751
- if (!schema) {
2752
- return undefined;
2753
- }
2754
- return {
2755
- focusable:
2756
- !readOnly &&
2757
- !schema.disabled &&
2758
- !(schema as { readonly?: boolean }).readonly &&
2759
- !dependencyLockedNames.includes(fieldName),
2760
- // Single source of truth: the same bucket the read-first status boxes
2761
- // show, so a filled-but-invalid required row is a target too.
2762
- needsAttention: getOptionBucket(fieldName) === 'attention',
2763
- };
2764
- });
2765
-
2766
- // Mark done even when nothing needs attention, so later value edits or an
2767
- // in-place schema reload never re-scan or re-focus. Only a remount arms it
2768
- // again.
2769
- hasAutoFocusedRef.current = true;
2770
-
2771
- if (target) {
2772
- setExpandedOptions((prev) =>
2773
- prev.includes(target) ? prev
2774
- : expandMode === 'multi' ? [...prev, target]
2775
- // `expandMode: 'single'` is a rule about what a CLICK does: opening a
2776
- // row closes the one the reader last opened. It is not a licence to
2777
- // close a row the HOST named through `initialExpandedOptions`. That
2778
- // is an address — a statement about what the page is FOR, made before
2779
- // the reader did anything — and this scan is a convenience layered on
2780
- // top of it. Keep the named rows; replace only what a click could.
2781
- : [...prev.filter((name) => initialExpandedOptions?.includes(name)), target]
2782
- );
2783
- }
2784
- }, [
2785
- autoFocusFirstRequired,
2786
- expandFirstRequired,
2787
- compact,
2788
- options,
2789
- availableOptions,
2790
- readOnly,
2791
- dependencyLockedNames,
2792
- getOptionBucket,
2793
- expandMode,
2794
- initialExpandedOptions,
2795
- ]);
2796
-
2797
- // Read-first completion summary (how many shown options have a value set),
2798
- // surfaced as a progress meter at the top of the compact form.
2799
- const readFirstCompletion = useMemo(
2800
- () =>
2801
- getReadFirstCompletion(
2802
- availableOptions as Record<string, IQorusFormField | undefined>,
2803
- (name) => getOptionBucket(name) === 'attention'
2804
- ),
2805
- [JSON.stringify(availableOptions), getOptionBucket]
2806
- );
2807
-
2808
- const optionalFields = useMemo(
2809
- () =>
2810
- Object.keys(filteredOptions).map((option) => ({
2811
- value: option,
2812
- desc: options?.[option]?.desc,
2813
- short_desc: options?.[option]?.short_desc,
2814
- disabled: options?.[option]?.disabled,
2815
- display_name: options?.[option]?.display_name,
2816
- intent: (options?.[option] as any)?.intent,
2817
- messages: (options?.[option] as any)?.messages,
2818
- })),
2819
- [JSON.stringify(filteredOptions), JSON.stringify(options)]
2820
- );
2821
-
2822
- // Compact "Fields" menu — Select all: add every not-yet-selected, enabled
2823
- // optional field (mirrors the IDE's handleAddAll).
2824
- const handleAddAllOptional = useCallback(() => {
2825
- forEach(filteredOptions, (schema, optionName) => {
2826
- if (!schema?.disabled) {
2827
- handleAddOptionalFieldChange('options', optionName);
2828
- }
2829
- });
2830
- }, [JSON.stringify(filteredOptions), handleAddOptionalFieldChange]);
2831
-
2832
- // Default fields: drop user-added optionals, keep required/preselected/valued,
2833
- // clear the required-only filter (mirrors the IDE's handleResetToDefault).
2834
- const handleResetToDefaultFields = useCallback(() => {
2835
- setLocalValue(({ fields }) => {
2836
- const current = (fields || {}) as TQorusForm;
2837
- const next: TQorusForm = {};
2838
- forEach(current, (value, optionName) => {
2839
- const schema = options?.[optionName];
2840
- const isDefault = !!(
2841
- schema?.required ||
2842
- schema?.required_groups ||
2843
- schema?.preselected ||
2844
- originalValue.current?.[optionName]
2845
- );
2846
- if (isDefault) {
2847
- next[optionName] = value as IQorusFormField;
2848
- }
2849
- });
2850
- return {
2851
- fields: fixOptions(next, options || {}, operators, isRendererOnly),
2852
- meta: undefined,
2853
- };
2854
- });
2855
- setRequiredOnly(false);
2856
- }, [JSON.stringify(options), JSON.stringify(operators), isRendererOnly]);
2857
-
2858
- const getCustomMenuTemplateItems = useCallback<(optionName: string) => TCustomTemplateItems>(
2859
- (optionName) => {
2860
- return compactValue?.map((type) => ({
2861
- label: type.display_name,
2862
- description: type.short_desc,
2863
- onClick: (_e: any, removeTemplate: any) => {
2864
- removeTemplate?.();
2865
- handleValueChange(optionName, undefined, type.name);
2866
- },
2867
- }));
2868
- },
2869
- [compactValue, handleValueChange]
2870
- );
2871
-
2872
- const getTypeForOption = useCallback((type: string) => type, []);
2873
-
2874
- const renderOption = useCallback(
2875
- (
2876
- optionName: string,
2877
- { type, ...other }: IQorusFormField,
2878
- // Inline (in-row) editing renders the editor a size down so it fits the
2879
- // read row's height without shifting the rows around it.
2880
- editorSize?: 'small',
2881
- // The info panel below the row keeps showing schema messages while editing —
2882
- // rendering them in the editor too would balloon a one-line edit.
2883
- suppressSchemaMessages?: boolean
2884
- ) => {
2885
- const operatorParts = fixOperatorValue(other.op);
2886
- // The RENDERER type for this row — which editor mounts. Distinct from the
2887
- // storage type on the value envelope: a `richtext` field stores a string
2888
- // but must render the richtext editor, so `ui_type` wins here.
2889
- //
2890
- // The exception is an any-like `ui_type`. Those schemas say
2891
- // `ui_type: 'any'` precisely so the user can pick a concrete type, and
2892
- // that pick lives on the field's stored `type` — letting 'any' win would
2893
- // pin the row to the untyped editor forever.
2894
- //
2895
- // The trailing fallbacks keep the row rendering when a server-driven
2896
- // schema has not arrived yet (`type` undefined) instead of crashing.
2897
- const optionSchema = options?.[optionName];
2898
- const schemaUiType = optionSchema?.ui_type as TQorusType;
2899
- const uiTypeIsAnyLike = isUntypedOptionType(schemaUiType);
2900
- /* An EXPRESSION's stored type is skipped in this chain.
2901
-
2902
- The value is `{ is_expression: true, value: {...} }`, so the type saved
2903
- beside it describes that envelope — `hash` — not what the field holds.
2904
- With no `ui_type` to win first, that `hash` beat the schema's `auto`
2905
- and was handed to the expression editor as the return type to check
2906
- against: `1 + 2` on a field declared `auto` came back "This does not
2907
- fit. The expression returns int, and this field holds hash". A literal
2908
- `1` was fine, because a literal is stored as a plain value whose type
2909
- matches.
2910
-
2911
- Dropping the stored type here lets the schema decide, which is the
2912
- only party that knows what the field accepts. This is the row
2913
- renderer's OWN resolution and does not go through
2914
- `getOptionFieldStorageType`, so guarding that one alone left this
2915
- untouched. */
2916
- const storedRowType = optionHoldsExpression(other) ? undefined : type;
2917
-
2918
- /* A RELOADED expression is handed to the editor in the shape the editor
2919
- was written against.
2920
-
2921
- An expression is written as `{ type, is_expression: true, value: ast }`
2922
- and saved as `{ type, value: { is_expression: true, value: ast } }` —
2923
- the flag moves ONTO the value. The editor was only ever given
2924
- `other.is_expression`, which is absent in the saved shape, so a
2925
- reloaded expression was not recognised as one: an `auto` field then
2926
- resolved its type from the value it could see, decided the envelope was
2927
- a `hash`, and rendered `1 + 2` as a raw tree of `is_expression` /
2928
- `exp` / `args` for the author to edit by hand.
2929
-
2930
- Both halves have to move together. Flipping the flag alone would put
2931
- the editor into expression mode and then hand it the ENVELOPE where it
2932
- expects the expression, so the value is unwrapped here too — after
2933
- which a saved expression is indistinguishable from one just typed,
2934
- which is what every component downstream already assumes.
2935
-
2936
- Reported as "after Run assertion the value shows as raw data", but the
2937
- run is incidental: the field beside it, which no run touched, was
2938
- equally raw. The trigger is a RELOAD, which is the only thing that puts
2939
- an expression into its saved shape. */
2940
- const nestedExpression = !!(other as { value?: { is_expression?: unknown } })?.value
2941
- ?.is_expression;
2942
- const expressionAwareValue =
2943
- nestedExpression ?
2944
- (other as unknown as { value: { value: unknown } }).value.value
2945
- : other.value;
2946
- const resolvedType =
2947
- (isFixedCompactAllowedValueOption(optionSchema) ?
2948
- getOptionSchemaStorageType(optionSchema, isRendererOnly)
2949
- : schemaUiType && !uiTypeIsAnyLike ? schemaUiType
2950
- : undefined) ||
2951
- storedRowType ||
2952
- schemaUiType ||
2953
- (optionSchema?.type as TQorusType) ||
2954
- 'any';
2955
- const effectiveForceDropdown =
2956
- forceDropdown ??
2957
- (templateFieldProps as { forceDropdown?: boolean } | undefined)?.forceDropdown;
2958
-
2959
- return (
2960
- <>
2961
- {(() => {
2962
- const schemaMsgs = (
2963
- suppressSchemaMessages ?
2964
- []
2965
- : getShownSchemaMessages(
2966
- (options?.[optionName] as any)?.messages,
2967
- availableOptions,
2968
- options
2969
- )) as {
2970
- intent?: string;
2971
- title?: string;
2972
- content?: string;
2973
- }[];
2974
- if (!schemaMsgs.length) return null;
2975
- const items = schemaMsgs.map(({ intent, title, content }, index) => (
2976
- <ReqoreMessage
2977
- intent={intent as never}
2978
- title={title}
2979
- key={title || index}
2980
- opaque={false}
2981
- size='small'
2982
- // Compact: flat (no border) to match the read-row info panels;
2983
- // classic forms keep the bordered, bottom-margined message.
2984
- flat={compact || undefined}
2985
- margin={compact ? undefined : 'bottom'}
2986
- >
2987
- {content}
2988
- </ReqoreMessage>
2989
- ));
2990
- // Compact: stack them in a 4px-gap panel so a field's messages look
2991
- // identical whether the row is collapsed (read panel) or expanded.
2992
- return compact ?
2993
- <div
2994
- className='options-readfirst-info-panel'
2995
- style={{ display: 'flex', flexFlow: 'column', gap: 4, marginBottom: 8 }}
2996
- >
2997
- {items}
2998
- </div>
2999
- : <>{items}</>;
3000
- })()}
3001
- {operators && size(operators) ?
3002
- <>
3003
- <ReqoreControlGroup fill wrap className='operators'>
3004
- {operatorParts.map((operator, index) => (
3005
- <React.Fragment key={index}>
3006
- <SelectFormField
3007
- items={map(operators, (op) => ({
3008
- ...op,
3009
- value: op.name,
3010
- }))}
3011
- disabled={readOnly}
3012
- value={operator && `${(operators as any)?.[operator as string]?.name}`}
3013
- onChange={(val) => {
3014
- if (val !== undefined) {
3015
- handleOperatorChange(
3016
- optionName,
3017
- fixedValue,
3018
- findKey(operators, (op) => op.name === val) as string,
3019
- index
3020
- );
3021
- }
3022
- }}
3023
- />
3024
- {(
3025
- index === operatorParts.length - 1 &&
3026
- operator &&
3027
- (operators as any)[operator as string]?.supports_nesting
3028
- ) ?
3029
- <ReqoreButton
3030
- icon='AddLine'
3031
- disabled={readOnly}
3032
- fixed
3033
- effect={PositiveColorEffect}
3034
- onClick={() => handleAddOperator(optionName, fixedValue, index + 1)}
3035
- />
3036
- : null}
3037
- {size(operatorParts) > 1 ?
3038
- <ReqoreButton
3039
- disabled={readOnly}
3040
- icon='DeleteBinLine'
3041
- effect={NegativeColorEffect}
3042
- fixed
3043
- onClick={() => handleRemoveOperator(optionName, fixedValue, index)}
3044
- />
3045
- : null}
3046
- </React.Fragment>
3047
- ))}
3048
- </ReqoreControlGroup>
3049
- <ReqoreVerticalSpacer height={5} />
3050
- </>
3051
- : null}
3052
- <TemplateField
3053
- fluid
3054
- {...(options?.[optionName] as any)}
3055
- // qorus#347-followup: resolve the field's `inherit_props` against
3056
- // the CURRENT sibling values (with a fallback to
3057
- // `inheritedFromParent` — the bag threaded in from an outer
3058
- // FormEngine when this scope is an `arg_schema` sub-form),
3059
- // threading each entry as a top-level prop. Each
3060
- // `<prop-name>: <sibling-field-name>` mapping copies the
3061
- // sibling's value onto the rendered field's renderer — e.g. a
3062
- // `code-editor` with `inherit_props: { language: 'language' }`
3063
- // picks up the live `language` value as a `language` prop without
3064
- // a schema refetch. Spread AFTER `{...options?.[optionName]}` so
3065
- // the runtime value wins over any schema-defined default of the
3066
- // same key. Mirrored in qorus-ide's `systemOptions.tsx`; see the
3067
- // CLAUDE.md rule there.
3068
- {...resolveInheritProps(
3069
- options?.[optionName]?.inherit_props,
3070
- availableOptions,
3071
- inheritedFromParent
3072
- )}
3073
- // qorus#347-followup (scope forwarding): merge accumulated
3074
- // inheritance (`inheritedFromParent`) with THIS field's freshly
3075
- // resolved inherit_props, and pass down as a single bag so any
3076
- // nested `arg_schema` sub-form (mounted by AutoFormField for
3077
- // `hash` / `free-hash` / list-of-hash fields) sees every value
3078
- // the ancestor chain forwarded. This is the plumbing that lets a
3079
- // service-method row's `body` sub-field pick up the parent
3080
- // service form's `language` — the parent field declares
3081
- // `inherit_props: { language: 'language' }`, the list renderer
3082
- // forwards it into each row, and the row's body resolves against
3083
- // the accumulated bag.
3084
- //
3085
- // Read from the memoised map rather than built here: this is a
3086
- // PROP, and one built inline is a new object on every render of
3087
- // the form, which re-renders every field that receives it. The
3088
- // `??` chain answers for a field the schema does not describe —
3089
- // `inheritedScopeFor` returns the SAME references, so an
3090
- // undescribed field is as stable as a described one.
3091
- inheritedFromParent={
3092
- inheritedScopes.get(optionName) ?? inheritedFromParent ?? NO_INHERITED_SCOPE
3093
- }
3094
- // Propagate compact so an arg_schema field renders a COMPACT sub-form
3095
- // (consistent with the parent) rather than the classic FormEngine.
3096
- compact={compact}
3097
- // SEAM: forwarded through TemplateField's rest-spread to AutoFormField,
3098
- // which renders consumer-injected editors by field type/ui_type.
3099
- componentOverrides={componentOverrides}
3100
- templateAwareUiTypes={templateAwareUiTypes}
3101
- allowTemplates={!!(allowTemplates && options?.[optionName]?.supports_templates)}
3102
- allowFunctions={!!options?.[optionName]?.supports_expressions}
3103
- // reqraft: form-level expression fields get the Visual/Text shell
3104
- // (DPQL text mode); opt out per-form via `templateFieldProps`.
3105
- allowTextExpressions
3106
- // A fixed allowed-value field still needs its selector even when
3107
- // arbitrary custom values are forbidden. TemplateField uses this
3108
- // flag to decide whether to mount AutoFormField at all; treating
3109
- // `supports_custom_values: false` as "no input" left dependent
3110
- // delivery fields (Discord server/channel, for example) as empty
3111
- // control groups. The allowed-values renderer itself enforces the
3112
- // fixed catalogue and never exposes free-form entry.
3113
- allowCustomValues={
3114
- isFixedCompactAllowedValueOption(optionSchema) ||
3115
- (options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any')
3116
- }
3117
- // A field may declare a template list OF ITS OWN, and when it does
3118
- // that list is the one to offer. The engine-wide `stringTemplates`
3119
- // are the form's shared vocabulary - config items, system properties
3120
- // - and every field draws on the same set. A per-field list answers a
3121
- // question only that field asks: a test assertion's `$.` references
3122
- // are the values ITS case captured, and no other field in the form
3123
- // should be offering them.
3124
- //
3125
- // Passing the engine's list unconditionally overwrote the schema's,
3126
- // so a field that declared templates had none by the time it
3127
- // rendered. On a typed field that only cost the picker; on an
3128
- // ANY-LIKE one it changed the question, because `TemplateField`
3129
- // opens on the template selector only when there is something to
3130
- // pick and otherwise falls back to the type picker. An untyped field
3131
- // therefore asked the author to choose `string`/`int`/`hash` before
3132
- // it would let them name a value they had already captured.
3133
- templates={
3134
- (optionSchema as TFieldWithOwnTemplates | undefined)?.templates ?? templates.value
3135
- }
3136
- {...getTypeAndCanBeNull(
3137
- // The RENDERER type picks the editor — the storage type lives on
3138
- // the value envelope. Passing storage here rendered a `richtext`
3139
- // field as a plain string input.
3140
- resolvedType,
3141
- options?.[optionName]?.allowed_values,
3142
- other.op
3143
- )}
3144
- ui_type={resolvedType}
3145
- name={optionName}
3146
- uniqueName={`${uniqueName ? `${uniqueName}.` : `${name ? `${name}.` : ''}`}${optionName}`}
3147
- onChange={
3148
- // Identity-stable on purpose: the typed fields debounce on
3149
- // `[localValue, onChange]` — an inline lambda resets the pending emit
3150
- // every render and the typed value can starve.
3151
- handleValueChange
3152
- }
3153
- key={optionName}
3154
- arg_schema={options?.[optionName]?.arg_schema}
3155
- value={expressionAwareValue}
3156
- isFunction={optionHoldsExpression(other)}
3157
- isDefaultFunction={options?.[optionName]?.default_view === 'expression'}
3158
- sensitive={options?.[optionName]?.sensitive}
3159
- default_value={getDefaultValue(options?.[optionName])}
3160
- isDefaultTemplate={options?.[optionName]?.default_view === 'template'}
3161
- allowed_values={options?.[optionName]?.allowed_values}
3162
- allowed_values_creatable={options?.[optionName]?.allowed_values_creatable}
3163
- disabled={
3164
- options?.[optionName]?.disabled ||
3165
- // a system-owned field states that as `readonly`; only the older
3166
- // compat projection also sent `disabled`, so honouring only
3167
- // `disabled` rendered an editable control for a value the user
3168
- // cannot change (see CompactRow's `fieldReadonly`)
3169
- (options?.[optionName] as { readonly?: boolean } | undefined)?.readonly ||
3170
- readOnly ||
3171
- !hasAllDependenciesFullfilled(
3172
- options?.[optionName]?.depends_on,
3173
- availableOptions,
3174
- options || {}
3175
- )
3176
- }
3177
- readOnly={readOnly}
3178
- size={editorSize || rest.size}
3179
- menuItems={
3180
- offersTypeChoices(options?.[optionName], anyLikeUiTypes) ?
3181
- getCustomMenuTemplateItems(optionName)
3182
- : undefined
3183
- }
3184
- {...templateFieldProps}
3185
- forceDropdown={effectiveForceDropdown}
3186
- />
3187
- <OptionFieldMessages
3188
- schema={options || {}}
3189
- allOptions={availableOptions}
3190
- name={optionName}
3191
- option={{ type: resolvedType, ...other }}
3192
- getType={getTypeForOption}
3193
- isRendererOnly={isRendererOnly}
3194
- />
3195
- {operators && size(operators) && size(other.op) ?
3196
- <>
3197
- <ReqoreVerticalSpacer height={5} />
3198
- <ReqoreMessage size='small'>
3199
- <ReqoreTagGroup>
3200
- <ReqoreTag size='small' labelKey='WHERE' label={optionName} />
3201
- <ReqoreTag size='small' labelKey='IS' label={operatorParts.join(' ')} />
3202
- <ReqoreTag
3203
- size='small'
3204
- intent='info'
3205
- label={
3206
- other.value ?
3207
- resolvedType === 'richtext' ?
3208
- richtextToString(other.value)
3209
- : JSON.stringify(other.value)
3210
- : ''
3211
- }
3212
- />
3213
- </ReqoreTagGroup>
3214
- </ReqoreMessage>
3215
- </>
3216
- : null}
3217
- </>
3218
- );
3219
- },
3220
- [
3221
- options,
3222
- operators,
3223
- readOnly,
3224
- allowTemplates,
3225
- templates,
3226
- name,
3227
- uniqueName,
3228
- componentOverrides,
3229
- templateFieldProps,
3230
- forceDropdown,
3231
- availableOptions,
3232
- fixedValue,
3233
- // Depend on the specific `rest` values used, not the whole `rest` object
3234
- // (which is a fresh `{...rest}` every render and would defeat the memo).
3235
- rest?.options,
3236
- rest?.size,
3237
- handleValueChange,
3238
- handleOperatorChange,
3239
- handleAddOperator,
3240
- handleRemoveOperator,
3241
- getCustomMenuTemplateItems,
3242
- getTypeForOption,
3243
- inheritedScopes,
3244
- inheritedFromParent,
3245
- ]
3246
- );
3247
-
3248
- // Compact (read-first) rendering.
3249
- // Theme-derived colours so the flat-row layout adapts to light/dark/custom themes.
3250
- const cText = theme?.text?.color || '#f1f0ee';
3251
- // Text emphasis tiers via reqore's readable-colour helper (key = full readable
3252
- // text, muted = its dimmed variant, faint = the dimmed variant softened
3253
- // further) instead of hand-rolled hex-alpha suffixes on the raw text colour.
3254
- const cKey = getReadableColor(theme);
3255
- const cMuted = getReadableColor(theme, undefined, undefined, true);
3256
- const cFaint = `${cMuted}99`;
3257
- const cDivider = `${cText}14`;
3258
- const cHover = `${cText}0d`;
3259
- const cDanger = theme?.intents?.danger || '#e35a5a';
3260
- const cWarning = theme?.intents?.warning || '#ffdf34';
3261
- const cInfo = theme?.intents?.info || '#3b8eea';
3262
- const cSuccess = theme?.intents?.success || '#36b37e';
3263
- const cBg = (theme as { main?: string } | undefined)?.main || '#181818';
3264
- const cRowBg = changeDarkness(getMainBackgroundColor(theme), 0.01);
3265
- const cGroupLine = `${cText}1f`;
3266
- // The "N more fields" row's dashed edge. Well above the divider tint: this
3267
- // one is an affordance the reader has to notice, not a hairline between rows.
3268
- const cMoreBorder = `${cText}4d`;
3269
-
3270
- // The closure surface the extracted CompactRow reads through context. Refs and
3271
- // setters are stable; the state/memo/handler fields change identity as they do
3272
- // today, so a row re-renders exactly when its inputs do.
3273
- const compactRowContextValue = useMemo<ICompactRowContext>(
3274
- () => ({
3275
- templates: templates.value,
3276
- readOnly,
3277
- onReadOnlyActivate,
3278
- commitMode,
3279
- expandMode,
3280
- options,
3281
- operators,
3282
- focusedEditing,
3283
- showFieldTypes,
3284
- codePreviewRenderer,
3285
- showAllDescriptions,
3286
- expandedOptions,
3287
- highlightedOptions,
3288
- flashedOptions,
3289
- infoPanelOverrides,
3290
- setHighlightedOptions,
3291
- setInfoPanelOverrides,
3292
- setFocusedEditing,
3293
- readRowHeights,
3294
- originalValue,
3295
- availableOptions,
3296
- requiredGroupsInfo,
3297
- handleValueChange,
3298
- handleAddOptionalFieldChange,
3299
- toggleExpandedOption,
3300
- flashOption,
3301
- hasOptionChanged,
3302
- handleOptionLabelClick,
3303
- removeSelectedOption,
3304
- getTypeForOption,
3305
- isRendererOnly,
3306
- isOptionValid,
3307
- confirmAction,
3308
- optionActions,
3309
- collapseOptionActions,
3310
- renderOption,
3311
- theme,
3312
- cText,
3313
- cMuted,
3314
- cFaint,
3315
- cKey,
3316
- cDivider,
3317
- cHover,
3318
- cDanger,
3319
- cWarning,
3320
- cInfo,
3321
- cBg,
3322
- }),
3323
- [
3324
- templates.value,
3325
- readOnly,
3326
- commitMode,
3327
- expandMode,
3328
- options,
3329
- operators,
3330
- focusedEditing,
3331
- showFieldTypes,
3332
- codePreviewRenderer,
3333
- showAllDescriptions,
3334
- expandedOptions,
3335
- highlightedOptions,
3336
- flashedOptions,
3337
- infoPanelOverrides,
3338
- setHighlightedOptions,
3339
- setInfoPanelOverrides,
3340
- setFocusedEditing,
3341
- readRowHeights,
3342
- originalValue,
3343
- availableOptions,
3344
- requiredGroupsInfo,
3345
- handleValueChange,
3346
- handleAddOptionalFieldChange,
3347
- toggleExpandedOption,
3348
- flashOption,
3349
- hasOptionChanged,
3350
- handleOptionLabelClick,
3351
- removeSelectedOption,
3352
- getTypeForOption,
3353
- isRendererOnly,
3354
- isOptionValid,
3355
- confirmAction,
3356
- optionActions,
3357
- collapseOptionActions,
3358
- renderOption,
3359
- theme,
3360
- cText,
3361
- cMuted,
3362
- cFaint,
3363
- cKey,
3364
- cDivider,
3365
- cHover,
3366
- cDanger,
3367
- cWarning,
3368
- cInfo,
3369
- cBg,
3370
- ]
3371
- );
3372
-
3373
- // `compactToolbar` resolved to a full object, so neither the context type nor
3374
- // the toolbar itself has to re-default anything. `null` means no toolbar.
3375
- const compactToolbarParts = useMemo(() => {
3376
- if (compactToolbar === false) return null;
3377
- const all = { completion: true, search: true, fields: true, help: true };
3378
- return compactToolbar === true ? all : { ...all, ...compactToolbar };
3379
- }, [JSON.stringify(compactToolbar)]);
3380
-
3381
- const compactToolbarContextValue = useMemo<ICompactToolbarContext>(
3382
- () => ({
3383
- readOnly,
3384
- invalidCount: size(validityData.invalidFields),
3385
- attentionCount: readFirstAttentionCount,
3386
- completion: readFirstCompletion,
3387
- showInvalidOnly: showInvalidOptionsOnly,
3388
- onToggleInvalidOnly: handleToggleInvalidOnly,
3389
- parts: compactToolbarParts ?? {
3390
- completion: false,
3391
- search: false,
3392
- fields: false,
3393
- help: false,
3394
- },
3395
- /* From the SCHEMA, not from the value.
3396
- *
3397
- * `availableOptions` is keyed off the form's VALUE, so it grows as data
3398
- * arrives — and the search row is rendered only when this is true
3399
- * (`CompactToolbar`). A form whose value lands after its first render
3400
- * therefore ADDED a 38px row to its header a beat later, and everything
3401
- * below it moved. Measured on the Qorus IDE's test editor, on the nested
3402
- * sub-form of a case: 33 elements jumped down 51px in one frame, on every
3403
- * cold load.
3404
- *
3405
- * How many fields a form has is a property of its schema. The two agree
3406
- * once the value has arrived — `fixOptions` materialises an entry per
3407
- * declared field — so this only changes the answer during the load, which
3408
- * is the whole point. */
3409
- hasMultipleOptions: size(options) > 1,
3410
- compactQuery,
3411
- setCompactQuery,
3412
- requiredOnly,
3413
- setRequiredOnly,
3414
- compactSort,
3415
- setCompactSort,
3416
- showFieldTypes,
3417
- showAllDescriptions,
3418
- onToggleFieldTypes: handleShowFieldTypesClick,
3419
- onToggleAllDescriptions: handleToggleAllDescriptions,
3420
- filteredCount: size(filteredOptions),
3421
- optionalFields,
3422
- canRevert: !!(originalValue.current && !isEqual(localValue.fields, originalValue.current)),
3423
- onAddOptionalField: (value) => handleAddOptionalFieldChange('options', value),
3424
- onAddAll: handleAddAllOptional,
3425
- onResetDefaults: handleResetToDefaultFields,
3426
- onRevertAll: handleRevertChangesClick,
3427
- }),
3428
- [
3429
- readOnly,
3430
- onReadOnlyActivate,
3431
- validityData,
3432
- readFirstAttentionCount,
3433
- readFirstCompletion,
3434
- showInvalidOptionsOnly,
3435
- handleToggleInvalidOnly,
3436
- availableOptions,
3437
- compactQuery,
3438
- setCompactQuery,
3439
- requiredOnly,
3440
- setRequiredOnly,
3441
- compactSort,
3442
- setCompactSort,
3443
- showFieldTypes,
3444
- showAllDescriptions,
3445
- handleShowFieldTypesClick,
3446
- handleToggleAllDescriptions,
3447
- filteredOptions,
3448
- optionalFields,
3449
- originalValue,
3450
- localValue,
3451
- handleAddOptionalFieldChange,
3452
- handleAddAllOptional,
3453
- handleResetToDefaultFields,
3454
- handleRevertChangesClick,
3455
- ]
3456
- );
3457
-
3458
- // The engine's own toolbar action, plus whatever the consumer added through
3459
- // `compactPanelProps.actions` — appended, so an outside action can never
3460
- // knock the form's toolbar out of the header. Only `compactToolbar={false}`
3461
- // removes it, and then the header carries just the consumer's own actions.
3462
- const compactHeaderActions = useMemo(
3463
- () => [
3464
- ...(compactToolbarParts ? [{ as: CompactToolbar, responsive: false }] : []),
3465
- ...(compactPanelProps?.actions || []),
3466
- ],
3467
- [compactPanelProps?.actions, compactToolbarParts]
3468
- );
3469
- const renderCompact = () => {
3470
- const headerBg = `${changeDarkness(getMainBackgroundColor(theme), 0.02)}${percentToHexAlpha(88)}`;
3471
- // Toolbar filters narrow the listed rows; the meter reflects the full set.
3472
- const query = compactQuery.trim().toLowerCase();
3473
- const matchesQuery = (optionName: string): boolean =>
3474
- !query || (options?.[optionName]?.display_name || optionName).toLowerCase().includes(query);
3475
- const matchesFilters = (optionName: string): boolean => {
3476
- const schema = options?.[optionName];
3477
- if (requiredOnly && !(schema?.required || schema?.required_groups)) {
3478
- return false;
3479
- }
3480
- // A field that absorbs a sibling also answers for it: searching
3481
- // "language" has to surface the row that now holds the language control,
3482
- // or the field becomes unreachable through the filter.
3483
- if (matchesQuery(optionName)) return true;
3484
- const absorb = (schema as { absorb_fields?: string[] } | undefined)?.absorb_fields;
3485
- return !!absorb?.some((absorbedName) => matchesQuery(absorbedName));
3486
- };
3487
-
3488
- // Fields a sibling absorbs into its own container.
3489
- //
3490
- // A field declaring `absorb_fields: ['language']` renders that sibling's
3491
- // control inside its own row instead of letting it have a row of its own.
3492
- // The pair this exists for is a code editor and its language: they are one
3493
- // decision — "what is this code, and in what language" — and reading them
3494
- // as two unrelated rows one above the other is what the layout used to say.
3495
- //
3496
- // Absorbed fields stay in `grouped`, and therefore in the Set / Optional /
3497
- // Needs-attention counts and in the completion meter. Only the ROW is
3498
- // skipped. Dropping them from the buckets instead would leave the meter
3499
- // ("13/18 set", computed from `availableOptions`) disagreeing with the box
3500
- // badge beside it, which counts row entries.
3501
- const absorbedBy: Record<string, string> = {};
3502
- forEach(options, (option, optionName) => {
3503
- const absorb = (option as { absorb_fields?: string[] } | undefined)?.absorb_fields;
3504
- // A read-only form absorbs nothing. A host row shows an absorbed
3505
- // sibling only inside its EDITOR, and a read view never opens one — so
3506
- // the language a code field had absorbed would be nowhere on the page.
3507
- // Every set field gets a row of its own instead.
3508
- if (!absorb?.length || readOnly) return;
3509
- absorb.forEach((absorbedName) => {
3510
- // Only absorb a sibling that exists and is not already spoken for; a
3511
- // schema naming a missing field must not make the field disappear.
3512
- if (options?.[absorbedName] && !absorbedBy[absorbedName]) {
3513
- absorbedBy[absorbedName] = optionName;
3514
- }
3515
- });
3516
- });
3517
-
3518
- // Group the options by their raw `group` key, remembering the order groups
3519
- // first appear in the schema. Each row carries a `hidden` flag.
3520
- const groupOrder: string[] = [];
3521
- const grouped: Record<string, Array<{ name: string; hidden: boolean }>> = {};
3522
- const pushRow = (optionName: string, hidden: boolean) => {
3523
- const group = getOptionGroup(options?.[optionName]);
3524
- if (!grouped[group]) {
3525
- grouped[group] = [];
3526
- groupOrder.push(group);
3527
- }
3528
- grouped[group].push({ name: optionName, hidden });
3529
- };
3530
- // Listed (added / preselected / required) options that pass the filters.
3531
- forEach(shownOptions, (_option, optionName) => {
3532
- if (matchesFilters(optionName)) {
3533
- pushRow(optionName, false);
3534
- }
3535
- });
3536
- // Surface EVERY not-yet-added optional field as an addable (hidden) row, so
3537
- // the whole schema is browsable inline — they all land in the Optional box
3538
- // (hidden ⇒ 'optional' bucket) instead of being buried in the Fields menu.
3539
- // Narrowed by the same filters as the listed rows (search query + required-
3540
- // only). availableOptions (listed) and filteredOptions (these) are disjoint —
3541
- // the former is built from fixedValue keys, the latter excludes them — so a
3542
- // field is never both a listed and a hidden row.
3543
- // Not in a read-only form: a not-yet-added field is an offer to add it,
3544
- // and a reader cannot take it up.
3545
- if (!readOnly) {
3546
- forEach(filteredOptions, (_schema, optionName) => {
3547
- if (matchesFilters(optionName)) {
3548
- pushRow(optionName, true);
3549
- }
3550
- });
3551
- }
3552
-
3553
- // User sort (Fields menu → "Sort by"), applied WITHIN each group so the
3554
- // group sections and the required-group rails are preserved. Schema order is
3555
- // the default and the stable tiebreaker (Array.sort is stable, and each
3556
- // group's array is already in schema order). Clustering (renderGroupRows)
3557
- // still pulls required-group members together at the first member's — now
3558
- // sorted — slot.
3559
- if (compactSort !== 'schema') {
3560
- const labelOf = (name: string) => (options?.[name]?.display_name || name).toLowerCase();
3561
- const isUnset = (name: string) =>
3562
- isOptionValueEmpty((shownOptions as TQorusForm)[name]?.value);
3563
- const isFieldInvalid = (name: string) =>
3564
- !isOptionValid(
3565
- name,
3566
- getOptionFieldStorageType(
3567
- name,
3568
- (shownOptions as TQorusForm)[name]?.type,
3569
- options,
3570
- operators,
3571
- (shownOptions as TQorusForm)[name]?.op,
3572
- isRendererOnly
3573
- ),
3574
- (shownOptions as TQorusForm)[name]?.value
3575
- );
3576
- const comparator = (a: { name: string }, b: { name: string }): number => {
3577
- switch (compactSort) {
3578
- case 'alpha':
3579
- return labelOf(a.name).localeCompare(labelOf(b.name));
3580
- case 'alpha-desc':
3581
- return labelOf(b.name).localeCompare(labelOf(a.name));
3582
- // unset/invalid first — falsy(0) sorts after truthy(1), so b - a.
3583
- case 'unset':
3584
- return Number(isUnset(b.name)) - Number(isUnset(a.name));
3585
- case 'invalid':
3586
- return Number(isFieldInvalid(b.name)) - Number(isFieldInvalid(a.name));
3587
- default:
3588
- return 0;
3589
- }
3590
- };
3591
- forEach(grouped, (entries) => entries.sort(comparator));
3592
- }
3593
-
3594
- // Order groups by the consumer-supplied `sort` (when given), else first-seen.
3595
- const groupKeys = groupOrder.slice().sort((a, b) => {
3596
- const sa = groups?.[a]?.sort;
3597
- const sb = groups?.[b]?.sort;
3598
- if (sa != null && sb != null && sa !== sb) return sa - sb;
3599
- if (sa != null && sb == null) return -1;
3600
- if (sb != null && sa == null) return 1;
3601
- return groupOrder.indexOf(a) - groupOrder.indexOf(b);
3602
- });
3603
-
3604
- // Read-first STATUS per row → one of three boxes (Needs attention / Set /
3605
- // Optional), via the component-scope getOptionBucket (shared with the header
3606
- // count and the row dot, so box ↔ dot ↔ header always agree). Buckets keep
3607
- // schema-group order (thin sub-labels) and the required-group clustering: an
3608
- // unmet one-of group's members all land in attention and still rail up.
3609
- type TRowEntry = { name: string; hidden: boolean };
3610
- const buckets: Record<TFormEngineBoxKey, Record<string, TRowEntry[]>> = {
3611
- attention: {},
3612
- set: {},
3613
- optional: {},
3614
- };
3615
- const bucketGroups: Record<TFormEngineBoxKey, string[]> = {
3616
- attention: [],
3617
- set: [],
3618
- optional: [],
3619
- };
3620
- // Freeze the box of any field currently being edited (or whose one-of group
3621
- // has an edited member) to its last settled box — so finishing an edit that
3622
- // flips its status doesn't remount it in another box and steal focus.
3623
- const stableBucketOf = (entry: TRowEntry): TFormEngineBoxKey => {
3624
- const fresh = getOptionBucket(entry.name, entry.hidden);
3625
- const groupBeingEdited =
3626
- !entry.hidden &&
3627
- ((options?.[entry.name]?.required_groups as string[] | undefined) || []).some((g) =>
3628
- (requiredGroupsInfo.members[g] || []).some((m) => expandedOptions.includes(m))
3629
- );
3630
- if (!entry.hidden && (expandedOptions.includes(entry.name) || groupBeingEdited)) {
3631
- const memo = settledBucket.current[entry.name];
3632
- if (memo) return memo;
3633
- }
3634
- settledBucket.current[entry.name] = fresh;
3635
- return fresh;
3636
- };
3637
- // Read-only: one list, not three boxes. "Needs attention" is an
3638
- // instruction, and a reader has nothing to do with it — a required field
3639
- // nobody filled in is shown unset, without the amber. "Optional" holds
3640
- // the fields nobody set, which in a read view is a list of things that
3641
- // are not there, so it is left out altogether.
3642
- const readBucketOf = (entry: TRowEntry): TFormEngineBoxKey | undefined => {
3643
- const fresh = stableBucketOf(entry);
3644
- if (!readOnly) return fresh;
3645
- return fresh === 'optional' ? undefined : 'set';
3646
- };
3647
- groupKeys.forEach((groupName) => {
3648
- grouped[groupName].forEach((entry) => {
3649
- const b = readBucketOf(entry);
3650
- if (!b) return;
3651
- if (!buckets[b][groupName]) {
3652
- buckets[b][groupName] = [];
3653
- bucketGroups[b].push(groupName);
3654
- }
3655
- buckets[b][groupName].push(entry);
3656
- });
3657
- });
3658
- const bucketCount = (b: TFormEngineBoxKey) =>
3659
- bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
3660
-
3661
- /* The default is to collapse the Optional box, and an explicit pass is
3662
- what waives the "never collapse the sole box" escape below. Resolving it
3663
- here rather than in the parameter list is what makes the two
3664
- distinguishable — `['optional']` as a default and `['optional']` from a
3665
- caller mean different things. */
3666
- const compactCollapsedGroups = compactCollapsedGroupsProp ?? ['optional'];
3667
- const collapsedGroupsRequested = compactCollapsedGroupsProp !== undefined;
3668
- // "Is the Optional box the whole form?" — when nothing needs attention and
3669
- // nothing is set, collapsing it leaves a card with no visible content at all.
3670
- const onlyOptionalRows =
3671
- bucketCount('optional') > 0 && bucketCount('attention') === 0 && bucketCount('set') === 0;
3672
- // 'general' / 'optional' are the SYNTHETIC fallback group keys getOptionGroup
3673
- // assigns to fields with no explicit `group` — printing a "General"/"Optional"
3674
- // sub-label for those is just noise, so suppress it. BUT a consumer may also
3675
- // use 'general' as a REAL group (defining it in the `groups` prop and tagging
3676
- // fields with `group: 'general'`); in that case it's a named group like any
3677
- // other and DOES get its sub-label.
3678
- const showGroupSubLabel = (groupName: string) =>
3679
- (groupName !== 'general' && groupName !== 'optional') || !!groups?.[groupName];
3680
-
3681
- // `preselected` means "show this option by default when a new form is
3682
- // loaded, so it's visible to the user". An empty non-required preselected
3683
- // field buckets to 'optional' (nothing is wrong with it, and it has no
3684
- // value), and the Optional box is collapsed by default — so the very
3685
- // fields the schema asked to put in front of the author were the ones
3686
- // nobody could see. A preselected row therefore forces its box open.
3687
- //
3688
- // A hidden row is an addable the author has not put in the form yet, so it
3689
- // is never what `preselected` is talking about, however its schema is
3690
- // flagged.
3691
- const isPreselectedRow = (entry: TRowEntry) =>
3692
- !entry.hidden &&
3693
- !!(options?.[entry.name] as { preselected?: boolean } | undefined)?.preselected;
3694
- // ONLY the Optional box. A preselected field that HAS a value buckets to
3695
- // 'set' and one that fails validation to 'attention'; in both boxes the
3696
- // field is already accounted for, and whether those boxes start collapsed
3697
- // is the consumer's deliberate `compactCollapsedGroups` choice. Overriding
3698
- // them here forced the Set box open against an explicit request.
3699
- const groupHasPreselectedRow = (boxKey: TFormEngineBoxKey, groupName: string) =>
3700
- boxKey === 'optional' && (buckets[boxKey][groupName] || []).some(isPreselectedRow);
3701
- const boxHasPreselectedRows = (boxKey: TFormEngineBoxKey) =>
3702
- boxKey === 'optional' &&
3703
- bucketGroups[boxKey].some((groupName) => groupHasPreselectedRow(boxKey, groupName));
3704
- /* The pin, and where it lands. A nested sub-form sits inside the parent's
3705
- card and owns no scroll context, so it never pins. */
3706
- const stickyBoxHeaders = compactStickyBoxHeaders && !compactNested;
3707
- const boxStickyOffset = compactStickyOffset + compactToolbarHeight;
3708
- const STATUS_BOXES: Array<{
3709
- key: TFormEngineBoxKey;
3710
- label: string;
3711
- intent?: 'warning' | 'success';
3712
- icon: IReqoreIconName;
3713
- }> = [
3714
- {
3715
- key: 'attention',
3716
- label: compactBoxLabels?.attention ?? 'Needs attention',
3717
- intent: 'warning',
3718
- icon: 'ErrorWarningLine',
3719
- },
3720
- { key: 'set', label: compactBoxLabels?.set ?? 'Set', intent: 'success', icon: 'CheckLine' },
3721
- {
3722
- key: 'optional',
3723
- label: compactBoxLabels?.optional ?? 'Optional',
3724
- icon: 'CheckboxBlankCircleLine',
3725
- },
3726
- ];
3727
-
3728
- // Build the rows for one group: contiguous required-group members are pulled
3729
- // together at the first member's slot and rendered as a connected rail (flat
3730
- // rows — no wrapper — so the value surface applies normally; the rail + nodes
3731
- // are drawn per member). Narrow stacks fall back to flat rows.
3732
- // The absorbed siblings a host row must render, in schema order. Empty for
3733
- // every row that absorbs nothing, which is all of them by default.
3734
- const absorbedNamesFor = (hostName: string): string[] =>
3735
- Object.keys(absorbedBy).filter((absorbedName) => absorbedBy[absorbedName] === hostName);
3736
-
3737
- const renderGroupRows = (names: Array<{ name: string; hidden: boolean }>) => {
3738
- const renderRow = (
3739
- entry: { name: string; hidden?: boolean },
3740
- clustered: boolean,
3741
- clusterFirst?: boolean,
3742
- clusterLast?: boolean
3743
- ) => (
3744
- <CompactRow
3745
- key={entry.name}
3746
- optionName={entry.name}
3747
- optionField={
3748
- entry.hidden ?
3749
- ({
3750
- type: getOptionSchemaStorageType(options?.[entry.name], isRendererOnly),
3751
- value: undefined,
3752
- } as IQorusFormField)
3753
- : ((shownOptions as TQorusForm)[entry.name] as IQorusFormField)
3754
- }
3755
- hidden={entry.hidden}
3756
- clustered={clustered}
3757
- clusterFirst={clusterFirst}
3758
- clusterLast={clusterLast}
3759
- // The siblings this row renders inside itself instead of leaving
3760
- // them a row of their own — see `absorbedBy`.
3761
- absorbedFields={absorbedNamesFor(entry.name)}
3762
- />
3763
- );
3764
- // (Clustering runs in narrow mode too now — the "One of the below is
3765
- // required" box wraps the members regardless of width; it no longer relies
3766
- // on a contiguous rail.)
3767
- const emitted = new Set<string>();
3768
- const groupOf = (name: string) =>
3769
- (options?.[name]?.required_groups as string[] | undefined)?.[0];
3770
- // An absorbed field keeps its place in the buckets — so the Set /
3771
- // Optional counts and the completion meter still see it — but its host
3772
- // renders its control, so it gets no row of its own here.
3773
- names = names.filter((entry) => !absorbedBy[entry.name]);
3774
- return names.map((entry) => {
3775
- const grp = groupOf(entry.name);
3776
- if (!grp) return renderRow(entry, false);
3777
- if (emitted.has(grp)) return null;
3778
- const memberEntries = names.filter((e) => !e.hidden && groupOf(e.name) === grp);
3779
- if (memberEntries.length < 2) return renderRow(entry, false);
3780
- emitted.add(grp);
3781
- const railed = memberEntries.map((e, idx) =>
3782
- renderRow(e, true, idx === 0, idx === memberEntries.length - 1)
3783
- );
3784
- // An UNMET one-of group gets the explicit "One of the below is required"
3785
- // box (the Focus cluster). A met group needs no banner — the rail + the
3786
- // "Covers"/"Covered by" chips already say which member satisfies it.
3787
- if (requiredGroupsInfo.satisfiedBy[grp]) return railed;
3788
- return (
3789
- <StyledRequiredClusterBox
3790
- key={grp}
3791
- className='options-readfirst-required-cluster'
3792
- $border={`${cWarning}33`}
3793
- $tint={`${cWarning}0d`}
3794
- >
3795
- <StyledRequiredClusterHeader $color={cWarning}>
3796
- <ReqoreIcon icon='LinkM' size='11px' style={{ color: cWarning }} />
3797
- One of the below is required
3798
- </StyledRequiredClusterHeader>
3799
- {railed}
3800
- </StyledRequiredClusterBox>
3801
- );
3802
- });
3803
- };
3804
-
3805
- // One schema group inside a status box: its thin sub-label, the group's
3806
- // subtitle, then its rows. Factored out of the box body so the SAME markup
3807
- // renders both the groups a box shows directly and the ones it defers
3808
- // behind the inner collapse.
3809
- /** @param only when given, render just these rows of the group — the group
3810
- can be split across the shown and folded halves of a capped box, and its
3811
- label must appear above whichever half holds its first row. */
3812
- const renderBoxGroup = (boxKey: TFormEngineBoxKey, groupName: string, only?: Set<string>) => {
3813
- const groupConfig = groups?.[groupName];
3814
- const rows =
3815
- only ?
3816
- (buckets[boxKey][groupName] || []).filter((entry) => only.has(entry.name))
3817
- : buckets[boxKey][groupName];
3818
- if (only && !rows.length) {
3819
- return null;
3820
- }
3821
- return (
3822
- <React.Fragment key={groupName}>
3823
- {showGroupSubLabel(groupName) ?
3824
- <StyledStatusBoxGroupLabel>
3825
- {getOptionGroupLabel(groupName, groups)}
3826
- </StyledStatusBoxGroupLabel>
3827
- : null}
3828
- {showGroupSubLabel(groupName) && groupConfig?.subtitle ?
3829
- <ReqoreP
3830
- size='small'
3831
- effect={{ opacity: 0.6 }}
3832
- style={{
3833
- marginTop: 2,
3834
- marginBottom: 8,
3835
- marginLeft: GROUP_INDENT,
3836
- paddingRight: 10,
3837
- }}
3838
- >
3839
- {groupConfig.subtitle}
3840
- </ReqoreP>
3841
- : null}
3842
- {renderGroupRows(rows)}
3843
- </React.Fragment>
3844
- );
3845
- };
3846
-
3847
- return (
3848
- <OptionsContext.Provider value={{ schema: options, value: availableOptions }}>
3849
- <CompactRowContext.Provider value={compactRowContextValue}>
3850
- <ReqoreErrorBoundary>
3851
- {showHelpForOption && (
3852
- <OptionsHelpDialog
3853
- onClose={() => setShowHelpForOption(undefined)}
3854
- option={options[showHelpForOption]}
3855
- />
3856
- )}
3857
- <CompactToolbarContext.Provider value={compactToolbarContextValue}>
3858
- <StyledCompactWrap
3859
- ref={setCompactWrap}
3860
- className='options-readfirst-scroll'
3861
- // A nested sub-form is never the scroller — it sits inside the
3862
- // parent form's card, which is inside whatever actually scrolls.
3863
- $ownScroll={compactScroll === 'own' && !compactNested}
3864
- >
3865
- <StyledCompactPanel
3866
- // Mirrors the toolbar's own search-row gate: when no search
3867
- // row renders, the header is a thin strip and sits tight to
3868
- // the first status box (see StyledCompactPanel).
3869
- $tightHeader={!(compactToolbarParts?.search && size(options) > 1)}
3870
- // The top-level form scrolls, so its toolbar STICKS and carries a
3871
- // dark blurred backdrop so content ghosts cleanly beneath it. A
3872
- // nested (arg_schema) sub-form owns no scroll context — drop the
3873
- // sticky, the backdrop, and the stacking context so its header is
3874
- // transparent inside the parent's card.
3875
- $headerBg={compactNested ? 'transparent' : headerBg}
3876
- $nested={compactNested}
3877
- // Keeps the wrapper from clipping, which is what a pinned box
3878
- // header inside it needs — see StyledCompactPanel.
3879
- $stickyBoxes={compactStickyBoxHeaders && !compactNested}
3880
- // A narrow header the consumer dressed stacks its toolbar
3881
- // under the label and actions (see StyledCompactPanel).
3882
- $stackHeader={
3883
- compactNarrow &&
3884
- !!(
3885
- compactPanelProps?.label ||
3886
- compactPanelProps?.icon ||
3887
- compactPanelProps?.actions?.length
3888
- )
3889
- }
3890
- flat
3891
- raised
3892
- minimal
3893
- // No panel background: the form sits transparently on whatever
3894
- // hosts it (page, drawer, or — for an arg_schema field — the
3895
- // parent's edit card) instead of stacking its own dark surface.
3896
- // The status boxes keep their own tints; the sticky toolbar keeps
3897
- // its blurred header via the $headerBg override.
3898
- // NARROW: the toolbar gives up its pin so the box headers
3899
- // can keep theirs.
3900
- //
3901
- // Measured at 380px, a pinned toolbar is 124px and a pinned
3902
- // box header is 48 — together a third of a phone's readable
3903
- // body, before the form has drawn a line. Only one of them
3904
- // can be afforded there, and the box header is the better
3905
- // buy: the toolbar is chrome you GO to (a search, a field
3906
- // picker, a meter — all of it still one scroll away at the
3907
- // top of the form), while the box header is chrome you READ,
3908
- // and it is the only thing that says which bucket the rows
3909
- // under the pointer are in.
3910
- //
3911
- // Same shape of decision the engine already makes one level
3912
- // down, where `optionActionsCollapse: 'auto'` folds a row's
3913
- // actions into a menu because a narrow row has no width for
3914
- // a button strip.
3915
- //
3916
- // A consumer can still force either way: `compactPanelProps`
3917
- // spreads after this.
3918
- stickyHeader={!compactNested && !(compactNarrow && compactStickyBoxHeaders)}
3919
- // Below the host's own pinned chrome, if it has any — see
3920
- // `compactStickyOffset`.
3921
- stickyHeaderOffset={compactStickyOffset || undefined}
3922
- // Consumer overrides land last so they win over every default
3923
- // above; `actions` and `contentStyle` below merge rather than
3924
- // replace (see `compactPanelProps`).
3925
- {...compactPanelProps}
3926
- actions={compactHeaderActions}
3927
- contentStyle={{
3928
- display: 'flex',
3929
- flexFlow: 'column',
3930
- gap: '10px',
3931
- /* The panel body must not be a SCROLL CONTAINER.
3932
-
3933
- `StyledPanelContent` ships `overflow: auto` so a
3934
- height-capped panel can scroll its body. This one is never
3935
- capped — in `compactScroll: 'host'` the page scrolls and in
3936
- 'own' the wrap above does — so the `auto` never engages and
3937
- reads as inert. It is not: an `overflow: auto` box is a
3938
- scrollport whether or not it scrolls, and every
3939
- `position: sticky` inside resolves against IT. The status
3940
- box headers (and the toolbar, in a nested host) would pin
3941
- to a box that never moves, which looks exactly like sticky
3942
- being ignored.
3943
-
3944
- `overflow-x: clip`, NOT `hidden`, for the reason spelled
3945
- out on `StyledCompactWrap`: CSS coerces an `overflow-y:
3946
- visible` to `auto` unless the other axis is visible or
3947
- clip, which would put the scrollport straight back. `clip`
3948
- keeps the horizontal guard the row ellipsis relies on. */
3949
- overflowY: 'visible',
3950
- overflowX: 'clip',
3951
- ...compactPanelProps?.contentStyle,
3952
- }}
3953
- >
3954
- {size(groupKeys) === 0 || (readOnly && !bucketCount('set')) ?
3955
- <ReqoreMessage flat opaque={false} size='small'>
3956
- {readOnly && !query ?
3957
- 'Nothing is set.'
3958
- : 'No fields match the current filters.'}
3959
- </ReqoreMessage>
3960
- : null}
3961
-
3962
- {/* A read-only form is one flat list of what is set: no
3963
- "Set" heading with a count over it (there is nothing else
3964
- it could be), no green tint (a colour that means "done"
3965
- on a form nobody is doing), nothing to collapse — and no
3966
- `maxFieldsShown` fold: "12 more fields" behind a click is
3967
- the reader's record withheld, and a page scrolls. */}
3968
- {readOnly ?
3969
- bucketCount('set') ?
3970
- <div className='options-readfirst-read' style={{ padding: '4px 4px 6px' }}>
3971
- <StyledGroupBody
3972
- $divider={cDivider}
3973
- $hover={cHover}
3974
- $focus={cWarning}
3975
- $success={cSuccess}
3976
- $rowBg={cRowBg}
3977
- $lineColor={cGroupLine}
3978
- $moreBorder={cMoreBorder}
3979
- className={compactNarrow ? 'readfirst-narrow' : undefined}
3980
- >
3981
- {bucketGroups.set.map((groupName) => renderBoxGroup('set', groupName))}
3982
- </StyledGroupBody>
3983
- </div>
3984
- : null
3985
- : STATUS_BOXES.map((box) => {
3986
- const groupsInBox = bucketGroups[box.key];
3987
- const count = bucketCount(box.key);
3988
- if (!count) return null;
3989
- const hasPreselectedRows = boxHasPreselectedRows(box.key);
3990
- const wouldCollapse =
3991
- compactCollapsedGroups.includes(box.key) &&
3992
- !query &&
3993
- !(box.key === 'optional' && onlyOptionalRows && !collapsedGroupsRequested);
3994
- // Only a box that was opened SOLELY to reveal a preselected
3995
- // row splits its body; a box that renders open anyway shows
3996
- // all of its groups as before.
3997
- const splitForPreselected = wouldCollapse && hasPreselectedRows;
3998
-
3999
- // Which rows this box shows, and which fold away, decided at
4000
- // ROW level because `maxFieldsShown` counts fields — a
4001
- // group-level split can only ever cut at a group boundary.
4002
- // Group order is preserved so the visible rows read in the
4003
- // same order they would if nothing were folded.
4004
- const orderedRows: TRowEntry[] = groupsInBox.flatMap(
4005
- (groupName) => buckets[box.key][groupName] || []
4006
- );
4007
- // Optional's own rule first: a box opened solely to reveal
4008
- // preselected rows offers those, not an arbitrary first-N.
4009
- const candidateRows =
4010
- splitForPreselected ? orderedRows.filter(isPreselectedRow) : orderedRows;
4011
- // …then the cap applies to whatever that left.
4012
- const capped =
4013
- maxFieldsShown !== undefined && candidateRows.length > maxFieldsShown;
4014
- const shownRows =
4015
- capped ? candidateRows.slice(0, maxFieldsShown) : candidateRows;
4016
- const shownRowSet = new Set(shownRows.map((entry) => entry.name));
4017
- // Everything the box holds that is not on show. Once the
4018
- // reader asks for more they get the whole box — withholding
4019
- // the non-preselected remainder behind a second control is
4020
- // the same defect one level down.
4021
- const deferredRowSet = new Set(
4022
- orderedRows
4023
- .filter((entry) => !shownRowSet.has(entry.name))
4024
- .map((entry) => entry.name)
4025
- );
4026
- const deferredCount = deferredRowSet.size;
4027
- const shownGroups = groupsInBox.filter((groupName) =>
4028
- (buckets[box.key][groupName] || []).some((entry) =>
4029
- shownRowSet.has(entry.name)
4030
- )
4031
- );
4032
- const deferredGroups = groupsInBox.filter((groupName) =>
4033
- (buckets[box.key][groupName] || []).some(
4034
- (entry) => !shownRowSet.has(entry.name)
4035
- )
4036
- );
4037
- const moreOpen = Boolean(query) || expandedMoreBoxes.includes(box.key);
4038
- const accent =
4039
- box.key === 'attention' ? cWarning
4040
- : box.key === 'set' ? cSuccess
4041
- : cMuted;
4042
- // The muted "Optional" box reads as a quieter, recessed
4043
- // surface — a touch darker than the page rather than the
4044
- // faint grey tint the accent would give.
4045
- const boxBg =
4046
- box.key === 'optional' ?
4047
- changeDarkness(getMainBackgroundColor(theme), 0.06)
4048
- : undefined;
4049
- return (
4050
- <StyledStatusBox
4051
- $accent={accent}
4052
- $bg={boxBg}
4053
- // Only a box that PINS needs an opaque header (see
4054
- // StyledStatusBox); passing it unconditionally would
4055
- // repaint every form's boxes for nothing.
4056
- $surface={stickyBoxHeaders ? getMainBackgroundColor(theme) : undefined}
4057
- key={box.key}
4058
- ref={(node: HTMLDivElement | null) => {
4059
- statusBoxRefs.current[box.key] = node;
4060
- }}
4061
- // Opening arms the reveal; collapsing disarms it, so a
4062
- // close never scrolls.
4063
- onCollapseChange={(collapsed) =>
4064
- setOpenedStatusBox(collapsed ? undefined : box.key)
4065
- }
4066
- flat
4067
- minimal
4068
- /* The header stays on screen while its own box is
4069
- being read — see `compactStickyBoxHeaders`. Sticky
4070
- is bounded by its containing block, so the header
4071
- leaves with its box and only one is ever pinned. */
4072
- stickyHeader={stickyBoxHeaders}
4073
- stickyHeaderOffset={stickyBoxHeaders ? boxStickyOffset : undefined}
4074
- collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
4075
- collapsible
4076
- // Which boxes start closed is the consumer's call
4077
- // (`compactCollapsedGroups`, default `['optional']`: the
4078
- // Optional box holds every not-yet-added field, so a form
4079
- // opens on what is in use). Two rules override it:
4080
- //
4081
- // A SEARCH forces every box open — ReqorePanel unmounts
4082
- // collapsed content, so a match inside a closed box would
4083
- // be unreachable. (isCollapsed is the panel's controllable
4084
- // state; manual toggling still works with no query set.)
4085
- //
4086
- // The Optional box stays open when it is the ONLY box,
4087
- // because collapsing it renders a card that looks empty and
4088
- // broken: an auth profile's Authorization block, whose nine
4089
- // requirement fields are all optional, showed as a titled
4090
- // card containing one collapsed "Optional 9" strip and
4091
- // nothing else. This does not generalise to the other two —
4092
- // a lone Set box collapsed is a deliberate ask, and its
4093
- // header still reports what is in there.
4094
- //
4095
- // A PRESELECTED row overrides it too: the schema asked
4096
- // for that field to be visible on a new form, so the
4097
- // box it lands in opens. The groups it does NOT appear
4098
- // in stay behind an inner collapse below, so opening
4099
- // the box reveals the preselected fields without also
4100
- // dumping every not-yet-added optional field on the
4101
- // author.
4102
- isCollapsed={wouldCollapse && !hasPreselectedRows}
4103
- label={
4104
- <StyledGroupHeader>
4105
- <ReqoreP effect={{ weight: 'bold' }} size='normal'>
4106
- {box.label}
4107
- </ReqoreP>
4108
- <ReqoreTag
4109
- size='small'
4110
- minimal
4111
- compact
4112
- intent={box.intent}
4113
- label={String(count)}
4114
- />
4115
- </StyledGroupHeader>
4116
- }
4117
- icon={box.icon}
4118
- className='options-readfirst-group'
4119
- padded={false}
4120
- contentStyle={{ padding: '4px 4px 6px' }}
4121
- >
4122
- {/* ONE group body per box: every field block (and the
4123
- thin schema sub-labels) is a direct flex child, so the
4124
- inter-field gap is identical everywhere — including
4125
- across schema-group boundaries. */}
4126
- <StyledGroupBody
4127
- $divider={cDivider}
4128
- $hover={cHover}
4129
- $focus={cWarning}
4130
- $success={cSuccess}
4131
- $rowBg={cRowBg}
4132
- $lineColor={cGroupLine}
4133
- $moreBorder={cMoreBorder}
4134
- className={compactNarrow ? 'readfirst-narrow' : undefined}
4135
- >
4136
- {shownGroups.map((groupName) =>
4137
- renderBoxGroup(box.key, groupName, shownRowSet)
4138
- )}
4139
- {/* The control lives INSIDE the group body, as the last
4140
- row, so it inherits the rows' grid, padding and
4141
- hover and lines up with them. Outside it was a bare
4142
- line of text in the box's corner, which is what the
4143
- build #154 review rejected. */}
4144
- {deferredCount ?
4145
- <button
4146
- type='button'
4147
- className='readfirst-row readfirst-more-row options-readfirst-more'
4148
- aria-expanded={moreOpen}
4149
- onClick={() =>
4150
- setExpandedMoreBoxes((prev) =>
4151
- prev.includes(box.key) ?
4152
- prev.filter((k) => k !== box.key)
4153
- : [...prev, box.key]
4154
- )
4155
- }
4156
- >
4157
- <ReqoreP size='small' effect={{ opacity: 0.8 }}>
4158
- {moreOpen ?
4159
- 'Show fewer'
4160
- : `${deferredCount} more ${box.key === 'optional' ? 'optional ' : ''}field${deferredCount === 1 ? '' : 's'}`
4161
- }
4162
- </ReqoreP>
4163
- <ReqoreIcon
4164
- icon='ArrowDownSLine'
4165
- size='small'
4166
- className='readfirst-more-chevron'
4167
- effect={{ opacity: 0.8 }}
4168
- />
4169
- </button>
4170
- : null}
4171
- {/* A search forces the fold open for the same reason
4172
- the boxes open: an unmounted row cannot be found. */}
4173
- {deferredCount && moreOpen ?
4174
- deferredGroups.map((groupName) =>
4175
- renderBoxGroup(box.key, groupName, deferredRowSet)
4176
- )
4177
- : null}
4178
- </StyledGroupBody>
4179
- </StyledStatusBox>
4180
- );
4181
- })
4182
- }
4183
- </StyledCompactPanel>
4184
-
4185
- {/* Batched commit: the Save/Discard bar docks bottom-right, floating
4186
- over the rows while anything is staged — the product's draft
4187
- convention (edits are a draft until explicitly applied). Save is
4188
- gated on overall validity; Discard is the existing revert-all. */}
4189
- {commitMode === 'batched' && !readOnly && dirtyOptionNames.length ?
4190
- <StyledCommitDock $bg={cBg} $border={cDivider}>
4191
- <ReqoreControlGroup
4192
- className='options-readfirst-commitbar'
4193
- verticalAlign='center'
4194
- wrap
4195
- >
4196
- <ReqoreTag
4197
- size='tiny'
4198
- minimal
4199
- flat
4200
- compact
4201
- effect={{ uppercase: true, spaced: 1 }}
4202
- intent='warning'
4203
- icon='EditLine'
4204
- label={`${dirtyOptionNames.length} unsaved change${dirtyOptionNames.length === 1 ? '' : 's'}`}
4205
- />
4206
- <ReqoreButton
4207
- size='small'
4208
- intent='success'
4209
- icon='CheckLine'
4210
- fixed
4211
- className='options-readfirst-save'
4212
- disabled={!validityData.isValid}
4213
- tooltip={
4214
- validityData.isValid ?
4215
- 'Apply the staged changes'
4216
- : 'Resolve the invalid fields before saving'
4217
- }
4218
- onClick={handleCommitClick}
4219
- >
4220
- Save
4221
- </ReqoreButton>
4222
- <ReqoreButton
4223
- size='small'
4224
- minimal
4225
- flat
4226
- fixed
4227
- icon='HistoryLine'
4228
- className='options-readfirst-discard'
4229
- onClick={handleRevertChangesClick}
4230
- >
4231
- Discard
4232
- </ReqoreButton>
4233
- </ReqoreControlGroup>
4234
- </StyledCommitDock>
4235
- : null}
4236
- </StyledCompactWrap>
4237
- </CompactToolbarContext.Provider>
4238
- </ReqoreErrorBoundary>
4239
- </CompactRowContext.Provider>
4240
- </OptionsContext.Provider>
4241
- );
4242
- };
4243
-
4244
- /* Which of the seven waits is holding the form, in the order they are
4245
- tested. Rendered as `data-wait` on the placeholder so a page can be asked
4246
- what it is waiting FOR instead of only that it is waiting. */
4247
- const waitReason =
4248
- rest.skeleton ? (rest.skeletonReason ? `host:${rest.skeletonReason}` : 'host')
4249
- : templates.loading ? 'templates'
4250
- : typesLoading ? 'types'
4251
- : optionsLoading ? 'options'
4252
- : loading ? 'fetch'
4253
- : operatorsUrl && !operators ? 'operators'
4254
- : (url || customUrl) && !options ? 'schema'
4255
- : undefined;
4256
-
4257
- if (waitReason) {
4258
- /* The shape of the FORM, not a generic block arrangement. This used to be
4259
- three bars over three big panels, which resembles nothing this gate is
4260
- waiting for and shared no vocabulary with the other waits on the same
4261
- page — see `FormFieldsSkeleton`. */
4262
- return <FormFieldsSkeleton fill className='options-loading-skeleton' reason={waitReason} />;
4263
- }
4264
-
4265
- // A loader that rejected (and produced no usable schema) surfaces its error
4266
- // instead of the generic "No options available" empty state.
4267
- if (optionsError && (!options || !size(options))) {
4268
- return (
4269
- <ReqoreMessage intent='danger' opaque={false}>
4270
- {optionsError}
4271
- </ReqoreMessage>
4272
- );
4273
- }
4274
-
4275
- if (!options || !size(options)) {
4276
- return (
4277
- <ReqoreMessage intent='warning' opaque={false}>
4278
- No options available
4279
- </ReqoreMessage>
4280
- );
4281
- }
4282
-
4283
- if (compact) {
4284
- return renderCompact();
4285
- }
4286
-
4287
- return (
4288
- <OptionsContext.Provider value={{ schema: options, value: availableOptions }}>
4289
- <ReqoreErrorBoundary>
4290
- {showHelpForOption && (
4291
- <OptionsHelpDialog
4292
- onClose={() => setShowHelpForOption(undefined)}
4293
- option={options[showHelpForOption]}
4294
- />
4295
- )}
4296
- {recordRequiresSearchOptions && !readOnly ?
4297
- <>
4298
- <ReqoreMessage intent='info'>
4299
- This provider record requires some search options to be set. You can set them below.
4300
- </ReqoreMessage>
4301
- <ReqoreVerticalSpacer height={10} />
4302
- </>
4303
- : null}
4304
- <ReqoreCollection
4305
- minColumnWidth='350px'
4306
- flat
4307
- padded={false}
4308
- minimal
4309
- {...rest}
4310
- label={compact ? undefined : rest.label}
4311
- responsiveTitle={false}
4312
- inputProps={{
4313
- disabled: size(availableOptions) < 2,
4314
- }}
4315
- labelSize={4}
4316
- filterable={'filterable' in rest ? rest.filterable : !compact}
4317
- sortable={'sortable' in rest ? rest.sortable : !compact && size(availableOptions) > 1}
4318
- defaultSortBy={null}
4319
- contentRenderer={(children) => (
4320
- <>
4321
- {size(validityData.invalidFields) && !readOnly ?
4322
- <ReqoreMessage
4323
- intent={showInvalidOptionsOnly ? 'info' : 'danger'}
4324
- opaque={false}
4325
- size='small'
4326
- margin='bottom'
4327
- onClick={() => setShowInvalidOptionsOnly(!showInvalidOptionsOnly)}
4328
- >
4329
- {showInvalidOptionsOnly ?
4330
- 'Showing invalid fields only. Click here again to show all fields.'
4331
- : `${size(validityData.invalidFields) < 2 ? 'A field is not valid and requires' : `${size(validityData.invalidFields)} fields are not valid and require`} attention. Click here to only show invalid fields.`
4332
- }
4333
- </ReqoreMessage>
4334
- : null}
4335
- {unavailableOptionsCount.current ?
4336
- <>
4337
- <ReqoreMessage intent='warning' opaque={false} size='small' margin='bottom'>
4338
- {`${unavailableOptionsCount.current} fields(s) hidden because they are not supported on the current instance`}
4339
- </ReqoreMessage>
4340
- <ReqoreVerticalSpacer height={10} />
4341
- </>
4342
- : null}
4343
- {children}
4344
- {size(filteredOptions) >= 1 && !readOnly ?
4345
- <>
4346
- <ReqoreVerticalSpacer height={10} />
4347
- <SelectFormField
4348
- items={optionalFields}
4349
- onChange={(val) => handleAddOptionalFieldChange('options', val)}
4350
- placeholder={placeholder || 'More Options Available'}
4351
- minimal
4352
- icon='ListView'
4353
- showDescription={false}
4354
- fluid
4355
- size={rest.size}
4356
- />
4357
- </>
4358
- : null}
4359
- </>
4360
- )}
4361
- sortButtonProps={{ minimal: true }}
4362
- showLayoutSwitch={false}
4363
- style={{ width: '100%' }}
4364
- items={map(
4365
- shownOptions,
4366
- ({ type, ...other }, optionName): IReqoreCollectionItemProps => ({
4367
- label: options[optionName]?.display_name || optionName,
4368
- tags: (options[optionName] as any)?.tags,
4369
- labelEffect: {
4370
- textSize: '11px',
4371
- uppercase: true,
4372
- opacity: 0.9,
4373
- },
4374
- labelProps: {
4375
- style:
4376
- options[optionName]?.short_desc ?
4377
- {
4378
- cursor: 'help',
4379
- }
4380
- : undefined,
4381
- onClick:
4382
- options[optionName]?.desc ? () => handleOptionLabelClick(optionName) : undefined,
4383
- },
4384
- showLabelTooltip: !!options[optionName]?.short_desc,
4385
- customLabelTooltip: {
4386
- delay: 300,
4387
- content: (
4388
- <Description
4389
- longDescription={`${options[optionName]?.short_desc}${options[optionName]?.desc ? '. Click to learn more.' : ''}`}
4390
- margin='none'
4391
- />
4392
- ),
4393
- },
4394
- description:
4395
- showFieldTypes ?
4396
- `<${options[optionName].ui_type || options[optionName].type}${options[optionName]?.ui_element_type ? `[${options[optionName].ui_element_type}]` : ''}> ${options[optionName].short_desc || ''}`
4397
- : options[optionName].short_desc || '',
4398
- descriptionEffect: {
4399
- textSize: '11px',
4400
- opacity: 0.3,
4401
- underline: options[optionName]?.desc ? 'underline dotted #878787' : undefined,
4402
- },
4403
- customTheme: {
4404
- main: 'main:darken:1',
4405
- },
4406
- flat: false,
4407
- icon:
4408
- !isOptionValid(optionName, type as TQorusType, (other as any).value) ? 'SpamFill'
4409
- : undefined,
4410
- iconColor:
4411
- !isOptionValid(optionName, type as TQorusType, (other as any).value) ?
4412
- 'danger:lighten:5'
4413
- : undefined,
4414
- transparent: false,
4415
- intent: getIntent(
4416
- optionName,
4417
- type as TQorusType,
4418
- (other as any).value,
4419
- (other as any).op
4420
- ),
4421
- badge: buildBadges(options[optionName], optionName),
4422
- className: 'system-option',
4423
- // Ported from IDE Options (commit 8e6b7781): full-width layout
4424
- // for stretchable option types.
4425
- stretch:
4426
- STRECHABLE_TYPES.has(options[optionName].type as TQorusType) ||
4427
- (options[optionName] as { stretch?: boolean }).stretch,
4428
- size: 'small',
4429
- /* No floating bar. It is portalled over whatever sits above the
4430
- row — on the first row, the form's own header buttons, which it
4431
- made unclickable — and a bar that only appears on a hover is out
4432
- of reach on a phone. The row's actions render in its own
4433
- header: injected ones as buttons (all of them in the menu on
4434
- touch or a narrow viewport), the row's own secondary ones in
4435
- its ⋯ menu. */
4436
- actions: optionRowActions({
4437
- injected: resolveOptionActions(optionActions, {
4438
- name: optionName,
4439
- schema: options[optionName],
4440
- value: availableOptions?.[optionName] as TOption,
4441
- }).filter((action) => action.show !== false),
4442
- collapse: collapseOptionActions,
4443
- menu: [
4444
- !readOnly &&
4445
- type !== 'code-editor' &&
4446
- ((options[optionName] as any)?.ui_type || options[optionName]?.type) !==
4447
- 'code-editor' && {
4448
- label: 'Focused editing',
4449
- icon: 'FullscreenLine',
4450
- className: 'options-item-fullscreen',
4451
- onClick: () => setFocusedEditing(optionName),
4452
- },
4453
- !readOnly &&
4454
- hasOptionChanged((other as any).value, optionName) && {
4455
- label: 'Revert changes',
4456
- icon: 'HistoryLine',
4457
- className: 'options-item-revert',
4458
- onClick: () => {
4459
- handleValueChange(
4460
- optionName,
4461
- originalValue.current?.[optionName]?.value,
4462
- originalValue.current?.[optionName]?.type
4463
- );
4464
- },
4465
- },
4466
- !readOnly &&
4467
- (other as any).value &&
4468
- !isEqual((other as any).value, getDefaultValue(options[optionName])) &&
4469
- !(options[optionName]?.disabled || (options[optionName] as any)?.readonly) && {
4470
- label: 'Remove value',
4471
- icon: 'CloseLine',
4472
- className: 'options-item-remove',
4473
- onClick: () => handleValueChange(optionName, undefined),
4474
- },
4475
- !readOnly &&
4476
- !options[optionName]?.preselected &&
4477
- !options[optionName]?.required && {
4478
- label: 'Remove option',
4479
- icon: 'DeleteBinLine',
4480
- intent: 'danger',
4481
- className: 'options-optional-remove',
4482
- onClick: () => {
4483
- confirmAction({
4484
- title: 'Remove Selected Option',
4485
- onConfirm: () => {
4486
- removeSelectedOption(optionName);
4487
- },
4488
- });
4489
- },
4490
- },
4491
- ],
4492
- }),
4493
- content: (
4494
- <FocusedEditing
4495
- isFullscreen={focusedEditing === optionName}
4496
- onClose={() => setFocusedEditing(undefined)}
4497
- description={options[optionName]?.display_name}
4498
- intent={getIntent(
4499
- optionName,
4500
- type as TQorusType,
4501
- (other as any).value,
4502
- (other as any).op
4503
- )}
4504
- >
4505
- {focusedEditing === optionName ?
4506
- <Description
4507
- longDescription={options[optionName]?.desc}
4508
- shortDescription={options[optionName]?.short_desc}
4509
- longDescriptionShownByDefault
4510
- />
4511
- : null}
4512
- {renderOption(optionName, { type, ...other } as IQorusFormField)}
4513
- </FocusedEditing>
4514
- ),
4515
- })
4516
- )}
4517
- defaultZoom={1}
4518
- zoomable={false}
4519
- actions={[
4520
- ...(rest.actions || []).map((action) => ({
4521
- ...action,
4522
- minimal: true,
4523
- })),
4524
- {
4525
- icon: 'HistoryLine',
4526
- className: 'fields-revert',
4527
- tooltip: 'Revert Changes',
4528
- minimal: true,
4529
- position: 'right',
4530
- show:
4531
- showTypeToggle &&
4532
- !readOnly &&
4533
- !!originalValue.current &&
4534
- !isEqual(localValue.fields, originalValue.current),
4535
- onClick: handleRevertChangesClick,
4536
- },
4537
- {
4538
- icon: 'CodeLine',
4539
- minimal: true,
4540
- className: 'fields-show-types',
4541
- tooltip: showFieldTypes ? 'Hide field types' : 'Show field types',
4542
- intent: showFieldTypes ? 'info' : undefined,
4543
- show: showTypeToggle,
4544
- position: 'right',
4545
- onClick: handleShowFieldTypesClick,
4546
- },
4547
- ]}
4548
- />
4549
- </ReqoreErrorBoundary>
4550
- </OptionsContext.Provider>
4551
- );
4552
- };
4553
-
4554
- /**
4555
- * Publishes the host's markdown renderer to every description this form draws.
4556
- *
4557
- * A provider rather than prop-drilling because the descriptions are not in one
4558
- * place: the inline row description, the focused-editing header and the field
4559
- * help dialog are three components at three depths, and the dialog is not even
4560
- * a child of the row it belongs to. Threading a renderer to each of them by
4561
- * hand is how one of them gets missed.
4562
- */
4563
- export const FormEngine = (props: IFormEngineProps) => (
4564
- <MarkdownRendererContext.Provider value={props.markdownRenderer}>
4565
- <FormEngineImpl {...props} />
4566
- </MarkdownRendererContext.Provider>
4567
- );
4568
-
4569
- export default FormEngine;