@servicetitan/dte-unlayer 0.153.0 → 0.155.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.
Files changed (85) hide show
  1. package/dist/display-conditions/ConditionRow.d.ts.map +1 -1
  2. package/dist/display-conditions/ConditionRow.js +111 -95
  3. package/dist/display-conditions/ConditionRow.js.map +1 -1
  4. package/dist/display-conditions/DisplayConditionModal.d.ts +3 -3
  5. package/dist/display-conditions/DisplayConditionModal.d.ts.map +1 -1
  6. package/dist/display-conditions/DisplayConditionModal.js +8 -95
  7. package/dist/display-conditions/DisplayConditionModal.js.map +1 -1
  8. package/dist/display-conditions/useConditionForms.d.ts +22 -0
  9. package/dist/display-conditions/useConditionForms.d.ts.map +1 -0
  10. package/dist/display-conditions/useConditionForms.js +110 -0
  11. package/dist/display-conditions/useConditionForms.js.map +1 -0
  12. package/dist/e-sign-offset/ESignDateSignedOffsetModal.d.ts +11 -0
  13. package/dist/e-sign-offset/ESignDateSignedOffsetModal.d.ts.map +1 -0
  14. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js +192 -0
  15. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js.map +1 -0
  16. package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts +20 -0
  17. package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts.map +1 -0
  18. package/dist/e-sign-offset/ESignOffsetRuleRow.js +87 -0
  19. package/dist/e-sign-offset/ESignOffsetRuleRow.js.map +1 -0
  20. package/dist/e-sign-offset/eSignOffsetController.d.ts +10 -0
  21. package/dist/e-sign-offset/eSignOffsetController.d.ts.map +1 -0
  22. package/dist/e-sign-offset/eSignOffsetController.js +27 -0
  23. package/dist/e-sign-offset/eSignOffsetController.js.map +1 -0
  24. package/dist/editor-core-source.d.ts +2 -2
  25. package/dist/editor-core-source.d.ts.map +1 -1
  26. package/dist/editor-core-source.js +2 -2
  27. package/dist/editor-core-source.js.map +1 -1
  28. package/dist/editor.d.ts.map +1 -1
  29. package/dist/editor.js +8 -0
  30. package/dist/editor.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/merge-tags.d.ts +6 -10
  36. package/dist/merge-tags.d.ts.map +1 -1
  37. package/dist/merge-tags.js +35 -44
  38. package/dist/merge-tags.js.map +1 -1
  39. package/dist/shared/const.d.ts +30 -0
  40. package/dist/shared/const.d.ts.map +1 -1
  41. package/dist/shared/const.js +5 -0
  42. package/dist/shared/const.js.map +1 -1
  43. package/dist/shared/e-sign-offset.d.ts +21 -0
  44. package/dist/shared/e-sign-offset.d.ts.map +1 -0
  45. package/dist/shared/e-sign-offset.js +130 -0
  46. package/dist/shared/e-sign-offset.js.map +1 -0
  47. package/dist/shared/instance-id.d.ts +13 -0
  48. package/dist/shared/instance-id.d.ts.map +1 -0
  49. package/dist/shared/instance-id.js +22 -0
  50. package/dist/shared/instance-id.js.map +1 -0
  51. package/dist/store.d.ts +15 -1
  52. package/dist/store.d.ts.map +1 -1
  53. package/dist/store.js +80 -5
  54. package/dist/store.js.map +1 -1
  55. package/dist/tools.d.ts +18 -0
  56. package/dist/tools.d.ts.map +1 -1
  57. package/dist/tools.js +68 -1
  58. package/dist/tools.js.map +1 -1
  59. package/dist/unlayer-interface.d.ts +11 -1
  60. package/dist/unlayer-interface.d.ts.map +1 -1
  61. package/dist/unlayer-interface.js.map +1 -1
  62. package/dist/unlayer.d.ts.map +1 -1
  63. package/dist/unlayer.js +5 -3
  64. package/dist/unlayer.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/__tests__/e-sign-offset.test.ts +281 -0
  67. package/src/__tests__/merge-tags.test.ts +98 -76
  68. package/src/__tests__/store.merge-tags.test.ts +186 -0
  69. package/src/display-conditions/ConditionRow.tsx +86 -81
  70. package/src/display-conditions/DisplayConditionModal.tsx +12 -116
  71. package/src/display-conditions/useConditionForms.ts +146 -0
  72. package/src/e-sign-offset/ESignDateSignedOffsetModal.tsx +197 -0
  73. package/src/e-sign-offset/ESignOffsetRuleRow.tsx +101 -0
  74. package/src/e-sign-offset/eSignOffsetController.ts +39 -0
  75. package/src/editor-core-source.ts +2 -2
  76. package/src/editor.tsx +9 -0
  77. package/src/index.ts +1 -2
  78. package/src/merge-tags.ts +41 -60
  79. package/src/shared/const.ts +37 -0
  80. package/src/shared/e-sign-offset.ts +173 -0
  81. package/src/shared/instance-id.ts +23 -0
  82. package/src/store.ts +106 -5
  83. package/src/tools.ts +87 -1
  84. package/src/unlayer-interface.tsx +11 -1
  85. package/src/unlayer.tsx +5 -3
package/src/editor.tsx CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  useState,
9
9
  } from 'react';
10
10
  import { DisplayConditionModal } from './display-conditions/DisplayConditionModal';
11
+ import { ESignDateSignedOffsetModal } from './e-sign-offset/ESignDateSignedOffsetModal';
11
12
  import { getErrorMessage } from './log';
12
13
  import { UnlayerEditorTwin } from './shared/const';
13
14
  import { UnlayerStore, UnlayerDesignChangeInfo } from './store';
@@ -146,6 +147,14 @@ export const UnlayerEditor = forwardRef<UnlayerRef, UnlayerEditorProps>((props,
146
147
  onConditionFormSelect={props.opts.onConditionFormSelect}
147
148
  />
148
149
  )}
150
+ {!!props.opts.eSignRecipients?.length && (
151
+ <ESignDateSignedOffsetModal
152
+ portalContainer={props.opts.displayConditionModalPortalContainer}
153
+ schema={props.opts.schema}
154
+ onConditionalsOpen={props.opts.onConditionalsOpen}
155
+ onConditionFormSelect={props.opts.onConditionFormSelect}
156
+ />
157
+ )}
149
158
  </div>
150
159
  );
151
160
  });
package/src/index.ts CHANGED
@@ -3,13 +3,12 @@ export * from './tools';
3
3
  export * from './api-core';
4
4
  export * from './api-custom-tools';
5
5
  export * from './unlayer-interface';
6
- export { isUnlayerMergeTagGroup, scheduleUnlayerMergeTags, toUnlayerMergeTags } from './merge-tags';
6
+ export { isUnlayerMergeTagGroup, toUnlayerMergeTags } from './merge-tags';
7
7
  export type {
8
8
  ToUnlayerMergeTagsOptions,
9
9
  UnlayerMergeTagGroup,
10
10
  UnlayerMergeTagLeaf,
11
11
  UnlayerMergeTagsConfig,
12
- UnlayerMergeTagsEditor,
13
12
  } from './merge-tags';
14
13
  export * from './loadScript';
15
14
  export * from './shared/const';
package/src/merge-tags.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { logUnlayerError } from './log';
2
1
  import { UnlayerEditorMergeTagInfo } from './unlayer-interface';
3
2
 
4
3
  export interface UnlayerMergeTagLeaf {
@@ -8,7 +7,8 @@ export interface UnlayerMergeTagLeaf {
8
7
 
9
8
  export interface UnlayerMergeTagGroup {
10
9
  name: string;
11
- mergeTags: Record<string, UnlayerMergeTagLeaf>;
10
+ /** Nested submenu: leaves and/or further groups (Unlayer supports arbitrary depth). */
11
+ mergeTags: UnlayerMergeTagsConfig;
12
12
  }
13
13
 
14
14
  export type UnlayerMergeTagsConfig = Record<string, UnlayerMergeTagLeaf | UnlayerMergeTagGroup>;
@@ -21,11 +21,6 @@ export interface ToUnlayerMergeTagsOptions {
21
21
  groups?: boolean;
22
22
  }
23
23
 
24
- export interface UnlayerMergeTagsEditor {
25
- addEventListener(event: string, cb: (arg: any) => void): void;
26
- setMergeTags(tags: UnlayerMergeTagsConfig): void;
27
- }
28
-
29
24
  export function isUnlayerMergeTagGroup(
30
25
  entry: UnlayerMergeTagLeaf | UnlayerMergeTagGroup,
31
26
  ): entry is UnlayerMergeTagGroup {
@@ -42,6 +37,43 @@ const toMergeTagKey = (value: string) => {
42
37
  return key || value;
43
38
  };
44
39
 
40
+ /**
41
+ * Normalizes the host `group` into the submenu path the tag belongs to:
42
+ * `'Custom Fields'` → `['Custom Fields']`, `['Forms', 'Intake']` → `['Forms', 'Intake']`.
43
+ * Blank segments are dropped; an empty result means the tag stays at the top level.
44
+ */
45
+ const toGroupPath = (group: UnlayerEditorMergeTagInfo['group']): string[] => {
46
+ const segments = Array.isArray(group) ? group : group === undefined ? [] : [group];
47
+
48
+ return segments.map(segment => segment.trim()).filter(Boolean);
49
+ };
50
+
51
+ /** Returns the `mergeTags` record of the (possibly nested) group at `path`, creating groups on the way. */
52
+ const ensureGroup = (root: UnlayerMergeTagsConfig, path: string[]) => {
53
+ let current = root;
54
+
55
+ for (const groupName of path) {
56
+ const groupKey = toMergeTagKey(groupName);
57
+ const existing = current[groupKey];
58
+
59
+ if (existing && isUnlayerMergeTagGroup(existing)) {
60
+ current = existing.mergeTags;
61
+ continue;
62
+ }
63
+
64
+ const created: UnlayerMergeTagGroup = { name: groupName, mergeTags: {} };
65
+ current[groupKey] = created;
66
+ current = created.mergeTags;
67
+ }
68
+
69
+ return current;
70
+ };
71
+
72
+ /**
73
+ * Builds Unlayer's `mergeTags` config. With `groups` enabled (default) tags nest under the
74
+ * submenu path in `group`; `UnlayerStore` applies that shape once the editor is ready, since
75
+ * nested groups at `createEditor` time prevent DTE custom tools from loading.
76
+ */
45
77
  export const toUnlayerMergeTags = (
46
78
  tags: UnlayerEditorMergeTagInfo[] | undefined,
47
79
  options?: ToUnlayerMergeTagsOptions,
@@ -58,61 +90,10 @@ export const toUnlayerMergeTags = (
58
90
  value: tag.propertyPath,
59
91
  };
60
92
  const tagKey = toMergeTagKey(tag.id);
61
- const groupName = groupsEnabled ? tag.group?.trim() : undefined;
62
-
63
- if (!groupName) {
64
- out[tagKey] = mergeTag;
65
- return out;
66
- }
67
-
68
- const groupKey = toMergeTagKey(groupName);
69
- const existing = out[groupKey];
70
-
71
- if (existing && isUnlayerMergeTagGroup(existing)) {
72
- existing.mergeTags[tagKey] = mergeTag;
73
- return out;
74
- }
93
+ const groupPath = groupsEnabled ? toGroupPath(tag.group) : [];
75
94
 
76
- out[groupKey] = {
77
- name: groupName,
78
- mergeTags: {
79
- [tagKey]: mergeTag,
80
- },
81
- };
95
+ ensureGroup(out, groupPath)[tagKey] = mergeTag;
82
96
 
83
97
  return out;
84
98
  }, {});
85
99
  };
86
-
87
- /**
88
- * Nested Unlayer groups passed to createEditor prevent DTE custom tools from
89
- * loading in the host app. Apply the 0.151 flat shape at init, then upgrade to
90
- * grouped submenus once the editor (and customJS tools) are ready.
91
- */
92
- export const scheduleUnlayerMergeTags = (
93
- editor: UnlayerMergeTagsEditor,
94
- tags: UnlayerEditorMergeTagInfo[] | undefined,
95
- ) => {
96
- const grouped = toUnlayerMergeTags(tags);
97
-
98
- if (!grouped) {
99
- return;
100
- }
101
-
102
- let isApplied = false;
103
- const apply = () => {
104
- if (isApplied) {
105
- return;
106
- }
107
-
108
- try {
109
- editor.setMergeTags(grouped);
110
- isApplied = true;
111
- } catch (error: unknown) {
112
- logUnlayerError('Failed to apply grouped merge tags', error);
113
- }
114
- };
115
-
116
- editor.addEventListener('editor:ready', apply);
117
- editor.addEventListener('design:loaded', apply);
118
- };
@@ -1,3 +1,4 @@
1
+ import type { DisplayConditionState } from '../display-conditions/types';
1
2
  import { SchemaObject } from './schema';
2
3
 
3
4
  export const constGenericsEditor = {
@@ -12,6 +13,41 @@ export interface UnlayerEditorESignRecipientInfo {
12
13
  displayName: string;
13
14
  }
14
15
 
16
+ export const E_SIGN_TOOL_SLUG = 'e-sign';
17
+ export const E_SIGN_DATE_SIGNED_OFFSET_TYPE = 'dateSignedOffset';
18
+ /** Must match the backend `EsignFieldSubTypes` registry key read from the e-sign tool values. */
19
+ export const E_SIGN_DATE_SIGNED_OFFSET_VALUES_KEY = 'eSignDateSignedOffset';
20
+ export const MAX_E_SIGN_OFFSET_DAYS = 365;
21
+ /** Counts the trailing unconditional rule too. */
22
+ export const MAX_E_SIGN_OFFSET_RULES = 5;
23
+
24
+ export interface ESignDateSignedOffsetRule {
25
+ days: number;
26
+ /** Always `behavior: 'show'` with exactly one condition; the backend rejects anything else. */
27
+ condition: DisplayConditionState;
28
+ }
29
+
30
+ export interface ESignDateSignedOffsetFinalRule {
31
+ days: number;
32
+ condition?: never;
33
+ }
34
+
35
+ /**
36
+ * Ordered if / else-if / else chain: the first rule whose condition matches supplies the offset,
37
+ * and the last rule is always the unconditional fallback.
38
+ */
39
+ export interface ESignDateSignedOffsetSettings {
40
+ rules:
41
+ | [ESignDateSignedOffsetFinalRule]
42
+ | [...ESignDateSignedOffsetRule[], ESignDateSignedOffsetFinalRule];
43
+ }
44
+
45
+ /** Payload of both `--esign-offset-open` (iframe to parent) and `--esign-offset-result` (back). */
46
+ export interface UnlayerEventESignOffset {
47
+ requestId: string;
48
+ value: ESignDateSignedOffsetSettings;
49
+ }
50
+
15
51
  export interface UnlayerEditorUnit {
16
52
  id: string;
17
53
  generic: string;
@@ -33,6 +69,7 @@ export interface UnlayerEventConfig {
33
69
  isSnapshotMode?: boolean;
34
70
  units?: UnlayerEditorUnit[];
35
71
  holidays?: string[];
72
+ conditionalRulesEnabled?: boolean;
36
73
  }
37
74
 
38
75
  export interface UnlayerEventRegister {
@@ -0,0 +1,173 @@
1
+ import { defaultGroup } from '../display-conditions/constants';
2
+ import {
3
+ CONDITION_OPERATORS,
4
+ type DataPointOption,
5
+ type DisplayConditionState,
6
+ type SingleCondition,
7
+ VALUE_LESS_OPERATORS,
8
+ } from '../display-conditions/types';
9
+ import {
10
+ type ESignDateSignedOffsetFinalRule,
11
+ type ESignDateSignedOffsetRule,
12
+ type ESignDateSignedOffsetSettings,
13
+ MAX_E_SIGN_OFFSET_DAYS,
14
+ MAX_E_SIGN_OFFSET_RULES,
15
+ } from './const';
16
+
17
+ const NUMERIC_VALUE_RE = /^-?(?:\d+\.?\d*|\.\d+)$/;
18
+
19
+ function isRecord(value: unknown): value is Record<string, unknown> {
20
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
21
+ }
22
+
23
+ function isConditionState(value: unknown): value is DisplayConditionState {
24
+ return isRecord(value) && Array.isArray(value.groups) && value.groups.length > 0;
25
+ }
26
+
27
+ function isConditionedRule(
28
+ rule: ESignDateSignedOffsetRule | ESignDateSignedOffsetFinalRule,
29
+ ): rule is ESignDateSignedOffsetRule {
30
+ return rule.condition !== undefined;
31
+ }
32
+
33
+ export const clampESignOffsetDays = (days: unknown) => {
34
+ if (typeof days !== 'number' || !Number.isFinite(days)) {
35
+ return 0;
36
+ }
37
+
38
+ return Math.max(Math.min(MAX_E_SIGN_OFFSET_DAYS, Math.trunc(days)), 0);
39
+ };
40
+
41
+ export const createESignOffsetSettings = (days = 0): ESignDateSignedOffsetSettings => {
42
+ return { rules: [{ days: clampESignOffsetDays(days) }] };
43
+ };
44
+
45
+ export const createESignOffsetRule = (): ESignDateSignedOffsetRule => {
46
+ return { condition: { behavior: 'show', groups: [defaultGroup()] }, days: 0 };
47
+ };
48
+
49
+ export const buildESignOffsetSettings = (
50
+ conditionedRules: ESignDateSignedOffsetRule[],
51
+ finalDays: number,
52
+ ): ESignDateSignedOffsetSettings => {
53
+ return {
54
+ rules: [
55
+ ...conditionedRules.map(rule => ({ ...rule, days: clampESignOffsetDays(rule.days) })),
56
+ { days: clampESignOffsetDays(finalDays) },
57
+ ],
58
+ };
59
+ };
60
+
61
+ export const splitESignOffsetSettings = (settings: ESignDateSignedOffsetSettings) => {
62
+ const rules: readonly (ESignDateSignedOffsetRule | ESignDateSignedOffsetFinalRule)[] =
63
+ settings.rules;
64
+
65
+ return {
66
+ conditionedRules: rules.filter(isConditionedRule),
67
+ finalDays: rules[rules.length - 1]?.days ?? 0,
68
+ };
69
+ };
70
+
71
+ export function isESignOffsetSettings(value: unknown): value is ESignDateSignedOffsetSettings {
72
+ if (!isRecord(value) || !Array.isArray(value.rules)) {
73
+ return false;
74
+ }
75
+
76
+ const rules: unknown[] = value.rules;
77
+
78
+ if (rules.length === 0 || rules.length > MAX_E_SIGN_OFFSET_RULES) {
79
+ return false;
80
+ }
81
+
82
+ return rules.every((rule, index) => {
83
+ if (!isRecord(rule) || clampESignOffsetDays(rule.days) !== rule.days) {
84
+ return false;
85
+ }
86
+
87
+ return index === rules.length - 1
88
+ ? rule.condition === undefined
89
+ : isConditionState(rule.condition);
90
+ });
91
+ }
92
+
93
+ /** Coerces whatever is stored on the tool (missing, legacy or hand-edited) into a valid chain. */
94
+ export const normalizeESignOffsetSettings = (value: unknown): ESignDateSignedOffsetSettings => {
95
+ if (isESignOffsetSettings(value)) {
96
+ return value;
97
+ }
98
+
99
+ if (!isRecord(value) || !Array.isArray(value.rules)) {
100
+ return createESignOffsetSettings();
101
+ }
102
+
103
+ const rules: unknown[] = value.rules;
104
+ const lastRule = rules[rules.length - 1];
105
+ const conditionedRules = rules
106
+ .slice(0, -1)
107
+ .flatMap(rule =>
108
+ isRecord(rule) && isConditionState(rule.condition)
109
+ ? [{ condition: rule.condition, days: clampESignOffsetDays(rule.days) }]
110
+ : [],
111
+ )
112
+ .slice(0, MAX_E_SIGN_OFFSET_RULES - 1);
113
+
114
+ return buildESignOffsetSettings(
115
+ conditionedRules,
116
+ isRecord(lastRule) ? clampESignOffsetDays(lastRule.days) : 0,
117
+ );
118
+ };
119
+
120
+ export const canAddESignOffsetRule = (rules: ESignDateSignedOffsetRule[]) => {
121
+ return rules.length < MAX_E_SIGN_OFFSET_RULES - 1;
122
+ };
123
+
124
+ export const addESignOffsetRule = (rules: ESignDateSignedOffsetRule[]) => {
125
+ return canAddESignOffsetRule(rules) ? [...rules, createESignOffsetRule()] : rules;
126
+ };
127
+
128
+ export const updateESignOffsetRule = (
129
+ rules: ESignDateSignedOffsetRule[],
130
+ index: number,
131
+ nextRule: ESignDateSignedOffsetRule,
132
+ ) => {
133
+ return rules.map((rule, ruleIndex) => (ruleIndex === index ? nextRule : rule));
134
+ };
135
+
136
+ export const removeESignOffsetRule = (rules: ESignDateSignedOffsetRule[], index: number) => {
137
+ return rules.filter((_, ruleIndex) => ruleIndex !== index);
138
+ };
139
+
140
+ export const getESignOffsetRuleCondition = (rule: ESignDateSignedOffsetRule) => {
141
+ return rule.condition.groups[0]?.conditions[0];
142
+ };
143
+
144
+ export const isCompleteESignOffsetCondition = (
145
+ condition: SingleCondition | undefined,
146
+ fieldType: DataPointOption['fieldType'],
147
+ ) => {
148
+ if (!condition?.dataPointKey) {
149
+ return false;
150
+ }
151
+
152
+ if (VALUE_LESS_OPERATORS.includes(condition.operator)) {
153
+ return true;
154
+ }
155
+
156
+ const value = condition.value.trim();
157
+
158
+ return value !== '' && (fieldType !== 'number' || NUMERIC_VALUE_RE.test(value));
159
+ };
160
+
161
+ export const getESignOffsetConditionText = (condition: SingleCondition, dataPointTitle: string) => {
162
+ const operatorLabel = (
163
+ CONDITION_OPERATORS.find(operator => operator.value === condition.operator)?.label ??
164
+ condition.operator
165
+ )
166
+ .split(' (')[0]
167
+ .replace('==', '=')
168
+ .toLowerCase();
169
+
170
+ return VALUE_LESS_OPERATORS.includes(condition.operator)
171
+ ? `${dataPointTitle} ${operatorLabel}`
172
+ : `${dataPointTitle} ${operatorLabel} ${condition.value}`;
173
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Custom tools whose `values.id` is a per-instance GUID that must stay unique
3
+ * within a design. When such a tool is duplicated (content or row duplicate),
4
+ * Unlayer copies the values as-is, so the host regenerates the id of the copy.
5
+ */
6
+ export const INSTANCE_ID_TOOL_SLUGS = ['embedded-image'];
7
+
8
+ /**
9
+ * Generates an RFC 4122 v4 GUID (`3fa85f64-5717-4562-b3fc-2c963f66afa6`) with the
10
+ * platform `crypto.randomUUID()`. It is only exposed in secure contexts (https /
11
+ * localhost); when missing the error is logged and no id is produced.
12
+ */
13
+ export const generateInstanceId = (): string | undefined => {
14
+ if (typeof globalThis.crypto?.randomUUID !== 'function') {
15
+ // eslint-disable-next-line no-console
16
+ console.error(
17
+ 'crypto.randomUUID is not available (insecure context?), cannot generate an instance id',
18
+ );
19
+ return undefined;
20
+ }
21
+
22
+ return globalThis.crypto.randomUUID();
23
+ };
package/src/store.ts CHANGED
@@ -1,7 +1,15 @@
1
+ import { emitESignOffsetRequest } from './e-sign-offset/eSignOffsetController';
1
2
  import { loadScript } from './loadScript';
2
3
  import { logUnlayerError } from './log';
4
+ import { toUnlayerMergeTags, type UnlayerMergeTagsConfig } from './merge-tags';
3
5
  import { defaultImageValidation } from './shared/configs';
4
- import type { UnlayerEditorTwin, UnlayerEventConfig, UnlayerEventRegister } from './shared/const';
6
+ import type {
7
+ UnlayerEditorTwin,
8
+ UnlayerEventConfig,
9
+ UnlayerEventESignOffset,
10
+ UnlayerEventRegister,
11
+ } from './shared/const';
12
+ import { normalizeESignOffsetSettings } from './shared/e-sign-offset';
5
13
  import { unlayerSupportedFonts } from './shared/fonts';
6
14
  import {
7
15
  FormFieldInfo,
@@ -11,14 +19,23 @@ import {
11
19
  } from './shared/forms';
12
20
  import { schemaBuildMap, schemaIsDate } from './shared/schema';
13
21
  import { unlayerToolsParseTwinKey } from './shared/tools';
14
- import { unlayerToolsIterate } from './tools';
22
+ import {
23
+ unlayerToolsDedupeInstanceIds,
24
+ unlayerToolsIterate,
25
+ unlayerToolsNormalizeESignOffsets,
26
+ } from './tools';
15
27
  import {
16
28
  createUnlayerEditor,
17
29
  DesignUpdatedEventType,
18
30
  EventDesignUpdated,
19
31
  Unlayer,
20
32
  } from './unlayer';
21
- import { CreateUnlayerEditorProps, UnlayerDesignFormat, UnlayerRef } from './unlayer-interface';
33
+ import {
34
+ CreateUnlayerEditorProps,
35
+ UnlayerDesignFormat,
36
+ UnlayerEditorMergeTagInfo,
37
+ UnlayerRef,
38
+ } from './unlayer-interface';
22
39
 
23
40
  const defaultScriptUrl = 'https://editor.unlayer.com/embed.js?2';
24
41
  const UNLAYER_TOOLS_READY_TIMEOUT_MS = 30000;
@@ -191,6 +208,9 @@ export class UnlayerStore {
191
208
  private toolsReadyWatchdogId?: number;
192
209
  private formFieldsByFormId: Record<number, FormFieldInfo[]> = {};
193
210
  private formFieldSamples: Record<string, unknown> = {};
211
+ private isEditorReady = false;
212
+ private isMergeTagsApplied = false;
213
+ private mergeTagsConfig?: UnlayerMergeTagsConfig;
194
214
 
195
215
  private onMessageCB?: (type: string, data: any) => void;
196
216
  private onChangeCB?: (info: UnlayerDesignChangeInfo) => void;
@@ -211,18 +231,28 @@ export class UnlayerStore {
211
231
  .map(field => field.fullKey),
212
232
  );
213
233
 
214
- this.props.eSignFieldTypes = ['Signature', 'Initials', 'Date Signed', 'Full Name'];
234
+ this.props.eSignFieldTypes = [
235
+ 'Signature',
236
+ 'Initials',
237
+ 'Date Signed',
238
+ 'Date Signed Offset',
239
+ 'Full Name',
240
+ ];
215
241
 
216
242
  this.unlayerRef = {
217
243
  loadDesign: design => {
218
244
  this.editor?.loadDesign(design);
219
245
  },
220
246
  saveDesign: cb => {
221
- this.editor?.saveDesign(cb);
247
+ this.editor?.saveDesign(design => {
248
+ unlayerToolsNormalizeESignOffsets(design);
249
+ cb(design);
250
+ });
222
251
  },
223
252
  exportHtml: cb => {
224
253
  this.editor?.exportHtml(data => {
225
254
  const result = ensureChunksFonts(data);
255
+ unlayerToolsNormalizeESignOffsets(result?.design);
226
256
  result.demoData = this.buildCalculatedFieldDemoData(result.design);
227
257
  cb(result);
228
258
  });
@@ -236,6 +266,9 @@ export class UnlayerStore {
236
266
  sendCalcFieldLabels: labels => {
237
267
  this.sendCalcFieldLabels(labels);
238
268
  },
269
+ setMergeTags: tags => {
270
+ this.setMergeTags(tags);
271
+ },
239
272
  };
240
273
  }
241
274
 
@@ -256,6 +289,9 @@ export class UnlayerStore {
256
289
  }
257
290
 
258
291
  this.editor = createUnlayerEditor(container, this.props);
292
+ // The host may already have pushed a newer set through `setMergeTags`; keep it.
293
+ this.mergeTagsConfig ??= toUnlayerMergeTags(this.props.mergeTags);
294
+ this.editor.addEventListener('editor:ready', this.onEditorReady);
259
295
  this.editor.addEventListener('design:loaded', this.onDesignLoaded);
260
296
  this.editor.addEventListener('design:updated', this.onDesignUpdated);
261
297
  this.editor.registerCallback('image', this.uploadImage);
@@ -280,6 +316,7 @@ export class UnlayerStore {
280
316
  window.removeEventListener('message', this.onPostMessage);
281
317
 
282
318
  if (this.editor) {
319
+ this.editor.removeEventListener('editor:ready', this.onEditorReady);
283
320
  this.editor.removeEventListener('design:loaded', this.onDesignLoaded);
284
321
  this.editor.removeEventListener('design:updated', this.onDesignUpdated);
285
322
  this.editor.unregisterCallback('image', this.uploadImage);
@@ -355,7 +392,46 @@ export class UnlayerStore {
355
392
  this.sendMessage('--calc-field-labels', { labels });
356
393
  };
357
394
 
395
+ /**
396
+ * Replaces the editor's merge tag menu. Nested groups passed to `createEditor` stop DTE
397
+ * custom tools from registering, so init only carries the flat shape and the grouped
398
+ * config is applied here once the editor is ready. A call before that (including the
399
+ * initial props) is parked; a later call replaces it. A failed apply is retried on the
400
+ * next `design:loaded`.
401
+ */
402
+ setMergeTags = (tags: UnlayerEditorMergeTagInfo[] | undefined) => {
403
+ this.mergeTagsConfig = toUnlayerMergeTags(tags);
404
+ this.isMergeTagsApplied = false;
405
+ this.applyMergeTags();
406
+ };
407
+
408
+ private applyMergeTags = () => {
409
+ if (
410
+ !this.editor ||
411
+ !this.isEditorReady ||
412
+ this.isMergeTagsApplied ||
413
+ !this.mergeTagsConfig
414
+ ) {
415
+ return;
416
+ }
417
+
418
+ try {
419
+ this.editor.setMergeTags(this.mergeTagsConfig);
420
+ this.isMergeTagsApplied = true;
421
+ } catch (error: unknown) {
422
+ logUnlayerError('Failed to apply merge tags', error);
423
+ }
424
+ };
425
+
426
+ private onEditorReady = () => {
427
+ this.isEditorReady = true;
428
+ this.applyMergeTags();
429
+ };
430
+
358
431
  private onDesignLoaded = () => {
432
+ // A loaded design means the editor is up even if `editor:ready` never reached us.
433
+ this.onEditorReady();
434
+
359
435
  if (!this.hasDesign) {
360
436
  this.hasDesign = true;
361
437
  this.editor?.setBodyValues({
@@ -391,6 +467,11 @@ export class UnlayerStore {
391
467
  }
392
468
  });
393
469
 
470
+ // Duplicated contents/rows carry the original's instance GUIDs; give copies new ones.
471
+ if (unlayerToolsDedupeInstanceIds(data.design, event.item?.values?._meta?.htmlID)) {
472
+ hasChanges = true;
473
+ }
474
+
394
475
  if (hasChanges) {
395
476
  this.setDesign(data.design);
396
477
  }
@@ -427,6 +508,8 @@ export class UnlayerStore {
427
508
  genericConfigMode: this.props.genericConfigMode,
428
509
  isSnapshotMode: this.props.isSnapshotMode,
429
510
  holidays: this.props.holidays,
511
+ conditionalRulesEnabled:
512
+ !!this.props.displayConditions || !!this.props.onConditionalsOpen,
430
513
  };
431
514
 
432
515
  this.sendMessage('--config', JSON.parse(JSON.stringify(configData)));
@@ -434,6 +517,8 @@ export class UnlayerStore {
434
517
  this.onFormSelectCB?.(data?.formId);
435
518
  } else if (type === '--calc-field-open') {
436
519
  this.onCalcFieldOpenCB?.(data?.usedFormIds ?? []);
520
+ } else if (type === '--esign-offset-open') {
521
+ this.openESignOffsetRules(data);
437
522
  } else if (type === '--calc-field-select') {
438
523
  if (this.onCalcFieldSelectCB) {
439
524
  this.onCalcFieldSelectCB(data?.fieldKeys ?? []);
@@ -478,6 +563,22 @@ export class UnlayerStore {
478
563
  this.onMessageCB?.(type, data);
479
564
  };
480
565
 
566
+ private openESignOffsetRules = (data?: Partial<UnlayerEventESignOffset>) => {
567
+ const requestId = data?.requestId;
568
+
569
+ if (!requestId) {
570
+ return;
571
+ }
572
+
573
+ emitESignOffsetRequest({
574
+ value: normalizeESignOffsetSettings(data.value),
575
+ done: value => {
576
+ const result: UnlayerEventESignOffset = { requestId, value };
577
+ this.sendMessage('--esign-offset-result', JSON.parse(JSON.stringify(result)));
578
+ },
579
+ });
580
+ };
581
+
481
582
  private updateFormFieldSamples = (formId: number, fields: FormFieldInfo[]) => {
482
583
  fields.forEach((field, index) => {
483
584
  const normalizedFieldId = String(field.id ?? '').replaceAll('-', '');