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

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 (215) 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/RENDER_EXPRESSION_TRANSPORT.md +18 -0
  7. package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
  8. package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
  9. package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
  10. package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
  11. package/dist/components/dpqlEditor/index.d.ts +4 -0
  12. package/dist/components/dpqlEditor/index.d.ts.map +1 -1
  13. package/dist/components/dpqlEditor/index.js +5 -1
  14. package/dist/components/dpqlEditor/index.js.map +1 -1
  15. package/dist/components/dpqlEditor/types.d.ts +29 -2
  16. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  17. package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
  18. package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
  19. package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
  20. package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
  21. package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
  22. package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
  23. package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
  24. package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
  25. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  26. package/dist/components/form/engine/CompactRow.js +186 -21
  27. package/dist/components/form/engine/CompactRow.js.map +1 -1
  28. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  29. package/dist/components/form/engine/CompactToolbar.js +5 -2
  30. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  31. package/dist/components/form/engine/FormEngine.d.ts +159 -11
  32. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  33. package/dist/components/form/engine/FormEngine.js +376 -75
  34. package/dist/components/form/engine/FormEngine.js.map +1 -1
  35. package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
  36. package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
  37. package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
  38. package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
  39. package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
  40. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  41. package/dist/components/form/engine/OptionFieldMessages.js +61 -11
  42. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  43. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  44. package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
  45. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  46. package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
  47. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  48. package/dist/components/form/engine/readFirst.d.ts +42 -1
  49. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  50. package/dist/components/form/engine/readFirst.js +74 -7
  51. package/dist/components/form/engine/readFirst.js.map +1 -1
  52. package/dist/components/form/engine/rendererTypes.d.ts +13 -0
  53. package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
  54. package/dist/components/form/engine/rendererTypes.js.map +1 -1
  55. package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
  56. package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
  57. package/dist/components/form/engine/rowMenuContext.js +103 -0
  58. package/dist/components/form/engine/rowMenuContext.js.map +1 -0
  59. package/dist/components/form/engine/typeChoices.d.ts +37 -0
  60. package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
  61. package/dist/components/form/engine/typeChoices.js +25 -0
  62. package/dist/components/form/engine/typeChoices.js.map +1 -0
  63. package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
  64. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  65. package/dist/components/form/expressions/ExpressionField.js +181 -33
  66. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  67. package/dist/components/form/expressions/argumentPresence.d.ts +97 -0
  68. package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
  69. package/dist/components/form/expressions/argumentPresence.js +203 -0
  70. package/dist/components/form/expressions/argumentPresence.js.map +1 -0
  71. package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
  72. package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
  73. package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
  74. package/dist/components/form/expressions/builder/index.d.ts +15 -2
  75. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  76. package/dist/components/form/expressions/builder/index.js +19 -57
  77. package/dist/components/form/expressions/builder/index.js.map +1 -1
  78. package/dist/components/form/expressions/dpqlMockLsp.d.ts +17 -0
  79. package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
  80. package/dist/components/form/expressions/dpqlMockLsp.js +75 -9
  81. package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
  82. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  83. package/dist/components/form/expressions/renderExpressionToText.js +94 -19
  84. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  85. package/dist/components/form/fields/Field.d.ts.map +1 -1
  86. package/dist/components/form/fields/Field.js +3 -1
  87. package/dist/components/form/fields/Field.js.map +1 -1
  88. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
  89. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  90. package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
  91. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  92. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  93. package/dist/components/form/fields/auto/AutoFormField.js +13 -2
  94. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  95. package/dist/components/form/fields/rich-text/RichText.d.ts +11 -0
  96. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  97. package/dist/components/form/fields/rich-text/RichText.js +84 -5
  98. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  99. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
  100. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  101. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
  102. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  103. package/dist/components/form/fields/template/TemplateField.d.ts +31 -1
  104. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  105. package/dist/components/form/fields/template/TemplateField.js +507 -40
  106. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  107. package/dist/components/form/index.d.ts +1 -0
  108. package/dist/components/form/index.d.ts.map +1 -1
  109. package/dist/components/form/index.js +1 -0
  110. package/dist/components/form/index.js.map +1 -1
  111. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +1 -0
  112. package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
  113. package/dist/components/smartEditor/__fixtures__/mockLspServer.js +6 -1
  114. package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
  115. package/dist/helpers/dpqlDetection.d.ts +58 -0
  116. package/dist/helpers/dpqlDetection.d.ts.map +1 -0
  117. package/dist/helpers/dpqlDetection.js +164 -0
  118. package/dist/helpers/dpqlDetection.js.map +1 -0
  119. package/dist/helpers/optionUiTypes.d.ts +1 -1
  120. package/dist/helpers/options.d.ts +37 -0
  121. package/dist/helpers/options.d.ts.map +1 -1
  122. package/dist/helpers/options.js +78 -1
  123. package/dist/helpers/options.js.map +1 -1
  124. package/dist/helpers/templateItems.d.ts +47 -0
  125. package/dist/helpers/templateItems.d.ts.map +1 -0
  126. package/dist/helpers/templateItems.js +88 -0
  127. package/dist/helpers/templateItems.js.map +1 -0
  128. package/dist/helpers/templates.d.ts +21 -0
  129. package/dist/helpers/templates.d.ts.map +1 -1
  130. package/dist/helpers/templates.js +49 -3
  131. package/dist/helpers/templates.js.map +1 -1
  132. package/dist/helpers/validations.d.ts.map +1 -1
  133. package/dist/helpers/validations.js +56 -9
  134. package/dist/helpers/validations.js.map +1 -1
  135. package/dist/hooks/useArgSchema.d.ts +2 -1
  136. package/dist/hooks/useArgSchema.d.ts.map +1 -1
  137. package/dist/hooks/useFetch/useFetch.d.ts +3 -1
  138. package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
  139. package/dist/hooks/useFetch/useFetch.js +47 -9
  140. package/dist/hooks/useFetch/useFetch.js.map +1 -1
  141. package/dist/hooks/useTemplates.d.ts +20 -0
  142. package/dist/hooks/useTemplates.d.ts.map +1 -1
  143. package/dist/hooks/useTemplates.js +34 -2
  144. package/dist/hooks/useTemplates.js.map +1 -1
  145. package/dist/index.d.ts +2 -1
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +5 -1
  148. package/dist/index.js.map +1 -1
  149. package/dist/stories/storyNetwork.d.ts +36 -0
  150. package/dist/stories/storyNetwork.d.ts.map +1 -0
  151. package/dist/stories/storyNetwork.js +82 -0
  152. package/dist/stories/storyNetwork.js.map +1 -0
  153. package/dist/utils/fetch.d.ts +151 -1
  154. package/dist/utils/fetch.d.ts.map +1 -1
  155. package/dist/utils/fetch.js +256 -44
  156. package/dist/utils/fetch.js.map +1 -1
  157. package/dist/utils/lspClient.d.ts +6 -0
  158. package/dist/utils/lspClient.d.ts.map +1 -1
  159. package/dist/utils/lspClient.js +62 -11
  160. package/dist/utils/lspClient.js.map +1 -1
  161. package/dist/utils/websocket.d.ts +6 -0
  162. package/dist/utils/websocket.d.ts.map +1 -1
  163. package/dist/utils/websocket.js +15 -7
  164. package/dist/utils/websocket.js.map +1 -1
  165. package/package.json +2 -2
  166. package/src/components/dpqlEditor/DpqlEditor.stories.tsx +0 -2
  167. package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
  168. package/src/components/dpqlEditor/index.ts +4 -0
  169. package/src/components/dpqlEditor/types.ts +26 -2
  170. package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
  171. package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
  172. package/src/components/form/engine/CompactRow.tsx +231 -11
  173. package/src/components/form/engine/CompactToolbar.tsx +10 -3
  174. package/src/components/form/engine/FormEngine.stories.tsx +331 -30
  175. package/src/components/form/engine/FormEngine.tsx +504 -87
  176. package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
  177. package/src/components/form/engine/OptionFieldMessages.tsx +89 -11
  178. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
  179. package/src/components/form/engine/compactToolbarContext.ts +1 -1
  180. package/src/components/form/engine/readFirst.ts +86 -7
  181. package/src/components/form/engine/rendererTypes.ts +12 -0
  182. package/src/components/form/engine/rowMenuContext.ts +159 -0
  183. package/src/components/form/engine/typeChoices.ts +55 -0
  184. package/src/components/form/expressions/ExpressionField.stories.tsx +828 -31
  185. package/src/components/form/expressions/ExpressionField.tsx +234 -29
  186. package/src/components/form/expressions/argumentPresence.ts +200 -0
  187. package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
  188. package/src/components/form/expressions/builder/index.tsx +31 -75
  189. package/src/components/form/expressions/dpqlMockLsp.ts +81 -1
  190. package/src/components/form/expressions/renderExpressionToText.ts +106 -17
  191. package/src/components/form/fields/Field.tsx +3 -2
  192. package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
  193. package/src/components/form/fields/auto/AutoFormField.tsx +12 -2
  194. package/src/components/form/fields/rich-text/RichText.tsx +112 -19
  195. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
  196. package/src/components/form/fields/template/TemplateField.tsx +667 -95
  197. package/src/components/form/index.tsx +1 -0
  198. package/src/components/log/Log.stories.tsx +2 -1
  199. package/src/components/smartEditor/__fixtures__/mockLspServer.ts +6 -1
  200. package/src/helpers/dpqlDetection.ts +209 -0
  201. package/src/helpers/options.ts +70 -0
  202. package/src/helpers/templateItems.ts +81 -0
  203. package/src/helpers/templates.ts +51 -2
  204. package/src/helpers/validations.ts +59 -9
  205. package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
  206. package/src/hooks/useFetch/useFetch.tsx +53 -7
  207. package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
  208. package/src/hooks/useTemplates.ts +41 -0
  209. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
  210. package/src/index.tsx +14 -1
  211. package/src/stores/currentUser/currentUser.stories.tsx +2 -1
  212. package/src/stories/storyNetwork.ts +92 -0
  213. package/src/utils/fetch.ts +311 -21
  214. package/src/utils/lspClient.ts +68 -11
  215. package/src/utils/websocket.ts +20 -9
@@ -5,7 +5,6 @@ import { Meta, StoryObj } from '@storybook/react-vite';
5
5
  import { ChangeEvent, useEffect, useMemo, useState } from 'react';
6
6
  import { expect, fireEvent, fn, userEvent, waitFor, within } from 'storybook/test';
7
7
  import { validateField } from '../../../helpers/validations';
8
- import { defaultQorusTypes } from '../../../hooks/useQorusTypes';
9
8
  import {
10
9
  _testsChangeRichText,
11
10
  _testsChangeStringField,
@@ -92,29 +91,9 @@ const meta: Meta<typeof FormEngine> = {
92
91
  chromatic: {
93
92
  viewports: [2560],
94
93
  },
95
- // `useQorusTypes` resolves the type catalogue as `size(data) ? data : defaultQorusTypes`,
96
- // so a reachable, authenticated instance *replaces* the built-in list rather than
97
- // supplementing it. These stories never opt into live data (no `live: true`), but the
98
- // request fires anyway — which made `Option With Any Type` pass locally (401 → built-in
99
- // list, so "Boolean" exists) and fail in CI, where the token is valid and the server's
100
- // list decides the labels. Pin the catalogue so the type names the plays click are ours.
101
- mockData: [
102
- {
103
- // `query()` builds `${instance}api/latest/${url}`, and the hook's url is
104
- // `/system/qorus-type-info` — hence the doubled slash. Both spellings are listed
105
- // so the mock keeps matching if that leading slash is ever dropped.
106
- url: 'https://hq.qoretechnologies.com:8092/api/latest//system/qorus-type-info',
107
- method: 'GET',
108
- status: 200,
109
- response: defaultQorusTypes,
110
- },
111
- {
112
- url: 'https://hq.qoretechnologies.com:8092/api/latest/system/qorus-type-info',
113
- method: 'GET',
114
- status: 200,
115
- response: defaultQorusTypes,
116
- },
117
- ],
94
+ // The type catalogue the plays click through (`Option With Any Type` picks
95
+ // "Boolean") is pinned for every story by the global mocks in
96
+ // `src/stories/storyNetwork.ts`.
118
97
  },
119
98
  render: ({ value, onChange, ...rest }: IFormEngineProps) => {
120
99
  const [val, setValue] = useState(value);
@@ -510,11 +489,11 @@ export const OptionDependsOnOptionOrAnotherOption: Story = {
510
489
  });
511
490
 
512
491
  await _testsWaitForText(
513
- 'This field is disabled because some dependencies are not fulfilled: "Required Option 2", "Required Option 5"'
492
+ 'This field is disabled because some dependencies are not fulfilled: ("Required Option 2" or "Required Option 5")'
514
493
  );
515
494
  await _testsChangeRichText('I have value', 5);
516
495
  await _testsWaitForTextToNotExist(
517
- 'This field is disabled because some dependencies are not fulfilled: "Required Option 2", "Required Option 5"'
496
+ 'This field is disabled because some dependencies are not fulfilled: ("Required Option 2" or "Required Option 5")'
518
497
  );
519
498
  },
520
499
  };
@@ -7469,12 +7448,12 @@ export const CompactToolbarTrimmed: Story = {
7469
7448
  },
7470
7449
  };
7471
7450
 
7472
- export const CompactCollapseListLeavesLoneOptionalOpen: Story = {
7451
+ export const CompactLoneOptionalStaysOpenByDefault: Story = {
7473
7452
  parameters: {
7474
7453
  docs: {
7475
7454
  description: {
7476
7455
  story:
7477
- "Renders an all-optional compact form with an explicit `compactCollapsedGroups={['optional']}` — the Optional box stays open anyway, because it is the whole form and collapsing it would render a titled card containing one collapsed strip and nothing else. The invariant outranks the consumer's collapse list.",
7456
+ 'Renders an all-optional compact form with NO `compactCollapsedGroups` — the Optional box is the whole form, so it stays open. Collapsing the only box would render a titled card containing one collapsed strip and nothing else, which reads as broken rather than as tidy. This is the default half of the contract; its pair below is what an explicit collapse list changes.',
7478
7457
  },
7479
7458
  },
7480
7459
  },
@@ -7486,16 +7465,42 @@ export const CompactCollapseListLeavesLoneOptionalOpen: Story = {
7486
7465
  beta: { type: 'string', ui_type: 'string', display_name: 'Beta' },
7487
7466
  } as unknown as IOptionsSchema,
7488
7467
  value: {} as IOptions,
7489
- compactCollapsedGroups: ['optional'],
7490
7468
  },
7491
7469
  play: async () => {
7492
- // The lone Optional box is open — its rows are on screen with no click.
7470
+ // No collapse list was passed, so the invariant holds and the rows are on
7471
+ // screen with no click.
7493
7472
  await expectBoxOpen('Optional');
7494
7473
  await _testsWaitForText('Alpha');
7495
7474
  await _testsWaitForText('Beta');
7496
7475
  },
7497
7476
  };
7498
7477
 
7478
+ export const CompactCollapseListCollapsesLoneOptional: Story = {
7479
+ parameters: {
7480
+ docs: {
7481
+ description: {
7482
+ story:
7483
+ "Renders the same all-optional form with an explicit `compactCollapsedGroups={['optional']}` — and this time the Optional box IS collapsed. Passing the prop waives the \"never collapse the sole box\" invariant: it is a default, not a law. A host that supplies its own heading and explanation around the form — a run-options panel headed \"Change a setting for this run\", say — has already told the reader what the card is, and has asked for the settings to start folded away. The default (the story above) is unchanged for every caller that says nothing.",
7484
+ },
7485
+ },
7486
+ },
7487
+ args: {
7488
+ compact: true,
7489
+ minColumnWidth: '300px',
7490
+ options: {
7491
+ alpha: { type: 'string', ui_type: 'string', display_name: 'Alpha' },
7492
+ beta: { type: 'string', ui_type: 'string', display_name: 'Beta' },
7493
+ } as unknown as IOptionsSchema,
7494
+ value: {} as IOptions,
7495
+ compactCollapsedGroups: ['optional'],
7496
+ },
7497
+ play: async () => {
7498
+ // The caller asked for it folded, so it is folded even though it is the
7499
+ // only box: `ReqorePanel` unmounts collapsed content, so the rows are gone.
7500
+ await expectBoxCollapsed('Optional');
7501
+ },
7502
+ };
7503
+
7499
7504
  export const CompactNoSearchCollapseIsFinal: Story = {
7500
7505
  parameters: {
7501
7506
  docs: {
@@ -7831,3 +7836,299 @@ export const CompactNoMaxFieldsShown: Story = {
7831
7836
  expect(canvasElement.querySelector('.readfirst-more-row')).toBeNull();
7832
7837
  },
7833
7838
  };
7839
+
7840
+ /**
7841
+ * A test's cases read as chips on the collapsed row.
7842
+ *
7843
+ * `test-cases` is a renderer-only `ui_type`, so it reached none of the typed
7844
+ * branches in `renderReadFirstValue` and fell through to the generic summary,
7845
+ * which flattens the array of case hashes to `"case_1, resolved_expected_values"`.
7846
+ * The table the row opens already draws each name as a chip, so the collapsed
7847
+ * row was the only surface still printing an identifier as prose.
7848
+ *
7849
+ * The fall-throughs are the point of the other two fields: chips are all-or-
7850
+ * nothing, because a partly-chipped row reads as though the unnamed cases were
7851
+ * missing entirely.
7852
+ */
7853
+ export const CompactRowTestCaseChips: Story = {
7854
+ parameters: {
7855
+ docs: {
7856
+ description: {
7857
+ story:
7858
+ 'Renders a collapsed `test-cases` row: every case name is a chip, matching the cases table the row opens. A value whose entries are not all named, and an empty value, keep the plain summary instead — chips are all-or-nothing.',
7859
+ },
7860
+ },
7861
+ },
7862
+ args: {
7863
+ compact: true,
7864
+ minColumnWidth: '360px',
7865
+ value: {
7866
+ cases: {
7867
+ type: 'list',
7868
+ value: [
7869
+ { name: 'case_1', desc: 'the happy path' },
7870
+ { name: 'resolved_expected_values', desc: 'reference resolution' },
7871
+ ],
7872
+ },
7873
+ mixed: {
7874
+ type: 'list',
7875
+ value: [{ name: 'case_1' }, { desc: 'a case that has no name yet' }],
7876
+ },
7877
+ none: { type: 'list', value: [] },
7878
+ },
7879
+ // Cast for the same reason the markdown/cron/dpql stories need one: a
7880
+ // renderer-only `ui_type` names an EDITOR, and ts-toolkit's `TQorusType`
7881
+ // enumerates STORAGE types, so it has no member for one.
7882
+ options: {
7883
+ cases: { type: 'list', ui_type: 'test-cases', display_name: 'Cases' },
7884
+ mixed: { type: 'list', ui_type: 'test-cases', display_name: 'Mixed' },
7885
+ none: { type: 'list', ui_type: 'test-cases', display_name: 'None' },
7886
+ } as never,
7887
+ },
7888
+ play: async ({ canvasElement }) => {
7889
+ // (a) Every case name is a chip, in order.
7890
+ await waitFor(
7891
+ () => {
7892
+ const chips = canvasElement.querySelectorAll(
7893
+ '[data-field="cases"] .options-readfirst-case-tag'
7894
+ );
7895
+ expect(chips.length).toBe(2);
7896
+ expect(Array.from(chips).map((chip) => chip.textContent?.trim())).toEqual([
7897
+ 'case_1',
7898
+ 'resolved_expected_values',
7899
+ ]);
7900
+ },
7901
+ { timeout: 5000 }
7902
+ );
7903
+
7904
+ // (b) …and the comma-joined prose they replace is gone. Asserting the text
7905
+ // rather than only the chip count: the first cut rendered the chips
7906
+ // BESIDE the summary, which is the same identifiers twice.
7907
+ const casesText =
7908
+ canvasElement
7909
+ .querySelector('[data-field="cases"] .options-readfirst-valuetext')
7910
+ ?.textContent?.trim() ?? '';
7911
+ expect(casesText).not.toContain('case_1,');
7912
+
7913
+ // (c) NEGATIVE: one entry has no name, so no entry is chipped — a
7914
+ // half-chipped row would read as though that case were missing.
7915
+ const mixedRow = canvasElement.querySelector('[data-field="mixed"]');
7916
+ expect(mixedRow?.querySelectorAll('.options-readfirst-case-tag').length).toBe(0);
7917
+
7918
+ // (d) NEGATIVE: an empty value is not a SET value, so it never reaches
7919
+ // this branch at all — its row sits in the collapsed optional group and
7920
+ // is not in the DOM. Counting canvas-wide says the same thing more
7921
+ // strongly than probing for a row that does not exist: the only chips
7922
+ // on the whole form are the two named cases, so neither the unnamed nor
7923
+ // the empty field contributed one.
7924
+ expect(canvasElement.querySelectorAll('.options-readfirst-case-tag').length).toBe(2);
7925
+ },
7926
+ };
7927
+
7928
+ /**
7929
+ * Clear-value is offered only when there is something to clear.
7930
+ *
7931
+ * `[]` is neither `undefined`, `null` nor `''`, so a list field whose last item
7932
+ * had just been removed went on showing the red clear button, and confirming it
7933
+ * cleared nothing. It is visible wherever a list field stays open — a test's
7934
+ * cases table sits above exactly such a button once its last case is deleted.
7935
+ *
7936
+ * Both fields here are lists with the same schema; only the value differs, so
7937
+ * the button's presence is decided by the value alone.
7938
+ */
7939
+ export const ClearValueOnlyWhenThereIsAValue: Story = {
7940
+ parameters: {
7941
+ docs: {
7942
+ description: {
7943
+ story:
7944
+ 'A list field with items offers Clear value; the same field with an empty list does not, because there is nothing to clear.',
7945
+ },
7946
+ },
7947
+ },
7948
+ args: {
7949
+ compact: true,
7950
+ minColumnWidth: '360px',
7951
+ /* Both rows OPEN, because that is where this button lives. A collapsed list
7952
+ row shows no clear at all — the inline one is only for editors with no
7953
+ clear of their own (bools, fixed-choice pickers) — so the button under
7954
+ test is the expanded card's, which is what a consumer that opens a field
7955
+ by default puts permanently on screen. */
7956
+ initialExpandedOptions: ['filled', 'emptied'],
7957
+ value: {
7958
+ filled: { type: 'list', value: ['alpha', 'beta'] },
7959
+ emptied: { type: 'list', value: [] },
7960
+ },
7961
+ options: {
7962
+ /* Both REQUIRED, like a test's `cases`. An unset optional field folds
7963
+ into the collapsed Optional box and is not in the DOM at all, so an
7964
+ empty list only stays on screen — where this button can be misclicked —
7965
+ when the field is one the form insists on. */
7966
+ filled: {
7967
+ type: 'list',
7968
+ ui_type: 'list',
7969
+ element_type: 'string',
7970
+ display_name: 'Filled',
7971
+ required: true,
7972
+ },
7973
+ emptied: {
7974
+ type: 'list',
7975
+ ui_type: 'list',
7976
+ element_type: 'string',
7977
+ display_name: 'Emptied',
7978
+ required: true,
7979
+ },
7980
+ } as never,
7981
+ },
7982
+ play: async ({ canvasElement }) => {
7983
+ await waitFor(
7984
+ () => {
7985
+ expect(canvasElement.querySelector('[data-field="filled"]')).toBeTruthy();
7986
+ },
7987
+ { timeout: 5000 }
7988
+ );
7989
+
7990
+ // (a) items present -> the clear button is offered
7991
+ const filled = canvasElement.querySelector('[data-field="filled"]');
7992
+ expect(filled?.querySelectorAll('.options-readfirst-clear').length).toBe(1);
7993
+
7994
+ // (b) empty list -> nothing to clear, so no button and no dialog to reach
7995
+ const emptied = canvasElement.querySelector('[data-field="emptied"]');
7996
+ expect(emptied).toBeTruthy();
7997
+ expect(emptied?.querySelectorAll('.options-readfirst-clear').length).toBe(0);
7998
+ },
7999
+ };
8000
+
8001
+ /**
8002
+ * A creatable picker is ONE control, not two.
8003
+ *
8004
+ * `rendersCreatableValueSelect` exists so that exactly one of the two renderers
8005
+ * draws the value: the chip picker holds it and offers the candidates, and the
8006
+ * raw editor stands down. It asked for `type === 'string'` — but a picker field
8007
+ * declares a storage type (`*string`) AND a `ui_type` (`select-string`), and by
8008
+ * the time the schema arrives the two are flattened to `type: 'select-string'`.
8009
+ * So neither renderer stood down: the picker drew the value, and the
8010
+ * saved-and-suggested list drew the same candidates again beneath it.
8011
+ *
8012
+ * It showed on a test's service-method step, where Service and Method offered
8013
+ * 183 and 7 values twice over in two controls that set one value each.
8014
+ */
8015
+ export const CreatablePickerIsOneControl: Story = {
8016
+ parameters: {
8017
+ docs: {
8018
+ description: {
8019
+ story:
8020
+ 'A creatable select-string with candidates renders a single picker that both holds the value and offers the list — not a picker plus a second "Saved & Suggested Values" control listing the same items.',
8021
+ },
8022
+ },
8023
+ },
8024
+ args: {
8025
+ compact: true,
8026
+ minColumnWidth: '360px',
8027
+ initialExpandedOptions: ['method'],
8028
+ value: { method: { type: 'select-string', value: 'init' } },
8029
+ options: {
8030
+ method: {
8031
+ type: 'select-string',
8032
+ ui_type: 'select-string',
8033
+ display_name: 'Method',
8034
+ required: true,
8035
+ allowed_values_creatable: true,
8036
+ allowed_values: [
8037
+ { display_name: 'init', value: { type: 'string', value: 'init' } },
8038
+ { display_name: 'start', value: { type: 'string', value: 'start' } },
8039
+ { display_name: 'stop', value: { type: 'string', value: 'stop' } },
8040
+ ],
8041
+ },
8042
+ } as never,
8043
+ },
8044
+ play: async ({ canvasElement }) => {
8045
+ const row = await waitFor(
8046
+ () => {
8047
+ const el = canvasElement.querySelector('[data-field="method"]');
8048
+ expect(el).toBeTruthy();
8049
+ return el as HTMLElement;
8050
+ },
8051
+ { timeout: 5000 }
8052
+ );
8053
+
8054
+ // The value is still shown and still editable — one control, not none.
8055
+ expect(row.textContent).toContain('init');
8056
+
8057
+ // …and the second way to set the same field is gone. Asserting the label
8058
+ // rather than a count: it is what the duplicate control announces itself as,
8059
+ // and what a reader saw twice.
8060
+ expect(row.textContent).not.toContain('Saved & Suggested Values');
8061
+ },
8062
+ };
8063
+
8064
+ /**
8065
+ * A chosen template hovers ONCE, and says what the value is.
8066
+ *
8067
+ * Two defects met on the same row. The chip carries its own popover and the row
8068
+ * carried a native `title` as well, so hovering fired both — in two places, and
8069
+ * only the popover appears in a screenshot, which is what makes the pair hard to
8070
+ * report. `showsTemplateChips` had already closed this for prose with templates
8071
+ * embedded in it; a value that IS one template was left open.
8072
+ *
8073
+ * And both of them showed the reference — `$._case.title` — which is the
8074
+ * grammar's business, not the author's. The catalogue already carries what they
8075
+ * want: the entry's name and the prose describing it. The reference stays in the
8076
+ * field's own help and in the picker, where it is genuinely needed.
8077
+ */
8078
+ export const TemplateValueHoversOnceAndDescribesItself: Story = {
8079
+ parameters: {
8080
+ docs: {
8081
+ description: {
8082
+ story:
8083
+ 'A row whose value is a chosen template shows one hover — the chip’s own — carrying the value’s name and description rather than the raw reference, and the row adds no second browser tooltip.',
8084
+ },
8085
+ },
8086
+ },
8087
+ args: {
8088
+ compact: true,
8089
+ minColumnWidth: '360px',
8090
+ value: { expected: { type: 'string', value: '$._case.title' } },
8091
+ options: {
8092
+ expected: {
8093
+ type: 'string',
8094
+ display_name: 'Expected Value',
8095
+ supports_templates: true,
8096
+ templates: {
8097
+ items: [
8098
+ {
8099
+ label: 'Values this case captures',
8100
+ items: [
8101
+ {
8102
+ value: '$._case.title',
8103
+ label: 'title (this case)',
8104
+ description: 'A short line naming what this case verifies. (string)',
8105
+ },
8106
+ ],
8107
+ },
8108
+ ],
8109
+ },
8110
+ },
8111
+ } as never,
8112
+ },
8113
+ play: async ({ canvasElement }) => {
8114
+ const row = await waitFor(
8115
+ () => {
8116
+ const el = canvasElement.querySelector('[data-field="expected"]');
8117
+ expect(el).toBeTruthy();
8118
+ return el as HTMLElement;
8119
+ },
8120
+ { timeout: 5000 }
8121
+ );
8122
+
8123
+ // (a) The row draws the friendly name, not the reference.
8124
+ expect(row.textContent).toContain('title (this case)');
8125
+
8126
+ // (b) ONE hover on the value. The row must not add a native `title`
8127
+ // alongside the chip's own popover — the label keeps its own, which is
8128
+ // a different element and a different thing to say.
8129
+ const nativeTitles = Array.from(row.querySelectorAll('[title]')).map((el) =>
8130
+ el.getAttribute('title')
8131
+ );
8132
+ expect(nativeTitles).not.toContain('$._case.title');
8133
+ },
8134
+ };