@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.
- package/README.md +15 -0
- 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-fields-overlay.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +11 -1
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts +1 -0
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js +14 -2
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.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-fields-overlay/pdf-overlay-field.d.ts +1 -0
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +2 -2
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-calculated.d.ts +2 -1
- package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-calculated.js +5 -2
- package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.d.ts +10 -2
- package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.js +16 -12
- package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
- package/dist/components/pdf-view/pdf-view.d.ts +2 -2
- package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view.js +4 -4
- package/dist/components/pdf-view/pdf-view.js.map +1 -1
- package/dist/interface/types.d.ts +2 -1
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js.map +1 -1
- package/dist/utils/formula/expression.utils.d.ts +13 -0
- package/dist/utils/formula/expression.utils.d.ts.map +1 -1
- package/dist/utils/formula/expression.utils.js +42 -0
- package/dist/utils/formula/expression.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-fields-overlay.tsx +12 -1
- package/src/components/pdf-fields-overlay/pdf-overlay-field-calculated.tsx +20 -4
- package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +10 -0
- package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +5 -1
- package/src/components/pdf-view/pdf-view-calculated.tsx +13 -3
- package/src/components/pdf-view/pdf-view-fillable.tsx +62 -21
- package/src/components/pdf-view/pdf-view.tsx +11 -10
- package/src/interface/types.ts +12 -10
- package/src/styles/generic.css +6 -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/formula/expression.utils.ts +44 -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
|
@@ -1,30 +1,79 @@
|
|
|
1
1
|
import { FC } from 'react';
|
|
2
|
-
import { DataChangePayload, DataModelValues, PdfField,
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
viewMode,
|
|
19
72
|
}) => {
|
|
20
73
|
const resolvedValue = data?.[field.path!];
|
|
21
74
|
const isViewByCurrentRecipient = fillingBy?.includes(field.recipient!);
|
|
22
75
|
const isFillable =
|
|
23
|
-
|
|
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
|
-
<
|
|
139
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
*
|
|
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 (
|
|
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={
|
|
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={
|
|
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={
|
|
123
|
+
data={viewMode === 'preview' ? previewData : data}
|
|
123
124
|
field={field}
|
|
124
125
|
fillingBy={fillingBy}
|
|
125
|
-
|
|
126
|
+
viewMode={viewMode}
|
|
126
127
|
onDataChange={handleDataChange}
|
|
127
128
|
/>
|
|
128
129
|
)}
|
package/src/interface/types.ts
CHANGED
|
@@ -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
|
-
*
|
|
232
|
+
* PdfViewMode controls who is interacting with the PDF and how:
|
|
232
233
|
*
|
|
233
|
-
* - '
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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
|
-
* - '
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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
|
|
245
|
+
export type PdfViewMode = 'preview' | 'fill';
|
|
244
246
|
|
|
245
247
|
/**
|
|
246
248
|
* Display condition types for the Rules and Conditions modal and evaluation.
|
package/src/styles/generic.css
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
|
+
});
|