@servicetitan/dte-pdf-editor 1.51.0 → 1.53.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 +67 -51
- package/dist/components/display-conditions/condition-row.d.ts.map +1 -1
- package/dist/components/display-conditions/condition-row.js +33 -11
- package/dist/components/display-conditions/condition-row.js.map +1 -1
- package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +1 -0
- package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-config-panel-overlay.js +2 -2
- package/dist/components/field-config-panel/field-config-panel-overlay.js.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.d.ts +1 -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 +38 -15
- package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.js +1 -1
- package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.d.ts +2 -0
- package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.js +14 -10
- package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
- package/dist/components/pdf-view/pdf-view.d.ts +11 -0
- package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view.js +2 -2
- package/dist/components/pdf-view/pdf-view.js.map +1 -1
- package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
- package/dist/hooks/usePdfFieldDnD.js +5 -0
- package/dist/hooks/usePdfFieldDnD.js.map +1 -1
- package/dist/hooks/usePdfFieldSelection.d.ts +1 -0
- package/dist/hooks/usePdfFieldSelection.d.ts.map +1 -1
- package/dist/hooks/usePdfFieldSelection.js +4 -0
- package/dist/hooks/usePdfFieldSelection.js.map +1 -1
- package/dist/interface/types.d.ts +16 -0
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js +5 -0
- package/dist/interface/types.js.map +1 -1
- package/dist/utils/conditions/schema-data-points.utils.d.ts +1 -0
- package/dist/utils/conditions/schema-data-points.utils.d.ts.map +1 -1
- package/dist/utils/conditions/schema-data-points.utils.js +28 -5
- package/dist/utils/conditions/schema-data-points.utils.js.map +1 -1
- package/package.json +1 -1
- package/src/components/display-conditions/condition-row.tsx +80 -21
- package/src/components/field-config-panel/field-config-panel-overlay.tsx +6 -0
- package/src/components/field-config-panel/field-config-panel.tsx +61 -12
- package/src/components/pdf-editor/pdf-editor.tsx +1 -0
- package/src/components/pdf-view/pdf-view-fillable.tsx +18 -3
- package/src/components/pdf-view/pdf-view.tsx +8 -1
- package/src/hooks/usePdfFieldDnD.ts +5 -0
- package/src/hooks/usePdfFieldSelection.ts +8 -0
- package/src/interface/types.ts +15 -0
- package/src/utils/conditions/__tests__/schema-data-points.utils.test.ts +35 -0
- package/src/utils/conditions/schema-data-points.utils.ts +36 -5
|
@@ -20,6 +20,10 @@ interface FieldConfigPanelOverlayProps {
|
|
|
20
20
|
recipients?: RecipientInfo[];
|
|
21
21
|
hideConditionalLogic?: boolean;
|
|
22
22
|
onFieldConfigChange(updates: Partial<PdfField>): void;
|
|
23
|
+
onMatchingFieldsConfigChange?(
|
|
24
|
+
predicate: (field: PdfField) => boolean,
|
|
25
|
+
updates: Partial<PdfField>,
|
|
26
|
+
): void;
|
|
23
27
|
onDeleteField(): void;
|
|
24
28
|
onDeselectField?(): void;
|
|
25
29
|
onRequestFormFields(formId: number): void;
|
|
@@ -35,6 +39,7 @@ export const FieldConfigPanelOverlay: FC<FieldConfigPanelOverlayProps> = ({
|
|
|
35
39
|
onDeleteField,
|
|
36
40
|
onDeselectField,
|
|
37
41
|
onFieldConfigChange,
|
|
42
|
+
onMatchingFieldsConfigChange,
|
|
38
43
|
onRequestFormFields,
|
|
39
44
|
recipients,
|
|
40
45
|
selectedField,
|
|
@@ -67,6 +72,7 @@ export const FieldConfigPanelOverlay: FC<FieldConfigPanelOverlayProps> = ({
|
|
|
67
72
|
formulaLoadingFormId={formulaLoadingFormId}
|
|
68
73
|
recipients={recipients}
|
|
69
74
|
onFieldConfigChange={onFieldConfigChange}
|
|
75
|
+
onMatchingFieldsConfigChange={onMatchingFieldsConfigChange}
|
|
70
76
|
onDeleteField={onDeleteField}
|
|
71
77
|
hideConditionalLogic={hideConditionalLogic}
|
|
72
78
|
onRequestFormFields={onRequestFormFields}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
generateESignPath,
|
|
15
15
|
generateFillablePath,
|
|
16
16
|
getFieldFontSize,
|
|
17
|
+
parseFillablePathName,
|
|
17
18
|
supportsFieldFontSize,
|
|
18
19
|
} from '../../utils';
|
|
19
20
|
import { DisplayCondition } from '../display-conditions/display-condition';
|
|
@@ -31,6 +32,10 @@ interface FieldConfigPanelProps {
|
|
|
31
32
|
hideConditionalLogic?: boolean;
|
|
32
33
|
onDeleteField(): void;
|
|
33
34
|
onFieldConfigChange(updates: Partial<PdfField>): void;
|
|
35
|
+
onMatchingFieldsConfigChange?(
|
|
36
|
+
predicate: (field: PdfField) => boolean,
|
|
37
|
+
updates: Partial<PdfField>,
|
|
38
|
+
): void;
|
|
34
39
|
onRequestFormFields(formId: number): void;
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -44,6 +49,7 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
44
49
|
hideConditionalLogic = false,
|
|
45
50
|
onDeleteField,
|
|
46
51
|
onFieldConfigChange,
|
|
52
|
+
onMatchingFieldsConfigChange,
|
|
47
53
|
onRequestFormFields,
|
|
48
54
|
recipients = [],
|
|
49
55
|
}) => {
|
|
@@ -61,20 +67,42 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
61
67
|
[recipientOptions, field.recipient],
|
|
62
68
|
);
|
|
63
69
|
|
|
70
|
+
const isRadioGroupSibling = (documentField: PdfField) =>
|
|
71
|
+
documentField.subType === 'radio' && documentField.path === field.path;
|
|
72
|
+
|
|
64
73
|
const handleRecipientChange = (item: { id: string; name: string } | null) => {
|
|
65
|
-
if (item) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
path
|
|
76
|
-
|
|
74
|
+
if (!item) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let path = field.path;
|
|
79
|
+
if (field.type === FieldTypeEnum.eSign) {
|
|
80
|
+
path = generateESignPath(item.id, field.subType as ESignFieldType);
|
|
81
|
+
}
|
|
82
|
+
if (field.type === FieldTypeEnum.fillable) {
|
|
83
|
+
const pathName =
|
|
84
|
+
field.subType === 'radio' && field.path
|
|
85
|
+
? parseFillablePathName(field.path)
|
|
86
|
+
: field.id;
|
|
87
|
+
path = generateFillablePath(item.id, pathName);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const updates = {
|
|
91
|
+
recipient: item.id,
|
|
92
|
+
path,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
field.type === FieldTypeEnum.fillable &&
|
|
97
|
+
field.subType === 'radio' &&
|
|
98
|
+
field.path &&
|
|
99
|
+
onMatchingFieldsConfigChange
|
|
100
|
+
) {
|
|
101
|
+
onMatchingFieldsConfigChange(isRadioGroupSibling, updates);
|
|
102
|
+
return;
|
|
77
103
|
}
|
|
104
|
+
|
|
105
|
+
onFieldConfigChange(updates);
|
|
78
106
|
};
|
|
79
107
|
|
|
80
108
|
const selectedESignFieldType = useMemo(
|
|
@@ -104,6 +132,19 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
104
132
|
}
|
|
105
133
|
};
|
|
106
134
|
|
|
135
|
+
const handleRadioGroupNameChange = (groupName: string) => {
|
|
136
|
+
if (!field.path) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (onMatchingFieldsConfigChange) {
|
|
141
|
+
onMatchingFieldsConfigChange(isRadioGroupSibling, { groupName });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onFieldConfigChange({ groupName });
|
|
146
|
+
};
|
|
147
|
+
|
|
107
148
|
return (
|
|
108
149
|
<Flex direction="column" gap="4">
|
|
109
150
|
{[
|
|
@@ -123,6 +164,14 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
123
164
|
}
|
|
124
165
|
/>
|
|
125
166
|
)}
|
|
167
|
+
{field.type === FieldTypeEnum.fillable && field.subType === 'radio' && (
|
|
168
|
+
<TextField
|
|
169
|
+
required
|
|
170
|
+
label="Radio Group Name"
|
|
171
|
+
value={field.groupName!}
|
|
172
|
+
onChange={e => handleRadioGroupNameChange(e.target.value)}
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
126
175
|
{(field.type === FieldTypeEnum.fillable || field.type === FieldTypeEnum.eSign) && (
|
|
127
176
|
<Combobox
|
|
128
177
|
items={recipientOptions}
|
|
@@ -87,6 +87,7 @@ export const PdfEditor: FC<PdfEditorProps> = ({
|
|
|
87
87
|
formulaLoadingFormId={formFields.formulaLoadingFormId}
|
|
88
88
|
selectedField={pdfFieldSelection.selectedField}
|
|
89
89
|
onFieldConfigChange={pdfFieldSelection.updateField}
|
|
90
|
+
onMatchingFieldsConfigChange={pdfFieldSelection.updateMatchingFields}
|
|
90
91
|
onDeleteField={pdfFieldSelection.deleteSelectedField}
|
|
91
92
|
recipients={recipients}
|
|
92
93
|
onDeselectField={pdfFieldSelection.deselectField}
|
|
@@ -5,6 +5,7 @@ import { dateToUtcZero, getFieldPlaceholderText, toDateInputValue } from '../../
|
|
|
5
5
|
interface PdfViewFillableProps {
|
|
6
6
|
field: PdfField;
|
|
7
7
|
data?: DataModelValues;
|
|
8
|
+
readOnly?: boolean;
|
|
8
9
|
fillingBy?: string[];
|
|
9
10
|
viewMode?: PdfViewMode;
|
|
10
11
|
onDataChange?(changedData: DataChangePayload, field: PdfField): void;
|
|
@@ -14,6 +15,7 @@ interface FillableTextInputProps {
|
|
|
14
15
|
field: PdfField;
|
|
15
16
|
value: string;
|
|
16
17
|
disabled: boolean;
|
|
18
|
+
readOnly?: boolean;
|
|
17
19
|
onChange(value: string): void;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -29,10 +31,12 @@ export const FillableTextInput: FC<FillableTextInputProps> = ({
|
|
|
29
31
|
disabled,
|
|
30
32
|
field,
|
|
31
33
|
onChange,
|
|
34
|
+
readOnly,
|
|
32
35
|
value,
|
|
33
36
|
}) => {
|
|
34
37
|
return (
|
|
35
38
|
<input
|
|
39
|
+
readOnly={readOnly}
|
|
36
40
|
type="text"
|
|
37
41
|
style={fillableTextStyles}
|
|
38
42
|
disabled={disabled}
|
|
@@ -49,10 +53,12 @@ export const FillableTextareaInput: FC<FillableTextInputProps> = ({
|
|
|
49
53
|
disabled,
|
|
50
54
|
field,
|
|
51
55
|
onChange,
|
|
56
|
+
readOnly,
|
|
52
57
|
value,
|
|
53
58
|
}) => {
|
|
54
59
|
return (
|
|
55
60
|
<textarea
|
|
61
|
+
readOnly={readOnly}
|
|
56
62
|
style={{ ...fillableTextStyles, resize: 'none' }}
|
|
57
63
|
disabled={disabled}
|
|
58
64
|
id={field.path}
|
|
@@ -69,6 +75,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
69
75
|
field,
|
|
70
76
|
fillingBy,
|
|
71
77
|
onDataChange,
|
|
78
|
+
readOnly,
|
|
72
79
|
viewMode,
|
|
73
80
|
}) => {
|
|
74
81
|
const resolvedValue = data?.[field.path!];
|
|
@@ -76,7 +83,12 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
76
83
|
const isFillable =
|
|
77
84
|
viewMode === 'preview' ? true : viewMode === 'fill' ? false : isViewByCurrentRecipient;
|
|
78
85
|
|
|
86
|
+
const isInputDisabled = !isFillable || !!readOnly;
|
|
87
|
+
|
|
79
88
|
const handleDataChange = (fieldValue: DataChangePayload[string]) => {
|
|
89
|
+
if (readOnly) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
80
92
|
const changedData = { [field.path!]: fieldValue };
|
|
81
93
|
onDataChange?.(changedData, field);
|
|
82
94
|
};
|
|
@@ -101,9 +113,9 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
101
113
|
}}
|
|
102
114
|
type="checkbox"
|
|
103
115
|
checked={resolvedValue ?? false}
|
|
104
|
-
disabled={
|
|
116
|
+
disabled={isInputDisabled}
|
|
105
117
|
name={field.path}
|
|
106
|
-
onChange={e => handleDataChange
|
|
118
|
+
onChange={e => handleDataChange(e.target.checked)}
|
|
107
119
|
/>
|
|
108
120
|
{field.label}
|
|
109
121
|
</div>
|
|
@@ -131,7 +143,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
131
143
|
type="radio"
|
|
132
144
|
value={field.id}
|
|
133
145
|
checked={resolvedValue === field.id}
|
|
134
|
-
disabled={
|
|
146
|
+
disabled={isInputDisabled}
|
|
135
147
|
name={field.path}
|
|
136
148
|
onChange={() => handleDataChange(field.id)}
|
|
137
149
|
/>
|
|
@@ -144,6 +156,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
144
156
|
const dateInputValue = toDateInputValue(resolvedValue);
|
|
145
157
|
return (
|
|
146
158
|
<input
|
|
159
|
+
readOnly={readOnly}
|
|
147
160
|
type="date"
|
|
148
161
|
style={{
|
|
149
162
|
background: 'inherit',
|
|
@@ -168,6 +181,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
168
181
|
if (field.subType === 'number') {
|
|
169
182
|
return (
|
|
170
183
|
<input
|
|
184
|
+
readOnly={readOnly}
|
|
171
185
|
type="number"
|
|
172
186
|
style={{
|
|
173
187
|
background: 'inherit',
|
|
@@ -189,6 +203,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
189
203
|
const TextComponent = field.multiline ? FillableTextareaInput : FillableTextInput;
|
|
190
204
|
return (
|
|
191
205
|
<TextComponent
|
|
206
|
+
readOnly={readOnly}
|
|
192
207
|
field={field}
|
|
193
208
|
value={resolvedValue ?? ''}
|
|
194
209
|
disabled={!isFillable}
|
|
@@ -25,13 +25,18 @@ export interface PdfViewProps {
|
|
|
25
25
|
data?: DataModelValues;
|
|
26
26
|
pdfUrl: string;
|
|
27
27
|
loading?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* If true, all fillable fields will be readonly.
|
|
30
|
+
* This is useful for previewing the form without allowing any changes.
|
|
31
|
+
*/
|
|
32
|
+
makeFillableFieldsReadonly?: boolean;
|
|
28
33
|
/**
|
|
29
34
|
* UTC date strings (e.g. `'2026-03-17T00:00:00Z'`) treated as non-business days.
|
|
30
35
|
* When provided, date-based calculated fields skip weekends and these holidays
|
|
31
36
|
* when adding/subtracting days or computing date differences.
|
|
32
37
|
*/
|
|
33
38
|
holidays?: string[];
|
|
34
|
-
|
|
39
|
+
/**
|
|
35
40
|
* fillingBy defines the list of recipient names
|
|
36
41
|
* who are allowed to fill this field.
|
|
37
42
|
* This restriction is enforced only in 'fill' mode
|
|
@@ -54,6 +59,7 @@ export const PdfView: FC<PdfViewProps> = ({
|
|
|
54
59
|
holidays,
|
|
55
60
|
loading = false,
|
|
56
61
|
loadingPlaceholder,
|
|
62
|
+
makeFillableFieldsReadonly = false,
|
|
57
63
|
onDataChange,
|
|
58
64
|
onLoadSuccess,
|
|
59
65
|
pdfUrl,
|
|
@@ -122,6 +128,7 @@ export const PdfView: FC<PdfViewProps> = ({
|
|
|
122
128
|
<PdfViewFillable
|
|
123
129
|
data={viewMode === 'preview' ? previewData : data}
|
|
124
130
|
field={field}
|
|
131
|
+
readOnly={makeFillableFieldsReadonly}
|
|
125
132
|
fillingBy={fillingBy}
|
|
126
133
|
viewMode={viewMode}
|
|
127
134
|
onDataChange={handleDataChange}
|
|
@@ -43,6 +43,11 @@ const initializeDroppedFieldByType: Record<FieldTypeEnum, InitializeDroppedField
|
|
|
43
43
|
recipient: recipientName,
|
|
44
44
|
required: false,
|
|
45
45
|
path: generateFillablePath(recipientName, uuidv4()),
|
|
46
|
+
...(base.subType === 'radio'
|
|
47
|
+
? {
|
|
48
|
+
groupName: `Radio Group ${uuidv4()}`,
|
|
49
|
+
}
|
|
50
|
+
: {}),
|
|
46
51
|
...(defaultSize && {
|
|
47
52
|
width: defaultSize.width,
|
|
48
53
|
height: defaultSize.height,
|
|
@@ -26,6 +26,13 @@ export const usePdfFieldSelection = (
|
|
|
26
26
|
onFieldsChange(fields.map(f => (f.id === selectedFieldId ? { ...f, ...updates } : f)));
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
const updateMatchingFields = (
|
|
30
|
+
predicate: (field: PdfField) => boolean,
|
|
31
|
+
updates: Partial<PdfField>,
|
|
32
|
+
) => {
|
|
33
|
+
onFieldsChange(fields.map(f => (predicate(f) ? { ...f, ...updates } : f)));
|
|
34
|
+
};
|
|
35
|
+
|
|
29
36
|
const deleteSelectedField = () => {
|
|
30
37
|
if (!selectedFieldId) {
|
|
31
38
|
return;
|
|
@@ -47,6 +54,7 @@ export const usePdfFieldSelection = (
|
|
|
47
54
|
selectField,
|
|
48
55
|
deselectField,
|
|
49
56
|
updateField,
|
|
57
|
+
updateMatchingFields,
|
|
50
58
|
deleteSelectedField,
|
|
51
59
|
moveField,
|
|
52
60
|
resizeField,
|
package/src/interface/types.ts
CHANGED
|
@@ -89,6 +89,7 @@ export interface PdfField {
|
|
|
89
89
|
y: number;
|
|
90
90
|
page: number;
|
|
91
91
|
label: string;
|
|
92
|
+
groupName?: string;
|
|
92
93
|
width: number;
|
|
93
94
|
height: number;
|
|
94
95
|
required?: boolean;
|
|
@@ -187,6 +188,7 @@ export interface SchemaFieldBaseOptions {
|
|
|
187
188
|
useInCalculatedFields?: boolean;
|
|
188
189
|
useInConditionals?: boolean;
|
|
189
190
|
isHighlighted?: boolean;
|
|
191
|
+
fieldLookupSource?: string;
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
interface SchemaNodeProps {
|
|
@@ -280,12 +282,25 @@ export type ConditionOperator =
|
|
|
280
282
|
| (typeof STRING_OPERATORS)[number]['value']
|
|
281
283
|
| (typeof NUMBER_OPERATORS)[number]['value'];
|
|
282
284
|
|
|
285
|
+
/** Operators for radio-group fillable fields. */
|
|
286
|
+
export const RADIO_OPERATORS = [
|
|
287
|
+
{ label: 'Is equal to', value: 'is_equal_to' },
|
|
288
|
+
{ label: 'Is not equal to', value: 'is_not_equal_to' },
|
|
289
|
+
] as const;
|
|
290
|
+
|
|
283
291
|
/** Operators that do not require a value (empty/is not empty) */
|
|
284
292
|
export const VALUE_LESS_OPERATORS: ConditionOperator[] = ['is_empty', 'is_not_empty'];
|
|
285
293
|
|
|
294
|
+
export interface RadioGroupOption {
|
|
295
|
+
id: string;
|
|
296
|
+
label: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
286
299
|
export interface DataPointOption {
|
|
287
300
|
fieldType: 'number' | 'string';
|
|
288
301
|
fullKey: string;
|
|
289
302
|
title: string;
|
|
290
303
|
formSnapshot?: FormulaFieldFormSnapshot;
|
|
304
|
+
isRadioGroup?: boolean;
|
|
305
|
+
radioOptions?: RadioGroupOption[];
|
|
291
306
|
}
|
|
@@ -122,6 +122,41 @@ describe('getDocumentFieldsDataPointOptions', () => {
|
|
|
122
122
|
{ fieldType: 'string', fullKey: 'fillable_a_b', title: 'fillable_a_b' },
|
|
123
123
|
]);
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
test('groups radio fields with the same path into a single option using groupName', () => {
|
|
127
|
+
const sharedPath = 'fillable_signer1_payment';
|
|
128
|
+
const fields = [
|
|
129
|
+
buildField({
|
|
130
|
+
id: 'radio-yes',
|
|
131
|
+
path: sharedPath,
|
|
132
|
+
subType: 'radio',
|
|
133
|
+
label: 'Yes',
|
|
134
|
+
groupName: 'Payment Method',
|
|
135
|
+
}),
|
|
136
|
+
buildField({
|
|
137
|
+
id: 'radio-no',
|
|
138
|
+
path: sharedPath,
|
|
139
|
+
subType: 'radio',
|
|
140
|
+
label: 'No',
|
|
141
|
+
groupName: 'Payment Method',
|
|
142
|
+
}),
|
|
143
|
+
buildField({ id: 'text-1', path: 'fillable_signer1_name', label: 'Name', subType: 'text' }),
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
expect(subject(fields)).toEqual([
|
|
147
|
+
{ fieldType: 'string', fullKey: 'fillable_signer1_name', title: 'Name' },
|
|
148
|
+
{
|
|
149
|
+
fieldType: 'string',
|
|
150
|
+
fullKey: sharedPath,
|
|
151
|
+
title: 'Payment Method',
|
|
152
|
+
isRadioGroup: true,
|
|
153
|
+
radioOptions: [
|
|
154
|
+
{ id: 'radio-no', label: 'No' },
|
|
155
|
+
{ id: 'radio-yes', label: 'Yes' },
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
});
|
|
125
160
|
});
|
|
126
161
|
|
|
127
162
|
describe('getDataPointOptions', () => {
|
|
@@ -58,6 +58,7 @@ export function getSchemaDataPointOptions(schema: SchemaObject | undefined): Dat
|
|
|
58
58
|
/**
|
|
59
59
|
* Data point options from document fields: fillable only (no calculated fields).
|
|
60
60
|
* Uses field.path as fullKey so condition evaluation can read from the same data object.
|
|
61
|
+
* Radio buttons sharing the same path are grouped into a single option using groupName.
|
|
61
62
|
*/
|
|
62
63
|
export function getDocumentFieldsDataPointOptions(
|
|
63
64
|
fields: PdfField[] | undefined,
|
|
@@ -66,17 +67,47 @@ export function getDocumentFieldsDataPointOptions(
|
|
|
66
67
|
return [];
|
|
67
68
|
}
|
|
68
69
|
const result: DataPointOption[] = [];
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
const radioGroupsByPath = new Map<string, PdfField[]>();
|
|
71
|
+
|
|
72
|
+
for (const field of fields) {
|
|
73
|
+
if (!field.path || field.type !== FieldTypeEnum.fillable) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (field.subType === 'radio') {
|
|
77
|
+
const group = radioGroupsByPath.get(field.path) ?? [];
|
|
78
|
+
group.push(field);
|
|
79
|
+
radioGroupsByPath.set(field.path, group);
|
|
71
80
|
continue;
|
|
72
81
|
}
|
|
73
|
-
const fieldType =
|
|
82
|
+
const fieldType = field.subType === 'number' ? 'number' : 'string';
|
|
74
83
|
result.push({
|
|
75
84
|
fieldType,
|
|
76
|
-
fullKey:
|
|
77
|
-
title:
|
|
85
|
+
fullKey: field.path,
|
|
86
|
+
title: field.label || field.path,
|
|
78
87
|
});
|
|
79
88
|
}
|
|
89
|
+
|
|
90
|
+
for (const [path, radioFields] of radioGroupsByPath) {
|
|
91
|
+
const sortedRadioFields = [...radioFields].sort((a, b) =>
|
|
92
|
+
(a.label || '').localeCompare(b.label || ''),
|
|
93
|
+
);
|
|
94
|
+
const groupName =
|
|
95
|
+
sortedRadioFields.find(radioField => radioField.groupName)?.groupName ??
|
|
96
|
+
sortedRadioFields[0]?.label ??
|
|
97
|
+
path;
|
|
98
|
+
|
|
99
|
+
result.push({
|
|
100
|
+
fieldType: 'string',
|
|
101
|
+
fullKey: path,
|
|
102
|
+
title: groupName,
|
|
103
|
+
isRadioGroup: true,
|
|
104
|
+
radioOptions: sortedRadioFields.map(radioField => ({
|
|
105
|
+
id: radioField.id,
|
|
106
|
+
label: radioField.label || radioField.id,
|
|
107
|
+
})),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
return result.sort((a, b) => a.title.localeCompare(b.title));
|
|
81
112
|
}
|
|
82
113
|
|