@servicetitan/dte-pdf-editor 1.42.0 → 1.43.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 +89 -45
- package/dist/components/display-conditions/condition-group.d.ts +8 -3
- package/dist/components/display-conditions/condition-group.d.ts.map +1 -1
- package/dist/components/display-conditions/condition-group.js +7 -2
- package/dist/components/display-conditions/condition-group.js.map +1 -1
- package/dist/components/display-conditions/condition-groups-section.d.ts +8 -3
- package/dist/components/display-conditions/condition-groups-section.d.ts.map +1 -1
- package/dist/components/display-conditions/condition-groups-section.js +2 -2
- package/dist/components/display-conditions/condition-groups-section.js.map +1 -1
- package/dist/components/display-conditions/condition-row.d.ts +8 -3
- package/dist/components/display-conditions/condition-row.d.ts.map +1 -1
- package/dist/components/display-conditions/condition-row.js +154 -22
- package/dist/components/display-conditions/condition-row.js.map +1 -1
- package/dist/components/display-conditions/display-condition-modal.d.ts +6 -2
- package/dist/components/display-conditions/display-condition-modal.d.ts.map +1 -1
- package/dist/components/display-conditions/display-condition-modal.js +7 -7
- package/dist/components/display-conditions/display-condition-modal.js.map +1 -1
- package/dist/components/display-conditions/display-condition.d.ts +1 -1
- package/dist/components/display-conditions/display-condition.d.ts.map +1 -1
- package/dist/components/display-conditions/display-condition.js +2 -2
- package/dist/components/display-conditions/display-condition.js.map +1 -1
- package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +5 -2
- 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 +5 -2
- package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.js +9 -4
- package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
- package/dist/components/field-config-panel/field-sidebar.d.ts +5 -1
- package/dist/components/field-config-panel/field-sidebar.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-sidebar.js +42 -8
- package/dist/components/field-config-panel/field-sidebar.js.map +1 -1
- package/dist/components/field-config-panel/formula-generator.d.ts +5 -1
- package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-generator.js +2 -2
- package/dist/components/field-config-panel/formula-generator.js.map +1 -1
- package/dist/components/field-config-panel/formula-modal.d.ts +5 -1
- package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-modal.js +38 -8
- package/dist/components/field-config-panel/formula-modal.js.map +1 -1
- package/dist/components/field-sidebar/field-sidebar.d.ts +6 -1
- package/dist/components/field-sidebar/field-sidebar.d.ts.map +1 -1
- package/dist/components/field-sidebar/field-sidebar.js +11 -6
- package/dist/components/field-sidebar/field-sidebar.js.map +1 -1
- package/dist/components/field-sidebar/form-fields-type-list.d.ts +13 -0
- package/dist/components/field-sidebar/form-fields-type-list.d.ts.map +1 -0
- package/dist/components/field-sidebar/form-fields-type-list.js +14 -0
- package/dist/components/field-sidebar/form-fields-type-list.js.map +1 -0
- package/dist/components/pdf-editor/pdf-editor.d.ts +3 -1
- package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.js +6 -5
- package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
- package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view.js +2 -1
- package/dist/components/pdf-view/pdf-view.js.map +1 -1
- package/dist/constants/menu-group.d.ts.map +1 -1
- package/dist/constants/menu-group.js +2 -0
- package/dist/constants/menu-group.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useFormFields.d.ts +13 -0
- package/dist/hooks/useFormFields.d.ts.map +1 -0
- package/dist/hooks/useFormFields.js +121 -0
- package/dist/hooks/useFormFields.js.map +1 -0
- package/dist/hooks/useFormulaEditor.d.ts.map +1 -1
- package/dist/hooks/useFormulaEditor.js +7 -1
- package/dist/hooks/useFormulaEditor.js.map +1 -1
- package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
- package/dist/hooks/usePdfFieldDnD.js +4 -0
- package/dist/hooks/usePdfFieldDnD.js.map +1 -1
- package/dist/interface/types.d.ts +35 -3
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js +1 -0
- package/dist/interface/types.js.map +1 -1
- package/dist/utils/formula/expression.utils.d.ts +5 -2
- package/dist/utils/formula/expression.utils.d.ts.map +1 -1
- package/dist/utils/formula/expression.utils.js +8 -6
- package/dist/utils/formula/expression.utils.js.map +1 -1
- package/dist/utils/formula/form-fields.utils.d.ts +21 -0
- package/dist/utils/formula/form-fields.utils.d.ts.map +1 -0
- package/dist/utils/formula/form-fields.utils.js +101 -0
- package/dist/utils/formula/form-fields.utils.js.map +1 -0
- package/dist/utils/formula/index.d.ts +1 -0
- package/dist/utils/formula/index.d.ts.map +1 -1
- package/dist/utils/formula/index.js +1 -0
- package/dist/utils/formula/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/display-conditions/condition-group.tsx +32 -5
- package/src/components/display-conditions/condition-groups-section.tsx +24 -4
- package/src/components/display-conditions/condition-row.tsx +359 -80
- package/src/components/display-conditions/display-condition-modal.tsx +39 -10
- package/src/components/display-conditions/display-condition.tsx +19 -2
- package/src/components/field-config-panel/field-config-panel-overlay.tsx +19 -2
- package/src/components/field-config-panel/field-config-panel.tsx +25 -5
- package/src/components/field-config-panel/field-sidebar.tsx +187 -41
- package/src/components/field-config-panel/formula-generator.tsx +14 -0
- package/src/components/field-config-panel/formula-modal.tsx +62 -5
- package/src/components/field-sidebar/field-sidebar.tsx +35 -4
- package/src/components/field-sidebar/form-fields-type-list.tsx +113 -0
- package/src/components/pdf-editor/pdf-editor.tsx +39 -25
- package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +3 -1
- package/src/components/pdf-view/pdf-view.tsx +2 -1
- package/src/constants/menu-group.ts +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useFormFields.ts +157 -0
- package/src/hooks/useFormulaEditor.ts +7 -1
- package/src/hooks/usePdfFieldDnD.ts +4 -0
- package/src/interface/types.ts +43 -4
- package/src/styles/field-type-list.css +1 -0
- package/src/styles/formula-modal.css +66 -8
- package/src/styles/variables.css +4 -0
- package/src/utils/formula/expression.utils.ts +24 -6
- package/src/utils/formula/form-fields.utils.ts +165 -0
- package/src/utils/formula/index.ts +1 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Button, Flex, Text } from '@servicetitan/anvil2';
|
|
2
|
+
import IconArrowBack from '@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_left.svg';
|
|
3
|
+
import IconArrowForward from '@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_right.svg';
|
|
4
|
+
import { Stack } from '@servicetitan/design-system';
|
|
5
|
+
import { FC, Fragment } from 'react';
|
|
6
|
+
import { FieldTypeOption, FormInfo } from '../../interface/types';
|
|
7
|
+
import { FieldType } from './field-type';
|
|
8
|
+
|
|
9
|
+
export interface FormFieldsTypeListProps {
|
|
10
|
+
forms: FormInfo[];
|
|
11
|
+
selectedForm: FormInfo | null;
|
|
12
|
+
fieldOptions: FieldTypeOption[];
|
|
13
|
+
isLoadingFormFields?: boolean;
|
|
14
|
+
onSelectForm(form: FormInfo | null): void;
|
|
15
|
+
onDragEnd(): void;
|
|
16
|
+
onDragStart(fieldOption: FieldTypeOption): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const FormFieldsTypeList: FC<FormFieldsTypeListProps> = ({
|
|
20
|
+
fieldOptions,
|
|
21
|
+
forms,
|
|
22
|
+
isLoadingFormFields = false,
|
|
23
|
+
onDragEnd,
|
|
24
|
+
onDragStart,
|
|
25
|
+
onSelectForm,
|
|
26
|
+
selectedForm,
|
|
27
|
+
}) => {
|
|
28
|
+
return (
|
|
29
|
+
<Flex direction="column" gap={3} flex={1} style={{ minHeight: 0 }}>
|
|
30
|
+
<div className="dte-formula-sidebar-slide-outer">
|
|
31
|
+
<div
|
|
32
|
+
className={`dte-formula-sidebar-slide-track${selectedForm ? ' --detail' : ''}`}
|
|
33
|
+
>
|
|
34
|
+
<div className="dte-formula-sidebar-slide-panel">
|
|
35
|
+
{forms.length > 0 ? (
|
|
36
|
+
<ul
|
|
37
|
+
className="dte-formula-field-list"
|
|
38
|
+
role="listbox"
|
|
39
|
+
aria-label="Forms"
|
|
40
|
+
>
|
|
41
|
+
{forms.map(form => (
|
|
42
|
+
<li
|
|
43
|
+
key={form.id}
|
|
44
|
+
role="option"
|
|
45
|
+
className="dte-formula-field-list-item"
|
|
46
|
+
onMouseDown={e => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
onSelectForm(form);
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<Stack justifyContent="space-between" alignItems="center">
|
|
52
|
+
<span className="dte-text-ellipsis" title={form.name}>
|
|
53
|
+
{form.name}
|
|
54
|
+
</span>
|
|
55
|
+
<IconArrowForward />
|
|
56
|
+
</Stack>
|
|
57
|
+
</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
) : (
|
|
61
|
+
<Text size="small" subdued variant="body">
|
|
62
|
+
No forms configured.
|
|
63
|
+
</Text>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="dte-formula-sidebar-slide-panel --form-fields">
|
|
68
|
+
{selectedForm ? (
|
|
69
|
+
<Fragment>
|
|
70
|
+
<Button
|
|
71
|
+
style={{ width: '100%' }}
|
|
72
|
+
appearance="ghost"
|
|
73
|
+
size="small"
|
|
74
|
+
icon={IconArrowBack}
|
|
75
|
+
title={selectedForm.name}
|
|
76
|
+
onClick={() => onSelectForm(null)}
|
|
77
|
+
>
|
|
78
|
+
<Text
|
|
79
|
+
size="medium"
|
|
80
|
+
className="dte-text-ellipsis"
|
|
81
|
+
style={{ fontWeight: 'bold', width: '100%' }}
|
|
82
|
+
>
|
|
83
|
+
{selectedForm.name}
|
|
84
|
+
</Text>
|
|
85
|
+
</Button>
|
|
86
|
+
<div className="dte-field-type-group">
|
|
87
|
+
{isLoadingFormFields ? (
|
|
88
|
+
<Text size="small" subdued variant="body">
|
|
89
|
+
Loading fields…
|
|
90
|
+
</Text>
|
|
91
|
+
) : fieldOptions.length === 0 ? (
|
|
92
|
+
<Text size="small" subdued variant="body">
|
|
93
|
+
No fields on this form.
|
|
94
|
+
</Text>
|
|
95
|
+
) : (
|
|
96
|
+
fieldOptions.map(option => (
|
|
97
|
+
<FieldType
|
|
98
|
+
key={option.path}
|
|
99
|
+
label={option.label}
|
|
100
|
+
onDragEnd={onDragEnd}
|
|
101
|
+
onDragStart={() => onDragStart(option)}
|
|
102
|
+
/>
|
|
103
|
+
))
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</Fragment>
|
|
107
|
+
) : null}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</Flex>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { Flex } from '@servicetitan/anvil2';
|
|
2
2
|
import { FC, ReactNode, useCallback, useMemo, useRef } from 'react';
|
|
3
|
-
import { usePdfFieldDnD, usePdfFieldSelection } from '../../hooks';
|
|
4
|
-
import {
|
|
3
|
+
import { useFormFields, usePdfFieldDnD, usePdfFieldSelection } from '../../hooks';
|
|
4
|
+
import {
|
|
5
|
+
FieldTypeEnum,
|
|
6
|
+
FormFieldsByFormIdI,
|
|
7
|
+
FormInfo,
|
|
8
|
+
PdfField,
|
|
9
|
+
RecipientInfo,
|
|
10
|
+
SchemaObject,
|
|
11
|
+
} from '../../interface/types';
|
|
5
12
|
import { extractGroupedFieldsFromDataModel, mapColorsToRecipients } from '../../utils';
|
|
6
13
|
import { FieldConfigPanelOverlay } from '../field-config-panel/field-config-panel-overlay';
|
|
7
14
|
import { FieldSidebar } from '../field-sidebar/field-sidebar';
|
|
@@ -20,6 +27,11 @@ export interface PdfEditorProps {
|
|
|
20
27
|
fields?: PdfField[];
|
|
21
28
|
hideFields?: FieldTypeEnum[];
|
|
22
29
|
hideConditionalLogic?: boolean;
|
|
30
|
+
forms?: FormInfo[];
|
|
31
|
+
onFormSelect?: (
|
|
32
|
+
formIds: number[],
|
|
33
|
+
sendFormFields: (formFieldsByFormId: FormFieldsByFormIdI) => void,
|
|
34
|
+
) => void;
|
|
23
35
|
onLoadSuccess?(numPages: number): void;
|
|
24
36
|
onFieldsChange(fields: PdfField[]): void;
|
|
25
37
|
}
|
|
@@ -29,34 +41,27 @@ export const PdfEditor: FC<PdfEditorProps> = ({
|
|
|
29
41
|
errorPlaceholder,
|
|
30
42
|
errors = {},
|
|
31
43
|
fields = [],
|
|
44
|
+
forms,
|
|
32
45
|
hideConditionalLogic,
|
|
33
46
|
hideFields,
|
|
34
47
|
loading = false,
|
|
35
48
|
loadingPlaceholder,
|
|
36
49
|
onFieldsChange,
|
|
50
|
+
onFormSelect,
|
|
37
51
|
onLoadSuccess,
|
|
38
52
|
pdfUrl,
|
|
39
53
|
recipients = [],
|
|
40
54
|
}) => {
|
|
41
55
|
const pdfContainerRef = useRef<HTMLDivElement>(null);
|
|
42
56
|
const pdfWrapperRef = useRef<HTMLDivElement>(null);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
deselectField,
|
|
46
|
-
moveField,
|
|
47
|
-
resizeField,
|
|
48
|
-
selectField,
|
|
49
|
-
selectedField,
|
|
50
|
-
setSelectedFieldId,
|
|
51
|
-
updateField,
|
|
52
|
-
} = usePdfFieldSelection(fields, onFieldsChange);
|
|
53
|
-
|
|
57
|
+
const pdfFieldSelection = usePdfFieldSelection(fields, onFieldsChange);
|
|
58
|
+
const formFields = useFormFields(fields, onFormSelect);
|
|
54
59
|
const pdfFieldDnd = usePdfFieldDnD({
|
|
55
60
|
fields,
|
|
56
61
|
recipients,
|
|
57
62
|
onFieldsChange,
|
|
58
63
|
pdfWrapperRef,
|
|
59
|
-
onSelectField: setSelectedFieldId,
|
|
64
|
+
onSelectField: pdfFieldSelection.setSelectedFieldId,
|
|
60
65
|
});
|
|
61
66
|
|
|
62
67
|
const dataModelGroups = useMemo(
|
|
@@ -73,16 +78,20 @@ export const PdfEditor: FC<PdfEditorProps> = ({
|
|
|
73
78
|
|
|
74
79
|
return (
|
|
75
80
|
<Flex flex={1} className={`dte-pdf-editor ${loading ? 'skeleton' : ''}`}>
|
|
76
|
-
{selectedField && (
|
|
81
|
+
{pdfFieldSelection.selectedField && (
|
|
77
82
|
<FieldConfigPanelOverlay
|
|
78
83
|
dataModel={dataModel}
|
|
79
84
|
documentFields={fields}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
formFieldsByFormId={formFields.formFieldsByFormId}
|
|
86
|
+
forms={forms ?? []}
|
|
87
|
+
formulaLoadingFormId={formFields.formulaLoadingFormId}
|
|
88
|
+
selectedField={pdfFieldSelection.selectedField}
|
|
89
|
+
onFieldConfigChange={pdfFieldSelection.updateField}
|
|
90
|
+
onDeleteField={pdfFieldSelection.deleteSelectedField}
|
|
83
91
|
recipients={recipients}
|
|
84
|
-
onDeselectField={deselectField}
|
|
92
|
+
onDeselectField={pdfFieldSelection.deselectField}
|
|
85
93
|
hideConditionalLogic={hideConditionalLogic}
|
|
94
|
+
onRequestFormFields={formFields.requestFormFieldsById}
|
|
86
95
|
/>
|
|
87
96
|
)}
|
|
88
97
|
|
|
@@ -90,8 +99,13 @@ export const PdfEditor: FC<PdfEditorProps> = ({
|
|
|
90
99
|
<FieldSidebar
|
|
91
100
|
hideFields={hideFields}
|
|
92
101
|
dataModelGroups={dataModelGroups}
|
|
102
|
+
formFieldOptions={formFields.formFieldOptions}
|
|
103
|
+
forms={forms}
|
|
104
|
+
isLoadingFormFields={formFields.isLoading}
|
|
93
105
|
onDragStart={pdfFieldDnd.handleDragStart}
|
|
94
106
|
onDragEnd={pdfFieldDnd.handleDragEnd}
|
|
107
|
+
onSelectForm={formFields.handleSelectForm}
|
|
108
|
+
selectedForm={formFields.selectedForm}
|
|
95
109
|
/>
|
|
96
110
|
</Flex>
|
|
97
111
|
|
|
@@ -103,7 +117,7 @@ export const PdfEditor: FC<PdfEditorProps> = ({
|
|
|
103
117
|
errors={errors}
|
|
104
118
|
onLoadSuccess={onLoadSuccess}
|
|
105
119
|
recipientsColors={recipientsColors}
|
|
106
|
-
selectedField={selectedField}
|
|
120
|
+
selectedField={pdfFieldSelection.selectedField}
|
|
107
121
|
pdfContainerRef={pdfContainerRef}
|
|
108
122
|
pdfWrapperRef={pdfWrapperRef}
|
|
109
123
|
errorPlaceholder={errorPlaceholder}
|
|
@@ -111,11 +125,11 @@ export const PdfEditor: FC<PdfEditorProps> = ({
|
|
|
111
125
|
handleAddNewField={handleAddNewField}
|
|
112
126
|
onDrop={pdfFieldDnd.handleDrop}
|
|
113
127
|
onDragOver={pdfFieldDnd.handleDragOver}
|
|
114
|
-
onFieldClick={selectField}
|
|
115
|
-
onFieldMove={moveField}
|
|
116
|
-
onFieldResize={resizeField}
|
|
117
|
-
onDeselectField={deselectField}
|
|
118
|
-
onFieldConfigChange={updateField}
|
|
128
|
+
onFieldClick={pdfFieldSelection.selectField}
|
|
129
|
+
onFieldMove={pdfFieldSelection.moveField}
|
|
130
|
+
onFieldResize={pdfFieldSelection.resizeField}
|
|
131
|
+
onDeselectField={pdfFieldSelection.deselectField}
|
|
132
|
+
onFieldConfigChange={pdfFieldSelection.updateField}
|
|
119
133
|
/>
|
|
120
134
|
</Flex>
|
|
121
135
|
</Flex>
|
|
@@ -77,7 +77,9 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
|
|
|
77
77
|
>
|
|
78
78
|
{field.type === FieldTypeEnum.calculated && <PdfOverlayFieldCalculated field={field} />}
|
|
79
79
|
{field.type === FieldTypeEnum.eSign && <PdfOverlayFieldESign field={field} />}
|
|
80
|
-
{field.type === FieldTypeEnum.dataModel
|
|
80
|
+
{(field.type === FieldTypeEnum.dataModel || field.type === FieldTypeEnum.forms) && (
|
|
81
|
+
<PdfOverlayFieldDataModel field={field} />
|
|
82
|
+
)}
|
|
81
83
|
{field.type === FieldTypeEnum.fillable && (
|
|
82
84
|
<PdfOverlayFieldFillable field={field} handleAddNewField={handleAddNewField} />
|
|
83
85
|
)}
|
|
@@ -103,7 +103,8 @@ export const PdfView: FC<PdfViewProps> = ({
|
|
|
103
103
|
recipientsColors={recipientsColors}
|
|
104
104
|
pdfWrapperRef={pdfWrapperRef}
|
|
105
105
|
>
|
|
106
|
-
{field.type === FieldTypeEnum.dataModel
|
|
106
|
+
{(field.type === FieldTypeEnum.dataModel ||
|
|
107
|
+
field.type === FieldTypeEnum.forms) && (
|
|
107
108
|
<PdfViewDataModel field={field} data={data} />
|
|
108
109
|
)}
|
|
109
110
|
{field.type === FieldTypeEnum.eSign && (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import IconBorderColor from '@servicetitan/anvil2/assets/icons/material/round/border_color.svg';
|
|
2
2
|
import IconCalculate from '@servicetitan/anvil2/assets/icons/material/round/calculate.svg';
|
|
3
|
+
import IconFactCheck from '@servicetitan/anvil2/assets/icons/material/round/fact_check.svg';
|
|
3
4
|
import IconPhotoSizeSelectSmall from '@servicetitan/anvil2/assets/icons/material/round/photo_size_select_small.svg';
|
|
4
5
|
import IconCommercial from '@servicetitan/anvil2/assets/icons/st/commercial.svg';
|
|
5
6
|
import IconEstimate from '@servicetitan/anvil2/assets/icons/st/estimate.svg';
|
|
@@ -29,4 +30,5 @@ export const menuGroups: MenuGroupModel[] = [
|
|
|
29
30
|
label: 'Generic Fields',
|
|
30
31
|
key: FieldTypeEnum.generic,
|
|
31
32
|
},
|
|
33
|
+
{ svgIcon: IconFactCheck, label: 'Forms', key: FieldTypeEnum.forms },
|
|
32
34
|
];
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
FieldTypeEnum,
|
|
4
|
+
FieldTypeOption,
|
|
5
|
+
FormFieldsByFormIdI,
|
|
6
|
+
FormInfo,
|
|
7
|
+
PdfField,
|
|
8
|
+
} from '../interface/types';
|
|
9
|
+
import { formFieldInfoToFieldTypeOption } from '../utils';
|
|
10
|
+
|
|
11
|
+
interface UseFormFieldsResult {
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
formFieldsByFormId: FormFieldsByFormIdI;
|
|
14
|
+
formulaLoadingFormId: null | number;
|
|
15
|
+
formFieldOptions: FieldTypeOption[];
|
|
16
|
+
selectedForm: FormInfo | null;
|
|
17
|
+
handleSelectForm: (form: FormInfo | null) => void;
|
|
18
|
+
requestFormFieldsById: (formId: number) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const useFormFields = (
|
|
22
|
+
fields: PdfField[],
|
|
23
|
+
onFormSelect?: (
|
|
24
|
+
formIds: number[],
|
|
25
|
+
sendFormFields: (formFieldsByFormId: FormFieldsByFormIdI) => void,
|
|
26
|
+
) => void,
|
|
27
|
+
): UseFormFieldsResult => {
|
|
28
|
+
const [formFieldsByFormId, setFormFieldsByFormId] = useState<FormFieldsByFormIdI>({});
|
|
29
|
+
const [formulaLoadingFormId, setFormulaLoadingFormId] = useState<number | null>(null);
|
|
30
|
+
const [selectedForm, setSelectedForm] = useState<FormInfo | null>(null);
|
|
31
|
+
const [formFieldsLoadingFormIds, setFormFieldsLoadingFormIds] = useState<Record<number, true>>(
|
|
32
|
+
{},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const formFieldsByFormIdRef = useRef(formFieldsByFormId);
|
|
36
|
+
formFieldsByFormIdRef.current = formFieldsByFormId;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!onFormSelect) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const initialFormIds = new Set<number>();
|
|
44
|
+
fields.forEach(field => {
|
|
45
|
+
if (field.type === FieldTypeEnum.calculated) {
|
|
46
|
+
if (field?.formula?.tokens.length) {
|
|
47
|
+
field.formula.tokens.forEach(token => {
|
|
48
|
+
if (token.type === 'field' && token.formSnapshot) {
|
|
49
|
+
initialFormIds.add(token.formSnapshot.formId);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!initialFormIds.size) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
const rafId = requestAnimationFrame(() => {
|
|
62
|
+
if (cancelled) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const cache = formFieldsByFormIdRef.current;
|
|
66
|
+
const missingFormIds = [...initialFormIds].filter(formId => !cache[formId]);
|
|
67
|
+
if (!missingFormIds.length) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
setFormFieldsLoadingFormIds(prev => ({
|
|
71
|
+
...prev,
|
|
72
|
+
...Object.fromEntries(missingFormIds.map(id => [id, true])),
|
|
73
|
+
}));
|
|
74
|
+
onFormSelect(missingFormIds, receivedByFormId => {
|
|
75
|
+
if (cancelled) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
setFormFieldsByFormId(prev => ({ ...prev, ...receivedByFormId }));
|
|
79
|
+
setFormFieldsLoadingFormIds(prev => {
|
|
80
|
+
const next = { ...prev };
|
|
81
|
+
for (const id of Object.keys(receivedByFormId)) {
|
|
82
|
+
delete next[Number(id)];
|
|
83
|
+
}
|
|
84
|
+
return next;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
cancelAnimationFrame(rafId);
|
|
92
|
+
};
|
|
93
|
+
}, [fields, onFormSelect]);
|
|
94
|
+
|
|
95
|
+
const formFieldOptions = useMemo(() => {
|
|
96
|
+
if (!selectedForm) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const list = formFieldsByFormId[selectedForm.id];
|
|
100
|
+
if (!list?.length) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
return list.map(f => formFieldInfoToFieldTypeOption(selectedForm.id, f, selectedForm.name));
|
|
104
|
+
}, [formFieldsByFormId, selectedForm]);
|
|
105
|
+
|
|
106
|
+
const handleSelectForm = useCallback(
|
|
107
|
+
(form: FormInfo | null) => {
|
|
108
|
+
setSelectedForm(form);
|
|
109
|
+
if (form) {
|
|
110
|
+
if (!formFieldsByFormId[form.id]?.length && onFormSelect) {
|
|
111
|
+
setFormFieldsLoadingFormIds({ [form.id]: true });
|
|
112
|
+
onFormSelect([form.id], loadedByFormId => {
|
|
113
|
+
setFormFieldsByFormId(prev => ({ ...prev, ...loadedByFormId }));
|
|
114
|
+
setFormFieldsLoadingFormIds(prev => {
|
|
115
|
+
const next = { ...prev };
|
|
116
|
+
for (const id of Object.keys(loadedByFormId)) {
|
|
117
|
+
delete next[Number(id)];
|
|
118
|
+
}
|
|
119
|
+
return next;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
setFormFieldsLoadingFormIds({});
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
setFormFieldsLoadingFormIds({});
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[onFormSelect, formFieldsByFormId],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const requestFormFieldsById = useCallback(
|
|
133
|
+
(formId: number) => {
|
|
134
|
+
if (!onFormSelect || formFieldsByFormId[formId]?.length) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
setFormulaLoadingFormId(formId);
|
|
138
|
+
onFormSelect([formId], loadedByFormId => {
|
|
139
|
+
setFormFieldsByFormId(prev => ({ ...prev, ...loadedByFormId }));
|
|
140
|
+
setFormulaLoadingFormId(null);
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
[formFieldsByFormId, onFormSelect],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const isLoading = selectedForm != null && formFieldsLoadingFormIds[selectedForm.id];
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
isLoading,
|
|
150
|
+
formFieldsByFormId,
|
|
151
|
+
formulaLoadingFormId,
|
|
152
|
+
formFieldOptions,
|
|
153
|
+
selectedForm,
|
|
154
|
+
handleSelectForm,
|
|
155
|
+
requestFormFieldsById,
|
|
156
|
+
};
|
|
157
|
+
};
|
|
@@ -35,6 +35,11 @@ export const useFormulaEditor = (params: {
|
|
|
35
35
|
const historyRef = useRef<string[]>([currentExpression]);
|
|
36
36
|
const knownDateFieldsRef = useRef(knownDateFields);
|
|
37
37
|
|
|
38
|
+
/*
|
|
39
|
+
* Reset draft only when the modal opens or the saved formula from props changes.
|
|
40
|
+
* Do not depend on pathToLabel: merge-tag / form labels load async and a new Map
|
|
41
|
+
* reference would wipe in-progress edits (e.g. clear formula → pick a field).
|
|
42
|
+
*/
|
|
38
43
|
useEffect(() => {
|
|
39
44
|
if (opened) {
|
|
40
45
|
setDraftExpression(currentExpression);
|
|
@@ -49,7 +54,8 @@ export const useFormulaEditor = (params: {
|
|
|
49
54
|
placeCaretAtEnd(editor);
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
|
-
|
|
57
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- pathToLabel updates re-render via the draftExpression effect
|
|
58
|
+
}, [opened, currentExpression]);
|
|
53
59
|
|
|
54
60
|
useEffect(() => {
|
|
55
61
|
const editor = editorRef.current;
|
|
@@ -52,6 +52,10 @@ const initializeDroppedFieldByType: Record<FieldTypeEnum, InitializeDroppedField
|
|
|
52
52
|
path: base.id,
|
|
53
53
|
}),
|
|
54
54
|
[FieldTypeEnum.dataModel]: base => base,
|
|
55
|
+
[FieldTypeEnum.forms]: (base, option) => ({
|
|
56
|
+
...base,
|
|
57
|
+
...(option.formSnapshot ? { formSnapshot: option.formSnapshot } : {}),
|
|
58
|
+
}),
|
|
55
59
|
[FieldTypeEnum.generic]: base => {
|
|
56
60
|
const defaultSize = GENERIC_FIELD_DEFAULT_SIZES[base.subType!];
|
|
57
61
|
if (base.subType === 'text') {
|
package/src/interface/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export enum FieldTypeEnum {
|
|
2
2
|
dataModel = 'dataModel',
|
|
3
|
+
forms = 'forms',
|
|
3
4
|
eSign = 'eSign',
|
|
4
5
|
fillable = 'fillable',
|
|
5
6
|
calculated = 'calculated',
|
|
@@ -16,8 +17,23 @@ export enum ESignFieldType {
|
|
|
16
17
|
export type FillableFieldType = 'text' | 'date' | 'checkbox' | 'radio' | 'number';
|
|
17
18
|
|
|
18
19
|
export type GenericFieldType = 'table' | 'text';
|
|
20
|
+
export type FormFieldType = 'number' | 'text' | 'date';
|
|
19
21
|
|
|
20
|
-
export
|
|
22
|
+
export interface FormFieldsByFormIdI {
|
|
23
|
+
[formId: number]: FormFieldInfo[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FormFieldInfo {
|
|
27
|
+
id: string;
|
|
28
|
+
header: string;
|
|
29
|
+
itemType: FormFieldType;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FormInfo {
|
|
33
|
+
id: number;
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
export type PdfFieldSubType = FillableFieldType | ESignFieldType | GenericFieldType | FormFieldType;
|
|
21
37
|
|
|
22
38
|
export interface GenericFieldTableDataType {
|
|
23
39
|
showHeader: boolean;
|
|
@@ -54,10 +70,11 @@ export interface DisplayConditionGroup {
|
|
|
54
70
|
|
|
55
71
|
export interface DisplayConditionSingle {
|
|
56
72
|
id: string;
|
|
73
|
+
value: string;
|
|
74
|
+
operator: string;
|
|
57
75
|
dataPointKey: string;
|
|
58
76
|
logicalOperator?: 'and' | 'or';
|
|
59
|
-
|
|
60
|
-
value: string;
|
|
77
|
+
formSnapshot?: FormulaFieldFormSnapshot;
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
export interface PdfField {
|
|
@@ -72,6 +89,7 @@ export interface PdfField {
|
|
|
72
89
|
height: number;
|
|
73
90
|
required?: boolean;
|
|
74
91
|
path?: string;
|
|
92
|
+
formSnapshot?: FormulaFieldFormSnapshot;
|
|
75
93
|
recipient?: string;
|
|
76
94
|
description?: string;
|
|
77
95
|
data?: FieldDataType;
|
|
@@ -88,6 +106,7 @@ export interface FieldTypeOption {
|
|
|
88
106
|
subType?: PdfFieldSubType;
|
|
89
107
|
path?: string;
|
|
90
108
|
fieldType?: SchemaFieldType;
|
|
109
|
+
formSnapshot?: FormulaFieldFormSnapshot;
|
|
91
110
|
}
|
|
92
111
|
|
|
93
112
|
export interface DataModelFieldGroup {
|
|
@@ -99,13 +118,32 @@ export interface DataModelFieldGroup {
|
|
|
99
118
|
export const FORMULA_OPERATORS = ['+', '-', '*', '/'];
|
|
100
119
|
export type FormulaOperator = (typeof FORMULA_OPERATORS)[number];
|
|
101
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Snapshot metadata for a bound form submission field (`__submission_fields.*` path):
|
|
123
|
+
* used on `PdfField` with `type: forms`, on `FieldTypeOption` for drag payloads, on
|
|
124
|
+
* structured-formula `field` tokens, and on `DisplayConditionSingle` when the condition uses a
|
|
125
|
+
* form field.
|
|
126
|
+
*/
|
|
127
|
+
export interface FormulaFieldFormSnapshot {
|
|
128
|
+
formId: number;
|
|
129
|
+
fieldId: string;
|
|
130
|
+
formName: string;
|
|
131
|
+
fieldName: string;
|
|
132
|
+
fieldType: FormFieldType;
|
|
133
|
+
}
|
|
134
|
+
|
|
102
135
|
/** Single token in a structured formula (number, operator, paren, or field reference) */
|
|
103
136
|
export type FormulaToken =
|
|
104
137
|
| { type: 'number'; value: string }
|
|
105
138
|
| { type: 'operator'; value: FormulaOperator }
|
|
106
139
|
| { type: 'lparen' }
|
|
107
140
|
| { type: 'rparen' }
|
|
108
|
-
| {
|
|
141
|
+
| {
|
|
142
|
+
type: 'field';
|
|
143
|
+
path: string;
|
|
144
|
+
fieldType: SchemaFieldType;
|
|
145
|
+
formSnapshot?: FormulaFieldFormSnapshot;
|
|
146
|
+
};
|
|
109
147
|
|
|
110
148
|
/** Structured formula representation (AST-like token list) for validation and safe editing */
|
|
111
149
|
export interface StructuredFormula {
|
|
@@ -241,4 +279,5 @@ export interface DataPointOption {
|
|
|
241
279
|
fieldType: 'number' | 'string';
|
|
242
280
|
fullKey: string;
|
|
243
281
|
title: string;
|
|
282
|
+
formSnapshot?: FormulaFieldFormSnapshot;
|
|
244
283
|
}
|