@vectoriox/iox-builder 4.12.1 → 4.14.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.
@@ -180,6 +180,12 @@ var TraitInputType;
180
180
  /** Ordered list of images (URL / one-by-one / filebox; per-item binding in a later phase).
181
181
  * Controller registered in the consumer (iox-cms-client) as `mediaList`. Value: GalleryImage[]. */
182
182
  TraitInputType["MediaList"] = "mediaList";
183
+ /** Additive validation rules for a Field (`ValidationRule[]` from forms-core). Controller
184
+ * registered in the consumer (iox-cms-client) as `ruleList`. */
185
+ TraitInputType["RuleList"] = "ruleList";
186
+ /** The ordered "After submit" action list on a Form (`SubmitAction[]`). Controller registered
187
+ * in the consumer as `actionList`. */
188
+ TraitInputType["ActionList"] = "actionList";
183
189
  TraitInputType["FontFamily"] = "fontFamily";
184
190
  TraitInputType["GridTemplate"] = "gridTemplate";
185
191
  TraitInputType["SectionLabel"] = "sectionLabel";
@@ -701,15 +707,42 @@ const freshId = () => Math.random().toString(36).substr(2, 9);
701
707
  * A reference pointing OUTSIDE the cloned subtree (id not in the map) is left untouched — the copy
702
708
  * intentionally still points at the external node.
703
709
  *
710
+ * TRAIT values are NOT ids and are copied verbatim — with one exception. A trait that carries an
711
+ * IDENTITY rather than a setting must be reissued, or the copy silently shares the original's
712
+ * identity. `formId` is the first: two forms with the same id share one submissions stream, and the
713
+ * server resolves a submission against whichever page it reaches first. See REGENERATED_TRAITS.
714
+ *
704
715
  * `styleId` strategy:
705
716
  * - `'clear'` (default) — drop styleId so the clone OWNS its own CSS rule (independent copy).
706
717
  * - `'regenerate'` — keep a styleId but give it a fresh value (legacy builder.component path).
707
718
  */
719
+ /**
720
+ * Trait names whose value is an IDENTITY, not a setting, and must therefore be reissued on every
721
+ * clone. Add a name here the moment a trait starts identifying something rather than describing it.
722
+ *
723
+ * `formId` — a form's submissions stream. Duplicating a page, pasting a form or dropping a saved
724
+ * block twice used to produce two forms claiming the same stream.
725
+ */
726
+ const REGENERATED_TRAITS = new Set(['formId']);
727
+ /** Reissue identity-carrying traits on a node, preserving the original's prefix where it had one. */
728
+ function reissueIdentityTraits(node) {
729
+ for (const trait of (node.traits ?? [])) {
730
+ if (!trait?.name || !REGENERATED_TRAITS.has(trait.name))
731
+ continue;
732
+ if (trait.default === undefined || trait.default === '')
733
+ continue;
734
+ // Keep the human-readable prefix a preset gave it (`contact-a1b2c3` → `contact-<new>`), so a
735
+ // duplicated contact form is still recognisably a contact form in the inbox.
736
+ const prefix = String(trait.default).split('-')[0];
737
+ trait.default = `${prefix}-${freshId()}`;
738
+ }
739
+ }
708
740
  function regenerateNodeIds(root, opts = {}) {
709
741
  const idMap = new Map();
710
742
  const assign = (node) => {
711
743
  const oldId = node.id;
712
744
  node.id = freshId();
745
+ reissueIdentityTraits(node);
713
746
  if (opts.styleId === 'regenerate') {
714
747
  if (node.styleId)
715
748
  node.styleId = freshId();
@@ -9663,6 +9696,10 @@ class FormComponentConfig extends ComponentConfig {
9663
9696
  // On by default and invisible to the visitor (decision D12).
9664
9697
  new TraitConfig('honeypot', 'Honeypot field', TraitInputType.Checkbox, undefined, true),
9665
9698
  new TraitConfig('minFillMs', 'Minimum fill time (ms)', TraitInputType.Number, undefined, 1200),
9699
+ new TraitConfig('__afterSubmitLabel', 'After submit', TraitInputType.SectionLabel),
9700
+ // The ordered destination list. `store` is always first and cannot be removed — it is
9701
+ // the safety net the whole store-first design rests on.
9702
+ new TraitConfig('actions', 'Destinations', TraitInputType.ActionList, undefined, [{ type: 'store' }]),
9666
9703
  new TraitConfig('__dataLabel', 'Data', TraitInputType.SectionLabel),
9667
9704
  // Absent means keep forever — the confirmed default. A number opts into purging.
9668
9705
  new TraitConfig('retentionDays', 'Delete submissions after (days)', TraitInputType.Number, undefined, ''),
@@ -9710,13 +9747,16 @@ class FieldComponentConfig extends ComponentConfig {
9710
9747
  { label: 'Radio group', value: 'radio' },
9711
9748
  { label: 'Date', value: 'date' },
9712
9749
  { label: 'Time', value: 'time' },
9713
- { label: 'File upload', value: 'file' },
9750
+ // ⚠️ 'file' is DELIBERATELY ABSENT until uploads are implemented. A file input's
9751
+ // `value` is the browser's fake path (`C:\fakepath\cv.pdf`), and `fileType`/
9752
+ // `fileSize` operate on a file list — so on a string they pass vacuously. Offering
9753
+ // it meant an author could set "PDF only, 5MB" and silently get neither the
9754
+ // enforcement nor the file. See forms-and-inputs-plan.md → Remaining work P0/1.
9714
9755
  { label: 'Hidden', value: 'hidden' },
9715
9756
  ], 'text'),
9716
9757
  new TraitConfig('placeholder', 'Placeholder', TraitInputType.Text, undefined, '', false, { trait: 'control', values: ['text', 'email', 'tel', 'url', 'number', 'password', 'textarea', 'select'] }),
9717
9758
  new TraitConfig('help', 'Help text', TraitInputType.Text, undefined, ''),
9718
9759
  new TraitConfig('rows', 'Rows', TraitInputType.Number, undefined, 4, false, { trait: 'control', values: 'textarea' }),
9719
- new TraitConfig('accept', 'Accepted file types', TraitInputType.Text, undefined, '', false, { trait: 'control', values: 'file' }),
9720
9760
  new TraitConfig('labelHidden', 'Hide label (screen readers still read it)', TraitInputType.Checkbox, undefined, false),
9721
9761
  // The submitted key. Derived from the label; editable for external integrations.
9722
9762
  new TraitConfig('name', 'Submitted key', TraitInputType.Text, undefined, ''),
@@ -9727,9 +9767,9 @@ class FieldComponentConfig extends ComponentConfig {
9727
9767
  new TraitConfig('optionsText', 'Choices (one per line)', TraitInputType.Textarea, undefined, '', false, { trait: 'control', values: ['select', 'radio'] }),
9728
9768
  new TraitConfig('__validationLabel', 'Validation', TraitInputType.SectionLabel),
9729
9769
  new TraitConfig('required', 'Required', TraitInputType.Checkbox, undefined, false),
9730
- // `rules` is the additive list (minLength, pattern, matches, …). Its editor is a panel
9731
- // concern; the node just carries the array.
9732
- new TraitConfig('rules', 'Rules', TraitInputType.Textarea, undefined, ''),
9770
+ // The additive rule list (minLength, pattern, matches, …). Unused rules are invisible
9771
+ // rather than a wall of empty controls — the Effects-group pattern.
9772
+ new TraitConfig('rules', 'Rules', TraitInputType.RuleList, undefined, []),
9733
9773
  ];
9734
9774
  this.styleTraits = fullStyles();
9735
9775
  this.applyStyleDefaults({