@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
@@ -1,30 +1,79 @@
1
1
  import { FC } from 'react';
2
- import { DataChangePayload, DataModelValues, PdfField, PreviewMode } from '../../interface/types';
2
+ import { DataChangePayload, DataModelValues, PdfField, PdfViewMode } from '../../interface/types';
3
3
  import { dateToUtcZero, getFieldPlaceholderText, toDateInputValue } from '../../utils';
4
4
 
5
5
  interface PdfViewFillableProps {
6
6
  field: PdfField;
7
7
  data?: DataModelValues;
8
8
  fillingBy?: string[];
9
- previewMode?: PreviewMode;
9
+ viewMode?: PdfViewMode;
10
10
  onDataChange?(changedData: DataChangePayload, field: PdfField): void;
11
11
  }
12
12
 
13
+ interface FillableTextInputProps {
14
+ field: PdfField;
15
+ value: string;
16
+ disabled: boolean;
17
+ onChange(value: string): void;
18
+ }
19
+
20
+ const fillableTextStyles = {
21
+ background: 'inherit',
22
+ width: 'inherit',
23
+ height: 'inherit',
24
+ borderColor: 'inherit',
25
+ } as const;
26
+
27
+ export const FillableTextInput: FC<FillableTextInputProps> = ({
28
+ disabled,
29
+ field,
30
+ onChange,
31
+ value,
32
+ }) => {
33
+ return (
34
+ <input
35
+ type="text"
36
+ style={fillableTextStyles}
37
+ disabled={disabled}
38
+ id={field.path}
39
+ name={field.path}
40
+ value={value}
41
+ onChange={e => onChange(e.target.value)}
42
+ placeholder={getFieldPlaceholderText(field)}
43
+ />
44
+ );
45
+ };
46
+
47
+ export const FillableTextareaInput: FC<FillableTextInputProps> = ({
48
+ disabled,
49
+ field,
50
+ onChange,
51
+ value,
52
+ }) => {
53
+ return (
54
+ <textarea
55
+ style={{ ...fillableTextStyles, resize: 'none' }}
56
+ disabled={disabled}
57
+ id={field.path}
58
+ name={field.path}
59
+ value={value}
60
+ onChange={e => onChange(e.target.value)}
61
+ placeholder={getFieldPlaceholderText(field)}
62
+ />
63
+ );
64
+ };
65
+
13
66
  export const PdfViewFillable: FC<PdfViewFillableProps> = ({
14
67
  data,
15
68
  field,
16
69
  fillingBy,
17
70
  onDataChange,
18
- previewMode,
71
+ viewMode,
19
72
  }) => {
20
73
  const resolvedValue = data?.[field.path!];
21
74
  const isViewByCurrentRecipient = fillingBy?.includes(field.recipient!);
22
75
  const isFillable =
23
- previewMode === 'fillable'
24
- ? true
25
- : previewMode === 'view'
26
- ? false
27
- : isViewByCurrentRecipient;
76
+ viewMode === 'preview' ? true : viewMode === 'fill' ? false : isViewByCurrentRecipient;
28
77
 
29
78
  const handleDataChange = (fieldValue: DataChangePayload[string]) => {
30
79
  const changedData = { [field.path!]: fieldValue };
@@ -134,21 +183,13 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
134
183
  );
135
184
  }
136
185
 
186
+ const TextComponent = field.multiline ? FillableTextareaInput : FillableTextInput;
137
187
  return (
138
- <input
139
- type="text"
140
- style={{
141
- background: 'inherit',
142
- width: 'inherit',
143
- height: 'inherit',
144
- borderColor: 'inherit',
145
- }}
146
- disabled={!isFillable}
147
- id={field.path}
148
- name={field.path}
188
+ <TextComponent
189
+ field={field}
149
190
  value={resolvedValue ?? ''}
150
- onChange={e => handleDataChange(e.target.value)}
151
- placeholder={getFieldPlaceholderText(field)}
191
+ disabled={!isFillable}
192
+ onChange={handleDataChange}
152
193
  />
153
194
  );
154
195
  };
@@ -6,7 +6,7 @@ import {
6
6
  DataModelValues,
7
7
  FieldTypeEnum,
8
8
  PdfField,
9
- PreviewMode,
9
+ PdfViewMode,
10
10
  RecipientInfo,
11
11
  } from '../../interface/types';
12
12
  import { mapColorsToRecipients } from '../../utils';
@@ -21,7 +21,7 @@ import { PdfViewGeneric } from './pdf-view-generic';
21
21
  export interface PdfViewProps {
22
22
  fields: PdfField[];
23
23
  errors?: Record<string, string>;
24
- previewMode?: PreviewMode;
24
+ viewMode?: PdfViewMode;
25
25
  data?: DataModelValues;
26
26
  pdfUrl: string;
27
27
  loading?: boolean;
@@ -34,8 +34,8 @@ export interface PdfViewProps {
34
34
  /*
35
35
  * fillingBy defines the list of recipient names
36
36
  * who are allowed to fill this field.
37
- * This restriction is enforced only when PreviewMode
38
- * is NOT active (i.e., during actual form filling).
37
+ * This restriction is enforced only in 'fill' mode
38
+ * (i.e., during actual form filling), not in 'preview' mode.
39
39
  */
40
40
  fillingBy?: string[];
41
41
  loadingPlaceholder?: ReactNode;
@@ -57,8 +57,8 @@ export const PdfView: FC<PdfViewProps> = ({
57
57
  onDataChange,
58
58
  onLoadSuccess,
59
59
  pdfUrl,
60
- previewMode,
61
60
  recipients,
61
+ viewMode,
62
62
  }) => {
63
63
  const [isPdfLoaded, setIsPdfLoaded] = useState<boolean>(false);
64
64
  const [previewData, setPreviewData] = useState<DataModelValues>(data ?? {});
@@ -71,7 +71,7 @@ export const PdfView: FC<PdfViewProps> = ({
71
71
  };
72
72
 
73
73
  const handleDataChange = (data: DataChangePayload, field: PdfField) => {
74
- if (previewMode === 'fillable') {
74
+ if (viewMode === 'preview') {
75
75
  setPreviewData(prev => ({
76
76
  ...prev,
77
77
  ...data,
@@ -98,7 +98,7 @@ export const PdfView: FC<PdfViewProps> = ({
98
98
  <PdfViewFieldContainer
99
99
  key={field.id}
100
100
  field={field}
101
- data={previewMode === 'fillable' ? previewData : data}
101
+ data={viewMode === 'preview' ? previewData : data}
102
102
  error={errors[field.path!] || ''}
103
103
  recipientsColors={recipientsColors}
104
104
  pdfWrapperRef={pdfWrapperRef}
@@ -113,16 +113,17 @@ export const PdfView: FC<PdfViewProps> = ({
113
113
  {field.type === FieldTypeEnum.calculated && (
114
114
  <PdfViewCalculated
115
115
  field={field}
116
- data={previewMode === 'fillable' ? previewData : data}
116
+ data={viewMode === 'preview' ? previewData : data}
117
117
  holidays={holidays}
118
+ viewMode={viewMode}
118
119
  />
119
120
  )}
120
121
  {field.type === FieldTypeEnum.fillable && (
121
122
  <PdfViewFillable
122
- data={previewMode === 'fillable' ? previewData : data}
123
+ data={viewMode === 'preview' ? previewData : data}
123
124
  field={field}
124
125
  fillingBy={fillingBy}
125
- previewMode={previewMode}
126
+ viewMode={viewMode}
126
127
  onDataChange={handleDataChange}
127
128
  />
128
129
  )}
@@ -96,6 +96,7 @@ export interface PdfField {
96
96
  formula?: StructuredFormula;
97
97
  formulaFormat?: CalculatedFieldFormat;
98
98
  displayCondition?: DisplayConditionState | null;
99
+ multiline?: boolean;
99
100
  }
100
101
 
101
102
  export type SchemaFieldType = 'number' | 'date';
@@ -228,19 +229,20 @@ export interface DataChangePayload {
228
229
  }
229
230
 
230
231
  /*
231
- * PreviewMode controls how the form is displayed:
232
+ * PdfViewMode controls who is interacting with the PDF and how:
232
233
  *
233
- * - 'fillable':
234
- * Allows the user to fill all fillable fields,
235
- * but the data is NOT saved it’s only for previewing
236
- * how the form will look when filled.
234
+ * - 'preview':
235
+ * Author/admin preview. All fields are visible and interactive so
236
+ * the admin can try out the form and see its behavior, but data is
237
+ * NOT persisted. Recipient scoping is ignored.
237
238
  *
238
- * - 'view':
239
- * Does NOT allow editing any fields.
240
- * Fields are shown as read-only, and if a value exists,
241
- * it is displayed.
239
+ * - 'fill':
240
+ * Real recipient fill experience. Only the current recipient's own
241
+ * empty fields are editable. Fields owned by other recipients are
242
+ * hidden if empty, or shown read-only if already filled.
243
+ * Already-filled values from any recipient are shown read-only.
242
244
  */
243
- export type PreviewMode = 'fillable' | 'view';
245
+ export type PdfViewMode = 'preview' | 'fill';
244
246
 
245
247
  /**
246
248
  * Display condition types for the Rules and Conditions modal and evaluation.
@@ -1,3 +1,9 @@
1
1
  .full-width {
2
2
  width: 100%;
3
3
  }
4
+
5
+ .ellipsis {
6
+ white-space: nowrap;
7
+ overflow: hidden;
8
+ text-overflow: ellipsis;
9
+ }
@@ -17,4 +17,5 @@
17
17
 
18
18
  .inline-editable-html-text {
19
19
  margin: var(--spacing-0, 0);
20
+ white-space: pre-wrap;
20
21
  }
@@ -0,0 +1,163 @@
1
+ import {
2
+ DisplayConditionSingle,
3
+ DisplayConditionState,
4
+ } from '../../../interface/types';
5
+ import { evaluateDisplayCondition } from '../evaluate.utils';
6
+
7
+ const buildCondition = (overrides: Partial<DisplayConditionSingle> = {}): DisplayConditionSingle => ({
8
+ id: 'c1',
9
+ dataPointKey: 'name',
10
+ operator: 'is_equal_to',
11
+ value: 'John',
12
+ ...overrides,
13
+ });
14
+
15
+ const buildState = (
16
+ conditions: DisplayConditionSingle[],
17
+ behavior: DisplayConditionState['behavior'] = 'show',
18
+ ): DisplayConditionState => ({
19
+ behavior,
20
+ groups: [{ id: 'g1', conditions }],
21
+ });
22
+
23
+ describe('evaluateDisplayCondition', () => {
24
+ const subject = (state: DisplayConditionState, data: Record<string, unknown> | undefined) =>
25
+ evaluateDisplayCondition(state, data);
26
+
27
+ describe('when no valid conditions exist', () => {
28
+ test('returns true when groups have no usable conditions', () => {
29
+ const state = buildState([buildCondition({ dataPointKey: '', value: '' })]);
30
+ expect(subject(state, { name: 'John' })).toBe(true);
31
+ });
32
+
33
+ test('returns true when state has no groups', () => {
34
+ expect(subject({ behavior: 'show', groups: [] }, {})).toBe(true);
35
+ });
36
+ });
37
+
38
+ describe('with string operators', () => {
39
+ test.each([
40
+ ['is_equal_to', 'John', 'John', true],
41
+ ['is_equal_to', 'John', 'Jane', false],
42
+ ['is_not_equal_to', 'John', 'Jane', true],
43
+ ['is_not_equal_to', 'John', 'John', false],
44
+ ['contains', 'John Doe', 'Doe', true],
45
+ ['contains', 'John', 'Doe', false],
46
+ ['does_not_contain', 'John', 'Doe', true],
47
+ ['does_not_contain', 'John Doe', 'Doe', false],
48
+ ['starts_with', 'John', 'Jo', true],
49
+ ['starts_with', 'John', 'Do', false],
50
+ ['ends_with', 'John', 'hn', true],
51
+ ['ends_with', 'John', 'Do', false],
52
+ ])('%s "%s" vs "%s" returns %s', (operator, actual, expected, result) => {
53
+ const state = buildState([
54
+ buildCondition({ operator, value: expected }),
55
+ ]);
56
+ expect(subject(state, { name: actual })).toBe(result);
57
+ });
58
+ });
59
+
60
+ describe('with valueless operators', () => {
61
+ test.each([
62
+ ['is_empty', '', '', true],
63
+ ['is_empty', 'John', '', false],
64
+ ['is_not_empty', 'John', '', true],
65
+ ['is_not_empty', '', '', false],
66
+ ])('%s with actual "%s" returns %s', (operator, actual, value, expected) => {
67
+ const state = buildState([buildCondition({ operator, value })]);
68
+ expect(subject(state, { name: actual })).toBe(expected);
69
+ });
70
+ });
71
+
72
+ describe('with numeric operators', () => {
73
+ test.each([
74
+ ['num_eq', 5, '5', true],
75
+ ['num_eq', 5, '6', false],
76
+ ['num_neq', 5, '6', true],
77
+ ['num_gt', 10, '5', true],
78
+ ['num_gt', 5, '10', false],
79
+ ['num_lt', 5, '10', true],
80
+ ['num_gte', 5, '5', true],
81
+ ['num_gte', 4, '5', false],
82
+ ['num_lte', 5, '5', true],
83
+ ['num_lte', 6, '5', false],
84
+ ])('%s with %p vs "%s" returns %s', (operator, actual, value, expected) => {
85
+ const state = buildState([
86
+ buildCondition({ dataPointKey: 'amount', operator, value }),
87
+ ]);
88
+ expect(subject(state, { amount: actual })).toBe(expected);
89
+ });
90
+ });
91
+
92
+ describe('with nested data', () => {
93
+ test('reads value at nested path', () => {
94
+ const state = buildState([
95
+ buildCondition({ dataPointKey: 'user.name', value: 'John' }),
96
+ ]);
97
+ expect(subject(state, { user: { name: 'John' } })).toBe(true);
98
+ });
99
+
100
+ test('treats missing value as empty string', () => {
101
+ const state = buildState([
102
+ buildCondition({ dataPointKey: 'user.name', operator: 'is_empty', value: '' }),
103
+ ]);
104
+ expect(subject(state, { user: {} })).toBe(true);
105
+ });
106
+ });
107
+
108
+ describe('with multiple conditions in a group', () => {
109
+ test('combines conditions with AND when next has logicalOperator and', () => {
110
+ const state = buildState([
111
+ buildCondition({ dataPointKey: 'a', value: '1' }),
112
+ buildCondition({ id: 'c2', dataPointKey: 'b', value: '2', logicalOperator: 'and' }),
113
+ ]);
114
+ expect(subject(state, { a: '1', b: '2' })).toBe(true);
115
+ expect(subject(state, { a: '1', b: '3' })).toBe(false);
116
+ });
117
+
118
+ test('combines conditions with OR when next has logicalOperator or', () => {
119
+ const state = buildState([
120
+ buildCondition({ dataPointKey: 'a', value: '1' }),
121
+ buildCondition({ id: 'c2', dataPointKey: 'b', value: '2', logicalOperator: 'or' }),
122
+ ]);
123
+ expect(subject(state, { a: '0', b: '2' })).toBe(true);
124
+ expect(subject(state, { a: '0', b: '0' })).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe('with multiple groups', () => {
129
+ const state: DisplayConditionState = {
130
+ behavior: 'show',
131
+ groups: [
132
+ {
133
+ id: 'g1',
134
+ conditions: [buildCondition({ dataPointKey: 'a', value: '1' })],
135
+ },
136
+ {
137
+ id: 'g2',
138
+ logicalOperator: 'or',
139
+ conditions: [buildCondition({ id: 'c2', dataPointKey: 'b', value: '2' })],
140
+ },
141
+ ],
142
+ };
143
+
144
+ test('returns true when either group matches with OR logic', () => {
145
+ expect(subject(state, { a: '0', b: '2' })).toBe(true);
146
+ });
147
+
148
+ test('returns false when neither group matches with OR logic', () => {
149
+ expect(subject(state, { a: '0', b: '0' })).toBe(false);
150
+ });
151
+ });
152
+
153
+ describe('with hide behavior', () => {
154
+ test('inverts result when behavior is hide', () => {
155
+ const state = buildState(
156
+ [buildCondition({ dataPointKey: 'name', value: 'John' })],
157
+ 'hide',
158
+ );
159
+ expect(subject(state, { name: 'John' })).toBe(false);
160
+ expect(subject(state, { name: 'Jane' })).toBe(true);
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,149 @@
1
+ import { FieldTypeEnum, PdfField, SchemaObject } from '../../../interface/types';
2
+ import {
3
+ getDataPointOptions,
4
+ getDocumentFieldsDataPointOptions,
5
+ getSchemaDataPointOptions,
6
+ } from '../schema-data-points.utils';
7
+
8
+ const buildField = (overrides: Partial<PdfField> = {}): PdfField => ({
9
+ id: 'f1',
10
+ type: FieldTypeEnum.fillable,
11
+ x: 0,
12
+ y: 0,
13
+ page: 1,
14
+ label: 'Label',
15
+ width: 100,
16
+ height: 25,
17
+ ...overrides,
18
+ });
19
+
20
+ describe('getSchemaDataPointOptions', () => {
21
+ const subject = (schema: SchemaObject | undefined) => getSchemaDataPointOptions(schema);
22
+
23
+ test('returns empty array when schema is undefined', () => {
24
+ expect(subject(undefined)).toEqual([]);
25
+ });
26
+
27
+ test('returns empty array when schema has no properties', () => {
28
+ expect(subject({ type: 'object', properties: {} })).toEqual([]);
29
+ });
30
+
31
+ test('returns only nodes flagged with useInConditionals or useInCalculatedFields', () => {
32
+ const schema: SchemaObject = {
33
+ type: 'object',
34
+ properties: {
35
+ name: {
36
+ type: 'string',
37
+ title: 'Name',
38
+ options: { useInConditionals: true },
39
+ },
40
+ age: {
41
+ type: 'number',
42
+ title: 'Age',
43
+ options: { useInCalculatedFields: true },
44
+ },
45
+ hiddenField: { type: 'string', title: 'Hidden' },
46
+ },
47
+ };
48
+ expect(subject(schema)).toEqual([
49
+ { fieldType: 'number', fullKey: 'age', title: 'Age' },
50
+ { fieldType: 'string', fullKey: 'name', title: 'Name' },
51
+ ]);
52
+ });
53
+
54
+ test('walks nested objects and joins titles with dash', () => {
55
+ const schema: SchemaObject = {
56
+ type: 'object',
57
+ properties: {
58
+ user: {
59
+ type: 'object',
60
+ title: 'User',
61
+ properties: {
62
+ name: {
63
+ type: 'string',
64
+ title: 'Name',
65
+ options: { useInConditionals: true },
66
+ },
67
+ },
68
+ },
69
+ },
70
+ };
71
+ expect(subject(schema)).toEqual([
72
+ { fieldType: 'string', fullKey: 'user.name', title: 'User - Name' },
73
+ ]);
74
+ });
75
+
76
+ test('returns options sorted alphabetically by title', () => {
77
+ const schema: SchemaObject = {
78
+ type: 'object',
79
+ properties: {
80
+ zebra: {
81
+ type: 'string',
82
+ title: 'Zebra',
83
+ options: { useInConditionals: true },
84
+ },
85
+ alpha: {
86
+ type: 'string',
87
+ title: 'Alpha',
88
+ options: { useInConditionals: true },
89
+ },
90
+ },
91
+ };
92
+ expect(subject(schema).map(o => o.title)).toEqual(['Alpha', 'Zebra']);
93
+ });
94
+ });
95
+
96
+ describe('getDocumentFieldsDataPointOptions', () => {
97
+ const subject = (fields: PdfField[] | undefined) => getDocumentFieldsDataPointOptions(fields);
98
+
99
+ test.each([
100
+ ['undefined', undefined],
101
+ ['empty array', []],
102
+ ])('returns empty array when fields is %s', (_, fields) => {
103
+ expect(subject(fields)).toEqual([]);
104
+ });
105
+
106
+ test('includes only fillable fields with a path', () => {
107
+ const fields = [
108
+ buildField({ id: '1', path: 'fillable_a_b', label: 'Alpha', subType: 'text' }),
109
+ buildField({ id: '2', path: 'fillable_c_d', label: 'Number', subType: 'number' }),
110
+ buildField({ id: '3', label: 'No Path' }),
111
+ buildField({ id: '4', path: 'esign_x_y', type: FieldTypeEnum.eSign, label: 'Sig' }),
112
+ ];
113
+ expect(subject(fields)).toEqual([
114
+ { fieldType: 'string', fullKey: 'fillable_a_b', title: 'Alpha' },
115
+ { fieldType: 'number', fullKey: 'fillable_c_d', title: 'Number' },
116
+ ]);
117
+ });
118
+
119
+ test('falls back to path when label is empty', () => {
120
+ const fields = [buildField({ path: 'fillable_a_b', label: '' })];
121
+ expect(subject(fields)).toEqual([
122
+ { fieldType: 'string', fullKey: 'fillable_a_b', title: 'fillable_a_b' },
123
+ ]);
124
+ });
125
+ });
126
+
127
+ describe('getDataPointOptions', () => {
128
+ const subject = (schema: SchemaObject | undefined, documentFields: PdfField[] | undefined) =>
129
+ getDataPointOptions(schema, documentFields);
130
+
131
+ test('merges schema and document field options sorted by title', () => {
132
+ const schema: SchemaObject = {
133
+ type: 'object',
134
+ properties: {
135
+ name: {
136
+ type: 'string',
137
+ title: 'Name',
138
+ options: { useInConditionals: true },
139
+ },
140
+ },
141
+ };
142
+ const fields = [buildField({ path: 'fillable_a_b', label: 'Address' })];
143
+
144
+ expect(subject(schema, fields)).toEqual([
145
+ { fieldType: 'string', fullKey: 'fillable_a_b', title: 'Address' },
146
+ { fieldType: 'string', fullKey: 'name', title: 'Name' },
147
+ ]);
148
+ });
149
+ });