@qoretechnologies/reqraft 0.10.16 → 0.10.18
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 +82 -3
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +37 -11
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +187 -116
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +8 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +1 -1
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/optionActions.d.ts +28 -0
- package/dist/components/form/engine/optionActions.d.ts.map +1 -0
- package/dist/components/form/engine/optionActions.js +18 -0
- package/dist/components/form/engine/optionActions.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +9 -2
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +15 -3
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rendererTypes.d.ts +35 -0
- package/dist/components/form/engine/rendererTypes.d.ts.map +1 -0
- package/dist/components/form/engine/rendererTypes.js +63 -0
- package/dist/components/form/engine/rendererTypes.js.map +1 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +6 -1
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +2 -0
- package/dist/components/form/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/form/engine/CompactRow.tsx +139 -0
- package/src/components/form/engine/FormEngine.tsx +229 -70
- package/src/components/form/engine/FormEngineRemote.stories.tsx +329 -0
- package/src/components/form/engine/compactRowContext.ts +8 -0
- package/src/components/form/engine/compactRowStyles.ts +30 -0
- package/src/components/form/engine/optionActions.ts +40 -0
- package/src/components/form/engine/readFirst.ts +25 -6
- package/src/components/form/engine/rendererTypes.ts +55 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +6 -1
- package/src/components/form/index.tsx +2 -0
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { StoryObj } from '@storybook/react-vite';
|
|
12
12
|
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
13
13
|
import { useState } from 'react';
|
|
14
|
+
import { _testsClickButton, sleep } from '../../../stories/Tests/utils';
|
|
14
15
|
import { StoryMeta } from '../../../types';
|
|
15
16
|
import { FormEngine } from './FormEngine';
|
|
16
17
|
|
|
@@ -307,6 +308,334 @@ export const InjectedOptionActions: Story = {
|
|
|
307
308
|
},
|
|
308
309
|
};
|
|
309
310
|
|
|
311
|
+
/**
|
|
312
|
+
* The same injected actions seam must also work in compact/read-first mode. The
|
|
313
|
+
* IDE uses this path for option-based forms and relies on the action slot for
|
|
314
|
+
* small per-field Qonsole controls.
|
|
315
|
+
*/
|
|
316
|
+
export const InjectedCompactOptionActions: Story = {
|
|
317
|
+
args: {
|
|
318
|
+
name: 'compactOptionActionsSeam',
|
|
319
|
+
compact: true,
|
|
320
|
+
options: BASIC_SCHEMA as any,
|
|
321
|
+
optionActions: ({ name }) => [
|
|
322
|
+
{
|
|
323
|
+
icon: 'MagicLine',
|
|
324
|
+
className: 'option-compact-ai-assist',
|
|
325
|
+
tooltip: `AI assistance for ${name}`,
|
|
326
|
+
// The shape the IDE passes on the classic path — hover-gated here too.
|
|
327
|
+
show: 'hover',
|
|
328
|
+
size: 'tiny',
|
|
329
|
+
fixed: true,
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
icon: 'InformationLine',
|
|
333
|
+
className: 'option-compact-always',
|
|
334
|
+
tooltip: `Details for ${name}`,
|
|
335
|
+
size: 'tiny',
|
|
336
|
+
fixed: true,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
parameters: {
|
|
341
|
+
docs: {
|
|
342
|
+
description: {
|
|
343
|
+
story:
|
|
344
|
+
'Renders compact FormEngine rows with two injected per-option actions: one always visible, one declared `show: "hover"` that stays transparent until its row is hovered or focused. Hover-gating applies only where the pointer can hover — on touch both collapse into the row\'s overflow menu instead, so neither is ever hover-only.',
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
async play() {
|
|
349
|
+
await waitFor(
|
|
350
|
+
() => expect(document.querySelectorAll('.readfirst-row').length).toBeGreaterThan(0),
|
|
351
|
+
{ timeout: 10000 }
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const rowCount = document.querySelectorAll('.readfirst-row').length;
|
|
355
|
+
|
|
356
|
+
await waitFor(() => {
|
|
357
|
+
expect(document.querySelectorAll('.option-compact-always').length).toBe(rowCount);
|
|
358
|
+
expect(document.querySelectorAll('.option-compact-ai-assist').length).toBe(rowCount);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// The hover-only action carries the CSS gate; the always-on one does not.
|
|
362
|
+
const hoverAction = document.querySelector(
|
|
363
|
+
'.option-compact-ai-assist.options-injected-action-hover'
|
|
364
|
+
) as HTMLElement;
|
|
365
|
+
expect(hoverAction).toBeTruthy();
|
|
366
|
+
expect(
|
|
367
|
+
document.querySelectorAll('.option-compact-always.options-injected-action-hover').length
|
|
368
|
+
).toBe(0);
|
|
369
|
+
|
|
370
|
+
// Actually exercise the reveal condition rather than just asserting the
|
|
371
|
+
// class is present: gated the action is transparent and not hit-testable.
|
|
372
|
+
expect(getComputedStyle(hoverAction).opacity).toBe('0');
|
|
373
|
+
expect(getComputedStyle(hoverAction).pointerEvents).toBe('none');
|
|
374
|
+
|
|
375
|
+
// Focus is the reveal path a test can drive: the gate keys on
|
|
376
|
+
// `:hover, :focus-within`, and a synthetic mouse event does NOT put a real
|
|
377
|
+
// browser into `:hover`. Focusing the action is also the keyboard route a
|
|
378
|
+
// user takes, so this covers the accessibility path at the same time.
|
|
379
|
+
hoverAction.focus();
|
|
380
|
+
await waitFor(() => {
|
|
381
|
+
expect(getComputedStyle(hoverAction).opacity).toBe('1');
|
|
382
|
+
expect(getComputedStyle(hoverAction).pointerEvents).toBe('auto');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Left revealed so the captured frame shows the action rather than an
|
|
386
|
+
// empty slot — the whole point of the story.
|
|
387
|
+
await sleep(200);
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The no-hover case. A touch device never fires `:hover`, so a hover-gated
|
|
393
|
+
* action would be permanently unreachable; instead every injected action moves
|
|
394
|
+
* into the row's own overflow menu. `optionActionsCollapse='always'` forces the
|
|
395
|
+
* branch that `(hover: none)`/`(pointer: coarse)` picks on a real phone, which a
|
|
396
|
+
* desktop browser cannot emulate.
|
|
397
|
+
*/
|
|
398
|
+
export const InjectedCompactOptionActionsMobile: Story = {
|
|
399
|
+
args: {
|
|
400
|
+
name: 'compactOptionActionsMobile',
|
|
401
|
+
compact: true,
|
|
402
|
+
options: BASIC_SCHEMA as any,
|
|
403
|
+
optionActionsCollapse: 'always',
|
|
404
|
+
optionActions: ({ name }) => [
|
|
405
|
+
{
|
|
406
|
+
icon: 'MagicLine',
|
|
407
|
+
className: 'option-compact-ai-assist',
|
|
408
|
+
tooltip: `AI assistance for ${name}`,
|
|
409
|
+
show: 'hover',
|
|
410
|
+
size: 'tiny',
|
|
411
|
+
fixed: true,
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
icon: 'InformationLine',
|
|
415
|
+
className: 'option-compact-always',
|
|
416
|
+
tooltip: `Details for ${name}`,
|
|
417
|
+
size: 'tiny',
|
|
418
|
+
fixed: true,
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
},
|
|
422
|
+
parameters: {
|
|
423
|
+
qlip: { viewport: { width: 420, height: 900 } },
|
|
424
|
+
docs: {
|
|
425
|
+
description: {
|
|
426
|
+
story:
|
|
427
|
+
'Renders the compact rows at phone width with injected actions collapsed into each row\'s overflow menu — the no-hover path, where a hover-gated button would otherwise be unreachable. The menu is opened so the actions are visible.',
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
async play() {
|
|
432
|
+
await waitFor(
|
|
433
|
+
() => expect(document.querySelectorAll('.readfirst-row').length).toBeGreaterThan(0),
|
|
434
|
+
{ timeout: 10000 }
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Collapsed: no inline injected buttons at all, one overflow menu per row.
|
|
438
|
+
const rowCount = document.querySelectorAll('.readfirst-row').length;
|
|
439
|
+
await waitFor(() => {
|
|
440
|
+
expect(document.querySelectorAll('.options-injected-actions-menu').length).toBe(rowCount);
|
|
441
|
+
});
|
|
442
|
+
expect(document.querySelectorAll('.option-compact-ai-assist').length).toBe(0);
|
|
443
|
+
expect(document.querySelectorAll('.option-compact-always').length).toBe(0);
|
|
444
|
+
|
|
445
|
+
// Both actions are reachable from the menu — the reason the collapse exists.
|
|
446
|
+
await _testsClickButton({ selector: '.options-injected-actions-menu', nth: 0 });
|
|
447
|
+
await waitFor(
|
|
448
|
+
() => {
|
|
449
|
+
const labels = Array.from(document.querySelectorAll('.reqore-menu-item')).map(
|
|
450
|
+
(item) => item.textContent ?? ''
|
|
451
|
+
);
|
|
452
|
+
expect(labels.some((label) => label.includes('AI assistance for'))).toBe(true);
|
|
453
|
+
expect(labels.some((label) => label.includes('Details for'))).toBe(true);
|
|
454
|
+
},
|
|
455
|
+
{ timeout: 10000 }
|
|
456
|
+
);
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Many injected actions. Beyond the inline cap the extras overflow into the
|
|
462
|
+
* row's menu instead of squeezing the value out of the row, so a consumer can
|
|
463
|
+
* inject any number without breaking the layout.
|
|
464
|
+
*/
|
|
465
|
+
export const InjectedCompactOptionActionsMany: Story = {
|
|
466
|
+
args: {
|
|
467
|
+
name: 'compactOptionActionsMany',
|
|
468
|
+
compact: true,
|
|
469
|
+
options: BASIC_SCHEMA as any,
|
|
470
|
+
optionActions: ({ name }) =>
|
|
471
|
+
['MagicLine', 'InformationLine', 'FileCopyLine', 'DeleteBinLine', 'ShareLine'].map(
|
|
472
|
+
(icon, index) => ({
|
|
473
|
+
icon: icon as any,
|
|
474
|
+
className: `option-compact-many-${index}`,
|
|
475
|
+
tooltip: `${icon} for ${name}`,
|
|
476
|
+
size: 'tiny',
|
|
477
|
+
fixed: true,
|
|
478
|
+
})
|
|
479
|
+
),
|
|
480
|
+
},
|
|
481
|
+
parameters: {
|
|
482
|
+
docs: {
|
|
483
|
+
description: {
|
|
484
|
+
story:
|
|
485
|
+
'Renders the compact rows with five injected actions per option — the first two stay inline and the remaining three collapse into the row\'s overflow menu, which is opened here to show them.',
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
async play() {
|
|
490
|
+
await waitFor(
|
|
491
|
+
() => expect(document.querySelectorAll('.readfirst-row').length).toBeGreaterThan(0),
|
|
492
|
+
{ timeout: 10000 }
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
const rowCount = document.querySelectorAll('.readfirst-row').length;
|
|
496
|
+
|
|
497
|
+
// Only the first two render inline; the rest are in the menu.
|
|
498
|
+
await waitFor(() => {
|
|
499
|
+
expect(document.querySelectorAll('.option-compact-many-0').length).toBe(rowCount);
|
|
500
|
+
expect(document.querySelectorAll('.option-compact-many-1').length).toBe(rowCount);
|
|
501
|
+
});
|
|
502
|
+
expect(document.querySelectorAll('.option-compact-many-2').length).toBe(0);
|
|
503
|
+
expect(document.querySelectorAll('.option-compact-many-4').length).toBe(0);
|
|
504
|
+
expect(document.querySelectorAll('.options-injected-actions-menu').length).toBe(rowCount);
|
|
505
|
+
|
|
506
|
+
await _testsClickButton({ selector: '.options-injected-actions-menu', nth: 0 });
|
|
507
|
+
await waitFor(
|
|
508
|
+
() => {
|
|
509
|
+
const labels = Array.from(document.querySelectorAll('.reqore-menu-item')).map(
|
|
510
|
+
(item) => item.textContent ?? ''
|
|
511
|
+
);
|
|
512
|
+
expect(labels.some((label) => label.includes('DeleteBinLine for'))).toBe(true);
|
|
513
|
+
expect(labels.some((label) => label.includes('ShareLine for'))).toBe(true);
|
|
514
|
+
},
|
|
515
|
+
{ timeout: 10000 }
|
|
516
|
+
);
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// A compact row renders the SAME logical state three ways — the read row, the
|
|
521
|
+
// inline editor, and the edit card (complex types get the card). An injected
|
|
522
|
+
// action has to survive all three, and per the affordance-parity rule each
|
|
523
|
+
// branch needs its own story: the branch without one is where a dead action
|
|
524
|
+
// hides, because the branch under test is the branch that works.
|
|
525
|
+
const BRANCH_SCHEMA = {
|
|
526
|
+
host: {
|
|
527
|
+
type: 'string',
|
|
528
|
+
display_name: 'Host',
|
|
529
|
+
short_desc: 'Edits inline inside the row',
|
|
530
|
+
preselected: true,
|
|
531
|
+
default_value: 'localhost',
|
|
532
|
+
},
|
|
533
|
+
notes: {
|
|
534
|
+
type: 'string',
|
|
535
|
+
ui_type: 'long-string',
|
|
536
|
+
display_name: 'Notes',
|
|
537
|
+
short_desc: 'A complex type — edits in the card branch',
|
|
538
|
+
preselected: true,
|
|
539
|
+
// Needs a value: an empty optional field sits in the collapsed "Optional"
|
|
540
|
+
// group, where `initialExpandedOptions` has nothing to open.
|
|
541
|
+
default_value: 'Notes that are long enough to want the card editor.',
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const branchAction = ({ name }: { name: string }) => [
|
|
546
|
+
{
|
|
547
|
+
icon: 'MagicLine' as const,
|
|
548
|
+
className: 'option-branch-action',
|
|
549
|
+
tooltip: `AI assistance for ${name}`,
|
|
550
|
+
size: 'tiny' as const,
|
|
551
|
+
fixed: true,
|
|
552
|
+
},
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Branch 1 of 3: the INLINE editor. A simple type opens inside the row itself,
|
|
557
|
+
* and the injected action has to be reachable there, not just on the read row.
|
|
558
|
+
*/
|
|
559
|
+
export const InjectedOptionActionsInlineEditing: Story = {
|
|
560
|
+
args: {
|
|
561
|
+
name: 'compactOptionActionsInline',
|
|
562
|
+
compact: true,
|
|
563
|
+
options: BRANCH_SCHEMA as any,
|
|
564
|
+
initialExpandedOptions: ['host'],
|
|
565
|
+
optionActions: branchAction,
|
|
566
|
+
},
|
|
567
|
+
parameters: {
|
|
568
|
+
docs: {
|
|
569
|
+
description: {
|
|
570
|
+
story:
|
|
571
|
+
'Renders the compact form with the simple `host` field already open, so it edits inline inside the row — the injected per-option action is asserted inside that inline branch.',
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
async play() {
|
|
576
|
+
await waitFor(
|
|
577
|
+
() => expect(document.querySelector('.readfirst-row-editing')).toBeTruthy(),
|
|
578
|
+
{ timeout: 10000 }
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
// Scoped to the branch on purpose: a global query would pass on the read
|
|
582
|
+
// row's copy and prove nothing about the inline editor.
|
|
583
|
+
const injected = await waitFor(() => {
|
|
584
|
+
const el = document.querySelector(
|
|
585
|
+
'.readfirst-row-editing .option-branch-action'
|
|
586
|
+
) as HTMLElement;
|
|
587
|
+
expect(el).toBeTruthy();
|
|
588
|
+
return el;
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// The injected action must match the size of the row's own actions. The
|
|
592
|
+
// fixture deliberately asks for `size: 'tiny'` — the row overrides it,
|
|
593
|
+
// because a consumer-sized button rendered visibly smaller than the revert
|
|
594
|
+
// and More buttons beside it (qlip build #57 rejection).
|
|
595
|
+
const neighbour = document.querySelector(
|
|
596
|
+
'.readfirst-row-editing .options-readfirst-more'
|
|
597
|
+
) as HTMLElement;
|
|
598
|
+
expect(neighbour).toBeTruthy();
|
|
599
|
+
expect(injected.getBoundingClientRect().height).toBe(
|
|
600
|
+
neighbour.getBoundingClientRect().height
|
|
601
|
+
);
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Branch 2 of 3: the EDIT CARD. A complex type (`long-string`) is too tall to
|
|
607
|
+
* edit in-row, so it opens as a card — a different subtree, and historically
|
|
608
|
+
* where an action wired only in the other branch went missing.
|
|
609
|
+
*/
|
|
610
|
+
export const InjectedOptionActionsEditCard: Story = {
|
|
611
|
+
args: {
|
|
612
|
+
name: 'compactOptionActionsCard',
|
|
613
|
+
compact: true,
|
|
614
|
+
options: BRANCH_SCHEMA as any,
|
|
615
|
+
initialExpandedOptions: ['notes'],
|
|
616
|
+
optionActions: branchAction,
|
|
617
|
+
},
|
|
618
|
+
parameters: {
|
|
619
|
+
docs: {
|
|
620
|
+
description: {
|
|
621
|
+
story:
|
|
622
|
+
'Renders the compact form with the complex `notes` field already open, so it edits in the card branch rather than in-row — the injected per-option action is asserted inside that card.',
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
async play() {
|
|
627
|
+
await waitFor(() => expect(document.querySelector('.options-readfirst-card')).toBeTruthy(), {
|
|
628
|
+
timeout: 10000,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
await waitFor(() => {
|
|
632
|
+
expect(
|
|
633
|
+
document.querySelector('.options-readfirst-card .option-branch-action')
|
|
634
|
+
).toBeTruthy();
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
|
|
310
639
|
/**
|
|
311
640
|
* Opt-in template fetching: `interfaceContext` makes `useTemplates` fetch
|
|
312
641
|
* `system/getContextData` (the IDE's behavior); without it, no request is
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { MutableRefObject } from 'react';
|
|
10
10
|
import { createContext } from 'use-context-selector';
|
|
11
11
|
import { IOperatorsSchema } from './FormEngine';
|
|
12
|
+
import { TOptionActions } from './optionActions';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* The complete closure surface of the (former) `renderCompactRow` function,
|
|
@@ -74,6 +75,13 @@ export interface ICompactRowContext {
|
|
|
74
75
|
|
|
75
76
|
// Function passed through as a value (stays defined in FormEngine because the
|
|
76
77
|
// classic non-compact path uses it too).
|
|
78
|
+
optionActions?: TOptionActions;
|
|
79
|
+
/**
|
|
80
|
+
* Injected actions render inside the row's overflow menu instead of as inline
|
|
81
|
+
* buttons. True on touch (where a hover-gated button is unreachable) and on
|
|
82
|
+
* narrow viewports. Resolved once by FormEngine so rows share one subscription.
|
|
83
|
+
*/
|
|
84
|
+
collapseOptionActions?: boolean;
|
|
77
85
|
renderOption: (
|
|
78
86
|
optionName: string,
|
|
79
87
|
field: IQorusFormField,
|
|
@@ -73,6 +73,36 @@ export const StyledCompactPanel = styled(ReqorePanel)<{
|
|
|
73
73
|
flex: 1 1 auto;
|
|
74
74
|
min-width: 0;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/* An injected option action declared \`show: 'hover'\` — the shape ReqorePanel
|
|
78
|
+
honours on the classic path — is revealed only while its row or card is
|
|
79
|
+
hovered or holds focus. The compact slots render plain buttons, so the gate
|
|
80
|
+
has to be CSS here. Kept focus-visible too, so the action stays reachable by
|
|
81
|
+
keyboard. Opacity rather than display keeps the slot's width stable, so rows
|
|
82
|
+
don't reflow on hover. */
|
|
83
|
+
.options-injected-action-hover {
|
|
84
|
+
opacity: 0;
|
|
85
|
+
/* Not hit-testable while invisible — an opacity:0 button still takes taps
|
|
86
|
+
and clicks, which reads as a mystery control firing out of nowhere. */
|
|
87
|
+
pointer-events: none;
|
|
88
|
+
transition: opacity 0.15s ease;
|
|
89
|
+
}
|
|
90
|
+
.readfirst-row:hover .options-injected-action-hover,
|
|
91
|
+
.readfirst-row:focus-within .options-injected-action-hover,
|
|
92
|
+
.options-readfirst-card:hover .options-injected-action-hover,
|
|
93
|
+
.options-readfirst-card:focus-within .options-injected-action-hover {
|
|
94
|
+
opacity: 1;
|
|
95
|
+
pointer-events: auto;
|
|
96
|
+
}
|
|
97
|
+
/* Touch and other hover-less pointers never fire :hover, so a hover-gated
|
|
98
|
+
action would be permanently unreachable there. Show it unconditionally
|
|
99
|
+
instead — losing the tidiness beats losing the functionality. */
|
|
100
|
+
@media (hover: none), (pointer: coarse) {
|
|
101
|
+
.options-injected-action-hover {
|
|
102
|
+
opacity: 1;
|
|
103
|
+
pointer-events: auto;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
76
106
|
`;
|
|
77
107
|
|
|
78
108
|
// Compact group header laid out as the panel's `label`: the group name, a
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { IReqorePanelAction } from '@qoretechnologies/reqore/dist/components/Panel';
|
|
2
|
+
import { IQorusFormField, IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The context an `optionActions` factory receives for a single option — the
|
|
6
|
+
* option's name, its schema entry, and its current field value. This is the
|
|
7
|
+
* IDE's `AiAssistanceAction` context (it injects its `allowAi` button here).
|
|
8
|
+
*/
|
|
9
|
+
export interface IOptionActionsContext {
|
|
10
|
+
name: string;
|
|
11
|
+
schema: IQorusFormSchema[string];
|
|
12
|
+
value?: IQorusFormField;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SEAM (reqraft): per-option injected actions. Either a static list applied to
|
|
17
|
+
* every option, or a factory invoked once per option with that option's context.
|
|
18
|
+
*/
|
|
19
|
+
export type TOptionActions =
|
|
20
|
+
| IReqorePanelAction[]
|
|
21
|
+
| ((context: IOptionActionsContext) => IReqorePanelAction[]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the `optionActions` seam for one option.
|
|
25
|
+
*
|
|
26
|
+
* Both render paths go through this so a consumer's factory is called with the
|
|
27
|
+
* same context and its result shaped the same way: the classic path feeds the
|
|
28
|
+
* result to `ReqorePanel`'s `actions`, the compact path renders the buttons
|
|
29
|
+
* itself. Falsy entries are dropped so a factory can return conditional actions
|
|
30
|
+
* inline (`cond && {...}`) without each caller re-filtering.
|
|
31
|
+
*/
|
|
32
|
+
export const resolveOptionActions = (
|
|
33
|
+
optionActions: TOptionActions | undefined,
|
|
34
|
+
context: IOptionActionsContext
|
|
35
|
+
): IReqorePanelAction[] => {
|
|
36
|
+
const actions =
|
|
37
|
+
typeof optionActions === 'function' ? optionActions(context) : (optionActions ?? []);
|
|
38
|
+
|
|
39
|
+
return (actions ?? []).filter((action): action is IReqorePanelAction => !!action);
|
|
40
|
+
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
IQorusFormField,
|
|
3
|
+
TQorusFormFieldSchema,
|
|
4
|
+
TQorusType,
|
|
5
|
+
} from '@qoretechnologies/ts-toolkit';
|
|
6
|
+
import { isRendererOnlyUiType } from './rendererTypes';
|
|
2
7
|
import { isUiEncodedValue } from './_structuredData/structuredData';
|
|
3
8
|
import { renderExpressionToText } from '../expressions/renderExpressionToText';
|
|
4
9
|
import { IExpressionValue } from '../expressions/types';
|
|
@@ -223,15 +228,29 @@ export const getFileSize = (value: unknown): number | undefined => {
|
|
|
223
228
|
return typeof size === 'number' && Number.isFinite(size) && size >= 0 ? size : undefined;
|
|
224
229
|
};
|
|
225
230
|
|
|
226
|
-
/**
|
|
227
|
-
*
|
|
231
|
+
/**
|
|
232
|
+
* Effective UI type for DISPLAY — which preview/summary a value renders as.
|
|
233
|
+
*
|
|
234
|
+
* A renderer-only `ui_type` wins outright: the field's stored `type` holds the
|
|
235
|
+
* STORAGE type for those (a `code-editor` stores a string), so consulting the
|
|
236
|
+
* stored type here would render the bespoke editor's value as a plain string and
|
|
237
|
+
* silently drop its preview. Every other case keeps the stored type first, so an
|
|
238
|
+
* `any`/`auto` field still displays as whatever concrete type the user picked.
|
|
239
|
+
*/
|
|
228
240
|
export const getValueType = (
|
|
229
241
|
option?: IQorusFormField,
|
|
230
242
|
schema?: TQorusFormFieldSchema
|
|
231
|
-
): string | undefined =>
|
|
232
|
-
(
|
|
233
|
-
|
|
243
|
+
): string | undefined => {
|
|
244
|
+
const uiType = (schema as { ui_type?: string } | undefined)?.ui_type;
|
|
245
|
+
|
|
246
|
+
if (isRendererOnlyUiType(uiType as TQorusType)) {
|
|
247
|
+
return uiType;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return (option?.type ||
|
|
251
|
+
uiType ||
|
|
234
252
|
(schema as { type?: string } | undefined)?.type) as string | undefined;
|
|
253
|
+
};
|
|
235
254
|
|
|
236
255
|
/** Human-readable byte count (e.g. `1.2 KB`). */
|
|
237
256
|
export const formatBytes = (bytes: number): string => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { TQorusType } from '@qoretechnologies/ts-toolkit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A schema entry carries two type-ish keys with different jobs:
|
|
5
|
+
*
|
|
6
|
+
* - `type` names how the value is STORED and validated (`string`, `hash`, `list`…).
|
|
7
|
+
* - `ui_type` names which EDITOR renders it.
|
|
8
|
+
*
|
|
9
|
+
* For most fields `ui_type` is a storage-compatible refinement (`long-string`,
|
|
10
|
+
* `enum`, `url`) and it is safe to let it win for both jobs. But some `ui_type`s
|
|
11
|
+
* name a bespoke editor whose value is still stored as the plainer `type` — a
|
|
12
|
+
* `cron` editor stores a string, `processor-mappings` stores a hash. Writing
|
|
13
|
+
* those renderer names into the field's `type` breaks validation and round-trips
|
|
14
|
+
* the value through the wrong branch, which is what `RENDERER_ONLY_UI_TYPES`
|
|
15
|
+
* exists to prevent.
|
|
16
|
+
*
|
|
17
|
+
* There is deliberately no derivation here: a renderer-only `ui_type` is not
|
|
18
|
+
* distinguishable from a storage-compatible one by inspecting the value or the
|
|
19
|
+
* `TQorusType` union (`code-editor` and `tool-catalog` are members of it, and
|
|
20
|
+
* the validator has cases for `cron` and `schema-definition`). It is a fact
|
|
21
|
+
* about which editor a name selects, so it has to be declared.
|
|
22
|
+
*/
|
|
23
|
+
export const BUILT_IN_RENDERER_ONLY_UI_TYPES: readonly string[] = [
|
|
24
|
+
'active-windows',
|
|
25
|
+
'alert-threshold',
|
|
26
|
+
'code-editor',
|
|
27
|
+
'cron',
|
|
28
|
+
'dpql',
|
|
29
|
+
'processor-mappings',
|
|
30
|
+
'schema-definition',
|
|
31
|
+
'test-cases',
|
|
32
|
+
'test-value-contract',
|
|
33
|
+
'tool-catalog',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the renderer-only predicate for one FormEngine instance.
|
|
38
|
+
*
|
|
39
|
+
* Consumers inject their own editors through `componentOverrides`, so the set of
|
|
40
|
+
* renderer-only names is open-ended — the IDE ships editors reqraft has never
|
|
41
|
+
* heard of. `extraTypes` (the `rendererOnlyUiTypes` prop) lets a consumer declare
|
|
42
|
+
* its own without waiting on a reqraft release, which is what previously made
|
|
43
|
+
* every new IDE editor a silent value-corruption bug until this list caught up.
|
|
44
|
+
*/
|
|
45
|
+
export const createRendererOnlyUiTypeCheck = (
|
|
46
|
+
extraTypes?: readonly string[]
|
|
47
|
+
): ((type?: TQorusType | TQorusType[]) => boolean) => {
|
|
48
|
+
const known = new Set<string>([...BUILT_IN_RENDERER_ONLY_UI_TYPES, ...(extraTypes ?? [])]);
|
|
49
|
+
|
|
50
|
+
return (type?: TQorusType | TQorusType[]): boolean =>
|
|
51
|
+
typeof type === 'string' && known.has(type);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** The default predicate — built-ins only, for module-scope callers with no props. */
|
|
55
|
+
export const isRendererOnlyUiType = createRendererOnlyUiTypeCheck();
|
|
@@ -356,7 +356,12 @@ function AutoField<T = any>({
|
|
|
356
356
|
{...rest}
|
|
357
357
|
name={name}
|
|
358
358
|
value={value}
|
|
359
|
-
onChange={(val: any
|
|
359
|
+
onChange={(val: any, emittedType?: IQorusType, emittedIsFunction?: boolean) => {
|
|
360
|
+
const returnType = emittedType || currentType;
|
|
361
|
+
if (onChange && returnType) {
|
|
362
|
+
onChange(name, val, returnType, emittedIsFunction);
|
|
363
|
+
}
|
|
364
|
+
}}
|
|
360
365
|
/>
|
|
361
366
|
);
|
|
362
367
|
}
|
|
@@ -22,6 +22,8 @@ export * from './fields/select/SelectCollection';
|
|
|
22
22
|
export * from './fields/string/String';
|
|
23
23
|
export * from './fields/template/TemplateField';
|
|
24
24
|
export * from './engine/FormEngine';
|
|
25
|
+
export * from './engine/optionActions';
|
|
26
|
+
export * from './engine/rendererTypes';
|
|
25
27
|
export * from './expressions/types';
|
|
26
28
|
export * from './expressions/useExpressions';
|
|
27
29
|
// Named (not wildcard) so the `_resetRenderExpressionTransportForTests`
|