@qoretechnologies/reqraft 0.10.46-pr.95.ge595a13 → 0.10.46
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/.claude/CLAUDE.md +12 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
- package/dist/components/composer/Composer.d.ts.map +1 -1
- package/dist/components/composer/Composer.js +5 -1
- package/dist/components/composer/Composer.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts +14 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +199 -41
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +72 -72
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +65 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +276 -108
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +55 -13
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
- package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
- package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +15 -3
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +24 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +84 -6
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +1 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +64 -14
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +25 -4
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
- package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
- package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
- package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
- 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 +81 -27
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
- package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +1 -1
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/dist/helpers/common.d.ts +8 -1
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +9 -1
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/expressions.d.ts.map +1 -1
- package/dist/helpers/expressions.js +15 -2
- package/dist/helpers/expressions.js.map +1 -1
- package/dist/helpers/optionUiTypes.d.ts +30 -0
- package/dist/helpers/optionUiTypes.d.ts.map +1 -0
- package/dist/helpers/optionUiTypes.js +156 -0
- package/dist/helpers/optionUiTypes.js.map +1 -0
- package/dist/helpers/options.d.ts +19 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +25 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/providerValue.d.ts +68 -0
- package/dist/helpers/providerValue.d.ts.map +1 -0
- package/dist/helpers/providerValue.js +50 -0
- package/dist/helpers/providerValue.js.map +1 -0
- package/dist/helpers/singleLineString.d.ts +37 -0
- package/dist/helpers/singleLineString.d.ts.map +1 -0
- package/dist/helpers/singleLineString.js +66 -0
- package/dist/helpers/singleLineString.js.map +1 -0
- package/dist/helpers/templates.d.ts +100 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +237 -4
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +103 -2
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +858 -91
- package/dist/helpers/validations.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/composer/Composer.tsx +7 -3
- package/src/components/form/engine/CompactRow.tsx +279 -33
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +995 -12
- package/src/components/form/engine/FormEngine.tsx +377 -94
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +104 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +158 -101
- package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
- package/src/components/form/engine/compactRowStyles.ts +55 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +125 -35
- package/src/components/form/fields/Field.tsx +3 -1
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
- package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
- package/src/components/form/fields/long-string/LongString.tsx +46 -5
- package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
- package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
- package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
- package/src/components/form/fields/template/TemplateField.stories.tsx +389 -1
- package/src/components/form/fields/template/TemplateField.tsx +100 -35
- package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
- package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
- package/src/components/ticketThread/TicketThread.tsx +7 -0
- package/src/helpers/common.ts +11 -1
- package/src/helpers/expressions.ts +16 -2
- package/src/helpers/optionUiTypes.ts +161 -0
- package/src/helpers/options.ts +23 -0
- package/src/helpers/providerValue.ts +125 -0
- package/src/helpers/singleLineString.ts +61 -0
- package/src/helpers/templates.ts +272 -3
- package/src/helpers/validations.ts +1177 -109
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
- package/src/index.tsx +1 -0
|
@@ -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,238 @@ 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
|
+
};
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* An EMPTY `any` field opens on the template selector, not the type picker.
|
|
971
|
+
*
|
|
972
|
+
* An `any` field has no editor of its own until a concrete type is chosen, so it
|
|
973
|
+
* used to open on the type picker — making the author answer a question about
|
|
974
|
+
* storage ("Text or Number?") before they could say what they meant, and often a
|
|
975
|
+
* question they cannot answer, because the value they want is a reference to
|
|
976
|
+
* something else whose type is not theirs to pick.
|
|
977
|
+
*
|
|
978
|
+
* The literal is still one menu item away: "Set Custom Value" in the ⋮ menu.
|
|
979
|
+
* That is the right way round — picking a template is choosing from a list,
|
|
980
|
+
* picking a literal is a decision.
|
|
981
|
+
*/
|
|
982
|
+
export const EmptyAnyOpensOnTemplates: StoryObj<typeof meta> = {
|
|
983
|
+
args: {
|
|
984
|
+
component: auto,
|
|
985
|
+
type: 'any',
|
|
986
|
+
allowTemplates: true,
|
|
987
|
+
allowCustomValues: true,
|
|
988
|
+
value: undefined,
|
|
989
|
+
},
|
|
990
|
+
parameters: {
|
|
991
|
+
docs: {
|
|
992
|
+
description: {
|
|
993
|
+
story:
|
|
994
|
+
'An untyped, empty field that accepts templates opens showing the template selector rather than a data-type picker.',
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
play: async ({ canvasElement }) => {
|
|
999
|
+
await waitFor(() =>
|
|
1000
|
+
expect(canvasElement.querySelector('.template-selector')).toBeTruthy()
|
|
1001
|
+
);
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* An `any` field that ALREADY HOLDS A LITERAL opens on that literal.
|
|
1007
|
+
*
|
|
1008
|
+
* The other half of the same choice, and the one that keeps it safe: defaulting
|
|
1009
|
+
* to the template view unconditionally would hide a value the author had already
|
|
1010
|
+
* typed, which reads as the value having been lost.
|
|
1011
|
+
*/
|
|
1012
|
+
export const AnyWithLiteralOpensOnTheValue: StoryObj<typeof meta> = {
|
|
1013
|
+
args: {
|
|
1014
|
+
component: auto,
|
|
1015
|
+
type: 'any',
|
|
1016
|
+
allowTemplates: true,
|
|
1017
|
+
allowCustomValues: true,
|
|
1018
|
+
value: 'already-typed',
|
|
1019
|
+
},
|
|
1020
|
+
parameters: {
|
|
1021
|
+
docs: {
|
|
1022
|
+
description: {
|
|
1023
|
+
story:
|
|
1024
|
+
'An untyped field holding a literal keeps showing that literal — only an empty one defaults to the template selector.',
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
play: async ({ canvasElement }) => {
|
|
1029
|
+
// The property that matters is that the VALUE is still on screen. Asserting
|
|
1030
|
+
// the absence of `.template-selector` would be wrong: that class is also on
|
|
1031
|
+
// a control that renders either way, so it says nothing about which view is
|
|
1032
|
+
// active.
|
|
1033
|
+
await waitFor(() => {
|
|
1034
|
+
const held = [
|
|
1035
|
+
...canvasElement.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>(
|
|
1036
|
+
'input, textarea'
|
|
1037
|
+
),
|
|
1038
|
+
].some((el) => el.value === 'already-typed');
|
|
1039
|
+
expect(held).toBe(true);
|
|
1040
|
+
});
|
|
1041
|
+
},
|
|
1042
|
+
};
|
|
@@ -30,9 +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,
|
|
35
|
+
isBracedTemplateToken,
|
|
36
|
+
isCompleteTemplateToken,
|
|
37
|
+
describeTemplateReference,
|
|
36
38
|
isValueTemplate,
|
|
37
39
|
} from '../../../../helpers/templates';
|
|
38
40
|
import { getTypeFromValue } from '../../../../helpers/validations';
|
|
@@ -72,8 +74,17 @@ export const TemplatesListProps: IReqoreDropdownProps = {
|
|
|
72
74
|
// markup stays verbatim. `type`/`level`/`allowTemplates` are destructured
|
|
73
75
|
// only to keep them off the underlying ReQore component / DOM.
|
|
74
76
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
77
|
+
// `type` is FORWARDED here, unlike the other wrappers below: the long-string
|
|
78
|
+
// field consumes it to decide whether it holds one line (a `string` does, a
|
|
79
|
+
// `long-string`, `hash` or `list` does not) and never spreads it to the DOM.
|
|
80
|
+
// Dropping it made every string field in every FormEngine form a growing
|
|
81
|
+
// textarea, so an alert rule's Internal Name took Enter.
|
|
75
82
|
const LongStringField = ({ name, onChange, type, level, allowTemplates, ...rest }: any) => (
|
|
76
|
-
<LongStringFormField
|
|
83
|
+
<LongStringFormField
|
|
84
|
+
{...rest}
|
|
85
|
+
type={type}
|
|
86
|
+
onChange={(value: string) => onChange?.(name, value)}
|
|
87
|
+
/>
|
|
77
88
|
);
|
|
78
89
|
const Number = ({ name, onChange, type, level, allowTemplates, ...rest }: any) => (
|
|
79
90
|
<NumberFormField {...rest} onChange={(value: number | string) => onChange?.(name, value)} />
|
|
@@ -216,8 +227,13 @@ export const TemplateDropdownSelector = memo(
|
|
|
216
227
|
size,
|
|
217
228
|
...rest
|
|
218
229
|
}: ITemplateDropdownSelectorProps) => {
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
// One resolver for every surface that names a reference — the picker chip
|
|
231
|
+
// here, the read-only tag, and the compact row's expression summary. The
|
|
232
|
+
// row and this editor must agree: a reference that reads as its path when
|
|
233
|
+
// the row is closed cannot read as `$data:{…}` the moment it is opened.
|
|
234
|
+
const resolved = describeTemplateReference(templates, value);
|
|
235
|
+
const template = resolved.item;
|
|
236
|
+
const label = resolved.label || rest.label || 'Select Template';
|
|
221
237
|
const leftIconProps = useMemo(
|
|
222
238
|
(): IReqoreButtonProps['leftIconProps'] => ({
|
|
223
239
|
image: template?.metadata?.image,
|
|
@@ -304,8 +320,60 @@ export const TemplateField = memo(
|
|
|
304
320
|
});
|
|
305
321
|
const type = rest.ui_type || rest.type || rest.defaultType;
|
|
306
322
|
|
|
323
|
+
const filteredTemplates = useMemo<IReqoreFormTemplates>(():
|
|
324
|
+
| IReqoreFormTemplates
|
|
325
|
+
| undefined => {
|
|
326
|
+
if (!allowTemplates) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let result: IReqoreFormTemplates = templates;
|
|
331
|
+
|
|
332
|
+
if (filterTemplatesByType) {
|
|
333
|
+
result = templatesFilterFunc(templates, type, !!rest.arg_schema);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (filterTemplatesFunc) {
|
|
337
|
+
result = filterTemplatesFunc(result);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result;
|
|
341
|
+
}, [
|
|
342
|
+
JSON.stringify(templates),
|
|
343
|
+
type,
|
|
344
|
+
allowTemplates,
|
|
345
|
+
filterTemplatesByType,
|
|
346
|
+
JSON.stringify(rest.arg_schema),
|
|
347
|
+
filterTemplatesFunc,
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
// An `any`-typed field has no editor of its own until a concrete type is
|
|
351
|
+
// chosen, so it opened on the TYPE PICKER: before an author could say what
|
|
352
|
+
// they meant, they had to answer a question about storage — "is this a Text
|
|
353
|
+
// or a Number?" — that they often cannot answer, because the value they
|
|
354
|
+
// want is a reference to something else whose type is not theirs to pick.
|
|
355
|
+
//
|
|
356
|
+
// So an untyped field that accepts templates now opens on the template
|
|
357
|
+
// selector, which is the answer most of them want, and "Set Custom Value"
|
|
358
|
+
// in the ⋮ menu is the way to a literal of a chosen type. That is the
|
|
359
|
+
// reverse of the old default, and it is the right way round: choosing a
|
|
360
|
+
// template is picking from a list, choosing a literal is a decision.
|
|
361
|
+
//
|
|
362
|
+
// Only while the field is EMPTY. A field already holding a literal must
|
|
363
|
+
// open showing that literal — flipping to the template view would hide a
|
|
364
|
+
// value the author put there and make it look lost.
|
|
365
|
+
// ...and only when there is actually something to pick. A field with no
|
|
366
|
+
// templates on offer would otherwise open on an EMPTY picker, which is a
|
|
367
|
+
// worse place to start than the type picker it replaced.
|
|
368
|
+
const typeIsAnyLike = type === 'any' || type === 'auto';
|
|
369
|
+
const isEmptyValue = value === undefined || value === null || value === '';
|
|
370
|
+
const hasTemplatesOnOffer = !!size(filteredTemplates?.items);
|
|
307
371
|
const [isTemplate, setIsTemplate] = useState<boolean>(
|
|
308
|
-
(isDefaultTemplate ||
|
|
372
|
+
(isDefaultTemplate ||
|
|
373
|
+
isValueTemplate(value) ||
|
|
374
|
+
!allowCustomValues ||
|
|
375
|
+
(typeIsAnyLike && isEmptyValue && hasTemplatesOnOffer)) &&
|
|
376
|
+
allowTemplates
|
|
309
377
|
);
|
|
310
378
|
const [internalIsFunction, setInternalIsFunction] = useState<boolean>(
|
|
311
379
|
!!isDefaultFunction && !!allowFunctions
|
|
@@ -335,8 +403,15 @@ export const TemplateField = memo(
|
|
|
335
403
|
|
|
336
404
|
useEffect(() => {
|
|
337
405
|
if (!isTemplate && isValueTemplate(value) && allowTemplates) {
|
|
338
|
-
//
|
|
339
|
-
|
|
406
|
+
// In auto mode, leave user-typed dollar-strings alone ('$foo: hello'
|
|
407
|
+
// passes the loose check) — but a string that IS one well-formed token
|
|
408
|
+
// ($data:{…}, $config:item) must still flip into template mode, or an
|
|
409
|
+
// arg whose value hydrates after mount is stuck rendering raw text.
|
|
410
|
+
if (
|
|
411
|
+
type === 'auto' &&
|
|
412
|
+
getTypeFromValue(value) === 'string' &&
|
|
413
|
+
!isCompleteTemplateToken(value)
|
|
414
|
+
) {
|
|
340
415
|
return;
|
|
341
416
|
}
|
|
342
417
|
|
|
@@ -359,6 +434,15 @@ export const TemplateField = memo(
|
|
|
359
434
|
|
|
360
435
|
const showTemplateToggle = allowCustomValues && allowTemplates && !rest.arg_schema;
|
|
361
436
|
|
|
437
|
+
// Only a BRACED context ref (`$data:{…}` — machine-written, nobody types
|
|
438
|
+
// one) renders as the picker chip (named via `resolveTemplateLabel`)
|
|
439
|
+
// rather than as its raw text in a string editor. Plain word-path tokens
|
|
440
|
+
// (`$local:id`) are typeable, so per the build #123 review they keep the
|
|
441
|
+
// input that offers templates while typing. Mixed text-and-token strings
|
|
442
|
+
// keep the string editor too; the chip-in-editor treatment arrives with
|
|
443
|
+
// the rich-text string mode.
|
|
444
|
+
const templateValueIsBracedToken = isBracedTemplateToken(templateValue);
|
|
445
|
+
|
|
362
446
|
const templateSupportsCustomValues =
|
|
363
447
|
allowCustomValues && type === 'string' && !hasOnlyAllowedValues;
|
|
364
448
|
const showTemplatesDropdown =
|
|
@@ -374,34 +458,13 @@ export const TemplateField = memo(
|
|
|
374
458
|
showTemplatesDropdown;
|
|
375
459
|
|
|
376
460
|
const Component = componentFromType ? ComponentMap[type] : Comp;
|
|
461
|
+
// Only a ComponentMap component gets `type`: it is ours and CONSUMES the
|
|
462
|
+
// prop (the long-string field needs it to know whether it holds one line)
|
|
463
|
+
// rather than spreading it onto the DOM. A caller-supplied `Comp` is someone
|
|
464
|
+
// else's, which is why `type` was kept off this call in the first place.
|
|
465
|
+
const componentTypeProp = componentFromType ? { type } : {};
|
|
377
466
|
const fieldAriaLabel = rest['aria-label'] ?? label ?? rest.display_name ?? name;
|
|
378
467
|
|
|
379
|
-
const filteredTemplates = useMemo<IReqoreFormTemplates>(():
|
|
380
|
-
| IReqoreFormTemplates
|
|
381
|
-
| undefined => {
|
|
382
|
-
if (!allowTemplates) {
|
|
383
|
-
return undefined;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
let result: IReqoreFormTemplates = templates;
|
|
387
|
-
|
|
388
|
-
if (filterTemplatesByType) {
|
|
389
|
-
result = templatesFilterFunc(templates, type, !!rest.arg_schema);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (filterTemplatesFunc) {
|
|
393
|
-
result = filterTemplatesFunc(result);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return result;
|
|
397
|
-
}, [
|
|
398
|
-
JSON.stringify(templates),
|
|
399
|
-
type,
|
|
400
|
-
allowTemplates,
|
|
401
|
-
filterTemplatesByType,
|
|
402
|
-
JSON.stringify(rest.arg_schema),
|
|
403
|
-
filterTemplatesFunc,
|
|
404
|
-
]);
|
|
405
468
|
|
|
406
469
|
const handleTemplateFieldChange = useCallback(
|
|
407
470
|
(_name: string, val: string) => {
|
|
@@ -746,13 +809,14 @@ export const TemplateField = memo(
|
|
|
746
809
|
name={name}
|
|
747
810
|
level={level}
|
|
748
811
|
{...rest}
|
|
812
|
+
{...componentTypeProp}
|
|
749
813
|
aria-label={fieldAriaLabel}
|
|
750
814
|
className={`${className} template-selector`}
|
|
751
815
|
templates={componentTemplates}
|
|
752
816
|
/>
|
|
753
817
|
) : null}
|
|
754
818
|
|
|
755
|
-
{isTemplate && templateSupportsCustomValues ? (
|
|
819
|
+
{isTemplate && templateSupportsCustomValues && !templateValueIsBracedToken ? (
|
|
756
820
|
<LongStringField
|
|
757
821
|
className='template-selector'
|
|
758
822
|
type='string'
|
|
@@ -769,7 +833,8 @@ export const TemplateField = memo(
|
|
|
769
833
|
/>
|
|
770
834
|
) : null}
|
|
771
835
|
|
|
772
|
-
{showTemplatesDropdown
|
|
836
|
+
{showTemplatesDropdown ||
|
|
837
|
+
(isTemplate && templateSupportsCustomValues && templateValueIsBracedToken) ? (
|
|
773
838
|
<TemplateDropdownSelector
|
|
774
839
|
allowCustomValues={allowCustomValues}
|
|
775
840
|
templates={templates}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* A deterministic "whole base64 file" example value — the payload class the
|
|
2
|
+
example-value modal exists for (an email attachment body). No spaces on
|
|
3
|
+
purpose: the unbroken run is what breaks naive rendering. The sentinel
|
|
4
|
+
proves a consumer holds the value to the very end. */
|
|
5
|
+
export const LONG_EXAMPLE_VALUE = `${'JVBERi0xLjcKJcTl8uXrp9Og0MTGCjQgMCBvYmoKPDwvRmlsdGVyL0ZsYXRlRGVjb2RlCg=='.repeat(
|
|
6
|
+
90
|
|
7
|
+
)}THE_VERY_END`;
|
|
@@ -21,6 +21,7 @@ export * from './fields/rich-text/RichText';
|
|
|
21
21
|
export * from './fields/select/Select';
|
|
22
22
|
export * from './fields/select/SelectCollection';
|
|
23
23
|
export * from './fields/string/String';
|
|
24
|
+
export * from './fields/template/ExampleValueModal';
|
|
24
25
|
export * from './fields/template/TemplateField';
|
|
25
26
|
export * from './engine/FormEngine';
|
|
26
27
|
export * from './engine/optionActions';
|