@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.
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +27 -0
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +26 -4
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +8 -3
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/helpers/templates.d.ts +66 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +143 -1
- package/dist/helpers/templates.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +45 -0
- package/src/components/form/engine/FormEngine.stories.tsx +166 -8
- package/src/components/form/engine/readFirst.ts +29 -4
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
- package/src/components/form/fields/template/TemplateField.stories.tsx +49 -0
- package/src/components/form/fields/template/TemplateField.tsx +9 -4
- package/src/helpers/templates.ts +169 -0
|
@@ -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
|
-
|
|
222
|
-
|
|
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 (
|
|
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
|
package/src/helpers/templates.ts
CHANGED
|
@@ -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
|