@qoretechnologies/reqraft 0.10.41-pr.95.g294e5aa → 0.10.41

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 (137) 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/codeSize/index.d.ts +37 -0
  5. package/dist/components/codeSize/index.d.ts.map +1 -0
  6. package/dist/components/codeSize/index.js +87 -0
  7. package/dist/components/codeSize/index.js.map +1 -0
  8. package/dist/components/form/engine/CompactRow.d.ts +20 -1
  9. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  10. package/dist/components/form/engine/CompactRow.js +284 -42
  11. package/dist/components/form/engine/CompactRow.js.map +1 -1
  12. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  13. package/dist/components/form/engine/CompactToolbar.js +72 -72
  14. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  15. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  16. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  17. package/dist/components/form/engine/FormEngine.js +163 -75
  18. package/dist/components/form/engine/FormEngine.js.map +1 -1
  19. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  20. package/dist/components/form/engine/_structuredData/SchemaDataView.js +102 -87
  21. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  22. package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
  23. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  24. package/dist/components/form/engine/compactRowStyles.js +22 -5
  25. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  26. package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
  27. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  28. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  29. package/dist/components/form/engine/readFirst.d.ts +57 -1
  30. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  31. package/dist/components/form/engine/readFirst.js +143 -4
  32. package/dist/components/form/engine/readFirst.js.map +1 -1
  33. package/dist/components/form/fields/Field.d.ts.map +1 -1
  34. package/dist/components/form/fields/Field.js +1 -1
  35. package/dist/components/form/fields/Field.js.map +1 -1
  36. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
  37. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  38. package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
  39. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  40. package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
  41. package/dist/components/form/fields/array/ArrayAuto.js +44 -26
  42. package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
  43. package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
  44. package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
  45. package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
  46. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  47. package/dist/components/form/fields/auto/AutoFormField.js +32 -4
  48. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  49. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  50. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  51. package/dist/components/form/fields/long-string/LongString.js +25 -4
  52. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  53. package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
  54. package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
  55. package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
  56. package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
  57. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  58. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
  59. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  60. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  61. package/dist/components/form/fields/template/TemplateField.js +35 -6
  62. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  63. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
  64. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
  65. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
  66. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
  67. package/dist/components/form/index.d.ts +1 -0
  68. package/dist/components/form/index.d.ts.map +1 -1
  69. package/dist/components/form/index.js +1 -0
  70. package/dist/components/form/index.js.map +1 -1
  71. package/dist/helpers/common.d.ts +8 -1
  72. package/dist/helpers/common.d.ts.map +1 -1
  73. package/dist/helpers/common.js +9 -1
  74. package/dist/helpers/common.js.map +1 -1
  75. package/dist/helpers/optionUiTypes.d.ts +30 -0
  76. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  77. package/dist/helpers/optionUiTypes.js +148 -0
  78. package/dist/helpers/optionUiTypes.js.map +1 -0
  79. package/dist/helpers/options.d.ts +19 -0
  80. package/dist/helpers/options.d.ts.map +1 -1
  81. package/dist/helpers/options.js +25 -1
  82. package/dist/helpers/options.js.map +1 -1
  83. package/dist/helpers/providerValue.d.ts +68 -0
  84. package/dist/helpers/providerValue.d.ts.map +1 -0
  85. package/dist/helpers/providerValue.js +50 -0
  86. package/dist/helpers/providerValue.js.map +1 -0
  87. package/dist/helpers/singleLineString.d.ts +37 -0
  88. package/dist/helpers/singleLineString.d.ts.map +1 -0
  89. package/dist/helpers/singleLineString.js +66 -0
  90. package/dist/helpers/singleLineString.js.map +1 -0
  91. package/dist/helpers/templates.d.ts +100 -0
  92. package/dist/helpers/templates.d.ts.map +1 -1
  93. package/dist/helpers/templates.js +237 -4
  94. package/dist/helpers/templates.js.map +1 -1
  95. package/dist/helpers/validations.d.ts +103 -2
  96. package/dist/helpers/validations.d.ts.map +1 -1
  97. package/dist/helpers/validations.js +858 -91
  98. package/dist/helpers/validations.js.map +1 -1
  99. package/dist/index.d.ts +2 -1
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +3 -1
  102. package/dist/index.js.map +1 -1
  103. package/package.json +1 -1
  104. package/src/components/codeSize/index.tsx +79 -0
  105. package/src/components/form/engine/CompactRow.tsx +407 -34
  106. package/src/components/form/engine/CompactToolbar.tsx +105 -98
  107. package/src/components/form/engine/FormEngine.stories.tsx +863 -12
  108. package/src/components/form/engine/FormEngine.tsx +183 -33
  109. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +110 -103
  110. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +132 -80
  111. package/src/components/form/engine/compactRowStyles.ts +44 -1
  112. package/src/components/form/engine/compactToolbarContext.ts +8 -0
  113. package/src/components/form/engine/readFirst.ts +163 -4
  114. package/src/components/form/fields/Field.tsx +3 -1
  115. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  116. package/src/components/form/fields/array/ArrayAuto.tsx +23 -4
  117. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +134 -0
  118. package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
  119. package/src/components/form/fields/auto/AutoFormField.tsx +31 -1
  120. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  121. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
  122. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
  123. package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
  124. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
  125. package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
  126. package/src/components/form/fields/template/TemplateField.tsx +47 -8
  127. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
  128. package/src/components/form/index.tsx +1 -0
  129. package/src/helpers/common.ts +11 -1
  130. package/src/helpers/optionUiTypes.ts +153 -0
  131. package/src/helpers/options.ts +23 -0
  132. package/src/helpers/providerValue.ts +125 -0
  133. package/src/helpers/singleLineString.ts +61 -0
  134. package/src/helpers/templates.ts +272 -3
  135. package/src/helpers/validations.ts +1177 -109
  136. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  137. package/src/index.tsx +2 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The Qorus option `ui_type` vocabulary.
3
+ *
4
+ * A `ui_type` names the widget an option is rendered with, and — because the
5
+ * widget decides what a complete value looks like — it is also what
6
+ * `validateField` switches on. Both readers have to agree about which strings
7
+ * are in the vocabulary, so the lists live here rather than beside either one.
8
+ *
9
+ * Grouped by what the group means to a reader, not by alphabet: the scalar
10
+ * types hold a single value, the collection types hold several, the choice
11
+ * types pick from a supplied set, and the interface types name another Qorus
12
+ * object by its name — which is why `isOptionInterfaceUiType` is enough for
13
+ * validation to treat the whole group as a non-empty string.
14
+ */
15
+
16
+ export const OPTION_SCALAR_UI_TYPES = [
17
+ 'string',
18
+ 'long-string',
19
+ 'data',
20
+ 'binary',
21
+ 'richtext',
22
+ 'bool',
23
+ 'boolean',
24
+ 'date',
25
+ 'int',
26
+ 'integer',
27
+ 'float',
28
+ 'number',
29
+ 'byte-size',
30
+ 'url',
31
+ 'code-editor',
32
+ 'rgbcolor',
33
+ 'any',
34
+ 'auto',
35
+ // semantic string formats (string-backed types that carry a UI-renderable identity; see the qore
36
+ // DataProvider QoreStringFormatDataType)
37
+ 'email',
38
+ 'uri',
39
+ 'uuid',
40
+ 'hostname',
41
+ 'ipv4',
42
+ 'ipv6',
43
+ 'phone',
44
+ ] as const;
45
+
46
+ export const OPTION_COLLECTION_UI_TYPES = [
47
+ 'hash',
48
+ 'free-hash',
49
+ 'list',
50
+ 'free-list',
51
+ 'option_hash',
52
+ 'file',
53
+ 'file-as-string',
54
+ 'collection-documents',
55
+ ] as const;
56
+
57
+ export const OPTION_CHOICE_UI_TYPES = [
58
+ 'enum',
59
+ 'select-string',
60
+ 'select-array',
61
+ 'multi-select',
62
+ ] as const;
63
+
64
+ export const OPTION_INTERFACE_UI_TYPES = [
65
+ 'workflow',
66
+ 'service',
67
+ 'job',
68
+ 'connection',
69
+ 'mapper',
70
+ 'class',
71
+ 'value-map',
72
+ 'fsm',
73
+ 'qog',
74
+ 'api-schema',
75
+ 'auth-profile',
76
+ 'ai-collection',
77
+ 'ai-endpoint',
78
+ 'ai-guardrail',
79
+ 'ml-model',
80
+ 'release-config',
81
+ 'alertrule',
82
+ 'alertsilence',
83
+ 'installed-release',
84
+ 'business-process',
85
+ 'schema',
86
+ 'test',
87
+ ] as const;
88
+
89
+ export const OPTION_CUSTOM_UI_TYPES = [
90
+ 'processor',
91
+ 'processor-mappings',
92
+ 'data-provider',
93
+ 'tool-catalog',
94
+ 'schema-definition',
95
+ 'test-cases',
96
+ 'test-value-contract',
97
+ 'dpql',
98
+ 'active-windows',
99
+ ] as const;
100
+
101
+ export const OPTION_UI_TYPES = [
102
+ ...OPTION_SCALAR_UI_TYPES,
103
+ ...OPTION_COLLECTION_UI_TYPES,
104
+ ...OPTION_CHOICE_UI_TYPES,
105
+ ...OPTION_INTERFACE_UI_TYPES,
106
+ ...OPTION_CUSTOM_UI_TYPES,
107
+ ] as const;
108
+
109
+ export type TOptionUiType = (typeof OPTION_UI_TYPES)[number];
110
+ export type TOptionInterfaceUiType = (typeof OPTION_INTERFACE_UI_TYPES)[number];
111
+
112
+ export const LEGACY_FIELD_UI_TYPES = [
113
+ 'api-manager',
114
+ 'array',
115
+ 'array-auto',
116
+ 'array-of-pairs',
117
+ 'class-array',
118
+ 'class-connectors',
119
+ 'constant-array',
120
+ 'context-selector',
121
+ 'cron',
122
+ 'data-provider-options',
123
+ 'file-array',
124
+ 'fsm-list',
125
+ 'function-array',
126
+ 'http-content',
127
+ 'method-name',
128
+ 'opcua-method-arguments',
129
+ 'opcua-write-values',
130
+ 'options',
131
+ 'select',
132
+ 'service-events',
133
+ 'service-webhooks',
134
+ 'type-selector',
135
+ ] as const;
136
+
137
+ export const KNOWN_QORUS_UI_TYPES = [...OPTION_UI_TYPES, ...LEGACY_FIELD_UI_TYPES] as const;
138
+
139
+ export type TLegacyFieldUiType = (typeof LEGACY_FIELD_UI_TYPES)[number];
140
+ export type TKnownQorusUiType = (typeof KNOWN_QORUS_UI_TYPES)[number];
141
+
142
+ const optionUiTypeSet = new Set<string>(OPTION_UI_TYPES);
143
+ const interfaceUiTypeSet = new Set<string>(OPTION_INTERFACE_UI_TYPES);
144
+ const knownQorusUiTypeSet = new Set<string>(KNOWN_QORUS_UI_TYPES);
145
+
146
+ export const isOptionUiType = (type: unknown): type is TOptionUiType =>
147
+ typeof type === 'string' && optionUiTypeSet.has(type);
148
+
149
+ export const isOptionInterfaceUiType = (type: unknown): type is TOptionInterfaceUiType =>
150
+ typeof type === 'string' && interfaceUiTypeSet.has(type);
151
+
152
+ export const isKnownQorusUiType = (type: unknown): type is TKnownQorusUiType =>
153
+ typeof type === 'string' && knownQorusUiTypeSet.has(type);
@@ -29,3 +29,26 @@ export const getRequiredOptionMessage = (
29
29
 
30
30
  return `This field or ${requiredOptions} is required`;
31
31
  };
32
+
33
+ /**
34
+ * The value of one element of a list field.
35
+ *
36
+ * A list element arrives in either of two shapes, and both are legitimate. The
37
+ * editor keeps each element in a `{value, type}` envelope while the form is
38
+ * being worked on — that is what `formatToServerValue` writes — while a value
39
+ * read back from storage holds what the server contract asks for, which for a
40
+ * list of strings is the bare strings.
41
+ *
42
+ * Reaching straight for `.value` answered `undefined` for every stored element.
43
+ * On a read that emptied the row; on a validation pass it failed the element as
44
+ * empty; and on an autosaving form the emptiness was then written back over the
45
+ * stored value. One helper so the readers cannot disagree about which shape
46
+ * they are looking at.
47
+ *
48
+ * An array is never an envelope — a rich-text document is itself an array, and
49
+ * `'value' in []` is false anyway, but saying so keeps the intent legible.
50
+ */
51
+ export const getListElementValue = (element: unknown): unknown =>
52
+ element && typeof element === 'object' && !Array.isArray(element) && 'value' in element ?
53
+ (element as { value: unknown }).value
54
+ : element;
@@ -0,0 +1,125 @@
1
+ import { TQorusForm } from '@qoretechnologies/ts-toolkit';
2
+
3
+ export type TRecordType = 'search' | 'search-single' | 'create' | 'update' | 'delete';
4
+ export type TRealRecordType = 'read' | 'create' | 'update' | 'delete';
5
+
6
+ export type TProviderTypeSupports = {
7
+ [key in `supports_${TRealRecordType}`]?: boolean;
8
+ };
9
+
10
+ export type TProviderTypeArgs = {
11
+ [key in `${TRecordType}_args`]?: TQorusForm | TQorusForm[];
12
+ };
13
+
14
+ /**
15
+ * A data-provider selection, in the shape the form holds it.
16
+ *
17
+ * The `supports_*` flags say which record operations the selected provider can
18
+ * perform; validation reads them to decide whether a provider is a legitimate
19
+ * target for the operation the field was configured for.
20
+ */
21
+ export interface IProviderType extends TProviderTypeSupports, TProviderTypeArgs {
22
+ type: string;
23
+ name: string;
24
+ path?: string;
25
+ options?: TQorusForm;
26
+ subtype?: 'request' | 'response';
27
+ hasApiContext?: boolean;
28
+ optionsChanged?: boolean;
29
+ searchOptionsChanged?: boolean;
30
+ desc?: string;
31
+ use_args?: boolean;
32
+ args?: any;
33
+ up?: boolean;
34
+ supports_request?: boolean;
35
+ supports_messages?: 'ASYNC' | 'SYNC' | 'NONE';
36
+ supports_observable?: boolean;
37
+ transaction_management?: boolean;
38
+ record_requires_search_options?: boolean;
39
+ create_args_freeform?: TQorusForm[];
40
+ is_api_call?: boolean;
41
+ search_options?: TQorusForm;
42
+ descriptions?: string[];
43
+ message_id?: string;
44
+ message?: {
45
+ type: string;
46
+ value: any;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * A provider value as an object, from either representation a caller may hold.
52
+ *
53
+ * A provider reaches the form either already expanded into `IProviderType`, or
54
+ * as the flat path string the server stores — `"factory/db{driver=pgsql}/table"`
55
+ * or the plain `"type/name/path"`. Returns `null` for anything that is neither,
56
+ * so a caller reports an invalid value rather than reading fields off nothing.
57
+ */
58
+ export const maybeBuildOptionProvider = (provider: unknown): IProviderType | null => {
59
+ if (!provider) {
60
+ return null;
61
+ }
62
+ if (typeof provider === 'object') {
63
+ return provider as IProviderType;
64
+ }
65
+ if (typeof provider !== 'string') {
66
+ return null;
67
+ }
68
+
69
+ if (provider.startsWith('factory')) {
70
+ const fixedProvider = provider.endsWith('/') ? provider : `${provider}/`;
71
+ const [factoryType] = fixedProvider.split('/');
72
+ const factoryName = fixedProvider.substring(
73
+ fixedProvider.indexOf('/') + 1,
74
+ fixedProvider.lastIndexOf('{')
75
+ );
76
+ const options = fixedProvider.substring(
77
+ fixedProvider.indexOf('{') + 1,
78
+ fixedProvider.lastIndexOf('}')
79
+ );
80
+ const optionsObject = options
81
+ .split(',')
82
+ .filter(Boolean)
83
+ .reduce<Record<string, string>>((result, option) => {
84
+ const [key, value] = option.split('=');
85
+ result[key] = value;
86
+ return result;
87
+ }, {});
88
+ const result: IProviderType = {
89
+ type: factoryType,
90
+ name: factoryName,
91
+ path: fixedProvider.substring(fixedProvider.lastIndexOf('}/') + 2),
92
+ options: optionsObject as unknown as TQorusForm,
93
+ };
94
+ if (provider.includes('?options_changed')) {
95
+ result.optionsChanged = true;
96
+ }
97
+ return result;
98
+ }
99
+
100
+ const [type, name, ...path] = provider.split('/');
101
+ return { type, name, path: path.join('/') };
102
+ };
103
+
104
+ /**
105
+ * An FSM variable action — which variable, and what is done to it.
106
+ *
107
+ * The provider half is `Partial<IProviderType>` because the action's own
108
+ * `action_type` decides which provider fields have to be there; validation
109
+ * re-runs the matching provider validator over the merged value rather than
110
+ * duplicating those rules here.
111
+ */
112
+ export type TVariableActionValue = {
113
+ var_type: 'globalvar' | 'localvar' | 'autovar';
114
+ var_name: string;
115
+ transaction_action?: 'commit' | 'rollback' | 'begin-transaction';
116
+ action_type?:
117
+ | 'search'
118
+ | 'search-single'
119
+ | 'update'
120
+ | 'create'
121
+ | 'delete'
122
+ | 'transaction'
123
+ | 'send-message'
124
+ | 'apicall';
125
+ } & Partial<IProviderType>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Which string field types hold one line, and how to keep them that way.
3
+ *
4
+ * The server already draws this distinction and the IDE was throwing it away:
5
+ * a service's `name` is declared `ui_type: "string"` while its `description` is
6
+ * `ui_type: "long-string"` with `markdown: True` (see `ServiceMetadata.qc`).
7
+ * Every one of them — plus `email`, `uuid`, `hostname`, `ipv4`, `ipv6` and
8
+ * `phone` — rendered through the same growing textarea, so a technical name, a
9
+ * version and an IP address all accepted Enter.
10
+ *
11
+ * That is not a cosmetic problem. These values become YAML keys, class names,
12
+ * URLs and identifiers; a newline in one of them is invalid input that the form
13
+ * happily accepted and passed on, and the operator had no way to see it.
14
+ *
15
+ * The rule is the server's own contract, stated once here rather than guessed
16
+ * per field, and it is enforced in `LongString.tsx`.
17
+ *
18
+ * It lives in reqraft rather than in a consumer because the consumer is not the
19
+ * only renderer: qorus-ide fixed this in its OWN long-string field, and every
20
+ * form that goes through FormEngine — which is where an alert rule's Internal
21
+ * Name, Name and Short Description are — reaches THIS one instead. The fix was
22
+ * real and the field in front of the operator still took Enter.
23
+ */
24
+
25
+ /**
26
+ * The string types that hold exactly one line — the ones routed to a textarea
27
+ * today.
28
+ *
29
+ * Listed explicitly rather than derived by excluding the document types,
30
+ * because the two directions fail very differently. Missing a single-line type
31
+ * here leaves it behaving as it does now — no worse. Wrongly treating a new
32
+ * DOCUMENT type as single-line would flatten a user's text as they typed it,
33
+ * which is data loss. So an unrecognised type stays multi-line.
34
+ */
35
+ const SINGLE_LINE_STRING_TYPES = new Set([
36
+ 'string',
37
+ 'email',
38
+ 'uuid',
39
+ 'hostname',
40
+ 'ipv4',
41
+ 'ipv6',
42
+ 'phone',
43
+ ]);
44
+
45
+ /** True when a string field of this type holds a single line. */
46
+ export const isSingleLineStringType = (type?: string): boolean =>
47
+ !!type && SINGLE_LINE_STRING_TYPES.has(type);
48
+
49
+ /**
50
+ * Flattens any line break to a single space.
51
+ *
52
+ * A space rather than nothing, because the newline is usually where a word
53
+ * boundary was — pasting two lines into a name should read as two words, not
54
+ * one run-together one. CR, LF and CRLF all collapse to one space, and the
55
+ * result is trimmed of leading/trailing whitespace the break introduced.
56
+ */
57
+ export const flattenToSingleLine = (value: string): string =>
58
+ value.replace(/\r\n|\r|\n/g, ' ').replace(/ {2,}/g, ' ');
59
+
60
+ /** True when the value carries a line break this field should not hold. */
61
+ export const hasLineBreak = (value: string): boolean => /\r|\n/.test(value);
@@ -1,11 +1,25 @@
1
+ import { modalStore } from '@qoretechnologies/reqore';
1
2
  import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
2
3
  import {
3
4
  TReqoreDropdownItem,
4
5
  TReqoreDropdownItems,
5
6
  } from '@qoretechnologies/reqore/dist/components/Dropdown/list';
6
7
  import { cloneDeep, size } from 'lodash';
8
+ import { createElement } from 'react';
9
+ import { ReqraftTemplateExampleValueModal } from '../components/form/fields/template/ExampleValueModal';
7
10
  import { areQorusTypesCompatible } from './expressions';
8
11
 
12
+ /**
13
+ * The DOM ceiling for a serialized example value in a picker item — a
14
+ * binary-carrying field's example (e.g. an email attachment body) can be an
15
+ * entire base64 file, and it must not enter the item wholesale. The VISUAL
16
+ * truncation is not this number: past the ceiling the item also gets a
17
+ * single-line-ellipsis description (`descriptionEffect.noWrap`), which cuts at
18
+ * the popover's ACTUAL rendered width — container-intrinsic, no breakpoints —
19
+ * paired with the "?" action that shows the full value.
20
+ */
21
+ export const TEMPLATE_EXAMPLE_PREVIEW_LENGTH = 150;
22
+
9
23
  /**
10
24
  * Shared template string utilities.
11
25
  * Used by both validations and TemplateField.
@@ -38,6 +52,119 @@ export const getTemplateValue = (value: string): string => {
38
52
  return value.substring(colonIndex + 1);
39
53
  };
40
54
 
55
+ /**
56
+ * The canonical template-token grammar (twin of the IDE's
57
+ * `helpers/templateValue.ts` — keep the two in step). A token is
58
+ * `$<key>:<segment>(:<segment>)*` where the key may carry dashes
59
+ * (`-expr`) and every segment is either a plain word or a braced
60
+ * context reference — `{W2n….filename}` — whose content may hold dots,
61
+ * colons, spaces and dashes (`:{deep:test:list}`,
62
+ * `{id.Created by.name}`). `isValueTemplate` above stays deliberately
63
+ * LOOSER (starts with `$`, has `:`): it answers "does the user mean a
64
+ * template here" while typing; use the strict check below when a decision
65
+ * must not misfire on user-typed dollar-strings.
66
+ */
67
+ export const TEMPLATE_TOKEN_SOURCE =
68
+ '\\$[A-Za-z_][\\w-]*(?::(?:\\{[^}]*\\}|[A-Za-z_][\\w-]*))+';
69
+
70
+ const COMPLETE_TEMPLATE_TOKEN = new RegExp(`^${TEMPLATE_TOKEN_SOURCE}$`);
71
+
72
+ /** STRICT whole-value check: the entire string is one well-formed template
73
+ * token (`:{id.path}`, `:item`, …) — nothing before or after.
74
+ * `'$foo: hello'` passes the loose check but not this one. */
75
+ export const isCompleteTemplateToken = (value?: unknown): value is string =>
76
+ typeof value === 'string' && COMPLETE_TEMPLATE_TOKEN.test(value);
77
+
78
+ /** A complete token carrying at least one BRACED context segment —
79
+ * `$data:{W2n….filename}`, `$qore-expr:{1 + 2}`. These are machine-written
80
+ * references nobody types by hand, so a surface may render them as a picker
81
+ * chip. Plain word-path tokens (`$local:id`) stay typeable and belong in the
82
+ * template-offering input (build #123 review). Word segments cannot contain
83
+ * `{`, so `:{` inside a complete token always starts a braced segment. */
84
+ export const isBracedTemplateToken = (value?: unknown): value is string =>
85
+ isCompleteTemplateToken(value) && value.includes(':{');
86
+
87
+ /**
88
+ * An FSM state carries TWO identities: the key it occupies in the `states`
89
+ * hash and its own `id`. The IDE gives both the same nanoid, so nothing
90
+ * authored there can tell them apart — but a template hand-writes numbered
91
+ * keys (`'1'`, `'2'`, …) over meaningful ids (`dc_ai_reply`), its saved
92
+ * `$data:{…}` references use the **id**, and the server's design-time
93
+ * catalogue spells its item values with the **key**. The two never match as
94
+ * text, so a template-derived Qog renders the raw token where a named chip
95
+ * belongs.
96
+ *
97
+ * The producer of the catalogue (the IDE, which is the side that holds the
98
+ * states) attaches the alternate spelling here rather than rewriting either
99
+ * side — a picked value must keep storing exactly what the catalogue says.
100
+ * Consuming it is therefore purely a display concern.
101
+ */
102
+ const matchesTemplateValue = (item: TReqoreDropdownItem, value: string): boolean =>
103
+ item.value === value ||
104
+ !!(item.metadata as { aliasValues?: string[] } | undefined)?.aliasValues?.includes(value);
105
+
106
+ /** Splits `$data:{a.b.c}` into its key (`$data`) and inner path (`a.b.c`). */
107
+ const TEMPLATE_TOKEN_PATH = /^(\$[A-Za-z_][\w-]*):\{(.*)\}$/;
108
+
109
+ const getTokenPath = (value?: unknown): { key: string; path: string } | undefined => {
110
+ const match = typeof value === 'string' ? TEMPLATE_TOKEN_PATH.exec(value) : null;
111
+ return match ? { key: match[1], path: match[2] } : undefined;
112
+ };
113
+
114
+ /**
115
+ * The longest catalogue item whose path is a PREFIX of `value`, plus the part
116
+ * of the path it does not cover.
117
+ *
118
+ * A catalogue can only offer what the action's output type declares, so it
119
+ * stops at a list: `choices` is offered, `choices[0].message.content` is not —
120
+ * an operator hand-extends the path past the named field. Such a value has no
121
+ * item to match and used to render as its own raw token. Naming it by its
122
+ * nearest ancestor (`Choices[0].message.content`) keeps the chip readable
123
+ * without inventing a catalogue entry that does not exist.
124
+ *
125
+ * The match must break at a path boundary (`.` or `[`) so `choicesOther` is
126
+ * never named after `choices`, and the token keys must agree so a `$config:`
127
+ * item never names a `$data:` value.
128
+ */
129
+ export const findTemplateByPath = (
130
+ templates: IReqoreFormTemplates,
131
+ value: string
132
+ ): { item: TReqoreDropdownItem; remainder: string } | undefined => {
133
+ const ref = getTokenPath(value);
134
+ if (!ref) return undefined;
135
+
136
+ let best: { item: TReqoreDropdownItem; path: string; remainder: string } | undefined;
137
+
138
+ const consider = (item: TReqoreDropdownItem, candidate?: string) => {
139
+ const itemPath = getTokenPath(candidate);
140
+ if (!itemPath || itemPath.key !== ref.key || !ref.path.startsWith(itemPath.path)) {
141
+ return;
142
+ }
143
+ const boundary = ref.path[itemPath.path.length];
144
+ if (boundary !== '.' && boundary !== '[') {
145
+ return;
146
+ }
147
+ if (!best || itemPath.path.length > best.path.length) {
148
+ best = { item, path: itemPath.path, remainder: ref.path.slice(itemPath.path.length) };
149
+ }
150
+ };
151
+
152
+ const walk = (items?: TReqoreDropdownItems) => {
153
+ items?.forEach((item) => {
154
+ consider(item, item.value as string);
155
+ (item.metadata as { aliasValues?: string[] } | undefined)?.aliasValues?.forEach((alias) =>
156
+ consider(item, alias)
157
+ );
158
+ if (item.items) {
159
+ walk(item.items);
160
+ }
161
+ });
162
+ };
163
+
164
+ walk(templates?.items);
165
+ return best ? { item: best.item, remainder: best.remainder } : undefined;
166
+ };
167
+
41
168
  export const findTemplate = (
42
169
  templates: IReqoreFormTemplates,
43
170
  value: string
@@ -48,7 +175,7 @@ export const findTemplate = (
48
175
 
49
176
  const findItem = (items: TReqoreDropdownItems) => {
50
177
  items?.forEach((item) => {
51
- if (item.value === value) {
178
+ if (matchesTemplateValue(item, value)) {
52
179
  result = item;
53
180
  return;
54
181
  }
@@ -63,6 +190,113 @@ export const findTemplate = (
63
190
  return result;
64
191
  };
65
192
 
193
+ /**
194
+ * Joins a catalogue item's name to the part of the path it does not cover.
195
+ * A field step reads as a step (`Choices › message`), while an array index
196
+ * binds to the name it indexes (`Choices[0]`) rather than floating off it.
197
+ */
198
+ const composeExtendedLabel = (label: string, remainder: string): string =>
199
+ remainder.startsWith('.') ? `${label} › ${remainder.slice(1)}` : `${label}${remainder}`;
200
+
201
+ /**
202
+ * The one answer to "what is this reference called?", so a value reads the
203
+ * same in the picker chip, the read-only tag and the compact row's summary.
204
+ * Exact match first (aliases included), then the nearest catalogue ancestor
205
+ * for a hand-extended path, and finally the raw token — which is honest when
206
+ * the catalogue holds nothing that explains the value.
207
+ */
208
+ export const resolveTemplateLabel = (
209
+ templates: IReqoreFormTemplates | undefined,
210
+ value: string
211
+ ): { label: string; item?: TReqoreDropdownItem } => {
212
+ if (!templates || !value) {
213
+ return { label: value };
214
+ }
215
+
216
+ const exact = findTemplate(templates, value);
217
+ if (exact) {
218
+ return { label: (exact.label as string) || value, item: exact };
219
+ }
220
+
221
+ const extended = findTemplateByPath(templates, value);
222
+ if (extended) {
223
+ return {
224
+ label: composeExtendedLabel(extended.item.label as string, extended.remainder),
225
+ item: extended.item,
226
+ };
227
+ }
228
+
229
+ return { label: value };
230
+ };
231
+
232
+ /**
233
+ * What to call a reference when the catalogue explains nothing about it — the
234
+ * reference's own path, with the token wrapper stripped:
235
+ * `$data:{dc_ai_reply.choices[0].message.content}` reads as
236
+ * `dc_ai_reply.choices[0].message.content`.
237
+ *
238
+ * Surfaces exist with no catalogue at all (the Automation Hub template preview
239
+ * never fetches one), and there the raw token is the only thing left to show.
240
+ * Showing it as `$data:{…}` renders a value as code; showing the path renders
241
+ * it as a name, which is the honest floor.
242
+ */
243
+ export const getTemplateReferencePath = (value: string): string =>
244
+ getTokenPath(value)?.path ?? value;
245
+
246
+ /**
247
+ * The best available name for a reference, for a surface that must show
248
+ * something readable no matter what: the catalogue's name when it has one,
249
+ * otherwise the bare path. `resolveTemplateLabel` keeps falling back to the
250
+ * raw token, because a picker that cannot resolve a value should say exactly
251
+ * what the value is.
252
+ */
253
+ export const describeTemplateReference = (
254
+ templates: IReqoreFormTemplates | undefined,
255
+ value: string
256
+ ): { label: string; item?: TReqoreDropdownItem } => {
257
+ const resolved = resolveTemplateLabel(templates, value);
258
+ return resolved.item ? resolved : { label: getTemplateReferencePath(value) };
259
+ };
260
+
261
+ const EMBEDDED_TEMPLATE_TOKEN = new RegExp(TEMPLATE_TOKEN_SOURCE, 'g');
262
+
263
+ export type TTemplateTextSegment =
264
+ | { kind: 'text'; text: string }
265
+ | { kind: 'token'; text: string };
266
+
267
+ /**
268
+ * Splits prose that embeds template tokens into its literal and token parts —
269
+ * `trim("$data:{…}")` becomes `trim("`, the token, `")`. Used to chip the
270
+ * references inside a rendered expression instead of printing them raw.
271
+ */
272
+ export const splitTemplateTokens = (text?: string): TTemplateTextSegment[] => {
273
+ if (!text) {
274
+ return [];
275
+ }
276
+
277
+ const segments: TTemplateTextSegment[] = [];
278
+ let lastIndex = 0;
279
+
280
+ // A fresh regex per call: a shared /g instance carries `lastIndex` between
281
+ // calls and would skip tokens on the second string it is given.
282
+ const pattern = new RegExp(EMBEDDED_TEMPLATE_TOKEN.source, 'g');
283
+ let match: RegExpExecArray | null;
284
+
285
+ while ((match = pattern.exec(text)) !== null) {
286
+ if (match.index > lastIndex) {
287
+ segments.push({ kind: 'text', text: text.slice(lastIndex, match.index) });
288
+ }
289
+ segments.push({ kind: 'token', text: match[0] });
290
+ lastIndex = match.index + match[0].length;
291
+ }
292
+
293
+ if (lastIndex < text.length) {
294
+ segments.push({ kind: 'text', text: text.slice(lastIndex) });
295
+ }
296
+
297
+ return segments;
298
+ };
299
+
66
300
  // Ported verbatim from qorus-ide `helpers/functions.tsx` (FIELD_STACK_REPORT
67
301
  // batch) — the previous reqraft version used naive badge equality; the IDE
68
302
  // routes compatibility through `areQorusTypesCompatible` (int↔string coercion
@@ -192,11 +426,26 @@ export const buildTemplates = (
192
426
  items: childItems,
193
427
  data_role: itemDataRole,
194
428
  }): TReqoreDropdownItem => {
429
+ const serializedExample =
430
+ example_value !== undefined ? JSON.stringify(example_value) : undefined;
431
+ const exampleIsLong =
432
+ serializedExample !== undefined &&
433
+ serializedExample.length > TEMPLATE_EXAMPLE_PREVIEW_LENGTH;
434
+
195
435
  const item: TReqoreDropdownItem = {
196
436
  label: itemDisplayName,
197
- description: example_value
198
- ? `Example value: ${JSON.stringify(example_value)}`
437
+ description: serializedExample
438
+ ? `Example value: ${
439
+ exampleIsLong
440
+ ? `${serializedExample.slice(0, TEMPLATE_EXAMPLE_PREVIEW_LENGTH)}…`
441
+ : serializedExample
442
+ }`
199
443
  : undefined,
444
+ // Ellipsize ONLY where the "?" full-value affordance exists — for a
445
+ // short value an ellipsis would hide information with no way to
446
+ // reveal it. The single line cuts at the rendered width, so the
447
+ // preview adapts to any popover/panel width without breakpoints.
448
+ descriptionEffect: exampleIsLong ? { noWrap: true } : undefined,
200
449
  badge: type,
201
450
  metadata: {
202
451
  image: logo,
@@ -213,6 +462,26 @@ export const buildTemplates = (
213
462
  minimal: true,
214
463
  };
215
464
 
465
+ if (exampleIsLong) {
466
+ // Menu-item actions stop propagation, so the click never selects or
467
+ // expands the item. The modal rides reqore's global modal queue
468
+ // (rendered by the provider portal), so no host component is needed
469
+ // — the modals wrapper injects `isOpen`/`onClose` itself.
470
+ item.rightAction = {
471
+ icon: 'QuestionLine',
472
+ compact: true,
473
+ tooltip: 'Show the full example value',
474
+ onClick: () => {
475
+ modalStore.getState().addModal(
476
+ createElement(ReqraftTemplateExampleValueModal, {
477
+ label: itemDisplayName,
478
+ value: serializedExample,
479
+ })
480
+ );
481
+ },
482
+ };
483
+ }
484
+
216
485
  if (childItems?.length) {
217
486
  item.items = mapTemplates(
218
487
  childItems,