@vectoriox/iox-builder 4.12.0 → 4.13.0

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.
@@ -701,15 +701,42 @@ const freshId = () => Math.random().toString(36).substr(2, 9);
701
701
  * A reference pointing OUTSIDE the cloned subtree (id not in the map) is left untouched — the copy
702
702
  * intentionally still points at the external node.
703
703
  *
704
+ * TRAIT values are NOT ids and are copied verbatim — with one exception. A trait that carries an
705
+ * IDENTITY rather than a setting must be reissued, or the copy silently shares the original's
706
+ * identity. `formId` is the first: two forms with the same id share one submissions stream, and the
707
+ * server resolves a submission against whichever page it reaches first. See REGENERATED_TRAITS.
708
+ *
704
709
  * `styleId` strategy:
705
710
  * - `'clear'` (default) — drop styleId so the clone OWNS its own CSS rule (independent copy).
706
711
  * - `'regenerate'` — keep a styleId but give it a fresh value (legacy builder.component path).
707
712
  */
713
+ /**
714
+ * Trait names whose value is an IDENTITY, not a setting, and must therefore be reissued on every
715
+ * clone. Add a name here the moment a trait starts identifying something rather than describing it.
716
+ *
717
+ * `formId` — a form's submissions stream. Duplicating a page, pasting a form or dropping a saved
718
+ * block twice used to produce two forms claiming the same stream.
719
+ */
720
+ const REGENERATED_TRAITS = new Set(['formId']);
721
+ /** Reissue identity-carrying traits on a node, preserving the original's prefix where it had one. */
722
+ function reissueIdentityTraits(node) {
723
+ for (const trait of (node.traits ?? [])) {
724
+ if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
725
+ continue;
726
+ if (trait.default === undefined || trait.default === '')
727
+ continue;
728
+ // Keep the human-readable prefix a preset gave it (`contact-a1b2c3` → `contact-<new>`), so a
729
+ // duplicated contact form is still recognisably a contact form in the inbox.
730
+ const prefix = String(trait.default).split('-')[0];
731
+ trait.default = `${prefix}-${freshId()}`;
732
+ }
733
+ }
708
734
  function regenerateNodeIds(root, opts = {}) {
709
735
  const idMap = new Map();
710
736
  const assign = (node) => {
711
737
  const oldId = node.id;
712
738
  node.id = freshId();
739
+ reissueIdentityTraits(node);
713
740
  if (opts.styleId === 'regenerate') {
714
741
  if (node.styleId)
715
742
  node.styleId = freshId();
@@ -5907,9 +5934,14 @@ class BuilderComponent {
5907
5934
  return;
5908
5935
  // Straight onto the live instance: this is a VIEW state, not page data — it must never be
5909
5936
  // persisted, or a page would ship showing its own success message.
5910
- const instance = this.overlayService.getNodeRef(form)?.componentRef?.instance;
5911
- if (instance) {
5912
- safeSetInput(this.overlayService.getNodeRef(form).componentRef, 'renderState', state);
5937
+ //
5938
+ // Two input names on purpose: the CANVAS renders the container twin (`formState`) while the
5939
+ // ENGINE renders IoxFormComponent (`renderState`). safeSetInput drops whichever the rendered
5940
+ // component does not declare, so this works on either path without branching on type.
5941
+ const ref = this.overlayService.getNodeRef(form)?.componentRef;
5942
+ if (ref) {
5943
+ safeSetInput(ref, 'formState', state);
5944
+ safeSetInput(ref, 'renderState', state);
5913
5945
  }
5914
5946
  this.cdr.detectChanges();
5915
5947
  }
@@ -9705,13 +9737,16 @@ class FieldComponentConfig extends ComponentConfig {
9705
9737
  { label: 'Radio group', value: 'radio' },
9706
9738
  { label: 'Date', value: 'date' },
9707
9739
  { label: 'Time', value: 'time' },
9708
- { label: 'File upload', value: 'file' },
9740
+ // ⚠️ 'file' is DELIBERATELY ABSENT until uploads are implemented. A file input's
9741
+ // `value` is the browser's fake path (`C:\fakepath\cv.pdf`), and `fileType`/
9742
+ // `fileSize` operate on a file list — so on a string they pass vacuously. Offering
9743
+ // it meant an author could set "PDF only, 5MB" and silently get neither the
9744
+ // enforcement nor the file. See forms-and-inputs-plan.md → Remaining work P0/1.
9709
9745
  { label: 'Hidden', value: 'hidden' },
9710
9746
  ], 'text'),
9711
9747
  new TraitConfig('placeholder', 'Placeholder', TraitInputType.Text, undefined, '', false, { trait: 'control', values: ['text', 'email', 'tel', 'url', 'number', 'password', 'textarea', 'select'] }),
9712
9748
  new TraitConfig('help', 'Help text', TraitInputType.Text, undefined, ''),
9713
9749
  new TraitConfig('rows', 'Rows', TraitInputType.Number, undefined, 4, false, { trait: 'control', values: 'textarea' }),
9714
- new TraitConfig('accept', 'Accepted file types', TraitInputType.Text, undefined, '', false, { trait: 'control', values: 'file' }),
9715
9750
  new TraitConfig('labelHidden', 'Hide label (screen readers still read it)', TraitInputType.Checkbox, undefined, false),
9716
9751
  // The submitted key. Derived from the label; editable for external integrations.
9717
9752
  new TraitConfig('name', 'Submitted key', TraitInputType.Text, undefined, ''),