@servicetitan/dte-pdf-editor 1.45.0 → 1.47.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 (76) hide show
  1. package/README.md +15 -0
  2. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  3. package/dist/components/field-config-panel/field-config-panel.js +4 -2
  4. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  5. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts.map +1 -1
  6. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +11 -1
  7. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  8. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts +1 -0
  9. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts.map +1 -1
  10. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js +14 -2
  11. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js.map +1 -1
  12. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
  13. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +3 -0
  14. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
  15. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts +1 -0
  16. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  17. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +2 -2
  18. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  19. package/dist/components/pdf-view/pdf-view-calculated.d.ts +2 -1
  20. package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -1
  21. package/dist/components/pdf-view/pdf-view-calculated.js +5 -2
  22. package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -1
  23. package/dist/components/pdf-view/pdf-view-fillable.d.ts +10 -2
  24. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  25. package/dist/components/pdf-view/pdf-view-fillable.js +16 -12
  26. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  27. package/dist/components/pdf-view/pdf-view.d.ts +2 -2
  28. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  29. package/dist/components/pdf-view/pdf-view.js +4 -4
  30. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  31. package/dist/interface/types.d.ts +2 -1
  32. package/dist/interface/types.d.ts.map +1 -1
  33. package/dist/interface/types.js.map +1 -1
  34. package/dist/utils/formula/expression.utils.d.ts +13 -0
  35. package/dist/utils/formula/expression.utils.d.ts.map +1 -1
  36. package/dist/utils/formula/expression.utils.js +42 -0
  37. package/dist/utils/formula/expression.utils.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/__tests__/field-types/date-fields.test.ts +212 -0
  40. package/src/__tests__/field-types/display-condition-fields.test.ts +340 -0
  41. package/src/__tests__/field-types/e-sign-fields.test.ts +97 -0
  42. package/src/__tests__/field-types/fillable-fields.test.ts +180 -0
  43. package/src/__tests__/field-types/form-fields.test.ts +193 -0
  44. package/src/__tests__/field-types/formula-fields.test.ts +245 -0
  45. package/src/__tests__/field-types/merge-tag-fields.test.ts +208 -0
  46. package/src/components/field-config-panel/field-config-panel.tsx +11 -0
  47. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +12 -1
  48. package/src/components/pdf-fields-overlay/pdf-overlay-field-calculated.tsx +20 -4
  49. package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +10 -0
  50. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +5 -1
  51. package/src/components/pdf-view/pdf-view-calculated.tsx +13 -3
  52. package/src/components/pdf-view/pdf-view-fillable.tsx +62 -21
  53. package/src/components/pdf-view/pdf-view.tsx +11 -10
  54. package/src/interface/types.ts +12 -10
  55. package/src/styles/generic.css +6 -0
  56. package/src/styles/inline-editable.css +1 -0
  57. package/src/utils/conditions/__tests__/evaluate.utils.test.ts +163 -0
  58. package/src/utils/conditions/__tests__/schema-data-points.utils.test.ts +149 -0
  59. package/src/utils/data-model/__tests__/extract-fields.utils.test.ts +154 -0
  60. package/src/utils/data-model/__tests__/resolve-values.utils.test.ts +60 -0
  61. package/src/utils/field/__tests__/field-background-color.utils.test.ts +26 -0
  62. package/src/utils/field/__tests__/field-placeholder-text.utils.test.ts +46 -0
  63. package/src/utils/formula/__tests__/dom.utils.test.ts +33 -0
  64. package/src/utils/formula/__tests__/evaluate-formula.utils.test.ts +202 -0
  65. package/src/utils/formula/__tests__/expression.utils.test.ts +119 -0
  66. package/src/utils/formula/__tests__/form-fields.utils.test.ts +274 -0
  67. package/src/utils/formula/__tests__/format-calculated-result.utils.test.ts +105 -0
  68. package/src/utils/formula/__tests__/render-formula.utils.test.ts +43 -0
  69. package/src/utils/formula/__tests__/validate-formula.utils.test.ts +168 -0
  70. package/src/utils/formula/expression.utils.ts +44 -0
  71. package/src/utils/path/__tests__/generate-e-sign-path.test.ts +26 -0
  72. package/src/utils/path/__tests__/generate-fillable-path.test.ts +17 -0
  73. package/src/utils/path/__tests__/parse-fillable-path.test.ts +25 -0
  74. package/src/utils/recipients/__tests__/map-colors.test.ts +40 -0
  75. package/src/utils/shared/__tests__/date.utils.test.ts +58 -0
  76. package/src/utils/shared/__tests__/number.utils.test.ts +48 -0
@@ -0,0 +1,245 @@
1
+ import {
2
+ CalculatedFormatForNumber,
3
+ FormFieldsByFormIdI,
4
+ FormInfo,
5
+ StructuredFormula,
6
+ } from '../../interface/types';
7
+ import {
8
+ buildFormFieldKey,
9
+ evaluateFormula,
10
+ formatCalculatedResult,
11
+ generateFillablePath,
12
+ parseExpression,
13
+ tokensToExpression,
14
+ validateFormula,
15
+ } from '../../utils';
16
+
17
+ /**
18
+ * Scenario tests for calculated (formula) fields.
19
+ *
20
+ * Formulas can reference three operand types — merge tags, fillable fields,
21
+ * form submission fields — and three numeric operations plus parentheses.
22
+ * These tests validate the full pipeline: parse expression -> structured
23
+ * formula -> validate -> evaluate at runtime -> format the result.
24
+ */
25
+
26
+ const fillablePath = generateFillablePath('signer1', 'TipAmount');
27
+
28
+ const forms: FormInfo[] = [{ id: 7, name: 'Inspection' }];
29
+ const formFieldsByFormId: FormFieldsByFormIdI = {
30
+ 7: [{ id: 'abc-123', header: 'Score', itemType: 'number' }],
31
+ };
32
+ const formScoreKey = buildFormFieldKey(7, 'abc-123');
33
+
34
+ const validPaths = new Set(['Job.Total', 'Job.Discount', fillablePath, formScoreKey]);
35
+
36
+ const baseNumberFormat: CalculatedFormatForNumber = {
37
+ resultType: 'number',
38
+ thousandsSeparator: true,
39
+ decimals: 2,
40
+ roundingMode: 'round',
41
+ decimalSeparatorEnabled: false,
42
+ decimalSeparator: '.',
43
+ prefixText: '$',
44
+ postfixText: '',
45
+ };
46
+
47
+ describe('formula (calculated) field scenarios', () => {
48
+ describe('with parsing an editor expression', () => {
49
+ const subject = (expression: string) =>
50
+ parseExpression(expression, validPaths, undefined, {
51
+ forms,
52
+ formFieldsByFormId,
53
+ });
54
+
55
+ test('produces structured tokens for a formula mixing merge tag and fillable', () => {
56
+ const tokens = subject('Job.Total - ' + fillablePath);
57
+ expect(tokens).toEqual([
58
+ { type: 'field', path: 'Job.Total', fieldType: 'number' },
59
+ { type: 'operator', value: '-' },
60
+ { type: 'field', path: fillablePath, fieldType: 'number' },
61
+ ]);
62
+ });
63
+
64
+ test('attaches form snapshot when token references a form submission field', () => {
65
+ const tokens = subject(formScoreKey);
66
+ expect(tokens).toHaveLength(1);
67
+ expect(tokens[0]).toEqual(
68
+ expect.objectContaining({
69
+ type: 'field',
70
+ path: formScoreKey,
71
+ formSnapshot: expect.objectContaining({
72
+ formId: 7,
73
+ formName: 'Inspection',
74
+ fieldName: 'Score',
75
+ }),
76
+ }),
77
+ );
78
+ });
79
+
80
+ test('drops unknown field references silently', () => {
81
+ const tokens = subject('Job.Total + Unknown.Field');
82
+ expect(tokens.filter(t => t.type === 'field').map(t => t.type)).toEqual(['field']);
83
+ });
84
+
85
+ test('round-trips through tokensToExpression', () => {
86
+ const tokens = subject('( Job.Total - Job.Discount ) * 2');
87
+ const formula: StructuredFormula = { tokens };
88
+ expect(tokensToExpression(formula)).toBe('( Job.Total - Job.Discount ) * 2');
89
+ });
90
+ });
91
+
92
+ describe('with validation', () => {
93
+ test('accepts a balanced formula referencing known paths', () => {
94
+ const formula: StructuredFormula = {
95
+ tokens: parseExpression('Job.Total - Job.Discount', validPaths),
96
+ };
97
+ const result = validateFormula(formula, validPaths);
98
+ expect(result.valid).toBe(true);
99
+ expect(result.errors).toEqual([]);
100
+ });
101
+
102
+ test('rejects a formula with unbalanced parentheses', () => {
103
+ const formula: StructuredFormula = {
104
+ tokens: [
105
+ { type: 'lparen' },
106
+ { type: 'number', value: '1' },
107
+ ],
108
+ };
109
+ expect(validateFormula(formula, validPaths).errors).toContain(
110
+ 'Unbalanced parentheses',
111
+ );
112
+ });
113
+
114
+ test('rejects a formula referencing an unknown field', () => {
115
+ const formula: StructuredFormula = {
116
+ tokens: [
117
+ { type: 'field', path: 'Unknown.Field', fieldType: 'number' },
118
+ { type: 'operator', value: '+' },
119
+ { type: 'number', value: '1' },
120
+ ],
121
+ };
122
+ const result = validateFormula(formula, validPaths);
123
+ expect(result.valid).toBe(false);
124
+ expect(result.errors.some(e => e.includes('Unknown field'))).toBe(true);
125
+ });
126
+
127
+ test('rejects multiply/divide with date fields', () => {
128
+ const formula: StructuredFormula = {
129
+ tokens: [
130
+ { type: 'field', path: 'd', fieldType: 'date' },
131
+ { type: 'operator', value: '*' },
132
+ { type: 'number', value: '2' },
133
+ ],
134
+ };
135
+ const result = validateFormula(formula, new Set(['d']), new Set(['d']));
136
+ expect(result.errors.some(e => e.includes('Multiply'))).toBe(true);
137
+ });
138
+ });
139
+
140
+ describe('with evaluation across operand sources', () => {
141
+ test('evaluates merge tag + fillable + form field operands together', () => {
142
+ const formula: StructuredFormula = {
143
+ tokens: [
144
+ { type: 'field', path: 'Job.Total', fieldType: 'number' },
145
+ { type: 'operator', value: '+' },
146
+ { type: 'field', path: fillablePath, fieldType: 'number' },
147
+ { type: 'operator', value: '+' },
148
+ { type: 'field', path: formScoreKey, fieldType: 'number' },
149
+ ],
150
+ };
151
+ // formScoreKey resolves through __submission_fields.7.abc123 nested path
152
+ const data = {
153
+ Job: { Total: 100 },
154
+ [fillablePath]: '20',
155
+ __submission_fields: { 7: { abc123: '5' } },
156
+ };
157
+ expect(evaluateFormula(formula, data)).toBe(125);
158
+ });
159
+
160
+ test('honors parentheses and operator precedence', () => {
161
+ const formula: StructuredFormula = {
162
+ tokens: [
163
+ { type: 'lparen' },
164
+ { type: 'field', path: 'Job.Total', fieldType: 'number' },
165
+ { type: 'operator', value: '-' },
166
+ { type: 'field', path: 'Job.Discount', fieldType: 'number' },
167
+ { type: 'rparen' },
168
+ { type: 'operator', value: '*' },
169
+ { type: 'number', value: '0.1' },
170
+ ],
171
+ };
172
+ expect(evaluateFormula(formula, { Job: { Total: 200, Discount: 50 } })).toBeCloseTo(
173
+ 15,
174
+ 5,
175
+ );
176
+ });
177
+
178
+ test('returns null when any field value is missing', () => {
179
+ const formula: StructuredFormula = {
180
+ tokens: [
181
+ { type: 'field', path: 'Job.Total', fieldType: 'number' },
182
+ { type: 'operator', value: '+' },
183
+ { type: 'field', path: 'Job.Discount', fieldType: 'number' },
184
+ ],
185
+ };
186
+ expect(evaluateFormula(formula, { Job: { Total: 100 } })).toBeNull();
187
+ });
188
+
189
+ test('returns 0 when dividing by zero rather than throwing', () => {
190
+ const formula: StructuredFormula = {
191
+ tokens: [
192
+ { type: 'number', value: '10' },
193
+ { type: 'operator', value: '/' },
194
+ { type: 'number', value: '0' },
195
+ ],
196
+ };
197
+ expect(evaluateFormula(formula, {})).toBe(0);
198
+ });
199
+ });
200
+
201
+ describe('with result formatting', () => {
202
+ test('applies currency prefix with thousands and decimals', () => {
203
+ const formatted = formatCalculatedResult(1234.5, {
204
+ ...baseNumberFormat,
205
+ resultType: 'currency',
206
+ });
207
+ expect(formatted).toBe('$1,234.50');
208
+ });
209
+
210
+ test('applies percent postfix when configured', () => {
211
+ const formatted = formatCalculatedResult(0.42, {
212
+ ...baseNumberFormat,
213
+ prefixText: '',
214
+ postfixText: '%',
215
+ decimals: 1,
216
+ thousandsSeparator: false,
217
+ });
218
+ expect(formatted).toBe('0.4%');
219
+ });
220
+
221
+ test('returns empty string for non-finite formula result', () => {
222
+ expect(formatCalculatedResult(NaN, baseNumberFormat)).toBe('');
223
+ });
224
+ });
225
+
226
+ describe('with full pipeline', () => {
227
+ test('parses, validates, evaluates, and formats a customer-discount formula', () => {
228
+ const expression = '( Job.Total - Job.Discount ) * 0.1';
229
+ const tokens = parseExpression(expression, validPaths);
230
+ const formula: StructuredFormula = { tokens };
231
+
232
+ const validation = validateFormula(formula, validPaths);
233
+ expect(validation.valid).toBe(true);
234
+
235
+ const value = evaluateFormula(formula, { Job: { Total: 1500, Discount: 200 } });
236
+ expect(value).toBe(130);
237
+
238
+ const formatted = formatCalculatedResult(value as number, {
239
+ ...baseNumberFormat,
240
+ resultType: 'currency',
241
+ });
242
+ expect(formatted).toBe('$130.00');
243
+ });
244
+ });
245
+ });
@@ -0,0 +1,208 @@
1
+ import {
2
+ FieldTypeEnum,
3
+ SchemaObject,
4
+ StructuredFormula,
5
+ } from '../../interface/types';
6
+ import {
7
+ evaluateFormula,
8
+ extractGroupedFieldsFromDataModel,
9
+ getDataPointOptions,
10
+ getSchemaDataPointOptions,
11
+ resolvePdfDataValues,
12
+ } from '../../utils';
13
+
14
+ /**
15
+ * Scenario tests for merge tag (data model) fields.
16
+ *
17
+ * Merge tags come from a JSON schema describing the data model. They appear
18
+ * in the sidebar (grouped), in display-condition drop-downs (only fields
19
+ * flagged useInConditionals/useInCalculatedFields), and resolve to runtime
20
+ * values via dotted paths.
21
+ */
22
+
23
+ const customerSchema: SchemaObject = {
24
+ type: 'object',
25
+ properties: {
26
+ FirstName: {
27
+ type: 'string',
28
+ title: 'First Name',
29
+ options: { useInConditionals: true },
30
+ },
31
+ LastName: { type: 'string', title: 'Last Name' },
32
+ Job: {
33
+ type: 'object',
34
+ title: 'Job',
35
+ properties: {
36
+ Total: {
37
+ type: 'number',
38
+ title: 'Total',
39
+ options: { useInCalculatedFields: true, useInConditionals: true },
40
+ },
41
+ Discount: {
42
+ type: 'number',
43
+ title: 'Discount',
44
+ options: { useInCalculatedFields: true },
45
+ },
46
+ Notes: { type: 'string', title: 'Notes' },
47
+ Technicians: {
48
+ type: 'array',
49
+ items: {
50
+ type: 'object',
51
+ properties: { Name: { type: 'string' } },
52
+ },
53
+ },
54
+ },
55
+ },
56
+ },
57
+ };
58
+
59
+ const customerData = {
60
+ FirstName: 'Jane',
61
+ LastName: 'Doe',
62
+ Job: {
63
+ Total: 150,
64
+ Discount: 20,
65
+ Notes: 'Standard service',
66
+ Price: { Amount: 100, Currency: 'USD' },
67
+ Custom: { Value: 'wrapped value' },
68
+ },
69
+ };
70
+
71
+ describe('merge tag (data model) field scenarios', () => {
72
+ describe('with sidebar field extraction', () => {
73
+ const subject = () => extractGroupedFieldsFromDataModel(customerSchema);
74
+
75
+ test('groups top-level simple fields under "Fields"', () => {
76
+ const groups = subject();
77
+ const fieldsGroup = groups.find(g => g.groupName === 'Fields');
78
+ expect(fieldsGroup?.fields.map(f => f.path)).toEqual(['FirstName', 'LastName']);
79
+ });
80
+
81
+ test('groups nested object fields under the object title', () => {
82
+ const groups = subject();
83
+ const jobGroup = groups.find(g => g.groupName === 'Job');
84
+ expect(jobGroup?.fields.map(f => f.path).sort()).toEqual([
85
+ 'Job.Discount',
86
+ 'Job.Notes',
87
+ 'Job.Total',
88
+ ]);
89
+ });
90
+
91
+ test('excludes array properties', () => {
92
+ const groups = subject();
93
+ const jobGroup = groups.find(g => g.groupName === 'Job');
94
+ expect(jobGroup?.fields.some(f => f.path?.includes('Technicians'))).toBe(false);
95
+ });
96
+
97
+ test('marks numeric fields with fieldType number', () => {
98
+ const groups = subject();
99
+ const jobGroup = groups.find(g => g.groupName === 'Job');
100
+ const total = jobGroup?.fields.find(f => f.path === 'Job.Total');
101
+ expect(total?.fieldType).toBe('number');
102
+ expect(total?.type).toBe(FieldTypeEnum.dataModel);
103
+ });
104
+ });
105
+
106
+ describe('with calculated-only field extraction', () => {
107
+ test('returns only fields flagged useInCalculatedFields', () => {
108
+ const groups = extractGroupedFieldsFromDataModel(customerSchema, {
109
+ onlyUseInCalculatedFields: true,
110
+ });
111
+ const allPaths = groups.flatMap(g => g.fields.map(f => f.path)).sort();
112
+ expect(allPaths).toEqual(['Job.Discount', 'Job.Total']);
113
+ });
114
+ });
115
+
116
+ describe('with display-condition options', () => {
117
+ test('exposes fields flagged useInConditionals or useInCalculatedFields', () => {
118
+ const options = getSchemaDataPointOptions(customerSchema);
119
+ expect(options.map(o => o.fullKey).sort()).toEqual([
120
+ 'FirstName',
121
+ 'Job.Discount',
122
+ 'Job.Total',
123
+ ]);
124
+ });
125
+
126
+ test('omits fields without either flag', () => {
127
+ const options = getSchemaDataPointOptions(customerSchema);
128
+ expect(options.some(o => o.fullKey === 'Job.Notes')).toBe(false);
129
+ expect(options.some(o => o.fullKey === 'LastName')).toBe(false);
130
+ });
131
+
132
+ test('uses dash-joined titles for nested fields', () => {
133
+ const options = getSchemaDataPointOptions(customerSchema);
134
+ const total = options.find(o => o.fullKey === 'Job.Total');
135
+ expect(total?.title).toBe('Job - Total');
136
+ });
137
+
138
+ test('combines schema and document field options sorted alphabetically', () => {
139
+ const fillableField = {
140
+ id: 'f1',
141
+ type: FieldTypeEnum.fillable,
142
+ subType: 'text' as const,
143
+ x: 0,
144
+ y: 0,
145
+ page: 1,
146
+ label: 'Custom Note',
147
+ width: 100,
148
+ height: 25,
149
+ path: 'fillable_signer1_CustomNote',
150
+ };
151
+ const options = getDataPointOptions(customerSchema, [fillableField]);
152
+ expect(options.map(o => o.title)).toEqual([
153
+ 'Custom Note',
154
+ 'First Name',
155
+ 'Job - Discount',
156
+ 'Job - Total',
157
+ ]);
158
+ });
159
+ });
160
+
161
+ describe('with value resolution at runtime', () => {
162
+ const subject = (path: string) => resolvePdfDataValues(customerData, path);
163
+
164
+ test('resolves a top-level string property', () => {
165
+ expect(subject('FirstName')).toBe('Jane');
166
+ });
167
+
168
+ test('resolves a nested numeric property as string', () => {
169
+ expect(subject('Job.Total')).toBe('150');
170
+ });
171
+
172
+ test('formats Amount/Currency objects as "{Currency} {Amount}"', () => {
173
+ expect(subject('Job.Price')).toBe('USD 100');
174
+ });
175
+
176
+ test('unwraps Value-wrapped objects', () => {
177
+ expect(subject('Job.Custom')).toBe('wrapped value');
178
+ });
179
+
180
+ test('returns empty string for missing path', () => {
181
+ expect(subject('Job.NonExistent')).toBe('');
182
+ });
183
+ });
184
+
185
+ describe('with merge tags used in a formula', () => {
186
+ test('evaluates Job.Total - Job.Discount using runtime data', () => {
187
+ const formula: StructuredFormula = {
188
+ tokens: [
189
+ { type: 'field', path: 'Job.Total', fieldType: 'number' },
190
+ { type: 'operator', value: '-' },
191
+ { type: 'field', path: 'Job.Discount', fieldType: 'number' },
192
+ ],
193
+ };
194
+ expect(evaluateFormula(formula, customerData)).toBe(130);
195
+ });
196
+
197
+ test('returns null when merge tag value is missing', () => {
198
+ const formula: StructuredFormula = {
199
+ tokens: [
200
+ { type: 'field', path: 'Job.NonExistent', fieldType: 'number' },
201
+ { type: 'operator', value: '+' },
202
+ { type: 'number', value: '10' },
203
+ ],
204
+ };
205
+ expect(evaluateFormula(formula, customerData)).toBeNull();
206
+ });
207
+ });
208
+ });
@@ -162,6 +162,17 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
162
162
  }
163
163
  />
164
164
  )}
165
+ {field.type === FieldTypeEnum.fillable && field.subType === 'text' && (
166
+ <Checkbox
167
+ label="Multi-line"
168
+ checked={field.multiline ?? false}
169
+ onChange={() =>
170
+ onFieldConfigChange({
171
+ multiline: !field.multiline,
172
+ })
173
+ }
174
+ />
175
+ )}
165
176
  {field.type === FieldTypeEnum.calculated && (
166
177
  <FormulaGenerator
167
178
  dataModel={dataModel}
@@ -1,4 +1,4 @@
1
- import { FC, MouseEvent, RefObject } from 'react';
1
+ import { FC, MouseEvent, RefObject, useMemo } from 'react';
2
2
  import { PdfField } from '../../interface/types';
3
3
  import { PdfOverlayField } from './pdf-overlay-field';
4
4
 
@@ -27,6 +27,16 @@ export const PdfFieldsOverlay: FC<PdfFieldsOverlayProps> = ({
27
27
  recipientsColors,
28
28
  selectedField,
29
29
  }) => {
30
+ const pathToLabel = useMemo(() => {
31
+ const map = new Map<string, string>();
32
+ for (const f of fields) {
33
+ if (f.path && f.label) {
34
+ map.set(f.path, f.label);
35
+ }
36
+ }
37
+ return map;
38
+ }, [fields]);
39
+
30
40
  return (
31
41
  <div className="dte-pdf-field-overlay skeleton-item">
32
42
  {fields.map(field => (
@@ -43,6 +53,7 @@ export const PdfFieldsOverlay: FC<PdfFieldsOverlayProps> = ({
43
53
  isSelected={selectedField?.id === field.id}
44
54
  recipientsColors={recipientsColors}
45
55
  isSameGroup={selectedField?.path === field.path}
56
+ pathToLabel={pathToLabel}
46
57
  />
47
58
  ))}
48
59
  </div>
@@ -1,14 +1,30 @@
1
- import { FC } from 'react';
1
+ import { FC, useMemo } from 'react';
2
2
  import { PdfField } from '../../interface/types';
3
+ import { formulaToDisplayText } from '../../utils';
3
4
 
4
5
  interface PdfOverlayFieldCalculatedProps {
5
6
  field: PdfField;
7
+ pathToLabel?: Map<string, string>;
6
8
  }
7
9
 
8
- export const PdfOverlayFieldCalculated: FC<PdfOverlayFieldCalculatedProps> = ({ field }) => {
10
+ export const PdfOverlayFieldCalculated: FC<PdfOverlayFieldCalculatedProps> = ({
11
+ field,
12
+ pathToLabel,
13
+ }) => {
14
+ const displayText = useMemo(() => {
15
+ const formulaText = formulaToDisplayText(field.formula, pathToLabel);
16
+ if (formulaText) {
17
+ return formulaText;
18
+ }
19
+ if (field.label) {
20
+ return field.label;
21
+ }
22
+ return field.path;
23
+ }, [field.formula, field.label, field.path, pathToLabel]);
24
+
9
25
  return (
10
- <span>
11
- {field.label ? field.label : field.path}
26
+ <span className="ellipsis" title={displayText}>
27
+ {displayText}
12
28
  {field.required ? '*' : ''}
13
29
  </span>
14
30
  );
@@ -56,6 +56,16 @@ export const PdfOverlayFieldFillable: FC<PdfOverlayFieldFillableProps> = ({
56
56
  );
57
57
 
58
58
  default:
59
+ if (field.multiline) {
60
+ return (
61
+ <textarea
62
+ readOnly
63
+ placeholder={placeholderText}
64
+ className="dte-pdf-field-fillable"
65
+ style={{ resize: 'none' }}
66
+ />
67
+ );
68
+ }
59
69
  return (
60
70
  <input
61
71
  type="text"
@@ -17,6 +17,7 @@ interface PdfOverlayFieldProps {
17
17
  isSameGroup: boolean;
18
18
  recipientsColors: Record<string, string>;
19
19
  pdfWrapperRef: RefObject<HTMLDivElement>;
20
+ pathToLabel?: Map<string, string>;
20
21
  onFieldConfigChange(updates: Partial<PdfField>): void;
21
22
  handleAddNewField(field: PdfField): void;
22
23
  onFieldClick(fieldId: string, e: MouseEvent): void;
@@ -34,6 +35,7 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
34
35
  onFieldConfigChange,
35
36
  onFieldMove,
36
37
  onFieldResize,
38
+ pathToLabel,
37
39
  pdfWrapperRef,
38
40
  recipientsColors,
39
41
  }) => {
@@ -75,7 +77,9 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
75
77
  className={`dte-pdf-field ${isSelected ? '--selected' : '--unselected'} ${isDragging ? '--dragging' : ''} ${isSameGroup ? '--colored' : ''} ${error ? '--error' : ''}`}
76
78
  style={style}
77
79
  >
78
- {field.type === FieldTypeEnum.calculated && <PdfOverlayFieldCalculated field={field} />}
80
+ {field.type === FieldTypeEnum.calculated && (
81
+ <PdfOverlayFieldCalculated field={field} pathToLabel={pathToLabel} />
82
+ )}
79
83
  {field.type === FieldTypeEnum.eSign && <PdfOverlayFieldESign field={field} />}
80
84
  {(field.type === FieldTypeEnum.dataModel || field.type === FieldTypeEnum.forms) && (
81
85
  <PdfOverlayFieldDataModel field={field} />
@@ -1,14 +1,20 @@
1
1
  import { FC, useMemo } from 'react';
2
- import { DataModelValues, PdfField } from '../../interface/types';
2
+ import { DataModelValues, PdfField, PdfViewMode } from '../../interface/types';
3
3
  import { evaluateFormula, formatCalculatedResult, resolvePdfDataValues } from '../../utils';
4
4
 
5
5
  interface PdfViewCalculatedProps {
6
6
  field: PdfField;
7
7
  data?: DataModelValues;
8
8
  holidays?: string[];
9
+ viewMode?: PdfViewMode;
9
10
  }
10
11
 
11
- export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({ data, field, holidays }) => {
12
+ export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({
13
+ data,
14
+ field,
15
+ holidays,
16
+ viewMode,
17
+ }) => {
12
18
  const displayValue = useMemo(() => {
13
19
  if (field.formula?.tokens?.length) {
14
20
  const value = evaluateFormula(field.formula, data, holidays);
@@ -23,5 +29,9 @@ export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({ data, field, hol
23
29
  return field.label ?? '';
24
30
  }, [data, field.formula, field.formulaFormat, field.label, field.path, holidays]);
25
31
 
26
- return <div className="dte-pdf-field-value">{displayValue}</div>;
32
+ const isPreview = viewMode === 'preview';
33
+ const shouldShowPreviewFallback = isPreview && displayValue === '';
34
+ const content = shouldShowPreviewFallback ? field.label || 'Calculated Field' : displayValue;
35
+
36
+ return <div className="dte-pdf-field-value">{content}</div>;
27
37
  };