@qoretechnologies/reqraft 0.10.9-pr.74.g452d4d3 → 0.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  2. package/dist/components/form/engine/CompactRow.js +28 -3
  3. package/dist/components/form/engine/CompactRow.js.map +1 -1
  4. package/dist/components/form/engine/FormEngine.d.ts +16 -1
  5. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  6. package/dist/components/form/engine/FormEngine.js +83 -1
  7. package/dist/components/form/engine/FormEngine.js.map +1 -1
  8. package/dist/components/form/engine/compactRowContext.d.ts +1 -0
  9. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  10. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  11. package/dist/components/form/engine/compactRowStyles.d.ts +1 -0
  12. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  13. package/dist/components/form/engine/compactRowStyles.js +19 -4
  14. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  15. package/dist/components/form/engine/readFirst.d.ts +28 -0
  16. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  17. package/dist/components/form/engine/readFirst.js +24 -1
  18. package/dist/components/form/engine/readFirst.js.map +1 -1
  19. package/dist/components/form/fields/binary/Binary.d.ts +15 -0
  20. package/dist/components/form/fields/binary/Binary.d.ts.map +1 -0
  21. package/dist/components/form/fields/binary/Binary.js +74 -0
  22. package/dist/components/form/fields/binary/Binary.js.map +1 -0
  23. package/dist/index.d.ts +0 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +0 -2
  26. package/dist/index.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/components/form/engine/CompactRow.tsx +63 -3
  29. package/src/components/form/engine/FormEngine.stories.tsx +236 -0
  30. package/src/components/form/engine/FormEngine.tsx +108 -0
  31. package/src/components/form/engine/compactRowContext.ts +6 -0
  32. package/src/components/form/engine/compactRowStyles.ts +21 -0
  33. package/src/components/form/engine/readFirst.ts +42 -0
  34. package/src/components/form/fields/binary/Binary.stories.tsx +90 -0
  35. package/src/components/form/fields/binary/Binary.tsx +81 -0
  36. package/src/index.tsx +0 -2
  37. package/dist/components/supportTicket/AttachmentChips.d.ts +0 -7
  38. package/dist/components/supportTicket/AttachmentChips.d.ts.map +0 -1
  39. package/dist/components/supportTicket/AttachmentChips.js +0 -26
  40. package/dist/components/supportTicket/AttachmentChips.js.map +0 -1
  41. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +0 -25
  42. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +0 -1
  43. package/dist/components/supportTicket/InterfaceReferenceTags.js +0 -28
  44. package/dist/components/supportTicket/InterfaceReferenceTags.js.map +0 -1
  45. package/dist/components/supportTicket/TicketMetaTags.d.ts +0 -24
  46. package/dist/components/supportTicket/TicketMetaTags.d.ts.map +0 -1
  47. package/dist/components/supportTicket/TicketMetaTags.js +0 -21
  48. package/dist/components/supportTicket/TicketMetaTags.js.map +0 -1
  49. package/dist/components/supportTicket/TicketReplyBox.d.ts +0 -59
  50. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +0 -1
  51. package/dist/components/supportTicket/TicketReplyBox.js +0 -169
  52. package/dist/components/supportTicket/TicketReplyBox.js.map +0 -1
  53. package/dist/components/supportTicket/fileToAttachment.d.ts +0 -9
  54. package/dist/components/supportTicket/fileToAttachment.d.ts.map +0 -1
  55. package/dist/components/supportTicket/fileToAttachment.js +0 -28
  56. package/dist/components/supportTicket/fileToAttachment.js.map +0 -1
  57. package/dist/components/supportTicket/index.d.ts +0 -7
  58. package/dist/components/supportTicket/index.d.ts.map +0 -1
  59. package/dist/components/supportTicket/index.js +0 -23
  60. package/dist/components/supportTicket/index.js.map +0 -1
  61. package/dist/components/supportTicket/meta.d.ts +0 -55
  62. package/dist/components/supportTicket/meta.d.ts.map +0 -1
  63. package/dist/components/supportTicket/meta.js +0 -77
  64. package/dist/components/supportTicket/meta.js.map +0 -1
  65. package/dist/components/ticketThread/TicketThread.d.ts +0 -36
  66. package/dist/components/ticketThread/TicketThread.d.ts.map +0 -1
  67. package/dist/components/ticketThread/TicketThread.js +0 -137
  68. package/dist/components/ticketThread/TicketThread.js.map +0 -1
  69. package/dist/components/ticketThread/index.d.ts +0 -3
  70. package/dist/components/ticketThread/index.d.ts.map +0 -1
  71. package/dist/components/ticketThread/index.js +0 -6
  72. package/dist/components/ticketThread/index.js.map +0 -1
  73. package/src/components/supportTicket/AttachmentChips.tsx +0 -50
  74. package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +0 -102
  75. package/src/components/supportTicket/InterfaceReferenceTags.tsx +0 -62
  76. package/src/components/supportTicket/TicketMetaTags.stories.tsx +0 -71
  77. package/src/components/supportTicket/TicketMetaTags.tsx +0 -58
  78. package/src/components/supportTicket/TicketReplyBox.stories.tsx +0 -111
  79. package/src/components/supportTicket/TicketReplyBox.tsx +0 -251
  80. package/src/components/supportTicket/fileToAttachment.ts +0 -24
  81. package/src/components/supportTicket/index.ts +0 -6
  82. package/src/components/supportTicket/meta.ts +0 -135
  83. package/src/components/ticketThread/TicketThread.stories.tsx +0 -192
  84. package/src/components/ticketThread/TicketThread.tsx +0 -331
  85. package/src/components/ticketThread/index.ts +0 -6
@@ -715,6 +715,73 @@ export const OptionInheritsRenderPropFromSiblingCompact: Story = {
715
715
  },
716
716
  };
717
717
 
718
+ // Compact-row code-editor preview: a `code-editor` field with a multi-line
719
+ // string value renders (a) a "N lines · N chars" tag in the value cell instead
720
+ // of the truncated raw string, and (b) a monospace `<pre>` block under the row
721
+ // capped by a `ReqoreCollapsibleContent` — the "Show more" affordance the value
722
+ // cell couldn't provide on its own. Locks the compact preview so a future
723
+ // CompactRow refactor can't silently reduce a Qorus source-code field to an
724
+ // ellipsised one-liner again.
725
+ export const CompactRowCodeEditorPreview: Story = {
726
+ args: {
727
+ compact: true,
728
+ minColumnWidth: '360px',
729
+ componentOverrides: { 'code-editor': CodeEditorStandin },
730
+ value: {
731
+ language: { type: 'string', value: 'qore' },
732
+ source: {
733
+ type: 'string',
734
+ value:
735
+ '%new-style\n%require-types\n%strict-args\n' +
736
+ '%enable-all-warnings\n\n' +
737
+ 'class ExampleJob inherits QorusJob {\n' +
738
+ ' run() {\n' +
739
+ ' logInfo("running");\n' +
740
+ ' }\n' +
741
+ '}\n',
742
+ },
743
+ },
744
+ options: {
745
+ language: {
746
+ type: 'string',
747
+ ui_type: 'string',
748
+ display_name: 'Language',
749
+ allowed_values: [
750
+ { display_name: 'Qore', value: { type: 'string', value: 'qore' } },
751
+ { display_name: 'Python', value: { type: 'string', value: 'python' } },
752
+ { display_name: 'Java', value: { type: 'string', value: 'java' } },
753
+ ],
754
+ },
755
+ source: {
756
+ type: 'string',
757
+ ui_type: 'code-editor',
758
+ display_name: 'Source Code',
759
+ inherit_props: { language: 'language' },
760
+ },
761
+ },
762
+ },
763
+ play: async ({ canvasElement }) => {
764
+ // (a) The monospace preview mounted under the row — contains a substring
765
+ // only the source has, proving `showCodePreview` kicked in and the
766
+ // `StyledCodePreview` block is in the DOM.
767
+ await waitFor(
768
+ () => {
769
+ const preview = canvasElement.querySelector('.options-readfirst-code');
770
+ expect(preview).toBeTruthy();
771
+ expect(preview?.textContent).toContain('class ExampleJob inherits QorusJob');
772
+ },
773
+ { timeout: 5000 }
774
+ );
775
+ // (b) The value cell replaced its truncated raw string with a summary tag —
776
+ // query the tag directly (its label + labelKey render on separate spans,
777
+ // so text-matching across them is fragile). Look for the CodeLine icon
778
+ // that only this tag mounts alongside the source-code row.
779
+ const sourceRow = canvasElement.querySelector('[data-field="source"]');
780
+ expect(sourceRow).toBeTruthy();
781
+ expect(sourceRow?.textContent ?? '').toMatch(/\d+\s*lines?/);
782
+ },
783
+ };
784
+
718
785
  // qorus#347-followup, scope forwarding: this story exercises the nested
719
786
  // case of the OptionInheritsRenderPropFromSibling contract. The parent
720
787
  // form declares `methods: { ui_type: 'list', element_type: 'hash',
@@ -4335,3 +4402,172 @@ export const CompactFieldSortWithinGroups: Story = {
4335
4402
  await expect(document.querySelectorAll('.readfirst-cluster-first')).toHaveLength(2);
4336
4403
  },
4337
4404
  };
4405
+
4406
+ // `autoFocusFirstRequired` drops the user straight into the first field they
4407
+ // must fill. Here `name` is required-but-filled and `description` is
4408
+ // required-but-empty, so the engine expands the `description` row on mount and
4409
+ // its editor takes focus — no click, no DOM scraping.
4410
+ export const CompactAutoFocusFirstRequired: Story = {
4411
+ args: {
4412
+ compact: true,
4413
+ minColumnWidth: '300px',
4414
+ options: CompactSchema,
4415
+ value: CompactValue,
4416
+ groups: CompactGroups,
4417
+ autoFocusFirstRequired: true,
4418
+ },
4419
+ play: async () => {
4420
+ await _testsWaitForText('order-fulfilment');
4421
+
4422
+ // The first empty required field (`description`) auto-expands into its
4423
+ // inline editor without any interaction.
4424
+ await waitFor(
4425
+ () =>
4426
+ expect(
4427
+ document.querySelector(
4428
+ '[data-field="description"] input, [data-field="description"] textarea'
4429
+ )
4430
+ ).toBeTruthy(),
4431
+ { timeout: 10000 }
4432
+ );
4433
+
4434
+ // …and that editor receives focus (CompactRow focuses the expanded row's
4435
+ // control). `name` is required but already filled, so it is skipped.
4436
+ await waitFor(
4437
+ () => {
4438
+ const active = document.activeElement as HTMLElement | null;
4439
+ const field = document.querySelector('[data-field="description"]');
4440
+ expect(!!active && !!field && field.contains(active)).toBe(true);
4441
+ },
4442
+ { timeout: 10000 }
4443
+ );
4444
+
4445
+ // The already-satisfied required field is NOT expanded/focused.
4446
+ const nameField = document.querySelector('[data-field="name"]');
4447
+ expect(nameField?.contains(document.activeElement)).toBeFalsy();
4448
+ },
4449
+ };
4450
+
4451
+ // A required field can be FILLED yet INVALID — here `endpoint` has a value that
4452
+ // fails its own `validation_regex` (it must be an http(s) URL). It still "needs
4453
+ // attention", so autofocus must land on IT and not skip ahead to the empty
4454
+ // `description` (which the old empty-only selector would have chosen). This is
4455
+ // the regression guard for the "filled-but-invalid" drift.
4456
+ const CompactInvalidFilledSchema: Record<string, TCompactField> = {
4457
+ endpoint: {
4458
+ type: 'string',
4459
+ ui_type: 'string',
4460
+ display_name: 'Endpoint URL',
4461
+ required: true,
4462
+ group: 'info',
4463
+ // Real format constraint: the value must be an http(s) URL. `validation_regex`
4464
+ // is read by the engine's own validation (helpers/validations.ts); it's not on
4465
+ // the base schema type, so cast.
4466
+ validation_regex: '^https?://',
4467
+ } as TCompactField,
4468
+ description: {
4469
+ type: 'string',
4470
+ ui_type: 'string',
4471
+ display_name: 'Description',
4472
+ required: true,
4473
+ group: 'general',
4474
+ },
4475
+ };
4476
+
4477
+ const CompactInvalidFilledValue: IOptions = {
4478
+ endpoint: { type: 'string', value: 'bad-endpoint' }, // filled, but not an http(s) URL → fails validation_regex
4479
+ // description left empty
4480
+ };
4481
+
4482
+ export const CompactAutoFocusTargetsInvalidFilledField: Story = {
4483
+ args: {
4484
+ compact: true,
4485
+ minColumnWidth: '300px',
4486
+ options: CompactInvalidFilledSchema,
4487
+ value: CompactInvalidFilledValue,
4488
+ groups: CompactGroups,
4489
+ autoFocusFirstRequired: true,
4490
+ },
4491
+ play: async () => {
4492
+ await _testsWaitForText('Endpoint URL');
4493
+
4494
+ // Sanity: the value genuinely fails the field's own validation (it isn't an
4495
+ // http(s) URL), using the very same validator the engine runs — so this is a
4496
+ // real filled-but-INVALID field, not a synthetically-flagged one.
4497
+ expect(validateField('string', 'bad-endpoint', { validation_regex: '^https?://' })).toBe(false);
4498
+
4499
+ // `endpoint` has a value but is invalid, so it needs attention and must be
4500
+ // the target — even though `description` is the empty required field the old
4501
+ // empty-only logic would have picked.
4502
+ await waitFor(
4503
+ () =>
4504
+ expect(
4505
+ document.querySelector('[data-field="endpoint"] input, [data-field="endpoint"] textarea')
4506
+ ).toBeTruthy(),
4507
+ { timeout: 10000 }
4508
+ );
4509
+
4510
+ await waitFor(
4511
+ () => {
4512
+ const active = document.activeElement as HTMLElement | null;
4513
+ const field = document.querySelector('[data-field="endpoint"]');
4514
+ expect(!!active && !!field && field.contains(active)).toBe(true);
4515
+ },
4516
+ { timeout: 10000 }
4517
+ );
4518
+
4519
+ // The empty `description` is NOT the one expanded/focused.
4520
+ const descField = document.querySelector('[data-field="description"]');
4521
+ expect(descField?.contains(document.activeElement)).toBeFalsy();
4522
+ },
4523
+ };
4524
+
4525
+ // DEMO / manual-test story: the first "needs attention" field is a BOOLEAN,
4526
+ // whose compact editor is a `<div tabindex=0>` (ReqoreCheckbox) — NOT an
4527
+ // input/textarea/contenteditable that CompactRow's focus selector matches. Both
4528
+ // `enabled` and `name` are required and unset, so both need attention; `enabled`
4529
+ // is first. Open this in Storybook to see the UX: autofocus TARGETS the boolean
4530
+ // (its row expands) and does NOT skip ahead to the focusable `name` field — but
4531
+ // no element inside actually receives keyboard focus, so the caret is left
4532
+ // nowhere. (This is the non-text-editor focus gap; kept as a playground rather
4533
+ // than a hard assertion until we decide how CompactRow should focus such rows.)
4534
+ const CompactNonFocusableFirstSchema: Record<string, TCompactField> = {
4535
+ enabled: {
4536
+ type: 'bool',
4537
+ ui_type: 'bool',
4538
+ display_name: 'Enabled',
4539
+ short_desc: 'A boolean — the first field that needs attention',
4540
+ required: true,
4541
+ preselected: true,
4542
+ group: 'info',
4543
+ },
4544
+ name: {
4545
+ type: 'string',
4546
+ ui_type: 'string',
4547
+ display_name: 'Name',
4548
+ short_desc: 'A focusable text field — comes second',
4549
+ required: true,
4550
+ preselected: true,
4551
+ group: 'general',
4552
+ },
4553
+ };
4554
+
4555
+ const CompactNonFocusableFirstValue: IOptions = {}; // both unset → both need attention
4556
+
4557
+ export const CompactAutoFocusNonFocusableFirstField: Story = {
4558
+ args: {
4559
+ compact: true,
4560
+ minColumnWidth: '300px',
4561
+ options: CompactNonFocusableFirstSchema,
4562
+ value: CompactNonFocusableFirstValue,
4563
+ groups: CompactGroups,
4564
+ autoFocusFirstRequired: true,
4565
+ },
4566
+ play: async () => {
4567
+ // Smoke: both required-but-unset fields render (both land in "needs
4568
+ // attention"), with the boolean first. Focus behaviour is intentionally left
4569
+ // for manual observation — see the note above.
4570
+ await _testsWaitForText('Enabled');
4571
+ await _testsWaitForText('Name');
4572
+ },
4573
+ };
@@ -94,6 +94,7 @@ import {
94
94
  import { OptionFieldMessages } from './OptionFieldMessages';
95
95
  import { OptionsHelpDialog } from './OptionsHelpDialog';
96
96
  import {
97
+ getFirstAttentionOptionName,
97
98
  getOptionGroup,
98
99
  getOptionGroupLabel,
99
100
  getReadFirstBucket,
@@ -564,6 +565,22 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
564
565
  * plumbing for the inherit_props scope-forwarding contract.
565
566
  */
566
567
  inheritedFromParent?: Record<string, unknown>;
568
+
569
+ /**
570
+ * Opt-in: on mount, drop the user straight into the first field they must
571
+ * fill — the first empty, focusable field (in schema/sort order) that is
572
+ * `required` or a member of a still-unsatisfied one-of `required_groups`
573
+ * group. Disabled, readonly, read-only-form, and dependency-locked fields are
574
+ * skipped. In compact (read-first) mode the target row is expanded through
575
+ * the engine's own `expandedOptions` state, so its editor gains focus with no
576
+ * DOM scraping or synthetic clicks. Strictly one-shot: it fires the first
577
+ * time the form has focusable content (so an async-loaded schema is covered)
578
+ * and then never again — a later value edit, server-driven field update, or
579
+ * in-place schema reload will not re-focus; only a remount re-arms it. It also
580
+ * never steals focus from a control the user has already moved into. No-op in
581
+ * classic (non-compact) mode. Default: off.
582
+ */
583
+ autoFocusFirstRequired?: boolean;
567
584
  }
568
585
 
569
586
  // Option types rendered full-width (IDE Options parity, commit 8e6b7781).
@@ -602,6 +619,7 @@ export const FormEngine = ({
602
619
  optionActions,
603
620
  componentOverrides,
604
621
  inheritedFromParent,
622
+ autoFocusFirstRequired,
605
623
  ...rest
606
624
  }: IFormEngineProps) => {
607
625
  const [options, setOptions] = useState<IQorusFormSchema | undefined>(rest?.options || undefined);
@@ -1517,6 +1535,94 @@ export const FormEngine = ({
1517
1535
  [expandMode]
1518
1536
  );
1519
1537
 
1538
+ // --- First-attention-field autofocus (opt-in) -----------------------------
1539
+ // With `autoFocusFirstRequired`, drop the user straight into the first field
1540
+ // they must fix. We reuse the engine's own ordering (`availableOptions`), the
1541
+ // very same `getOptionBucket` the status boxes use to decide "needs attention"
1542
+ // (so we cover empty-required, unsatisfied one-of, AND filled-but-invalid rows
1543
+ // without ever drifting from what the user sees), and dependency gating
1544
+ // (`dependencyLockedNames`), then expand the target row through
1545
+ // `expandedOptions` — the row's editor-focus effect does the rest. No DOM
1546
+ // scraping, no synthetic clicks, no polling.
1547
+ //
1548
+ // Strictly one-shot: it fires the first time the form has focusable content
1549
+ // (so an async-loaded schema is still covered — the empty first pass doesn't
1550
+ // count), then never again for the life of this instance. A server-driven
1551
+ // field update or an in-place schema reload will NOT re-grab focus; only a
1552
+ // genuine remount (a fresh instance) auto-focuses again, which is the intended
1553
+ // on-mount behaviour.
1554
+ const hasAutoFocusedRef = useRef(false);
1555
+ // The field expanded programmatically for autofocus. A ref (not state) so
1556
+ // CompactRow's 60ms focus timer reads the current value regardless of render
1557
+ // batching; CompactRow focuses this one with `preventScroll` so an off-screen
1558
+ // (or below-the-fold) form is never scrolled into view on mount.
1559
+ const autoFocusNameRef = useRef<string | undefined>(undefined);
1560
+ useEffect(() => {
1561
+ if (!autoFocusFirstRequired || !compact || !options) {
1562
+ return;
1563
+ }
1564
+ if (hasAutoFocusedRef.current) {
1565
+ return;
1566
+ }
1567
+
1568
+ const orderedNames = Object.keys(availableOptions);
1569
+ if (!orderedNames.length) {
1570
+ return;
1571
+ }
1572
+
1573
+ // Never grab focus from a control the user is already in — including a field
1574
+ // *inside* this form. If a slow/async schema lets the user start typing
1575
+ // before this first-required scan runs, we must not steal their caret. On a
1576
+ // clean mount focus rests on the body, so the intended "drop into the first
1577
+ // field" still fires. Bailing here leaves `hasAutoFocusedRef` false, so it
1578
+ // retries once focus is free.
1579
+ const active = document.activeElement as HTMLElement | null;
1580
+ if (active && active !== document.body) {
1581
+ return;
1582
+ }
1583
+
1584
+ const target = getFirstAttentionOptionName(orderedNames, (fieldName) => {
1585
+ const schema = options[fieldName];
1586
+ if (!schema) {
1587
+ return undefined;
1588
+ }
1589
+ return {
1590
+ focusable:
1591
+ !readOnly &&
1592
+ !schema.disabled &&
1593
+ !(schema as { readonly?: boolean }).readonly &&
1594
+ !dependencyLockedNames.includes(fieldName),
1595
+ // Single source of truth: the same bucket the read-first status boxes
1596
+ // show, so a filled-but-invalid required row is a target too.
1597
+ needsAttention: getOptionBucket(fieldName) === 'attention',
1598
+ };
1599
+ });
1600
+
1601
+ // Mark done even when nothing needs attention, so later value edits or an
1602
+ // in-place schema reload never re-scan or re-focus. Only a remount arms it
1603
+ // again.
1604
+ hasAutoFocusedRef.current = true;
1605
+
1606
+ if (target) {
1607
+ // Set the ref before the state update so CompactRow's focus timer sees it.
1608
+ autoFocusNameRef.current = target;
1609
+ setExpandedOptions((prev) =>
1610
+ prev.includes(target) ? prev
1611
+ : expandMode === 'multi' ? [...prev, target]
1612
+ : [target]
1613
+ );
1614
+ }
1615
+ }, [
1616
+ autoFocusFirstRequired,
1617
+ compact,
1618
+ options,
1619
+ availableOptions,
1620
+ readOnly,
1621
+ dependencyLockedNames,
1622
+ getOptionBucket,
1623
+ expandMode,
1624
+ ]);
1625
+
1520
1626
  // Read-first completion summary (how many shown options have a value set),
1521
1627
  // surfaced as a progress meter at the top of the compact form.
1522
1628
  const readFirstCompletion = useMemo(
@@ -1871,6 +1977,7 @@ export const FormEngine = ({
1871
1977
  showFieldTypes,
1872
1978
  showAllDescriptions,
1873
1979
  expandedOptions,
1980
+ autoFocusNameRef,
1874
1981
  highlightedOptions,
1875
1982
  flashedOptions,
1876
1983
  infoPanelOverrides,
@@ -1915,6 +2022,7 @@ export const FormEngine = ({
1915
2022
  showFieldTypes,
1916
2023
  showAllDescriptions,
1917
2024
  expandedOptions,
2025
+ autoFocusNameRef,
1918
2026
  highlightedOptions,
1919
2027
  flashedOptions,
1920
2028
  infoPanelOverrides,
@@ -34,6 +34,12 @@ export interface ICompactRowContext {
34
34
  // display name in the read-first chip.
35
35
  templates?: IReqoreFormTemplates;
36
36
  expandedOptions: string[];
37
+ // Name of the field the engine expanded programmatically for
38
+ // `autoFocusFirstRequired`. CompactRow focuses this one with `preventScroll`
39
+ // so an off-screen / below-the-fold form is never scrolled into view on
40
+ // mount. A ref (not state) so it's current when the 60ms focus timer fires,
41
+ // regardless of render batching.
42
+ autoFocusNameRef?: MutableRefObject<string | undefined>;
37
43
  highlightedOptions: string[];
38
44
  flashedOptions: string[];
39
45
  infoPanelOverrides: Record<string, boolean>;
@@ -335,6 +335,27 @@ export const StyledRowInset = styled.div`
335
335
  margin-top: 4px;
336
336
  `;
337
337
 
338
+ // A collapsed code-block preview shown under the value summary for a
339
+ // `code-editor` field. Multi-line, monospace, subtle background — matches
340
+ // the aesthetic of the classic code-view surface but small enough to sit
341
+ // inside a read-row. Height is capped by the wrapping `ReqoreCollapsibleContent`
342
+ // so the "Show more" affordance stays useful.
343
+ export const StyledCodePreview = styled.pre<{ $bg: string; $border: string; $fg: string }>`
344
+ margin: 0;
345
+ padding: 8px 10px;
346
+ border-radius: 4px;
347
+ background: ${({ $bg }) => $bg};
348
+ border: 1px solid ${({ $border }) => $border};
349
+ color: ${({ $fg }) => $fg};
350
+ font-family:
351
+ ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
352
+ font-size: 11.5px;
353
+ line-height: 1.5;
354
+ white-space: pre;
355
+ overflow: auto;
356
+ max-width: 100%;
357
+ `;
358
+
338
359
  // A small inline colour swatch shown before an rgbcolor value's hex string.
339
360
  export const StyledColorSwatch = styled.span<{ $color: string; $border: string }>`
340
361
  width: 12px;
@@ -502,3 +502,45 @@ export const getReadFirstCompletion = (
502
502
 
503
503
  return { total, set, pct };
504
504
  };
505
+
506
+ /** What the "first field to fix" selector needs to know about one field.
507
+ * Kept deliberately minimal — both verdicts are supplied by the caller — so the
508
+ * selector is decoupled from the engine's value/schema shapes and unit-testable
509
+ * in isolation. */
510
+ export interface IFirstAttentionFieldMeta {
511
+ /** Whether the field can actually receive focus — the caller folds in
512
+ * disabled / readonly / read-only-form / unmet-dependency gating here. */
513
+ focusable: boolean;
514
+ /** Whether the form considers this field to "need attention". The caller
515
+ * supplies this from the SAME `getOptionBucket` the read-first status boxes
516
+ * use, so it covers every attention case — empty-required, an unsatisfied
517
+ * one-of group, AND a filled-but-invalid value — and the autofocus target can
518
+ * never drift from the visible needs-attention set. */
519
+ needsAttention: boolean;
520
+ }
521
+
522
+ /**
523
+ * Pick the first field the user must fix: the first focusable field — in the
524
+ * supplied (already ordered) name list — that the form buckets as "needs
525
+ * attention".
526
+ *
527
+ * Pure and side-effect free. It deliberately does NOT re-derive emptiness /
528
+ * required / validity itself; the caller passes `needsAttention` straight from
529
+ * the engine's own `getOptionBucket`, so there is a single source of truth and
530
+ * the "focus the first field to fix" affordance can never disagree with the
531
+ * per-row status the user sees (including filled-but-invalid rows). Returns
532
+ * `undefined` when nothing needs attention.
533
+ */
534
+ export const getFirstAttentionOptionName = (
535
+ orderedNames: string[],
536
+ getFieldMeta: (name: string) => IFirstAttentionFieldMeta | undefined
537
+ ): string | undefined => {
538
+ for (const name of orderedNames) {
539
+ const meta = getFieldMeta(name);
540
+ if (meta?.focusable && meta.needsAttention) {
541
+ return name;
542
+ }
543
+ }
544
+
545
+ return undefined;
546
+ };
@@ -0,0 +1,90 @@
1
+ import { ReqoreControlGroup } from '@qoretechnologies/reqore';
2
+ import { StoryObj } from '@storybook/react-vite';
3
+ import { useState } from 'react';
4
+ import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
5
+
6
+ import { StoryMeta } from '../../../../types';
7
+ import { ReqraftBinaryFormField } from './Binary';
8
+
9
+ const meta = {
10
+ component: ReqraftBinaryFormField,
11
+ title: 'Components/Form/Binary',
12
+ args: {
13
+ onChange: fn(),
14
+ 'aria-label': 'Binary',
15
+ },
16
+ render(args) {
17
+ const [value, setValue] = useState(args.value);
18
+
19
+ return (
20
+ <ReqoreControlGroup>
21
+ <ReqraftBinaryFormField
22
+ {...args}
23
+ value={value}
24
+ onChange={(value) => {
25
+ args.onChange?.(value);
26
+ setValue(value);
27
+ }}
28
+ />
29
+ </ReqoreControlGroup>
30
+ );
31
+ },
32
+ } as StoryMeta<typeof ReqraftBinaryFormField>;
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof meta>;
36
+
37
+ export const WithValue: Story = {
38
+ args: {
39
+ value: 'cGFzc3dvcmQ=',
40
+ },
41
+ async play({ canvasElement }) {
42
+ const canvas = within(canvasElement);
43
+ const textarea = canvas.getByLabelText('Binary');
44
+
45
+ await expect(textarea).toBeInTheDocument();
46
+ await expect(textarea).toHaveValue('cGFzc3dvcmQ=');
47
+ },
48
+ };
49
+
50
+ export const TypingUpdatesValue: Story = {
51
+ async play({ canvasElement, args }) {
52
+ const canvas = within(canvasElement);
53
+ const textarea = canvas.getByLabelText('Binary');
54
+
55
+ await userEvent.type(textarea, 'YWJj');
56
+ await expect(textarea).toHaveValue('YWJj');
57
+ await waitFor(() => expect(args.onChange).toHaveBeenLastCalledWith('YWJj'), {
58
+ timeout: 500,
59
+ });
60
+ },
61
+ };
62
+
63
+ export const UploadEncodesFileAsBase64: Story = {
64
+ async play({ canvasElement, args }) {
65
+ const file = new File(['hello'], 'test.bin', { type: 'application/octet-stream' });
66
+ // the hidden file input rendered by the reused ReqraftFileFormField drop zone
67
+ const input = canvasElement.querySelector('input[type="file"]') as HTMLInputElement;
68
+
69
+ await expect(input).toBeInTheDocument();
70
+ await userEvent.upload(input, file);
71
+
72
+ // base64 of "hello" is "aGVsbG8="; the data: URL prefix must be stripped
73
+ await waitFor(() => expect(args.onChange).toHaveBeenLastCalledWith('aGVsbG8='), {
74
+ timeout: 1000,
75
+ });
76
+ },
77
+ };
78
+
79
+ export const Disabled: Story = {
80
+ args: {
81
+ value: 'cGFzc3dvcmQ=',
82
+ disabled: true,
83
+ },
84
+ async play({ canvasElement }) {
85
+ const canvas = within(canvasElement);
86
+ const textarea = canvas.getByLabelText('Binary');
87
+
88
+ await expect(textarea).toBeDisabled();
89
+ },
90
+ };
@@ -0,0 +1,81 @@
1
+ import { ReqoreControlGroup, ReqoreTextarea } from '@qoretechnologies/reqore';
2
+ import { IReqoreTextareaProps } from '@qoretechnologies/reqore/dist/components/Textarea';
3
+ import { ChangeEvent, memo, useCallback, useEffect, useState } from 'react';
4
+ import { useDebounce } from 'react-use';
5
+ import { IFileFormFieldValue, ReqraftFileFormField } from '../file/File';
6
+
7
+ export interface IReqraftBinaryFormFieldProps extends Omit<IReqoreTextareaProps, 'onChange' | 'value'> {
8
+ value?: string;
9
+ onChange?: (value: string) => void;
10
+ }
11
+
12
+ /**
13
+ * Strip a `data:<mime-type>;base64,` prefix so the emitted value carries raw base64, matching the
14
+ * canonical binary wire form the Qorus server expects. `ReqraftFileFormField` reads uploads with
15
+ * `readAsDataURL`, so its `content` always carries that prefix.
16
+ */
17
+ const stripDataUrlPrefix = (dataUrl: string): string => {
18
+ const commaIndex = dataUrl.indexOf(',');
19
+ return commaIndex === -1 ? dataUrl : dataUrl.slice(commaIndex + 1);
20
+ };
21
+
22
+ /**
23
+ * A field for binary values encoded as base64. The value can be typed or pasted directly into the
24
+ * textarea, or populated by uploading a file — the upload reuses {@link ReqraftFileFormField} (the
25
+ * shared Reqraft file picker / drop zone) rather than a bespoke dropzone; its data-URL content is
26
+ * decoded to raw base64 for the field value.
27
+ */
28
+ export const ReqraftBinaryFormField = memo(
29
+ ({ value, onChange, ...rest }: IReqraftBinaryFormFieldProps) => {
30
+ const [localValue, setLocalValue] = useState<string>(value ?? '');
31
+
32
+ useEffect(() => {
33
+ if (value !== localValue) {
34
+ setLocalValue(value ?? '');
35
+ }
36
+ }, [value]);
37
+
38
+ useDebounce(
39
+ () => {
40
+ if (localValue !== value) {
41
+ onChange?.(localValue);
42
+ }
43
+ },
44
+ 100,
45
+ [localValue, onChange]
46
+ );
47
+
48
+ const handleChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>): void => {
49
+ setLocalValue(event.target.value);
50
+ }, []);
51
+
52
+ const handleFileChange = useCallback(
53
+ (file?: IFileFormFieldValue) => {
54
+ const base64 = stripDataUrlPrefix(file?.content ?? '');
55
+ setLocalValue(base64);
56
+ onChange?.(base64);
57
+ },
58
+ [onChange]
59
+ );
60
+
61
+ return (
62
+ <ReqoreControlGroup vertical fluid>
63
+ <ReqoreTextarea
64
+ scaleWithContent
65
+ fluid
66
+ value={localValue}
67
+ onChange={handleChange}
68
+ onClearClick={() => {
69
+ setLocalValue('');
70
+ onChange?.('');
71
+ }}
72
+ placeholder='Base64-encoded binary value'
73
+ {...rest}
74
+ />
75
+ <ReqraftFileFormField disabled={rest.disabled} onChange={handleFileChange} />
76
+ </ReqoreControlGroup>
77
+ );
78
+ }
79
+ );
80
+
81
+ export default ReqraftBinaryFormField;
package/src/index.tsx CHANGED
@@ -55,5 +55,3 @@ export type {
55
55
  export * from './components/smartEditor';
56
56
  export * from './components/dpqlEditor';
57
57
  export * from './components/qonsoleSmartInput';
58
- export * from './components/supportTicket';
59
- export * from './components/ticketThread';
@@ -1,7 +0,0 @@
1
- export interface IAttachmentChipsProps {
2
- files: File[];
3
- onRemove: (index: number) => void;
4
- disabled?: boolean;
5
- }
6
- export declare const AttachmentChips: ({ files, onRemove, disabled }: IAttachmentChipsProps) => import("react/jsx-runtime").JSX.Element;
7
- //# sourceMappingURL=AttachmentChips.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AttachmentChips.d.ts","sourceRoot":"","sources":["../../../src/components/supportTicket/AttachmentChips.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,eAAe,kCAAmC,qBAAqB,4CAgCnF,CAAC"}