@servicetitan/dte-unlayer 0.131.0 → 0.132.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.
@@ -7,6 +7,7 @@ export interface FormFieldInfo {
7
7
  id: string;
8
8
  header: string;
9
9
  itemType: string;
10
+ sampleData?: unknown;
10
11
  }
11
12
 
12
13
  export const enum FormItemType {
@@ -93,3 +94,27 @@ export const getConditionalFieldTypeFromFormItemType = (
93
94
  }
94
95
  return 'string';
95
96
  };
97
+
98
+ const DEFAULT_FORM_NUMBER_SAMPLE = 44;
99
+ const DEFAULT_FORM_DATE_SAMPLE = '2026-01-15';
100
+
101
+ export const getFormFieldSampleValue = (
102
+ field: Pick<FormFieldInfo, 'itemType' | 'sampleData'> & { header?: string },
103
+ index = 0,
104
+ ): number | string => {
105
+ if (typeof field.sampleData === 'number' || typeof field.sampleData === 'string') {
106
+ return field.sampleData;
107
+ }
108
+
109
+ if (field.itemType === FormItemType.Number) {
110
+ return DEFAULT_FORM_NUMBER_SAMPLE + index;
111
+ }
112
+
113
+ if (field.itemType === FormItemType.Date) {
114
+ const [year, month, day] = DEFAULT_FORM_DATE_SAMPLE.split('-');
115
+ const dayNumber = Number(day) + (index % 10);
116
+ return `${year}-${month}-${String(dayNumber).padStart(2, '0')}`;
117
+ }
118
+
119
+ return field.header ? `${field.header} value` : 'sample';
120
+ };
package/src/store.ts CHANGED
@@ -2,7 +2,13 @@ import { loadScript } from './loadScript';
2
2
  import { defaultImageValidation } from './shared/configs';
3
3
  import { UnlayerEditorTwin, UnlayerEventConfig, UnlayerEventRegister } from './shared/const';
4
4
  import { unlayerSupportedFonts } from './shared/fonts';
5
- import { FormFieldInfo, FormInfo } from './shared/forms';
5
+ import {
6
+ FormFieldInfo,
7
+ FormInfo,
8
+ getFormFieldSampleValue,
9
+ parseFormFieldKey,
10
+ } from './shared/forms';
11
+ import { schemaBuildMap, schemaIsDate } from './shared/schema';
6
12
  import { unlayerToolsParseTwinKey } from './shared/tools';
7
13
  import { unlayerToolsIterate } from './tools';
8
14
  import {
@@ -16,11 +22,7 @@ import { CreateUnlayerEditorProps, UnlayerDesignFormat, UnlayerRef } from './unl
16
22
  const defaultScriptUrl = 'https://editor.unlayer.com/embed.js?2';
17
23
 
18
24
  const normalizeFontToken = (font: string) =>
19
- font
20
- .replace(/["']/g, '')
21
- .split(',')[0]
22
- .trim()
23
- .toLowerCase();
25
+ font.replace(/["']/g, '').split(',')[0].trim().toLowerCase();
24
26
 
25
27
  const getDesignFontTokens = (value: any, out: Set<string>) => {
26
28
  if (!value || typeof value !== 'object') {
@@ -77,7 +79,7 @@ const ensureChunksFonts = (data: any) => {
77
79
  const labelToken = normalizeFontToken(font.label);
78
80
  const valueToken = normalizeFontToken(font.value);
79
81
  const isUsedInDesign = usedFontTokens.has(labelToken) || usedFontTokens.has(valueToken);
80
- const isUsedInHtml = !!html && (html.includes(labelToken) || html.includes(valueToken));
82
+ const isUsedInHtml = [labelToken, valueToken].some(token => html.includes(token));
81
83
 
82
84
  if ((isUsedInDesign || isUsedInHtml) && !existingLabels.has(font.label)) {
83
85
  existingFonts.push(font);
@@ -89,6 +91,69 @@ const ensureChunksFonts = (data: any) => {
89
91
  return data;
90
92
  };
91
93
 
94
+ const FORM_FIELD_DATA_POINT_PREFIX = 'FORM_FIELD_';
95
+
96
+ const getByPath = (data: unknown, path: string): unknown => {
97
+ if (!data || typeof data !== 'object') {
98
+ return undefined;
99
+ }
100
+
101
+ return path.split('.').reduce<unknown>((current, part) => {
102
+ if (!current || typeof current !== 'object') {
103
+ return undefined;
104
+ }
105
+
106
+ return (current as Record<string, unknown>)[part];
107
+ }, data);
108
+ };
109
+
110
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
111
+ !!value && typeof value === 'object' && !Array.isArray(value);
112
+
113
+ const deepMerge = (...sources: unknown[]): Record<string, unknown> => {
114
+ const out: Record<string, unknown> = {};
115
+
116
+ for (const source of sources) {
117
+ if (!isPlainObject(source)) {
118
+ continue;
119
+ }
120
+
121
+ for (const [key, value] of Object.entries(source)) {
122
+ if (isPlainObject(value) && isPlainObject(out[key])) {
123
+ out[key] = deepMerge(out[key], value);
124
+ } else {
125
+ out[key] = value;
126
+ }
127
+ }
128
+ }
129
+
130
+ return out;
131
+ };
132
+
133
+ const setByPath = (target: Record<string, unknown>, path: string, value: unknown) => {
134
+ const parts = path.split('.');
135
+ if (!parts.length) {
136
+ return;
137
+ }
138
+
139
+ let node: Record<string, unknown> = target;
140
+
141
+ for (let i = 0; i < parts.length - 1; i += 1) {
142
+ const key = parts[i];
143
+ if (!isPlainObject(node[key])) {
144
+ node[key] = {};
145
+ }
146
+ node = node[key] as Record<string, unknown>;
147
+ }
148
+
149
+ node[parts[parts.length - 1]] = value;
150
+ };
151
+
152
+ const tokenizeFormulaFields = (expression: string): string[] => {
153
+ const regex = /[A-Za-z_][A-Za-z0-9_.]*/g;
154
+ return expression.match(regex) ?? [];
155
+ };
156
+
92
157
  export interface UnlayerDesignChangeInfo {
93
158
  isToolsListChanged: boolean;
94
159
  }
@@ -112,11 +177,15 @@ const eventsNotAddingTools: DesignUpdatedEventType[] = [
112
177
 
113
178
  export class UnlayerStore {
114
179
  readonly unlayerRef: UnlayerRef;
180
+ private readonly schemaDummyData: Record<string, unknown>;
181
+ private readonly dateSchemaFieldKeys: Set<string>;
115
182
 
116
183
  private editor: Unlayer | undefined;
117
184
  private isInit = false;
118
185
  private iframe?: HTMLIFrameElement;
119
186
  private hasDesign = false;
187
+ private formFieldsByFormId: Record<number, FormFieldInfo[]> = {};
188
+ private formFieldSamples: Record<string, unknown> = {};
120
189
 
121
190
  private onMessageCB?: (type: string, data: any) => void;
122
191
  private onChangeCB?: (info: UnlayerDesignChangeInfo) => void;
@@ -129,6 +198,14 @@ export class UnlayerStore {
129
198
  private onCalcFieldSelectCB?: (fieldKeys: string[]) => void;
130
199
 
131
200
  constructor(readonly props: CreateUnlayerEditorProps) {
201
+ const schemaData = this.props.schema ? schemaBuildMap(this.props.schema) : undefined;
202
+ this.schemaDummyData = (schemaData?.dummyData ?? {}) as Record<string, unknown>;
203
+ this.dateSchemaFieldKeys = new Set(
204
+ Object.values(schemaData?.map ?? {})
205
+ .filter(field => field.isValue && schemaIsDate(field.node))
206
+ .map(field => field.fullKey),
207
+ );
208
+
132
209
  this.props.eSignFieldTypes = ['Signature', 'Initials', 'Date Signed', 'Full Name'];
133
210
 
134
211
  this.unlayerRef = {
@@ -140,7 +217,9 @@ export class UnlayerStore {
140
217
  },
141
218
  exportHtml: cb => {
142
219
  this.editor?.exportHtml(data => {
143
- cb(ensureChunksFonts(data));
220
+ const result = ensureChunksFonts(data);
221
+ result.demoData = this.buildCalculatedFieldDemoData(result.design);
222
+ cb(result);
144
223
  });
145
224
  },
146
225
  sendFormList: forms => {
@@ -244,6 +323,8 @@ export class UnlayerStore {
244
323
  };
245
324
 
246
325
  sendFormFields = (formId: number, fields: FormFieldInfo[]) => {
326
+ this.formFieldsByFormId[formId] = fields;
327
+ this.updateFormFieldSamples(formId, fields);
247
328
  this.sendMessage('--form-fields', { formId, fields });
248
329
  };
249
330
 
@@ -371,6 +452,109 @@ export class UnlayerStore {
371
452
  this.onMessageCB?.(type, data);
372
453
  };
373
454
 
455
+ private updateFormFieldSamples = (formId: number, fields: FormFieldInfo[]) => {
456
+ fields.forEach((field, index) => {
457
+ const normalizedFieldId = String(field.id ?? '').replaceAll('-', '');
458
+ if (!normalizedFieldId) {
459
+ return;
460
+ }
461
+
462
+ const sampleValue = getFormFieldSampleValue(field, index);
463
+ setByPath(
464
+ this.formFieldSamples,
465
+ `__submission_fields.${formId}.${normalizedFieldId}`,
466
+ sampleValue,
467
+ );
468
+ this.formFieldSamples[`${FORM_FIELD_DATA_POINT_PREFIX}${normalizedFieldId}`] =
469
+ sampleValue;
470
+ });
471
+ };
472
+
473
+ private collectCalculatedFieldKeys = (design?: UnlayerDesignFormat): Set<string> => {
474
+ const keys = new Set<string>();
475
+
476
+ if (!design) {
477
+ return keys;
478
+ }
479
+
480
+ unlayerToolsIterate(design, tool => {
481
+ const calc = tool.values?.calculation;
482
+ if (!calc || typeof calc !== 'object') {
483
+ return;
484
+ }
485
+
486
+ if (typeof calc.expression === 'string') {
487
+ for (const key of tokenizeFormulaFields(calc.expression)) {
488
+ keys.add(key);
489
+ }
490
+ }
491
+
492
+ if (calc.fieldLabels && typeof calc.fieldLabels === 'object') {
493
+ Object.keys(calc.fieldLabels).forEach(key => keys.add(key));
494
+ }
495
+ });
496
+
497
+ return keys;
498
+ };
499
+
500
+ private resolveDemoValue = (
501
+ key: string,
502
+ fallbackIndex: number,
503
+ sourceData: Record<string, unknown>,
504
+ ): unknown => {
505
+ const parsedFormField = parseFormFieldKey(key);
506
+ if (parsedFormField) {
507
+ const normalizedFieldId = parsedFormField.fieldId;
508
+ const formFields = this.formFieldsByFormId[parsedFormField.formId] ?? [];
509
+ const fieldIndex = formFields.findIndex(
510
+ field => String(field.id ?? '').replaceAll('-', '') === normalizedFieldId,
511
+ );
512
+ const field = fieldIndex >= 0 ? formFields[fieldIndex] : undefined;
513
+ const sample = field
514
+ ? getFormFieldSampleValue(field, fieldIndex)
515
+ : getByPath(
516
+ this.formFieldSamples,
517
+ `__submission_fields.${parsedFormField.formId}.${normalizedFieldId}`,
518
+ );
519
+
520
+ if (sample !== undefined) {
521
+ return sample;
522
+ }
523
+
524
+ return this.dateSchemaFieldKeys.has(key) ? '2026-01-15' : 44 + fallbackIndex;
525
+ }
526
+
527
+ const existing = getByPath(sourceData, key);
528
+ if (existing !== undefined) {
529
+ return existing;
530
+ }
531
+
532
+ if (this.dateSchemaFieldKeys.has(key)) {
533
+ return '2026-01-15';
534
+ }
535
+
536
+ return 44 + fallbackIndex;
537
+ };
538
+
539
+ private buildCalculatedFieldDemoData = (
540
+ design?: UnlayerDesignFormat,
541
+ ): Record<string, unknown> => {
542
+ const demoData: Record<string, unknown> = {};
543
+ const usedKeys = Array.from(this.collectCalculatedFieldKeys(design));
544
+ const sourceData = deepMerge(
545
+ this.schemaDummyData,
546
+ this.props.dummyData,
547
+ this.formFieldSamples,
548
+ );
549
+
550
+ usedKeys.forEach((key, index) => {
551
+ const value = this.resolveDemoValue(key, index, sourceData);
552
+ setByPath(demoData, key, value);
553
+ });
554
+
555
+ return demoData;
556
+ };
557
+
374
558
  private sendMessage = (type: string, data?: any) => {
375
559
  this.iframe?.contentWindow?.postMessage(
376
560
  {
@@ -35,6 +35,7 @@ export interface UnlayerExport {
35
35
  html: string;
36
36
  chunks: any;
37
37
  design: UnlayerDesignFormat;
38
+ demoData?: Record<string, unknown>;
38
39
  }
39
40
 
40
41
  export interface UnlayerRef {
package/src/unlayer.tsx CHANGED
@@ -13,6 +13,7 @@ export interface UnlayerExport {
13
13
  html: string;
14
14
  chunks: any;
15
15
  design: any;
16
+ demoData?: Record<string, unknown>;
16
17
  }
17
18
 
18
19
  export interface Unlayer {