@qoretechnologies/reqraft 0.10.53 → 0.10.54-pr.115.g9bc96ed

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 (283) hide show
  1. package/.claude/CLAUDE.md +25 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
  4. package/README.md +1 -1
  5. package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
  6. package/design/FORM_ENGINE_FIELD_MIGRATION.md +56 -1
  7. package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
  8. package/design/SMART_EDITOR_UX.md +21 -0
  9. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  10. package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
  11. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  12. package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
  13. package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
  14. package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
  15. package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
  16. package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
  17. package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
  18. package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
  19. package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
  20. package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
  21. package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
  22. package/dist/components/dpqlEditor/dpqlTags.js +5 -1
  23. package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
  24. package/dist/components/dpqlEditor/index.d.ts +4 -0
  25. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  26. package/dist/components/dpqlEditor/index.js +5 -1
  27. package/dist/components/dpqlEditor/index.js.map +1 -1
  28. package/dist/components/dpqlEditor/types.d.ts +42 -2
  29. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  30. package/dist/components/dpqlEditor/types.js +0 -4
  31. package/dist/components/dpqlEditor/types.js.map +1 -1
  32. package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
  33. package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
  34. package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
  35. package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
  36. package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
  37. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  38. package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
  39. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  40. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  41. package/dist/components/form/engine/CompactRow.js +192 -45
  42. package/dist/components/form/engine/CompactRow.js.map +1 -1
  43. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  44. package/dist/components/form/engine/CompactToolbar.js +5 -2
  45. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  46. package/dist/components/form/engine/FormEngine.d.ts +160 -12
  47. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  48. package/dist/components/form/engine/FormEngine.js +376 -75
  49. package/dist/components/form/engine/FormEngine.js.map +1 -1
  50. package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
  51. package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
  52. package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
  53. package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
  54. package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
  55. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  56. package/dist/components/form/engine/OptionFieldMessages.js +61 -11
  57. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  58. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  59. package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
  60. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  61. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  62. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  63. package/dist/components/form/engine/readFirst.d.ts +42 -1
  64. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  65. package/dist/components/form/engine/readFirst.js +74 -7
  66. package/dist/components/form/engine/readFirst.js.map +1 -1
  67. package/dist/components/form/engine/rendererTypes.d.ts +13 -0
  68. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
  69. package/dist/components/form/engine/rendererTypes.js.map +1 -1
  70. package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
  71. package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
  72. package/dist/components/form/engine/rowMenuContext.js +103 -0
  73. package/dist/components/form/engine/rowMenuContext.js.map +1 -0
  74. package/dist/components/form/engine/typeChoices.d.ts +37 -0
  75. package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
  76. package/dist/components/form/engine/typeChoices.js +25 -0
  77. package/dist/components/form/engine/typeChoices.js.map +1 -0
  78. package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
  79. package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
  80. package/dist/components/form/expressions/DpqlRendering.js +53 -0
  81. package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
  82. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  83. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  84. package/dist/components/form/expressions/ExpressionField.js +202 -45
  85. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  86. package/dist/components/form/expressions/argumentPresence.d.ts +97 -0
  87. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
  88. package/dist/components/form/expressions/argumentPresence.js +203 -0
  89. package/dist/components/form/expressions/argumentPresence.js.map +1 -0
  90. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  91. package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
  92. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  93. package/dist/components/form/expressions/builder/index.d.ts +15 -2
  94. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  95. package/dist/components/form/expressions/builder/index.js +20 -58
  96. package/dist/components/form/expressions/builder/index.js.map +1 -1
  97. package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
  98. package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
  99. package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
  100. package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
  101. package/dist/components/form/expressions/dpqlMockLanguage.d.ts +76 -0
  102. package/dist/components/form/expressions/dpqlMockLanguage.d.ts.map +1 -0
  103. package/dist/components/form/expressions/dpqlMockLanguage.js +525 -0
  104. package/dist/components/form/expressions/dpqlMockLanguage.js.map +1 -0
  105. package/dist/components/form/expressions/dpqlMockLsp.d.ts +31 -2
  106. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
  107. package/dist/components/form/expressions/dpqlMockLsp.js +86 -80
  108. package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
  109. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  110. package/dist/components/form/expressions/mockExpressions.js +8 -4
  111. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  112. package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
  113. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  114. package/dist/components/form/expressions/renderExpressionToText.js +95 -19
  115. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  116. package/dist/components/form/expressions/types.d.ts +7 -0
  117. package/dist/components/form/expressions/types.d.ts.map +1 -1
  118. package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
  119. package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
  120. package/dist/components/form/expressions/useRenderExpression.js +50 -126
  121. package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
  122. package/dist/components/form/fields/Field.d.ts.map +1 -1
  123. package/dist/components/form/fields/Field.js +3 -1
  124. package/dist/components/form/fields/Field.js.map +1 -1
  125. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
  126. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  127. package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
  128. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  129. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  130. package/dist/components/form/fields/auto/AutoFormField.js +32 -8
  131. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  132. package/dist/components/form/fields/date/Date.d.ts +1 -1
  133. package/dist/components/form/fields/date/Date.d.ts.map +1 -1
  134. package/dist/components/form/fields/date/Date.js +16 -2
  135. package/dist/components/form/fields/date/Date.js.map +1 -1
  136. package/dist/components/form/fields/rich-text/RichText.d.ts +21 -1
  137. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  138. package/dist/components/form/fields/rich-text/RichText.js +140 -8
  139. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  140. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  141. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  142. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  143. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  144. package/dist/components/form/fields/template/TemplateField.d.ts +31 -1
  145. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  146. package/dist/components/form/fields/template/TemplateField.js +528 -50
  147. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  148. package/dist/components/form/index.d.ts +1 -0
  149. package/dist/components/form/index.d.ts.map +1 -1
  150. package/dist/components/form/index.js +1 -0
  151. package/dist/components/form/index.js.map +1 -1
  152. package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
  153. package/dist/components/smartEditor/SmartEditor.js +29 -8
  154. package/dist/components/smartEditor/SmartEditor.js.map +1 -1
  155. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +8 -0
  156. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
  157. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +53 -10
  158. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
  159. package/dist/components/smartEditor/useLspSession.d.ts +5 -1
  160. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  161. package/dist/components/smartEditor/useLspSession.js +16 -2
  162. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  163. package/dist/helpers/dpqlDetection.d.ts +58 -0
  164. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  165. package/dist/helpers/dpqlDetection.js +164 -0
  166. package/dist/helpers/dpqlDetection.js.map +1 -0
  167. package/dist/helpers/optionUiTypes.d.ts +3 -3
  168. package/dist/helpers/options.d.ts +37 -0
  169. package/dist/helpers/options.d.ts.map +1 -1
  170. package/dist/helpers/options.js +78 -1
  171. package/dist/helpers/options.js.map +1 -1
  172. package/dist/helpers/templateItems.d.ts +47 -0
  173. package/dist/helpers/templateItems.d.ts.map +1 -0
  174. package/dist/helpers/templateItems.js +88 -0
  175. package/dist/helpers/templateItems.js.map +1 -0
  176. package/dist/helpers/templateText.d.ts +37 -0
  177. package/dist/helpers/templateText.d.ts.map +1 -0
  178. package/dist/helpers/templateText.js +64 -0
  179. package/dist/helpers/templateText.js.map +1 -0
  180. package/dist/helpers/templates.d.ts +19 -11
  181. package/dist/helpers/templates.d.ts.map +1 -1
  182. package/dist/helpers/templates.js +46 -29
  183. package/dist/helpers/templates.js.map +1 -1
  184. package/dist/helpers/validations.d.ts.map +1 -1
  185. package/dist/helpers/validations.js +56 -9
  186. package/dist/helpers/validations.js.map +1 -1
  187. package/dist/hooks/useArgSchema.d.ts +2 -1
  188. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  189. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  190. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  191. package/dist/hooks/useFetch/useFetch.js +47 -9
  192. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  193. package/dist/hooks/useTemplates.d.ts +20 -0
  194. package/dist/hooks/useTemplates.d.ts.map +1 -1
  195. package/dist/hooks/useTemplates.js +34 -2
  196. package/dist/hooks/useTemplates.js.map +1 -1
  197. package/dist/index.d.ts +2 -1
  198. package/dist/index.d.ts.map +1 -1
  199. package/dist/index.js +5 -1
  200. package/dist/index.js.map +1 -1
  201. package/dist/stories/storyNetwork.d.ts +36 -0
  202. package/dist/stories/storyNetwork.d.ts.map +1 -0
  203. package/dist/stories/storyNetwork.js +82 -0
  204. package/dist/stories/storyNetwork.js.map +1 -0
  205. package/dist/utils/fetch.d.ts +151 -1
  206. package/dist/utils/fetch.d.ts.map +1 -1
  207. package/dist/utils/fetch.js +256 -44
  208. package/dist/utils/fetch.js.map +1 -1
  209. package/dist/utils/lspClient.d.ts +6 -0
  210. package/dist/utils/lspClient.d.ts.map +1 -1
  211. package/dist/utils/lspClient.js +62 -11
  212. package/dist/utils/lspClient.js.map +1 -1
  213. package/dist/utils/websocket.d.ts +6 -0
  214. package/dist/utils/websocket.d.ts.map +1 -1
  215. package/dist/utils/websocket.js +15 -7
  216. package/dist/utils/websocket.js.map +1 -1
  217. package/package.json +2 -2
  218. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
  219. package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
  220. package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
  221. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  222. package/src/components/dpqlEditor/dpqlTags.ts +13 -0
  223. package/src/components/dpqlEditor/index.ts +4 -0
  224. package/src/components/dpqlEditor/types.ts +38 -2
  225. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  226. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  227. package/src/components/form/engine/CompactRow.tsx +240 -51
  228. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  229. package/src/components/form/engine/FormEngine.stories.tsx +423 -120
  230. package/src/components/form/engine/FormEngine.tsx +504 -87
  231. package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
  232. package/src/components/form/engine/OptionFieldMessages.tsx +89 -11
  233. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
  234. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  235. package/src/components/form/engine/readFirst.ts +86 -7
  236. package/src/components/form/engine/rendererTypes.ts +12 -0
  237. package/src/components/form/engine/rowMenuContext.ts +159 -0
  238. package/src/components/form/engine/typeChoices.ts +55 -0
  239. package/src/components/form/expressions/DpqlRendering.tsx +85 -0
  240. package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
  241. package/src/components/form/expressions/ExpressionField.tsx +265 -54
  242. package/src/components/form/expressions/argumentPresence.ts +200 -0
  243. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +20 -4
  244. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  245. package/src/components/form/expressions/builder/index.tsx +39 -77
  246. package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
  247. package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
  248. package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
  249. package/src/components/form/expressions/mockExpressions.ts +8 -4
  250. package/src/components/form/expressions/renderExpressionToText.ts +112 -22
  251. package/src/components/form/expressions/types.ts +7 -0
  252. package/src/components/form/expressions/useRenderExpression.ts +43 -127
  253. package/src/components/form/fields/Field.tsx +3 -2
  254. package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
  255. package/src/components/form/fields/auto/AutoFormField.stories.tsx +27 -20
  256. package/src/components/form/fields/auto/AutoFormField.tsx +33 -9
  257. package/src/components/form/fields/date/Date.tsx +17 -1
  258. package/src/components/form/fields/rich-text/RichText.tsx +199 -22
  259. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  260. package/src/components/form/fields/template/TemplateField.stories.tsx +31 -9
  261. package/src/components/form/fields/template/TemplateField.tsx +714 -122
  262. package/src/components/form/index.tsx +1 -0
  263. package/src/components/log/Log.stories.tsx +2 -1
  264. package/src/components/smartEditor/SmartEditor.tsx +41 -9
  265. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
  266. package/src/components/smartEditor/useLspSession.ts +21 -3
  267. package/src/helpers/dpqlDetection.ts +209 -0
  268. package/src/helpers/options.ts +70 -0
  269. package/src/helpers/templateItems.ts +81 -0
  270. package/src/helpers/templateText.ts +92 -0
  271. package/src/helpers/templates.ts +47 -37
  272. package/src/helpers/validations.ts +59 -9
  273. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  274. package/src/hooks/useFetch/useFetch.tsx +53 -7
  275. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  276. package/src/hooks/useTemplates.ts +41 -0
  277. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  278. package/src/index.tsx +14 -1
  279. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  280. package/src/stories/storyNetwork.ts +92 -0
  281. package/src/utils/fetch.ts +311 -21
  282. package/src/utils/lspClient.ts +68 -11
  283. package/src/utils/websocket.ts +20 -9
@@ -10,6 +10,7 @@ import {
10
10
  ReqoreTagGroup,
11
11
  } from '@qoretechnologies/reqore';
12
12
  import { IReqoreDropdownItem } from '@qoretechnologies/reqore/dist/components/Dropdown/list';
13
+ import { RowMenuContext, useRowMenuRegistry } from './rowMenuContext';
13
14
  import { IReqorePanelAction } from '@qoretechnologies/reqore/dist/components/Panel';
14
15
  import { resolveOptionActions } from './optionActions';
15
16
  import {
@@ -28,8 +29,7 @@ import {
28
29
  findTemplate,
29
30
  getTemplateTagStyle,
30
31
  isValueTemplate,
31
- describeTemplateReference,
32
- splitTemplateTokens,
32
+ templateTooltip,
33
33
  TTemplateMeta,
34
34
  } from '../../../helpers/templates';
35
35
  import { getDefaultValue, richtextToSegments, richtextToString } from '../../../helpers/common';
@@ -64,6 +64,7 @@ import {
64
64
  findAllowedValueOption,
65
65
  formatBytes,
66
66
  formatOptionValue,
67
+ getAllowedValueIcon,
67
68
  getAllowedValueImage,
68
69
  getFileSize,
69
70
  getHashEntries,
@@ -72,8 +73,11 @@ import {
72
73
  isFixedCompactAllowedValueOption,
73
74
  isOptionValueEmpty,
74
75
  optionHasImages,
76
+ getExpressionAst,
75
77
  } from './readFirst';
76
78
  import { StructuredDataView } from './_structuredData/StructuredDataView';
79
+ import { DpqlRendering } from '../expressions/DpqlRendering';
80
+ import { TFieldWithOwnTemplates } from './rendererTypes';
77
81
 
78
82
  /**
79
83
  * Does this markdown value have more document in it than one row can show?
@@ -299,6 +303,39 @@ export const CompactRow = memo(
299
303
  { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }
300
304
  : { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
301
305
 
306
+ /* A row has to SAY when its value is an expression.
307
+
308
+ Reported from the live IDE: an author accepted the *Use as expression*
309
+ offer on one field and not on the other, and the two rows read exactly
310
+ the same - `1 + 2` and `2 + 1`, both plain text - so there was no way
311
+ to tell which had been converted. The assertion then compared the
312
+ string `"1 + 2"` against the number `3` and the author had no way to
313
+ see why.
314
+
315
+ The same Σ the expression editor uses, so the mark means the same thing
316
+ wherever it appears. */
317
+ const withExpressionMarker = (rendered: React.ReactNode): React.ReactNode => {
318
+ if (!(field as { is_expression?: boolean })?.is_expression) {
319
+ return rendered;
320
+ }
321
+
322
+ return (
323
+ <span style={{ ...wrapStyle, gap: 4 }}>
324
+ <ReqoreIcon
325
+ icon='Functions'
326
+ size='tiny'
327
+ /* A stable hook: reqore renders every icon as a bare
328
+ `.reqore-icon`, so without this there is nothing to select the
329
+ mark by — in a test or in a consumer's stylesheet. */
330
+ className='reqraft-expression-marker'
331
+ style={{ flexShrink: 0, opacity: 0.7 }}
332
+ tooltip='This value is an expression'
333
+ />
334
+ {rendered}
335
+ </span>
336
+ );
337
+ };
338
+
302
339
  if (valueType === 'rgbcolor') {
303
340
  const swatch = colorToCss(field?.value);
304
341
  return (
@@ -351,6 +388,52 @@ export const CompactRow = memo(
351
388
  );
352
389
  }
353
390
 
391
+ // A test's cases read as CHIPS here, the same as the names in the table
392
+ // this row opens (`CasesTable`, qorus-ide). `test-cases` is a
393
+ // renderer-only ui_type, so it reached none of the branches above and
394
+ // fell through to the generic summary, which flattens the array of case
395
+ // hashes to a comma-joined string — "case_1, resolved_expected_values".
396
+ // A case name is an identifier the author types into `$.` references and
397
+ // skip conditions, and a chip is how the rest of the product writes an
398
+ // identifier down; the table already agreed, so the collapsed row was the
399
+ // only surface still printing them as prose.
400
+ //
401
+ // `intent` is a FILL, which is what a tag is — the hazard the other
402
+ // branches warn about is only ever using an intent as a TEXT colour.
403
+ //
404
+ // Falls through to the plain summary unless EVERY entry yields a name: a
405
+ // partly-chipped row would read as though the unnamed cases were missing.
406
+ if (valueType === 'test-cases' && Array.isArray(field?.value)) {
407
+ const caseNames = (field.value as unknown[]).map((item) => {
408
+ const name = (item as { name?: unknown } | null | undefined)?.name;
409
+ return typeof name === 'string' ? name.trim() : '';
410
+ });
411
+ if (caseNames.length > 0 && caseNames.every((name) => name !== '')) {
412
+ return (
413
+ <ReqoreControlGroup
414
+ gapSize='tiny'
415
+ verticalAlign='center'
416
+ // An editable row is ONE line: further chips clip with the row
417
+ // instead of growing it. A read-only row is the only rendering
418
+ // the value gets, so it wraps and shows every case.
419
+ wrap={full}
420
+ style={full ? undefined : { minWidth: 0, overflow: 'hidden' }}
421
+ >
422
+ {caseNames.map((name, index) => (
423
+ <ReqoreTag
424
+ key={`${name}-${index}`}
425
+ className='options-readfirst-case-tag'
426
+ size='small'
427
+ minimal
428
+ intent='info'
429
+ label={name}
430
+ />
431
+ ))}
432
+ </ReqoreControlGroup>
433
+ );
434
+ }
435
+ }
436
+
354
437
  // Code-editor read summary: replace the truncated string with a chip
355
438
  // showing line count + character count. The actual code renders below
356
439
  // the row as a monospace `<pre>` inside a `ReqoreCollapsibleContent` —
@@ -418,15 +501,40 @@ export const CompactRow = memo(
418
501
  );
419
502
  }
420
503
 
504
+ // The same, for an option whose mark is an ICON rather than a logo. Only
505
+ // images were drawn here, so a list whose entries are told apart by their
506
+ // icon — every interface kind is a bare word, `Service`, `Job`, `Qog` —
507
+ // showed the mark while choosing and lost it the moment the row went
508
+ // read-only, which is where a reader spends most of their time.
509
+ const allowedIcon = getAllowedValueIcon(field?.value, schema);
510
+ if (allowedIcon) {
511
+ return (
512
+ <span style={wrapStyle}>
513
+ <ReqoreIcon icon={allowedIcon as any} size='16px' style={{ flexShrink: 0 }} />
514
+ <span style={textStyle}>{formatted}</span>
515
+ </span>
516
+ );
517
+ }
518
+
421
519
  // Template value ($local:…): the read-only template picker — the SAME chip
422
520
  // TemplateField renders when disabled, so a template reads identically here
423
521
  // and in the editor (qorus-ide intent scheme: info / qorus purple).
522
+ //
523
+ // A field may also declare templates OF ITS OWN, and a value drawn from
524
+ // that list is just as much a template even when it does not look like
525
+ // `$name:key` — `isValueTemplate` is a guess at the shape of the built-in
526
+ // grammar, and a host's own grammar (a Qorus test's `$.order.id`) fails it.
527
+ // Asking the field's list whether it CONTAINS the value settles it without
528
+ // guessing at syntax, and hands back the entry whose label the chip prints,
529
+ // so a reference reads as the name it was chosen by here as well as in the
530
+ // editor rather than as a raw path.
531
+ const ownTemplates = (schema as TFieldWithOwnTemplates | undefined)?.templates ?? templates;
424
532
  if (
425
533
  typeof field?.value === 'string' &&
426
534
  !(field as { is_expression?: boolean }).is_expression &&
427
- isValueTemplate(field.value)
535
+ (isValueTemplate(field.value) || !!findTemplate(ownTemplates ?? {}, field.value))
428
536
  ) {
429
- return <ReadOnlyTemplateTag value={field.value} templates={templates} size='small' />;
537
+ return <ReadOnlyTemplateTag value={field.value} templates={ownTemplates} size='small' />;
430
538
  }
431
539
 
432
540
  // A LIST whose elements are rich-text envelopes is a list of strings, so it
@@ -465,7 +573,9 @@ export const CompactRow = memo(
465
573
  style={{ fontSize: 'inherit' }}
466
574
  icon='ExchangeDollarLine'
467
575
  label={segment.text || segment.value}
468
- tooltip={segment.value}
576
+ // The same card the chosen-template chip shows: what the
577
+ // value IS, not the reference that names it.
578
+ tooltip={templateTooltip(templates, segment.value)}
469
579
  {...getTemplateTagStyle(
470
580
  (templates ? findTemplate(templates, segment.value || '') : undefined)
471
581
  ?.metadata as TTemplateMeta | undefined
@@ -480,50 +590,20 @@ export const CompactRow = memo(
480
590
  }
481
591
  }
482
592
 
483
- // An expression summarises to one line of text, and a template reference
484
- // inside it printed as its own raw token — `$data:{…}` renders a value as
485
- // code where a name belongs. Chip every reference here, on the richtext
486
- // branch's pattern above, so the wrapper (`trim(`…`)`) still reads as the
487
- // text it is while the reference reads as a name.
488
- //
489
- // Unconditionally: a surface with no catalogue (the Automation Hub
490
- // template preview never fetches one) still shows the reference's own
491
- // path, which is the point — a conditional chip is no chip at all
492
- // exactly where the raw token looks worst.
593
+ // An expression reads as DPQL wherever it is shown without being edited:
594
+ // the same rendering its Explain panel and Preview use — monospace,
595
+ // coloured by the language server, template references as chips — so a
596
+ // collapsed row and the editor it opens never disagree about what the
597
+ // value looks like. See `DpqlRendering`.
493
598
  if ((field as { is_expression?: boolean })?.is_expression && formatted) {
494
- const segments = splitTemplateTokens(formatted);
495
- const named = segments.map((segment) =>
496
- segment.kind === 'token' ?
497
- { ...segment, ...describeTemplateReference(templates, segment.text) }
498
- : segment
599
+ return withExpressionMarker(
600
+ <span style={{ minWidth: 0, flex: '1 1 auto', ...(full ? {} : { overflow: 'hidden' }) }}>
601
+ <DpqlRendering text={formatted} templates={ownTemplates} />
602
+ </span>
499
603
  );
500
-
501
- if (named.some((segment) => segment.kind === 'token')) {
502
- return (
503
- <span style={{ ...wrapStyle, gap: 4, overflow: 'hidden' }}>
504
- {named.map((segment, index) =>
505
- segment.kind === 'token' ?
506
- <ReqoreTag
507
- key={index}
508
- size='tiny'
509
- // Inherit the row's font size so the chip shares a baseline
510
- // with the text around it — see the richtext branch above.
511
- style={{ fontSize: 'inherit' }}
512
- icon='ExchangeDollarLine'
513
- label={segment.label}
514
- tooltip={segment.text}
515
- {...getTemplateTagStyle(segment.item?.metadata as TTemplateMeta | undefined)}
516
- />
517
- : <span key={index} style={{ whiteSpace: full ? 'pre-wrap' : 'pre' }}>
518
- {segment.text}
519
- </span>
520
- )}
521
- </span>
522
- );
523
- }
524
604
  }
525
605
 
526
- return full ? <span style={textStyle}>{formatted}</span> : formatted;
606
+ return withExpressionMarker(full ? <span style={textStyle}>{formatted}</span> : formatted);
527
607
  };
528
608
 
529
609
  const schema = options?.[optionName];
@@ -627,8 +707,23 @@ export const CompactRow = memo(
627
707
  const changed = !hidden && !readOnly && hasOptionChanged(optionField?.value, optionName);
628
708
  // "Has a value" = set to anything non-empty. Drives the edit-row Clear
629
709
  // button and the cluster node's filled state (see `memberSet`).
710
+ /* "Is there anything to clear?" — this predicate has exactly one job, and
711
+ both of its uses are the Clear-value button.
712
+
713
+ An empty list is not a value. `[]` is neither `undefined`, `null` nor `''`,
714
+ so a field whose last item had just been removed went on offering to clear
715
+ it, and the confirmation asked whether to clear nothing. A test's cases
716
+ table is where this shows: delete the last case and a red clear-all sat
717
+ above an empty table, guarding a no-op.
718
+
719
+ Deliberately arrays only. An empty hash is arguable — "present but empty"
720
+ can be a real state for a hash — and nothing has demonstrated the same
721
+ defect there, so widening this predicate would be a guess. */
630
722
  const hasValue =
631
- optionField?.value !== undefined && optionField?.value !== null && optionField?.value !== '';
723
+ optionField?.value !== undefined &&
724
+ optionField?.value !== null &&
725
+ optionField?.value !== '' &&
726
+ !(Array.isArray(optionField.value) && optionField.value.length === 0);
632
727
  // Required-group membership shows a PERSISTENT chip on every member: amber
633
728
  // "One of" while the group is unmet (tap → flash siblings), then a muted-green
634
729
  // resolution once satisfied — "Covers" on the field that satisfied it,
@@ -705,7 +800,7 @@ export const CompactRow = memo(
705
800
  : (schema?.ui_type as string) || (schema?.type as string)) ?? '';
706
801
  // Scalars edit in place inside the row; complex fields (tall or nested
707
802
  // editors) still open the expanded card below.
708
- const inlineEditable =
803
+ const inlineEditableNow =
709
804
  !readOnly &&
710
805
  !schema?.arg_schema &&
711
806
  !(operators && size(operators)) &&
@@ -715,6 +810,43 @@ export const CompactRow = memo(
715
810
  !optionHasImages(schema) &&
716
811
  !COMPACT_COMPLEX_TYPES.has(editType);
717
812
 
813
+ /* Which of the two editors a row gets is settled when the row OPENS.
814
+ *
815
+ * Every term above reads the VALUE or a schema derived from it, so the
816
+ * answer can change while the author is inside the control — and the two
817
+ * branches are different subtrees, so a changed answer does not restyle
818
+ * the editor, it unmounts one and mounts the other. Everything the old one
819
+ * held goes with it: what was typed, the caret, and any state the editor
820
+ * keeps about how it was entered.
821
+ *
822
+ * `is_expression` is the term that actually fires, and it fires on the one
823
+ * interaction that exists to keep the author where they are: accepting
824
+ * "Use as expression" is the moment the value becomes an expression, and
825
+ * the expression shell opens on its Text view precisely because the author
826
+ * has just written the expression as text. The remount discarded that and
827
+ * dropped them into the visual builder, so the sentence they had typed was
828
+ * nowhere on screen. Clicking Text then appeared to work and bounced back,
829
+ * because the next value change remounted the row again.
830
+ *
831
+ * So the row keeps the editor it opened with for as long as it is open,
832
+ * and asks the question again when it is next opened — the same trade
833
+ * `editEntryValue` above makes for a different reason, and the one
834
+ * qorus-ide's `referenceEditorLatch` makes for this exact one. A field that
835
+ * already holds an expression when it is opened still gets the card; it is
836
+ * only a field that BECOMES one under the author that stays put.
837
+ *
838
+ * Held in a ref and settled during render rather than in an effect: an
839
+ * effect lands one render late, and one render late is a visible flash of
840
+ * the wrong editor — which is the thing being fixed.
841
+ */
842
+ const inlineEditableLatch = React.useRef<boolean | undefined>(undefined);
843
+ if (!isExpanded) {
844
+ inlineEditableLatch.current = undefined;
845
+ } else if (inlineEditableLatch.current === undefined) {
846
+ inlineEditableLatch.current = inlineEditableNow;
847
+ }
848
+ const inlineEditable = isExpanded ? !!inlineEditableLatch.current : inlineEditableNow;
849
+
718
850
  // Auto-focus the editor's first input when a field is opened, so you can type
719
851
  // straight away (matches the prototype's tap-to-edit feel).
720
852
  const editorRef = React.useRef<HTMLDivElement>(null);
@@ -1061,6 +1193,19 @@ export const CompactRow = memo(
1061
1193
  // stacks children flush, and the class rule that used to carry a 4px gap
1062
1194
  // never reached this element at all — which is why two messages read as one
1063
1195
  // two-tone block with no space between them.
1196
+ /* A message is PROSE, so it is drawn by the host's prose renderer.
1197
+
1198
+ `markdownRendererContext` says this package renders markdown "in exactly
1199
+ one place — a field's description". That was one place short: a schema
1200
+ message is written by the same author, in the same dialect, and it landed
1201
+ here as a raw string. A Qorus warning reading "Give that step a **Fixture
1202
+ Output**" showed the asterisks, and every reference inside it stayed a raw
1203
+ `$.` path where the editor two lines above renders it as a named chip.
1204
+
1205
+ `compact` for the same reason the row inset asks for it: this is a strip
1206
+ inside a row, never a page, and markdown authored as a document opens with
1207
+ a heading that would outgrow the field label above it. Without a host
1208
+ renderer the string is drawn as it always was. */
1064
1209
  const renderInfoStrip = (m: TInfoMsg, index: number) => (
1065
1210
  <ReqoreMessage
1066
1211
  key={`${m.content}-${index}`}
@@ -1071,7 +1216,9 @@ export const CompactRow = memo(
1071
1216
  title={m.title}
1072
1217
  style={{ marginBottom: 8 }}
1073
1218
  >
1074
- {m.content}
1219
+ {markdownRenderer && typeof m.content === 'string' ?
1220
+ markdownRenderer({ value: m.content, compact: true })
1221
+ : m.content}
1075
1222
  </ReqoreMessage>
1076
1223
  );
1077
1224
  const reasonColor = (intent?: string) =>
@@ -1108,6 +1255,9 @@ export const CompactRow = memo(
1108
1255
  }
1109
1256
  : {};
1110
1257
 
1258
+ // Items the EDITOR inside this row published — see `rowMenuContext`.
1259
+ const { rowMenu, items: editorMenuItems } = useRowMenuRegistry();
1260
+
1111
1261
  // Secondary edit actions tuck into a "More" (⋮) menu so the card header stays
1112
1262
  // calm: Fullscreen always, plus Remove field for a removable option. Rendered
1113
1263
  // just before the Done ✓ in both expanded layouts.
@@ -1144,6 +1294,10 @@ export const CompactRow = memo(
1144
1294
  // wholesale on touch) land here — this row already has a menu, so they
1145
1295
  // reuse it rather than adding a second one beside it.
1146
1296
  ...injectedOptionActionMenuItems,
1297
+ // ...and for the same reason, so do the editor's own actions. Without
1298
+ // this an editor with affordances of its own had to draw a second ⋮
1299
+ // inside the value cell, beside this one.
1300
+ ...editorMenuItems,
1147
1301
  ]}
1148
1302
  />
1149
1303
  );
@@ -1316,7 +1470,13 @@ export const CompactRow = memo(
1316
1470
  editor they belong to — the language you are writing in is
1317
1471
  read before the code, not after it. */}
1318
1472
  {absorbedNodes}
1319
- {renderOption(optionName, optionField, 'small', true)}
1473
+ {/* The editor may publish into THIS row's ⋮ — see `rowMenuContext`.
1474
+ Only the editors rendered in the row are given the channel: the
1475
+ fullscreen modal below has no row menu on screen, so an editor
1476
+ there keeps drawing its own control. */}
1477
+ <RowMenuContext.Provider value={rowMenu}>
1478
+ {renderOption(optionName, optionField, 'small', true)}
1479
+ </RowMenuContext.Provider>
1320
1480
  </div>
1321
1481
  <StyledRowActions>
1322
1482
  {draftChip}
@@ -1556,7 +1716,9 @@ export const CompactRow = memo(
1556
1716
  longDescriptionShownByDefault
1557
1717
  />
1558
1718
  : null}
1559
- {renderOption(optionName, optionField)}
1719
+ <RowMenuContext.Provider value={rowMenu}>
1720
+ {renderOption(optionName, optionField)}
1721
+ </RowMenuContext.Provider>
1560
1722
  </FocusedEditing>
1561
1723
  </div>
1562
1724
  </StyledEditCard>
@@ -1596,11 +1758,21 @@ export const CompactRow = memo(
1596
1758
  ];
1597
1759
  // A hash row reveals its sub-fields as read-only sub-rows under a "view
1598
1760
  // more" disclosure; the row itself still expands the real editor on click.
1761
+ /* An EXPRESSION is not a hash, whatever shape it is stored in.
1762
+
1763
+ `{is_expression: true, value: {exp, args}}` is hash-shaped, so a field
1764
+ holding one earned the structured inset and the row printed the syntax
1765
+ tree — `is_expression true / value / exp + / args 1 2` — directly under a
1766
+ summary line that already read `1 + 2`. The AST is how an expression is
1767
+ STORED, not what it is; the row's line is the whole value, and the
1768
+ expression editor is where its parts are looked at. Same exclusion, and
1769
+ for the same reason, as `schema-definition` beside it. */
1599
1770
  const hashEntries =
1600
1771
  (
1601
1772
  !hidden &&
1602
1773
  (valueType === 'hash' || valueType === 'free-hash') &&
1603
- (schema as { ui_type?: string } | undefined)?.ui_type !== 'schema-definition'
1774
+ (schema as { ui_type?: string } | undefined)?.ui_type !== 'schema-definition' &&
1775
+ getExpressionAst(optionField) === undefined
1604
1776
  ) ?
1605
1777
  getHashEntries(optionField, schema)
1606
1778
  : [];
@@ -1647,6 +1819,22 @@ export const CompactRow = memo(
1647
1819
  valueType === 'richtext' &&
1648
1820
  Array.isArray(optionField?.value) &&
1649
1821
  richtextToSegments(optionField.value as never).some((segment) => segment.kind === 'tag');
1822
+ /* The same pair, for a value that IS one template rather than prose with
1823
+ templates embedded in it. `showsTemplateChips` above closed the richtext
1824
+ case and left this one open: a chosen reference renders as
1825
+ `ReadOnlyTemplateTag`, which carries its own popover, while the row went
1826
+ on setting a native `title` holding the raw reference. Hovering fired
1827
+ both — two tooltips in two places, and the one that says something useful
1828
+ is not the one a screenshot captures. */
1829
+ const showsTemplateTag =
1830
+ !hidden &&
1831
+ typeof optionField?.value === 'string' &&
1832
+ !(optionField as { is_expression?: boolean } | undefined)?.is_expression &&
1833
+ (isValueTemplate(optionField.value) ||
1834
+ !!findTemplate(
1835
+ (schema as TFieldWithOwnTemplates | undefined)?.templates ?? templates ?? {},
1836
+ optionField.value
1837
+ ));
1650
1838
  // Markdown reads the same way for the same reason: the row itself can only
1651
1839
  // show a line of text, and for markdown that line is the SOURCE — the reader
1652
1840
  // gets `## ` and `**` where the point of the value is what it looks like
@@ -1960,6 +2148,7 @@ export const CompactRow = memo(
1960
2148
  !showCodePreview &&
1961
2149
  !showMarkdownPreview &&
1962
2150
  !showsTemplateChips &&
2151
+ !showsTemplateTag &&
1963
2152
  typeof formatted === 'string'
1964
2153
  ) ?
1965
2154
  formatted
@@ -140,7 +140,10 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
140
140
  const cWarning = intents.warning || '#d17c29';
141
141
  const cText = (theme.text?.color as string) || '#e8e8e8';
142
142
  const cTrack = `${cText}1f`;
143
- const setPct = completion.total ? (completion.set / completion.total) * 100 : 0;
143
+ // The GREEN run is what is done, not merely what has a value: a field that is
144
+ // set but flagged belongs to the amber run, and counting it twice pushed that
145
+ // run off the end of the track.
146
+ const setPct = completion.total ? (completion.done / completion.total) * 100 : 0;
144
147
  const attentionPct = completion.total ? (attentionCount / completion.total) * 100 : 0;
145
148
 
146
149
  return (
@@ -155,10 +158,14 @@ export const CompactToolbar = memo((reqoreProps: Partial<IReqoreControlGroupProp
155
158
  <StyledCompletionLine>
156
159
  <StyledSummary
157
160
  className='options-readfirst-status'
158
- $color={invalidCount ? cWarning : cSuccess}
161
+ $color={invalidCount || attentionCount ? cWarning : cSuccess}
159
162
  style={{ fontWeight: 600 }}
160
163
  >
161
- {invalidCount ? 'Draft' : 'Ready'}
164
+ {/* "Ready" is a claim about the whole form, so anything the form
165
+ is asking for withdraws it — a value that fails validation and
166
+ a field the host has flagged alike. Saying Ready beside
167
+ "1 need attention" made the word mean nothing. */}
168
+ {invalidCount || attentionCount ? 'Draft' : 'Ready'}
162
169
  </StyledSummary>
163
170
  <StyledSummary style={{ opacity: 0.5 }}>
164
171
  · {completion.set}/{completion.total} set