@qoretechnologies/reqraft 0.10.38-pr.96.g9405cd1 → 0.10.38

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.
@@ -411,6 +411,55 @@ export const AliasedStateTemplateValue: StoryObj<typeof meta> = {
411
411
  },
412
412
  };
413
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
+
414
463
  export const ElementTypeInListShowsCorrectTemplates: StoryObj<typeof meta> = {
415
464
  args: {
416
465
  component: auto,
@@ -30,11 +30,11 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
30
30
  import { useUpdateEffect } from 'react-use';
31
31
  import {
32
32
  filterTemplatesByType as templatesFilterFunc,
33
- findTemplate,
34
33
  getTemplateKey,
35
34
  getTemplateValue,
36
35
  isBracedTemplateToken,
37
36
  isCompleteTemplateToken,
37
+ describeTemplateReference,
38
38
  isValueTemplate,
39
39
  } from '../../../../helpers/templates';
40
40
  import { getTypeFromValue } from '../../../../helpers/validations';
@@ -218,8 +218,13 @@ export const TemplateDropdownSelector = memo(
218
218
  size,
219
219
  ...rest
220
220
  }: ITemplateDropdownSelectorProps) => {
221
- const template = findTemplate(templates, value);
222
- const label = template?.label || value || rest.label || 'Select Template';
221
+ // One resolver for every surface that names a reference — the picker chip
222
+ // here, the read-only tag, and the compact row's expression summary. The
223
+ // row and this editor must agree: a reference that reads as its path when
224
+ // the row is closed cannot read as `$data:{…}` the moment it is opened.
225
+ const resolved = describeTemplateReference(templates, value);
226
+ const template = resolved.item;
227
+ const label = resolved.label || rest.label || 'Select Template';
223
228
  const leftIconProps = useMemo(
224
229
  (): IReqoreButtonProps['leftIconProps'] => ({
225
230
  image: template?.metadata?.image,
@@ -369,7 +374,7 @@ export const TemplateField = memo(
369
374
  const showTemplateToggle = allowCustomValues && allowTemplates && !rest.arg_schema;
370
375
 
371
376
  // Only a BRACED context ref (`$data:{…}` — machine-written, nobody types
372
- // one) renders as the picker chip (label + app image via findTemplate)
377
+ // one) renders as the picker chip (named via `resolveTemplateLabel`)
373
378
  // rather than as its raw text in a string editor. Plain word-path tokens
374
379
  // (`$local:id`) are typeable, so per the build #123 review they keep the
375
380
  // input that offers templates while typing. Mixed text-and-token strings
@@ -103,6 +103,68 @@ const matchesTemplateValue = (item: TReqoreDropdownItem, value: string): boolean
103
103
  item.value === value ||
104
104
  !!(item.metadata as { aliasValues?: string[] } | undefined)?.aliasValues?.includes(value);
105
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
+
106
168
  export const findTemplate = (
107
169
  templates: IReqoreFormTemplates,
108
170
  value: string
@@ -128,6 +190,113 @@ export const findTemplate = (
128
190
  return result;
129
191
  };
130
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
+
131
300
  // Ported verbatim from qorus-ide `helpers/functions.tsx` (FIELD_STACK_REPORT
132
301
  // batch) — the previous reqraft version used naive badge equality; the IDE
133
302
  // routes compatibility through `areQorusTypesCompatible` (int↔string coercion