@servicetitan/dte-pdf-editor 1.44.0 → 1.46.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.
- package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.js +4 -2
- package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +3 -0
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.d.ts +8 -0
- package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.js +17 -8
- package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
- package/dist/interface/types.d.ts +1 -0
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js.map +1 -1
- package/dist/utils/shared/date.utils.d.ts +1 -0
- package/dist/utils/shared/date.utils.d.ts.map +1 -1
- package/dist/utils/shared/date.utils.js +12 -0
- package/dist/utils/shared/date.utils.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/field-types/date-fields.test.ts +212 -0
- package/src/__tests__/field-types/display-condition-fields.test.ts +340 -0
- package/src/__tests__/field-types/e-sign-fields.test.ts +97 -0
- package/src/__tests__/field-types/fillable-fields.test.ts +180 -0
- package/src/__tests__/field-types/form-fields.test.ts +193 -0
- package/src/__tests__/field-types/formula-fields.test.ts +245 -0
- package/src/__tests__/field-types/merge-tag-fields.test.ts +208 -0
- package/src/components/field-config-panel/field-config-panel.tsx +11 -0
- package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +10 -0
- package/src/components/pdf-view/pdf-view-fillable.tsx +61 -15
- package/src/interface/types.ts +1 -0
- package/src/styles/inline-editable.css +1 -0
- package/src/utils/conditions/__tests__/evaluate.utils.test.ts +163 -0
- package/src/utils/conditions/__tests__/schema-data-points.utils.test.ts +149 -0
- package/src/utils/data-model/__tests__/extract-fields.utils.test.ts +154 -0
- package/src/utils/data-model/__tests__/resolve-values.utils.test.ts +60 -0
- package/src/utils/field/__tests__/field-background-color.utils.test.ts +26 -0
- package/src/utils/field/__tests__/field-placeholder-text.utils.test.ts +46 -0
- package/src/utils/formula/__tests__/dom.utils.test.ts +33 -0
- package/src/utils/formula/__tests__/evaluate-formula.utils.test.ts +202 -0
- package/src/utils/formula/__tests__/expression.utils.test.ts +119 -0
- package/src/utils/formula/__tests__/form-fields.utils.test.ts +274 -0
- package/src/utils/formula/__tests__/format-calculated-result.utils.test.ts +105 -0
- package/src/utils/formula/__tests__/render-formula.utils.test.ts +43 -0
- package/src/utils/formula/__tests__/validate-formula.utils.test.ts +168 -0
- package/src/utils/path/__tests__/generate-e-sign-path.test.ts +26 -0
- package/src/utils/path/__tests__/generate-fillable-path.test.ts +17 -0
- package/src/utils/path/__tests__/parse-fillable-path.test.ts +25 -0
- package/src/utils/recipients/__tests__/map-colors.test.ts +40 -0
- package/src/utils/shared/__tests__/date.utils.test.ts +58 -0
- package/src/utils/shared/__tests__/number.utils.test.ts +48 -0
- package/src/utils/shared/date.utils.ts +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FC } from 'react';
|
|
2
2
|
import { DataChangePayload, DataModelValues, PdfField, PreviewMode } from '../../interface/types';
|
|
3
|
-
import { dateToUtcZero, getFieldPlaceholderText } from '../../utils';
|
|
3
|
+
import { dateToUtcZero, getFieldPlaceholderText, toDateInputValue } from '../../utils';
|
|
4
4
|
|
|
5
5
|
interface PdfViewFillableProps {
|
|
6
6
|
field: PdfField;
|
|
@@ -10,6 +10,59 @@ interface PdfViewFillableProps {
|
|
|
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,
|
|
@@ -91,6 +144,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
91
144
|
}
|
|
92
145
|
|
|
93
146
|
if (field.subType === 'date') {
|
|
147
|
+
const dateInputValue = toDateInputValue(resolvedValue);
|
|
94
148
|
return (
|
|
95
149
|
<input
|
|
96
150
|
type="date"
|
|
@@ -103,7 +157,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
103
157
|
disabled={!isFillable}
|
|
104
158
|
id={field.path}
|
|
105
159
|
name={field.path}
|
|
106
|
-
value={
|
|
160
|
+
value={dateInputValue}
|
|
107
161
|
onChange={e => {
|
|
108
162
|
const dateStr = e.target.value;
|
|
109
163
|
handleDataChange(dateStr ? dateToUtcZero(dateStr) : '');
|
|
@@ -133,21 +187,13 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
133
187
|
);
|
|
134
188
|
}
|
|
135
189
|
|
|
190
|
+
const TextComponent = field.multiline ? FillableTextareaInput : FillableTextInput;
|
|
136
191
|
return (
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
style={{
|
|
140
|
-
background: 'inherit',
|
|
141
|
-
width: 'inherit',
|
|
142
|
-
height: 'inherit',
|
|
143
|
-
borderColor: 'inherit',
|
|
144
|
-
}}
|
|
145
|
-
disabled={!isFillable}
|
|
146
|
-
id={field.path}
|
|
147
|
-
name={field.path}
|
|
192
|
+
<TextComponent
|
|
193
|
+
field={field}
|
|
148
194
|
value={resolvedValue ?? ''}
|
|
149
|
-
|
|
150
|
-
|
|
195
|
+
disabled={!isFillable}
|
|
196
|
+
onChange={handleDataChange}
|
|
151
197
|
/>
|
|
152
198
|
);
|
|
153
199
|
};
|
package/src/interface/types.ts
CHANGED
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { FieldTypeEnum, SchemaObject } from '../../../interface/types';
|
|
2
|
+
import { extractGroupedFieldsFromDataModel } from '../extract-fields.utils';
|
|
3
|
+
|
|
4
|
+
const emptySchema: SchemaObject = { type: 'object', properties: {} };
|
|
5
|
+
|
|
6
|
+
describe('extractGroupedFieldsFromDataModel', () => {
|
|
7
|
+
const subject = (
|
|
8
|
+
schema: SchemaObject,
|
|
9
|
+
options?: Parameters<typeof extractGroupedFieldsFromDataModel>[1],
|
|
10
|
+
) => extractGroupedFieldsFromDataModel(schema, options);
|
|
11
|
+
|
|
12
|
+
describe('when schema has no properties', () => {
|
|
13
|
+
test('returns empty array', () => {
|
|
14
|
+
expect(subject(emptySchema)).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('with top-level simple fields', () => {
|
|
19
|
+
const schema: SchemaObject = {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
firstName: { type: 'string', title: 'First Name' },
|
|
23
|
+
age: { type: 'number' },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
test('groups simple fields under the default "Fields" group', () => {
|
|
28
|
+
const result = subject(schema);
|
|
29
|
+
expect(result).toHaveLength(1);
|
|
30
|
+
expect(result[0].groupName).toBe('Fields');
|
|
31
|
+
expect(result[0].fields).toEqual([
|
|
32
|
+
{
|
|
33
|
+
label: 'First Name',
|
|
34
|
+
type: FieldTypeEnum.dataModel,
|
|
35
|
+
path: 'firstName',
|
|
36
|
+
fieldType: undefined,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'age',
|
|
40
|
+
type: FieldTypeEnum.dataModel,
|
|
41
|
+
path: 'age',
|
|
42
|
+
fieldType: 'number',
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('with object properties', () => {
|
|
49
|
+
const schema: SchemaObject = {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
user: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
title: 'User Info',
|
|
55
|
+
properties: {
|
|
56
|
+
name: { type: 'string', title: 'Name' },
|
|
57
|
+
address: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
city: { type: 'string', title: 'City' },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
test('flattens nested object properties into one group per top-level object', () => {
|
|
69
|
+
const result = subject(schema);
|
|
70
|
+
expect(result).toHaveLength(1);
|
|
71
|
+
expect(result[0].groupName).toBe('User Info');
|
|
72
|
+
expect(result[0].fields).toEqual([
|
|
73
|
+
{
|
|
74
|
+
label: 'Name',
|
|
75
|
+
type: FieldTypeEnum.dataModel,
|
|
76
|
+
path: 'user.name',
|
|
77
|
+
fieldType: undefined,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: 'City',
|
|
81
|
+
type: FieldTypeEnum.dataModel,
|
|
82
|
+
path: 'user.address.city',
|
|
83
|
+
fieldType: undefined,
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('places default-group fields before object groups', () => {
|
|
90
|
+
const schema: SchemaObject = {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
top: { type: 'string' },
|
|
94
|
+
user: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
title: 'User',
|
|
97
|
+
properties: { name: { type: 'string' } },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const result = subject(schema);
|
|
102
|
+
expect(result.map(g => g.groupName)).toEqual(['Fields', 'User']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('skips array properties', () => {
|
|
106
|
+
const schema: SchemaObject = {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
items: { type: 'array', items: { type: 'string' } },
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
expect(subject(schema)).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('skips empty object groups', () => {
|
|
116
|
+
const schema: SchemaObject = {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
empty: { type: 'object', properties: {} },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
expect(subject(schema)).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('with onlyUseInCalculatedFields option', () => {
|
|
126
|
+
const schema: SchemaObject = {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
included: {
|
|
130
|
+
type: 'number',
|
|
131
|
+
options: { useInCalculatedFields: true },
|
|
132
|
+
},
|
|
133
|
+
excluded: { type: 'number' },
|
|
134
|
+
user: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
title: 'User',
|
|
137
|
+
properties: {
|
|
138
|
+
salary: {
|
|
139
|
+
type: 'number',
|
|
140
|
+
options: { useInCalculatedFields: true },
|
|
141
|
+
},
|
|
142
|
+
notes: { type: 'string' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
test('keeps only fields flagged with useInCalculatedFields', () => {
|
|
149
|
+
const result = subject(schema, { onlyUseInCalculatedFields: true });
|
|
150
|
+
const allPaths = result.flatMap(g => g.fields.map(f => f.path));
|
|
151
|
+
expect(allPaths.sort()).toEqual(['included', 'user.salary']);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { DataModelValues } from '../../../interface/types';
|
|
2
|
+
import { resolvePdfDataValues } from '../resolve-values.utils';
|
|
3
|
+
|
|
4
|
+
describe('resolvePdfDataValues', () => {
|
|
5
|
+
const subject = (data: DataModelValues | undefined, path: string | undefined) =>
|
|
6
|
+
resolvePdfDataValues(data, path);
|
|
7
|
+
|
|
8
|
+
describe('when data or path are missing', () => {
|
|
9
|
+
test.each([
|
|
10
|
+
['data is undefined', undefined, 'some.path'],
|
|
11
|
+
['path is undefined', { foo: 'bar' }, undefined],
|
|
12
|
+
['path is empty', { foo: 'bar' }, ''],
|
|
13
|
+
])('returns empty string when %s', (_, data, path) => {
|
|
14
|
+
expect(subject(data as DataModelValues | undefined, path)).toBe('');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('with primitive resolution', () => {
|
|
19
|
+
test('resolves a top-level string property', () => {
|
|
20
|
+
expect(subject({ name: 'John' }, 'name')).toBe('John');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('resolves a nested string property', () => {
|
|
24
|
+
expect(subject({ user: { name: 'John' } }, 'user.name')).toBe('John');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('resolves a numeric property as string', () => {
|
|
28
|
+
expect(subject({ count: 42 }, 'count')).toBe('42');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('resolves a boolean property as string', () => {
|
|
32
|
+
expect(subject({ active: true }, 'active')).toBe('true');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('when value is missing', () => {
|
|
37
|
+
test.each([
|
|
38
|
+
['null value', { user: null }, 'user.name'],
|
|
39
|
+
['undefined value', { user: undefined }, 'user.name'],
|
|
40
|
+
['unknown key', { user: { name: 'John' } }, 'user.email'],
|
|
41
|
+
['intermediate value is not object', { user: 'string' }, 'user.name'],
|
|
42
|
+
])('returns empty string for %s', (_, data, path) => {
|
|
43
|
+
expect(subject(data as DataModelValues, path)).toBe('');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('with object value resolution', () => {
|
|
48
|
+
test('formats amount/currency objects', () => {
|
|
49
|
+
expect(subject({ price: { Amount: 100, Currency: 'USD' } }, 'price')).toBe('USD 100');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('returns Value property when present', () => {
|
|
53
|
+
expect(subject({ field: { Value: 'inner' } }, 'field')).toBe('inner');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('stringifies generic objects via JSON', () => {
|
|
57
|
+
expect(subject({ obj: { foo: 'bar' } }, 'obj')).toBe('{"foo":"bar"}');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getFieldBackgroundColor } from '../field-background-color.utils';
|
|
2
|
+
|
|
3
|
+
describe('getFieldBackgroundColor', () => {
|
|
4
|
+
const subject = (recipient: string | undefined, recipientsColors: Record<string, string>) =>
|
|
5
|
+
getFieldBackgroundColor(recipient, recipientsColors);
|
|
6
|
+
|
|
7
|
+
test('returns color for recipient when present in map', () => {
|
|
8
|
+
expect(subject('signer1', { signer1: '#ff0000', signer2: '#00ff00' })).toBe('#ff0000');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('returns "none" when recipient is undefined', () => {
|
|
12
|
+
expect(subject(undefined, { signer1: '#ff0000' })).toBe('none');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('returns "none" when recipient is empty string', () => {
|
|
16
|
+
expect(subject('', { signer1: '#ff0000' })).toBe('none');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('returns "none" when recipient is not in the color map', () => {
|
|
20
|
+
expect(subject('unknown', { signer1: '#ff0000' })).toBe('none');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('returns "none" when color map is empty', () => {
|
|
24
|
+
expect(subject('signer1', {})).toBe('none');
|
|
25
|
+
});
|
|
26
|
+
});
|