@qoretechnologies/reqraft 0.10.44-pr.95.gb67bfb7 → 0.10.44

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 (144) hide show
  1. package/.claude/CLAUDE.md +12 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
  4. package/dist/components/composer/Composer.d.ts.map +1 -1
  5. package/dist/components/composer/Composer.js +5 -1
  6. package/dist/components/composer/Composer.js.map +1 -1
  7. package/dist/components/form/engine/CompactRow.d.ts +20 -1
  8. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  9. package/dist/components/form/engine/CompactRow.js +279 -41
  10. package/dist/components/form/engine/CompactRow.js.map +1 -1
  11. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  12. package/dist/components/form/engine/CompactToolbar.js +72 -72
  13. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  14. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  15. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  16. package/dist/components/form/engine/FormEngine.js +203 -98
  17. package/dist/components/form/engine/FormEngine.js.map +1 -1
  18. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  19. package/dist/components/form/engine/_structuredData/SchemaDataView.js +108 -20
  20. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  21. package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
  22. package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
  23. package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
  24. package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
  25. package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
  26. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  27. package/dist/components/form/engine/compactRowStyles.js +21 -4
  28. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  29. package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
  30. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  31. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  32. package/dist/components/form/engine/readFirst.d.ts +24 -0
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +84 -6
  35. package/dist/components/form/engine/readFirst.js.map +1 -1
  36. package/dist/components/form/fields/Field.d.ts.map +1 -1
  37. package/dist/components/form/fields/Field.js +1 -1
  38. package/dist/components/form/fields/Field.js.map +1 -1
  39. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
  40. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  41. package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
  42. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  43. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  44. package/dist/components/form/fields/auto/AutoFormField.js +64 -14
  45. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  46. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  47. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  48. package/dist/components/form/fields/long-string/LongString.js +25 -4
  49. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  50. package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
  51. package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
  52. package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
  53. package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
  54. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  55. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
  56. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  57. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  58. package/dist/components/form/fields/template/TemplateField.js +35 -6
  59. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  60. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
  61. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
  62. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
  63. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
  64. package/dist/components/form/index.d.ts +1 -0
  65. package/dist/components/form/index.d.ts.map +1 -1
  66. package/dist/components/form/index.js +1 -0
  67. package/dist/components/form/index.js.map +1 -1
  68. package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
  69. package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
  70. package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
  71. package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
  72. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  73. package/dist/components/ticketThread/TicketThread.js +1 -1
  74. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  75. package/dist/helpers/common.d.ts +8 -1
  76. package/dist/helpers/common.d.ts.map +1 -1
  77. package/dist/helpers/common.js +9 -1
  78. package/dist/helpers/common.js.map +1 -1
  79. package/dist/helpers/optionUiTypes.d.ts +30 -0
  80. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  81. package/dist/helpers/optionUiTypes.js +148 -0
  82. package/dist/helpers/optionUiTypes.js.map +1 -0
  83. package/dist/helpers/options.d.ts +19 -0
  84. package/dist/helpers/options.d.ts.map +1 -1
  85. package/dist/helpers/options.js +25 -1
  86. package/dist/helpers/options.js.map +1 -1
  87. package/dist/helpers/providerValue.d.ts +68 -0
  88. package/dist/helpers/providerValue.d.ts.map +1 -0
  89. package/dist/helpers/providerValue.js +50 -0
  90. package/dist/helpers/providerValue.js.map +1 -0
  91. package/dist/helpers/singleLineString.d.ts +37 -0
  92. package/dist/helpers/singleLineString.d.ts.map +1 -0
  93. package/dist/helpers/singleLineString.js +66 -0
  94. package/dist/helpers/singleLineString.js.map +1 -0
  95. package/dist/helpers/templates.d.ts +100 -0
  96. package/dist/helpers/templates.d.ts.map +1 -1
  97. package/dist/helpers/templates.js +237 -4
  98. package/dist/helpers/templates.js.map +1 -1
  99. package/dist/helpers/validations.d.ts +103 -2
  100. package/dist/helpers/validations.d.ts.map +1 -1
  101. package/dist/helpers/validations.js +858 -91
  102. package/dist/helpers/validations.js.map +1 -1
  103. package/dist/index.d.ts +1 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +2 -1
  106. package/dist/index.js.map +1 -1
  107. package/package.json +1 -1
  108. package/src/components/composer/Composer.tsx +7 -3
  109. package/src/components/form/engine/CompactRow.tsx +391 -29
  110. package/src/components/form/engine/CompactToolbar.tsx +105 -98
  111. package/src/components/form/engine/FormEngine.stories.tsx +863 -12
  112. package/src/components/form/engine/FormEngine.tsx +231 -68
  113. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +123 -0
  114. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +217 -95
  115. package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
  116. package/src/components/form/engine/compactRowStyles.ts +38 -1
  117. package/src/components/form/engine/compactToolbarContext.ts +8 -0
  118. package/src/components/form/engine/readFirst.ts +125 -35
  119. package/src/components/form/fields/Field.tsx +3 -1
  120. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  121. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
  122. package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
  123. package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
  124. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  125. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
  126. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
  127. package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
  128. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
  129. package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
  130. package/src/components/form/fields/template/TemplateField.tsx +47 -8
  131. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
  132. package/src/components/form/index.tsx +1 -0
  133. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
  134. package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
  135. package/src/components/ticketThread/TicketThread.tsx +7 -0
  136. package/src/helpers/common.ts +11 -1
  137. package/src/helpers/optionUiTypes.ts +153 -0
  138. package/src/helpers/options.ts +23 -0
  139. package/src/helpers/providerValue.ts +125 -0
  140. package/src/helpers/singleLineString.ts +61 -0
  141. package/src/helpers/templates.ts +272 -3
  142. package/src/helpers/validations.ts +1177 -109
  143. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  144. package/src/index.tsx +1 -0
@@ -1,17 +1,31 @@
1
1
  import { ReqoreTextarea } from '@qoretechnologies/reqore';
2
2
  import { IReqoreTextareaProps } from '@qoretechnologies/reqore/dist/components/Textarea';
3
- import { ChangeEvent, useCallback, useEffect, useState } from 'react';
3
+ import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react';
4
4
  import { useDebounce } from 'react-use';
5
+ import {
6
+ flattenToSingleLine,
7
+ hasLineBreak,
8
+ isSingleLineStringType,
9
+ } from '../../../../helpers/singleLineString';
5
10
 
6
11
  export interface ILongStringFormFieldProps extends Omit<IReqoreTextareaProps, 'onChange'> {
7
12
  value?: string;
8
13
  onChange?: (value: string, event?: ChangeEvent<HTMLTextAreaElement>) => void;
14
+ /**
15
+ * The field's declared type, which decides whether it holds one line.
16
+ *
17
+ * Without it every string field is a growing textarea, so a technical name, a
18
+ * version and an IP address all accept Enter — and become YAML keys, class
19
+ * names and URLs with a newline in them. See `helpers/singleLineString`.
20
+ */
21
+ type?: string;
9
22
  }
10
23
 
11
24
  export const LongStringFormField = ({
12
25
  value,
13
26
  onChange,
14
27
  onClearClick,
28
+ type,
15
29
  ...rest
16
30
  }: ILongStringFormFieldProps) => {
17
31
  const [localValue, setLocalValue] = useState<string>(value ?? '');
@@ -32,9 +46,32 @@ export const LongStringFormField = ({
32
46
  [localValue, onChange]
33
47
  );
34
48
 
35
- const handleChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>): void => {
36
- setLocalValue(event.target.value);
37
- }, []);
49
+ const singleLine = isSingleLineStringType(type);
50
+
51
+ const handleChange = useCallback(
52
+ (event: ChangeEvent<HTMLTextAreaElement>): void => {
53
+ const raw = event.target.value;
54
+ // Flatten here rather than only blocking the Enter key: a line break also
55
+ // arrives by paste, by drag-and-drop, from autofill and from an IME, and
56
+ // a keydown guard catches none of those.
57
+ setLocalValue(singleLine && hasLineBreak(raw) ? flattenToSingleLine(raw) : raw);
58
+ },
59
+ [singleLine]
60
+ );
61
+
62
+ // Stop Enter before it inserts anything, so the caret does not jump to a
63
+ // second line that is then flattened away under the cursor.
64
+ const handleKeyDown = useCallback(
65
+ (event: KeyboardEvent<HTMLTextAreaElement>): void => {
66
+ if (singleLine && event.key === 'Enter') {
67
+ event.preventDefault();
68
+ }
69
+ (rest as { onKeyDown?: (event: KeyboardEvent<HTMLTextAreaElement>) => void }).onKeyDown?.(
70
+ event
71
+ );
72
+ },
73
+ [singleLine, rest.onKeyDown]
74
+ );
38
75
 
39
76
  const handleClearClick = useCallback(() => {
40
77
  setLocalValue('');
@@ -44,12 +81,16 @@ export const LongStringFormField = ({
44
81
 
45
82
  return (
46
83
  <ReqoreTextarea
47
- scaleWithContent
84
+ // A one-line field must not grow, and must not offer a resize grip that
85
+ // implies it can hold more than it will keep.
86
+ scaleWithContent={!singleLine}
87
+ rows={singleLine ? 1 : undefined}
48
88
  fluid
49
89
  value={localValue}
50
90
  onChange={handleChange}
51
91
  onClearClick={handleClearClick}
52
92
  {...rest}
93
+ onKeyDown={handleKeyDown}
53
94
  />
54
95
  );
55
96
  };
@@ -55,6 +55,11 @@ export const Empty: Story = {
55
55
  await expect(canvas.getByText('Tables')).toBeInTheDocument();
56
56
  await expect(canvas.getByText('Sequences')).toBeInTheDocument();
57
57
  await expect(canvas.getByText('Migrations')).toBeInTheDocument();
58
+ // The form rows fade in after the chrome — wait for the Schema tab's own
59
+ // catalogue rows, or the capture shows empty shells where the form goes
60
+ // (rejected on build #123 for exactly that).
61
+ await expect(await canvas.findByText('Schema Name')).toBeInTheDocument();
62
+ await expect(await canvas.findByText('Version')).toBeInTheDocument();
58
63
  },
59
64
  };
60
65
 
@@ -0,0 +1,114 @@
1
+ import { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { expect, fireEvent, fn, waitFor } from 'storybook/test';
3
+ import { LONG_EXAMPLE_VALUE } from './__fixtures__/longExampleValue';
4
+ import { ReqraftTemplateExampleValueModal } from './ExampleValueModal';
5
+
6
+ const meta = {
7
+ component: ReqraftTemplateExampleValueModal,
8
+ title: 'Components/Form/TemplateExampleValueModal',
9
+ args: {
10
+ // Standalone render — in the app the modal rides reqore's global modal
11
+ // queue, whose wrapper injects `isOpen`/`onClose` itself.
12
+ isOpen: true,
13
+ label: 'Attachment Body',
14
+ value: LONG_EXAMPLE_VALUE,
15
+ onClose: fn(),
16
+ },
17
+ } as Meta<typeof ReqraftTemplateExampleValueModal>;
18
+
19
+ export default meta;
20
+
21
+ export const Default: StoryObj<typeof meta> = {
22
+ parameters: {
23
+ docs: {
24
+ description: {
25
+ story:
26
+ 'The full example-value viewer for template items whose value is too long for the picker: the whole value in a selectable, scrollable textarea filling the modal body, with a Copy action. Copy surfaces a notification either way — success, or a danger notice when the browser denies clipboard access — never an unhandled rejection.',
27
+ },
28
+ },
29
+ },
30
+ play: async () => {
31
+ const modal = await waitFor(
32
+ () => {
33
+ const el = document.querySelector('.reqraft-template-example-value-modal');
34
+ if (!el) throw new Error('modal not rendered yet');
35
+ return el;
36
+ },
37
+ { timeout: 10000 }
38
+ );
39
+
40
+ // The modal holds the WHOLE value — down to the sentinel at the very end.
41
+ const textarea = modal.querySelector('textarea') as HTMLTextAreaElement;
42
+ await expect(textarea.value).toBe(LONG_EXAMPLE_VALUE);
43
+ await expect(textarea.value.endsWith('THE_VERY_END')).toBe(true);
44
+
45
+ // Copy notifies (success locally, clipboard-denied danger in headless CI).
46
+ const copy = Array.from(modal.querySelectorAll('button')).find((button) =>
47
+ button.textContent?.includes('Copy')
48
+ );
49
+ await expect(copy).toBeTruthy();
50
+ fireEvent.click(copy);
51
+ await waitFor(
52
+ () => {
53
+ if (!document.querySelector('.reqore-notification')) {
54
+ throw new Error('no notification yet');
55
+ }
56
+ },
57
+ { timeout: 10000 }
58
+ );
59
+ },
60
+ };
61
+
62
+ export const Mobile: StoryObj<typeof meta> = {
63
+ args: {
64
+ // The overridable strings double as the prop-passthrough coverage: the
65
+ // play below asserts THIS notification text, proving consumers (e.g.
66
+ // qorus-ide under Lingui) can supply translated copy.
67
+ copyLabel: 'Copy value',
68
+ copyNotificationContent: 'Value copied',
69
+ copyFailedNotificationContent: 'Copy was blocked',
70
+ },
71
+ parameters: {
72
+ qlip: { viewport: { width: 390, height: 844 } },
73
+ docs: {
74
+ description: {
75
+ story:
76
+ 'The phone presentation of the full-value modal — the textarea still fills the modal body at phone width, and every user-visible string (Copy label, both notifications) is prop-overridden here, which doubles as coverage that consumers can pass translated copy.',
77
+ },
78
+ },
79
+ },
80
+ play: async () => {
81
+ const modal = await waitFor(
82
+ () => {
83
+ const el = document.querySelector('.reqraft-template-example-value-modal');
84
+ if (!el) throw new Error('modal not rendered yet');
85
+ return el;
86
+ },
87
+ { timeout: 10000 }
88
+ );
89
+
90
+ const textarea = modal.querySelector('textarea') as HTMLTextAreaElement;
91
+ await expect(textarea.value).toBe(LONG_EXAMPLE_VALUE);
92
+
93
+ // The overridden copy label renders…
94
+ const copy = Array.from(modal.querySelectorAll('button')).find((button) =>
95
+ button.textContent?.includes('Copy value')
96
+ );
97
+ await expect(copy).toBeTruthy();
98
+
99
+ // …and the overridden notification content is what surfaces (success or
100
+ // clipboard-denied, both prop-supplied).
101
+ fireEvent.click(copy);
102
+ await waitFor(
103
+ () => {
104
+ const notification = document.querySelector('.reqore-notification');
105
+ if (!notification) throw new Error('no notification yet');
106
+ const text = notification.textContent || '';
107
+ if (!text.includes('Value copied') && !text.includes('Copy was blocked')) {
108
+ throw new Error(`unexpected notification: ${text}`);
109
+ }
110
+ },
111
+ { timeout: 10000 }
112
+ );
113
+ },
114
+ };
@@ -0,0 +1,99 @@
1
+ import { ReqoreModal, ReqoreTextarea, useReqoreProperty } from '@qoretechnologies/reqore';
2
+ import { IReqoreModalProps } from '@qoretechnologies/reqore/dist/components/Modal';
3
+ import { CSSProperties, memo, useCallback } from 'react';
4
+
5
+ export interface IReqraftTemplateExampleValueModalProps extends Partial<IReqoreModalProps> {
6
+ /** The template field's display name — the modal heading. */
7
+ label?: string;
8
+ /** The FULL serialized example value. */
9
+ value: string;
10
+ /** Copy-action label. @default 'Copy' */
11
+ copyLabel?: string;
12
+ /**
13
+ * Notification shown after a successful copy (prop-overridable so consumers
14
+ * can pass translated copy — same contract as `ReqoreExportModal`).
15
+ * @default 'Example value copied to clipboard'
16
+ */
17
+ copyNotificationContent?: string;
18
+ /** Notification shown when the browser denies clipboard access. @default 'Could not copy to clipboard' */
19
+ copyFailedNotificationContent?: string;
20
+ }
21
+
22
+ /* Fill the modal's fixed-height body instead of scaling with content: a
23
+ content-scaled textarea either stops short of the body (dead space below, a
24
+ partial-height scrollbar) or explodes it on huge values. Filling hands the
25
+ whole body to the textarea, so its scrollbar spans the modal body. Module
26
+ scope for stable identity. */
27
+ const FILL_STYLE: CSSProperties = { height: '100%' };
28
+
29
+ /**
30
+ * Full example-value viewer for template/context-data items whose
31
+ * `example_value` is too long for the picker's item description (a
32
+ * binary-carrying field's example can be an entire base64 file). Opened by the
33
+ * "?" item action wired in `helpers/templates.ts` via reqore's global
34
+ * `modalStore.addModal()` — the modals wrapper injects `isOpen`/`onClose`, so
35
+ * this component stays presentation-only (pass `isOpen` yourself only when
36
+ * rendering it standalone, e.g. in a story).
37
+ */
38
+ export const ReqraftTemplateExampleValueModal = memo(
39
+ ({
40
+ label,
41
+ value,
42
+ copyLabel = 'Copy',
43
+ copyNotificationContent = 'Example value copied to clipboard',
44
+ copyFailedNotificationContent = 'Could not copy to clipboard',
45
+ ...rest
46
+ }: IReqraftTemplateExampleValueModalProps) => {
47
+ const addNotification = useReqoreProperty('addNotification');
48
+
49
+ const handleCopyClick = useCallback(async () => {
50
+ // Clipboard access can be denied (permissions, insecure context) — the
51
+ // failure surfaces as a notification instead of an unhandled rejection.
52
+ try {
53
+ await navigator.clipboard.writeText(value);
54
+ addNotification({
55
+ content: copyNotificationContent,
56
+ intent: 'success',
57
+ duration: 3000,
58
+ });
59
+ } catch (error) {
60
+ addNotification({
61
+ content: copyFailedNotificationContent,
62
+ intent: 'danger',
63
+ duration: 3000,
64
+ });
65
+ }
66
+ }, [value, addNotification, copyNotificationContent, copyFailedNotificationContent]);
67
+
68
+ return (
69
+ <ReqoreModal
70
+ label={label || 'Example value'}
71
+ icon='FileTextLine'
72
+ height='70vh'
73
+ className='reqraft-template-example-value-modal'
74
+ bottomActions={[
75
+ {
76
+ position: 'right',
77
+ label: copyLabel,
78
+ icon: 'ClipboardLine',
79
+ onClick: () => {
80
+ handleCopyClick();
81
+ },
82
+ },
83
+ ]}
84
+ {...rest}
85
+ >
86
+ <ReqoreTextarea
87
+ readOnly
88
+ value={value}
89
+ minimal
90
+ fluid
91
+ style={FILL_STYLE}
92
+ wrapperStyle={FILL_STYLE}
93
+ />
94
+ </ReqoreModal>
95
+ );
96
+ }
97
+ );
98
+
99
+ ReqraftTemplateExampleValueModal.displayName = 'ReqraftTemplateExampleValueModal';
@@ -2,7 +2,11 @@ import { ReqoreTag, ReqoreTagGroup } from '@qoretechnologies/reqore';
2
2
  import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
3
3
  import { TSizes } from '@qoretechnologies/reqore/dist/constants/sizes';
4
4
  import { memo } from 'react';
5
- import { findTemplate, getTemplateTagStyle, TTemplateMeta } from '../../../../helpers/templates';
5
+ import {
6
+ getTemplateTagStyle,
7
+ describeTemplateReference,
8
+ TTemplateMeta,
9
+ } from '../../../../helpers/templates';
6
10
 
7
11
  export interface IReadOnlyTemplateTagProps {
8
12
  /** The template reference value, e.g. `$local:test`. */
@@ -22,14 +26,14 @@ export interface IReadOnlyTemplateTagProps {
22
26
  */
23
27
  export const ReadOnlyTemplateTag = memo(
24
28
  ({ value, templates, size }: IReadOnlyTemplateTagProps) => {
25
- const template = templates ? findTemplate(templates, value) : undefined;
26
- const metadata = template?.metadata as TTemplateMeta | undefined;
29
+ const { label, item } = describeTemplateReference(templates, value);
30
+ const metadata = item?.metadata as TTemplateMeta | undefined;
27
31
  return (
28
32
  <ReqoreTagGroup size={size}>
29
33
  <ReqoreTag
30
34
  icon='ExchangeDollarLine'
31
35
  leftIconProps={{ image: metadata?.image }}
32
- label={template?.label || value}
36
+ label={label}
33
37
  tooltip={value}
34
38
  {...getTemplateTagStyle(metadata)}
35
39
  />
@@ -19,6 +19,7 @@ import { AutoFormField as auto } from '../auto/AutoFormField';
19
19
  import LongStringFormField from '../long-string/LongString';
20
20
  import NumberFormField from '../number/Number';
21
21
  import { StringFormField } from '../string/String';
22
+ import { LONG_EXAMPLE_VALUE } from './__fixtures__/longExampleValue';
22
23
  import { TemplateField } from './TemplateField';
23
24
  import multiLevelTemplates from './__fixtures__/multiLevelTemplates.json';
24
25
  import templates from './__fixtures__/templates.json';
@@ -307,6 +308,158 @@ export const TemplateValue: StoryObj<typeof meta> = {
307
308
  },
308
309
  };
309
310
 
311
+ /** REGRESSION — a rehydrated whole-token template value (the FSM state-output
312
+ * `$data:{…}` form above all) must render as the picker CHIP, not as its raw
313
+ * token text in a string editor. */
314
+ export const BracedTemplateValue: StoryObj<typeof meta> = {
315
+ args: {
316
+ component: LongStringField,
317
+ type: 'string',
318
+ allowTemplates: true,
319
+ allowCustomValues: true,
320
+ value: '$data:{W2n_BuSHbaNrbvV1MkfPF.filename}',
321
+ templates: buildTemplates({
322
+ state_outputs: {
323
+ display_name: 'Context Data',
324
+ short_desc: 'Outputs of previous states',
325
+ app: 'GoogleDrive',
326
+ items: [
327
+ {
328
+ name: 'filename',
329
+ display_name: 'Filename',
330
+ short_desc: 'The uploaded file name',
331
+ desc: 'The uploaded file name',
332
+ value: '$data:{W2n_BuSHbaNrbvV1MkfPF.filename}',
333
+ type: 'string',
334
+ },
335
+ ],
336
+ },
337
+ } as any),
338
+ },
339
+ parameters: {
340
+ docs: {
341
+ description: {
342
+ story:
343
+ 'Renders TemplateField for a string field whose saved value is a braced state-output reference ($data:{id.filename}) — the field shows the resolved picker chip ("Filename"), never the raw token in a textarea, which is what rehydrated drafts degraded to before the token grammar learned braces.',
344
+ },
345
+ },
346
+ },
347
+ play: async () => {
348
+ await waitFor(() => {
349
+ const chip = Array.from(document.querySelectorAll('.reqore-button')).find((button) =>
350
+ button.textContent?.includes('Filename')
351
+ );
352
+ expect(chip, 'the braced value renders as the labeled picker chip').toBeTruthy();
353
+ });
354
+ const rawTextareas = Array.from(document.querySelectorAll('textarea')).filter((textarea) =>
355
+ textarea.value.includes('$data:{')
356
+ );
357
+ expect(rawTextareas, 'no textarea holds the raw token').toHaveLength(0);
358
+ },
359
+ };
360
+
361
+ /** A template-authored Qog keys its states `'1'`, `'2'`, … while each state
362
+ * carries its own id, and its saved `$data:{…}` refs use that id — so the
363
+ * catalogue (spelled with the key) and the value (spelled with the id) never
364
+ * match as text. The producer supplies the alternate spelling as an alias so
365
+ * the chip still resolves to a name instead of printing the raw token. */
366
+ export const AliasedStateTemplateValue: StoryObj<typeof meta> = {
367
+ args: {
368
+ component: LongStringField,
369
+ type: 'string',
370
+ allowTemplates: true,
371
+ allowCustomValues: true,
372
+ // what the Qog has saved — the state's own id
373
+ value: '$data:{dc_ai_reply.choices}',
374
+ templates: {
375
+ items: [
376
+ {
377
+ label: 'Generate AI Reply',
378
+ description: 'Outputs of the Groq chat-completion state',
379
+ items: [
380
+ {
381
+ label: 'Choices',
382
+ // what the server's catalogue offers — the states-hash key
383
+ value: '$data:{3.choices}',
384
+ badge: 'list',
385
+ metadata: { aliasValues: ['$data:{dc_ai_reply.choices}'] },
386
+ },
387
+ ],
388
+ },
389
+ ],
390
+ } as any,
391
+ },
392
+ parameters: {
393
+ docs: {
394
+ description: {
395
+ story:
396
+ 'Renders TemplateField holding a state-output reference spelled with the state id ($data:{dc_ai_reply.choices}) while the catalogue offers the same item spelled with the states-hash key ($data:{3.choices}) — the chip resolves through the item alias and shows "Choices", where it used to print the raw token.',
397
+ },
398
+ },
399
+ },
400
+ play: async () => {
401
+ await waitFor(() => {
402
+ const chip = Array.from(document.querySelectorAll('.reqore-button')).find((button) =>
403
+ button.textContent?.includes('Choices')
404
+ );
405
+ expect(chip, 'the aliased value resolves to the labeled picker chip').toBeTruthy();
406
+ });
407
+ const rawChips = Array.from(document.querySelectorAll('.reqore-button')).filter((button) =>
408
+ button.textContent?.includes('$data:{')
409
+ );
410
+ expect(rawChips, 'no chip prints the raw token').toHaveLength(0);
411
+ },
412
+ };
413
+
414
+ /** A catalogue can only offer what the action's output type declares, so it
415
+ * stops at a list — `choices` is offered, `choices[0].message.content` is
416
+ * hand-written past it. Such a value has no item of its own and used to
417
+ * render as its raw token; it is now named after its nearest ancestor. */
418
+ export const ExtendedPathTemplateValue: StoryObj<typeof meta> = {
419
+ args: {
420
+ component: LongStringField,
421
+ type: 'string',
422
+ allowTemplates: true,
423
+ allowCustomValues: true,
424
+ value: '$data:{dc_ai_reply.choices[0].message.content}',
425
+ templates: {
426
+ items: [
427
+ {
428
+ label: 'Generate AI Reply',
429
+ items: [
430
+ {
431
+ label: 'Choices',
432
+ value: '$data:{3.choices}',
433
+ badge: 'list',
434
+ metadata: { aliasValues: ['$data:{dc_ai_reply.choices}'] },
435
+ },
436
+ ],
437
+ },
438
+ ],
439
+ } as any,
440
+ },
441
+ parameters: {
442
+ docs: {
443
+ description: {
444
+ story:
445
+ 'Renders TemplateField holding a path extended past the named field it starts from ($data:{dc_ai_reply.choices[0].message.content}, where the catalogue only offers "choices") — the chip is named after its nearest ancestor as "Choices[0].message.content" instead of printing the raw token.',
446
+ },
447
+ },
448
+ },
449
+ play: async () => {
450
+ await waitFor(() => {
451
+ const chip = Array.from(document.querySelectorAll('.reqore-button')).find((button) =>
452
+ button.textContent?.includes('Choices[0].message.content')
453
+ );
454
+ expect(chip, 'the extended path is named after its ancestor').toBeTruthy();
455
+ });
456
+ const rawChips = Array.from(document.querySelectorAll('.reqore-button')).filter((button) =>
457
+ button.textContent?.includes('$data:{')
458
+ );
459
+ expect(rawChips, 'no chip prints the raw token').toHaveLength(0);
460
+ },
461
+ };
462
+
310
463
  export const ElementTypeInListShowsCorrectTemplates: StoryObj<typeof meta> = {
311
464
  args: {
312
465
  component: auto,
@@ -521,7 +674,7 @@ export const TemplateCanBeSelected: StoryObj<typeof meta> = {
521
674
  docs: {
522
675
  description: {
523
676
  story:
524
- 'Renders TemplateField for a string, opens the templates popover and clicks the Interface ID template — the field switches to the $local:id template value.',
677
+ 'Renders TemplateField for a string, opens the templates popover and clicks the Interface ID template — the field switches to the $local:id template value inside the template-offering input (plain word tokens stay typeable; only braced context refs chip).',
525
678
  },
526
679
  },
527
680
  },
@@ -652,3 +805,163 @@ export const TemplateWithItemsCanBeSelected: StoryObj<typeof meta> = {
652
805
  await _testsWaitForText('Testing Hash');
653
806
  },
654
807
  };
808
+
809
+ /* One group, four items covering every affordance combination:
810
+ - "Attachment Body" — leaf, LONG example → "?" (right) only
811
+ - "Attachment Name" — leaf, short example → no actions at all
812
+ - "Message Author Props" — hash with children, LONG example (the whole
813
+ object) → BOTH: the "+" select action (left) and the "?" (right)
814
+ - "Delivery Flags" — hash with children, short example → "+" only
815
+ The sentinel at the very end proves truncation in the picker and
816
+ completeness in the modal. */
817
+ const longExampleTemplates = {
818
+ gmail_attachment: {
819
+ display_name: 'New Attachment',
820
+ short_desc: 'Data from the attachment trigger',
821
+ app: 'Gmail',
822
+ items: [
823
+ {
824
+ name: 'attachment_body',
825
+ display_name: 'Attachment Body',
826
+ value: '$data:{on_attachment.data}',
827
+ type: 'data',
828
+ example_value: LONG_EXAMPLE_VALUE,
829
+ },
830
+ {
831
+ name: 'attachment_name',
832
+ display_name: 'Attachment Name',
833
+ value: '$data:{on_attachment.name}',
834
+ type: 'string',
835
+ example_value: 'invoice.pdf',
836
+ },
837
+ {
838
+ name: 'author',
839
+ display_name: 'Message Author Properties',
840
+ value: '$data:{on_attachment.author}',
841
+ type: 'hash',
842
+ // A parent's example is the whole object, so it goes long almost by
843
+ // definition — this is the "+ AND ?" case.
844
+ example_value: {
845
+ name: 'Ada Lovelace',
846
+ email: 'ada@example.com',
847
+ signature: LONG_EXAMPLE_VALUE.slice(0, 600),
848
+ },
849
+ items: [
850
+ {
851
+ name: 'author_name',
852
+ display_name: 'Name',
853
+ value: '$data:{on_attachment.author.name}',
854
+ type: 'string',
855
+ example_value: 'Ada Lovelace',
856
+ },
857
+ {
858
+ name: 'author_email',
859
+ display_name: 'Email',
860
+ value: '$data:{on_attachment.author.email}',
861
+ type: 'string',
862
+ example_value: 'ada@example.com',
863
+ },
864
+ ],
865
+ },
866
+ {
867
+ name: 'flags',
868
+ display_name: 'Delivery Flags',
869
+ value: '$data:{on_attachment.flags}',
870
+ type: 'hash',
871
+ // Small object: the serialized example stays under the preview
872
+ // threshold, so this parent keeps ONLY the "+" select action.
873
+ example_value: { read: true },
874
+ items: [
875
+ {
876
+ name: 'flags_read',
877
+ display_name: 'Read',
878
+ value: '$data:{on_attachment.flags.read}',
879
+ type: 'bool',
880
+ example_value: true,
881
+ },
882
+ ],
883
+ },
884
+ ],
885
+ },
886
+ };
887
+
888
+ export const LongExampleValueTruncatedWithModal: StoryObj<typeof meta> = {
889
+ args: {
890
+ component: LongStringField,
891
+ value: 'Some string',
892
+ type: 'string',
893
+ templates: buildTemplates(longExampleTemplates as any),
894
+ },
895
+ parameters: {
896
+ docs: {
897
+ description: {
898
+ story:
899
+ 'Every item-affordance combination in one picker: a leaf with a whole-base64-file example gets a single-line ellipsized preview (cut at the popover\'s actual width — no breakpoints) and the "?" action (right) opening the full value in a modal; a hash parent whose own example object is long gets BOTH the "+" select action (left) and the "?"; a small hash parent keeps only the "+"; and a short leaf keeps its full wrapped preview with no actions. Neither previewing nor copying ever selects an item.',
900
+ },
901
+ },
902
+ },
903
+ play: async ({ args }) => {
904
+ await _testsOpenTemplates();
905
+ await _testsWaitForText('Attachment Body');
906
+
907
+ // The picker shows TRUNCATED previews — the long value's tail sentinel
908
+ // must not be in the popover…
909
+ const popover = document.querySelector('.reqore-popover-content');
910
+ await expect(popover.textContent).toContain('Example value: "JVBERi0x');
911
+ await expect(popover.textContent).not.toContain('THE_VERY_END');
912
+
913
+ // …and the affordances land exactly by shape: "?" on the long leaf AND
914
+ // the long-example hash parent (2), "+" on both hash parents (2) — the
915
+ // long-example parent carries both at once. The short leaf and the small
916
+ // parent's example stay modal-free.
917
+ await expect(popover.querySelectorAll('.reqore-menu-item-right-action').length).toBe(2);
918
+ await expect(popover.querySelectorAll('.reqore-menu-item-left-action').length).toBe(2);
919
+
920
+ // Long previews are single-line ellipsized at the RENDERED width
921
+ // (container-intrinsic — the same item adapts to any popover width);
922
+ // short previews keep normal wrapping.
923
+ const descriptions = popover.querySelectorAll('.reqore-button-description');
924
+ const longDescription = Array.from(descriptions).find((el) =>
925
+ el.textContent?.includes('JVBERi0x')
926
+ ) as HTMLElement;
927
+ const shortDescription = Array.from(descriptions).find((el) =>
928
+ el.textContent?.includes('invoice.pdf')
929
+ ) as HTMLElement;
930
+ await expect(getComputedStyle(longDescription).whiteSpace).toBe('nowrap');
931
+ await expect(longDescription.scrollWidth).toBeGreaterThan(longDescription.clientWidth);
932
+ await expect(getComputedStyle(shortDescription).whiteSpace).not.toBe('nowrap');
933
+
934
+ await _testsClickButton({ selector: '.reqore-menu-item-right-action' });
935
+
936
+ const modal = await waitFor(
937
+ () => {
938
+ const el = document.querySelector('.reqraft-template-example-value-modal');
939
+ if (!el) throw new Error('modal not open yet');
940
+ return el;
941
+ },
942
+ { timeout: 10000 }
943
+ );
944
+
945
+ // The modal holds the WHOLE value, down to the sentinel.
946
+ const textarea = modal.querySelector('textarea') as HTMLTextAreaElement;
947
+ await expect(textarea.value.endsWith('THE_VERY_END"')).toBe(true);
948
+
949
+ // Close it; opening/previewing must never have selected the item.
950
+ fireEvent.click(modal.querySelector('.reqore-drawer-close-button'));
951
+ await waitFor(
952
+ () => {
953
+ if (document.querySelector('.reqraft-template-example-value-modal')) {
954
+ throw new Error('modal still open');
955
+ }
956
+ },
957
+ { timeout: 10000 }
958
+ );
959
+ await expect(args.onChange).not.toHaveBeenCalled();
960
+
961
+ // End on the informative frame for the visual snapshot: the popover with
962
+ // the truncated preview and its "?" action visible (the assertions above
963
+ // close everything, which would leave the capture showing an empty field).
964
+ await _testsOpenTemplates();
965
+ await _testsWaitForText('Attachment Body');
966
+ },
967
+ };