@servicetitan/dte-pdf-editor 1.49.0 → 1.51.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 +15 -4
- package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
- package/dist/components/field-config-panel/field-sidebar.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-sidebar.js +42 -6
- package/dist/components/field-config-panel/field-sidebar.js.map +1 -1
- package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-modal.js +55 -2
- package/dist/components/field-config-panel/formula-modal.js.map +1 -1
- package/dist/components/field-config-panel/formula-workspace.d.ts +1 -0
- package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-workspace.js +3 -3
- package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
- package/dist/components/field-sidebar/data-model-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/data-model-field-type-list.js +2 -1
- package/dist/components/field-sidebar/data-model-field-type-list.js.map +1 -1
- package/dist/components/field-sidebar/field-type.d.ts +1 -0
- package/dist/components/field-sidebar/field-type.d.ts.map +1 -1
- package/dist/components/field-sidebar/field-type.js +3 -3
- package/dist/components/field-sidebar/field-type.js.map +1 -1
- package/dist/components/field-sidebar/form-fields-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/form-fields-type-list.js +2 -1
- package/dist/components/field-sidebar/form-fields-type-list.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 +4 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.js +4 -1
- package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.js +3 -0
- package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
- package/dist/constants/field.constants.d.ts +2 -0
- package/dist/constants/field.constants.d.ts.map +1 -1
- package/dist/constants/field.constants.js +4 -0
- package/dist/constants/field.constants.js.map +1 -1
- package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
- package/dist/hooks/usePdfFieldDnD.js +10 -3
- package/dist/hooks/usePdfFieldDnD.js.map +1 -1
- package/dist/interface/types.d.ts +5 -0
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js.map +1 -1
- package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
- package/dist/utils/data-model/extract-fields.utils.js +4 -2
- package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
- package/dist/utils/field/get-field-font-size.utils.d.ts +7 -0
- package/dist/utils/field/get-field-font-size.utils.d.ts.map +1 -0
- package/dist/utils/field/get-field-font-size.utils.js +23 -0
- package/dist/utils/field/get-field-font-size.utils.js.map +1 -0
- package/dist/utils/field/index.d.ts +1 -0
- package/dist/utils/field/index.d.ts.map +1 -1
- package/dist/utils/field/index.js +1 -0
- package/dist/utils/field/index.js.map +1 -1
- package/dist/utils/pdf/index.d.ts +1 -0
- package/dist/utils/pdf/index.d.ts.map +1 -1
- package/dist/utils/pdf/index.js +1 -0
- package/dist/utils/pdf/index.js.map +1 -1
- package/dist/utils/pdf/unit-conversion.utils.d.ts +22 -0
- package/dist/utils/pdf/unit-conversion.utils.d.ts.map +1 -0
- package/dist/utils/pdf/unit-conversion.utils.js +27 -0
- package/dist/utils/pdf/unit-conversion.utils.js.map +1 -0
- package/dist/utils/shared/index.d.ts +1 -0
- package/dist/utils/shared/index.d.ts.map +1 -1
- package/dist/utils/shared/index.js +1 -0
- package/dist/utils/shared/index.js.map +1 -1
- package/dist/utils/shared/sample-data.utils.d.ts +3 -0
- package/dist/utils/shared/sample-data.utils.d.ts.map +1 -0
- package/dist/utils/shared/sample-data.utils.js +20 -0
- package/dist/utils/shared/sample-data.utils.js.map +1 -0
- package/package.json +1 -1
- package/src/components/field-config-panel/field-config-panel.tsx +43 -3
- package/src/components/field-config-panel/field-sidebar.tsx +71 -7
- package/src/components/field-config-panel/formula-modal.tsx +61 -0
- package/src/components/field-config-panel/formula-workspace.tsx +8 -0
- package/src/components/field-sidebar/data-model-field-type-list.tsx +6 -1
- package/src/components/field-sidebar/field-type.tsx +16 -3
- package/src/components/field-sidebar/form-fields-type-list.tsx +5 -0
- package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +9 -1
- package/src/components/pdf-view/pdf-view-field-container.tsx +10 -1
- package/src/components/pdf-view/pdf-view-fillable.tsx +3 -0
- package/src/constants/field.constants.ts +6 -0
- package/src/hooks/usePdfFieldDnD.ts +10 -1
- package/src/interface/types.ts +6 -0
- package/src/styles/formula-modal.css +10 -0
- package/src/styles/pdf-field-overlay.css +1 -0
- package/src/utils/data-model/extract-fields.utils.ts +2 -0
- package/src/utils/field/get-field-font-size.utils.ts +28 -0
- package/src/utils/field/index.ts +1 -0
- package/src/utils/pdf/__tests__/unit-conversion.utils.test.ts +35 -0
- package/src/utils/pdf/index.ts +1 -0
- package/src/utils/pdf/unit-conversion.utils.ts +31 -0
- package/src/utils/shared/index.ts +1 -0
- package/src/utils/shared/sample-data.utils.ts +29 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Button, Checkbox, Combobox, Flex, Textarea, TextField } from '@servicetitan/anvil2';
|
|
2
2
|
import { FC, useMemo } from 'react';
|
|
3
|
-
import { E_SIGN_FIELD_TYPE_OPTIONS } from '../../constants';
|
|
3
|
+
import { E_SIGN_FIELD_TYPE_OPTIONS, FIELD_FONT_SIZE_OPTIONS } from '../../constants';
|
|
4
4
|
import {
|
|
5
5
|
ESignFieldType,
|
|
6
6
|
FieldTypeEnum,
|
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
RecipientInfo,
|
|
11
11
|
SchemaObject,
|
|
12
12
|
} from '../../interface/types';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
generateESignPath,
|
|
15
|
+
generateFillablePath,
|
|
16
|
+
getFieldFontSize,
|
|
17
|
+
supportsFieldFontSize,
|
|
18
|
+
} from '../../utils';
|
|
14
19
|
import { DisplayCondition } from '../display-conditions/display-condition';
|
|
15
20
|
import { FormulaGenerator } from './formula-generator';
|
|
16
21
|
import { TableConfigs } from './table-configs';
|
|
@@ -86,6 +91,19 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
86
91
|
}
|
|
87
92
|
};
|
|
88
93
|
|
|
94
|
+
const showFontSizeControl = supportsFieldFontSize(field);
|
|
95
|
+
|
|
96
|
+
const handleFontSizeChange = (item: number | null) => {
|
|
97
|
+
if (item) {
|
|
98
|
+
onFieldConfigChange({
|
|
99
|
+
styles: {
|
|
100
|
+
...field.styles,
|
|
101
|
+
fontSize: item,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
89
107
|
return (
|
|
90
108
|
<Flex direction="column" gap="4">
|
|
91
109
|
{[
|
|
@@ -151,7 +169,7 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
151
169
|
{field.type !== FieldTypeEnum.generic && (
|
|
152
170
|
<TextField label="Data Path" value={field.path} disabled />
|
|
153
171
|
)}
|
|
154
|
-
{field.type === FieldTypeEnum.fillable &&
|
|
172
|
+
{field.type === FieldTypeEnum.fillable && (
|
|
155
173
|
<Checkbox
|
|
156
174
|
label="Required"
|
|
157
175
|
checked={field.required}
|
|
@@ -192,6 +210,28 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
192
210
|
/>
|
|
193
211
|
)}
|
|
194
212
|
|
|
213
|
+
{showFontSizeControl && (
|
|
214
|
+
<Combobox
|
|
215
|
+
items={FIELD_FONT_SIZE_OPTIONS}
|
|
216
|
+
itemToString={item => item?.toString() ?? ''}
|
|
217
|
+
onChange={handleFontSizeChange}
|
|
218
|
+
selectedItem={getFieldFontSize(field)}
|
|
219
|
+
>
|
|
220
|
+
<Combobox.SearchField label="Font Size" />
|
|
221
|
+
<Combobox.Content>
|
|
222
|
+
{({ items }) => (
|
|
223
|
+
<Combobox.List>
|
|
224
|
+
{items.map((item, i) => (
|
|
225
|
+
<Combobox.Item key={item} item={item} index={i}>
|
|
226
|
+
{item}
|
|
227
|
+
</Combobox.Item>
|
|
228
|
+
))}
|
|
229
|
+
</Combobox.List>
|
|
230
|
+
)}
|
|
231
|
+
</Combobox.Content>
|
|
232
|
+
</Combobox>
|
|
233
|
+
)}
|
|
234
|
+
|
|
195
235
|
{field.type === FieldTypeEnum.generic && field.subType === 'table' && (
|
|
196
236
|
<TableConfigs field={field} onFieldConfigChange={onFieldConfigChange} />
|
|
197
237
|
)}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { Button, Flex, Text } from '@servicetitan/anvil2';
|
|
1
|
+
import { Button, Flex, SearchField, Text } from '@servicetitan/anvil2';
|
|
2
2
|
import IconArrowBack from '@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_left.svg';
|
|
3
3
|
import IconArrowForward from '@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_right.svg';
|
|
4
4
|
import { FC, Fragment, useMemo, useState } from 'react';
|
|
5
5
|
import { FieldTypeOption, FormFieldsByFormIdI, FormInfo } from '../../interface/types';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
formFieldInfosToCalculationOptions,
|
|
8
|
+
getFieldSampleData,
|
|
9
|
+
parseFormFieldKey,
|
|
10
|
+
} from '../../utils';
|
|
7
11
|
|
|
8
12
|
interface FieldSidebarProps {
|
|
9
13
|
fillableOptions: FieldTypeOption[];
|
|
@@ -31,15 +35,23 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
|
|
|
31
35
|
selectedPaths,
|
|
32
36
|
}) => {
|
|
33
37
|
const [selectedFormId, setSelectedFormId] = useState<number | null>(null);
|
|
38
|
+
const [topSearchValue, setTopSearchValue] = useState('');
|
|
39
|
+
const [formFieldsSearchValue, setFormFieldsSearchValue] = useState('');
|
|
34
40
|
|
|
35
41
|
const onFormClick = (formId: number) => {
|
|
36
42
|
setSelectedFormId(formId);
|
|
43
|
+
setFormFieldsSearchValue('');
|
|
37
44
|
const loaded = formFieldsByFormId[formId];
|
|
38
45
|
if (!loaded?.length) {
|
|
39
46
|
onRequestFormFields(formId);
|
|
40
47
|
}
|
|
41
48
|
};
|
|
42
49
|
|
|
50
|
+
const onBackToList = () => {
|
|
51
|
+
setSelectedFormId(null);
|
|
52
|
+
setFormFieldsSearchValue('');
|
|
53
|
+
};
|
|
54
|
+
|
|
43
55
|
const isFormFieldsLoading = selectedFormId != null && formulaLoadingFormId === selectedFormId;
|
|
44
56
|
|
|
45
57
|
const selectedForm = useMemo(
|
|
@@ -57,6 +69,36 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
|
|
|
57
69
|
);
|
|
58
70
|
}, [selectedFormId, formFieldsByFormId, selectedForm?.name]);
|
|
59
71
|
|
|
72
|
+
const filteredForms = useMemo(() => {
|
|
73
|
+
const query = topSearchValue.trim().toLowerCase();
|
|
74
|
+
if (!query) {
|
|
75
|
+
return forms;
|
|
76
|
+
}
|
|
77
|
+
return forms.filter(form => form.name.toLowerCase().includes(query));
|
|
78
|
+
}, [forms, topSearchValue]);
|
|
79
|
+
|
|
80
|
+
const filteredMergeTagOptions = useMemo(() => {
|
|
81
|
+
const query = topSearchValue.trim().toLowerCase();
|
|
82
|
+
if (!query) {
|
|
83
|
+
return mergeTagOptions;
|
|
84
|
+
}
|
|
85
|
+
return mergeTagOptions.filter(option => {
|
|
86
|
+
const label = (option.label ?? option.path ?? '').toLowerCase();
|
|
87
|
+
return label.includes(query);
|
|
88
|
+
});
|
|
89
|
+
}, [mergeTagOptions, topSearchValue]);
|
|
90
|
+
|
|
91
|
+
const filteredCalcOptions = useMemo(() => {
|
|
92
|
+
const query = formFieldsSearchValue.trim().toLowerCase();
|
|
93
|
+
if (!query) {
|
|
94
|
+
return calcOptions;
|
|
95
|
+
}
|
|
96
|
+
return calcOptions.filter(option => {
|
|
97
|
+
const label = (option.label ?? option.path ?? '').toLowerCase();
|
|
98
|
+
return label.includes(query);
|
|
99
|
+
});
|
|
100
|
+
}, [calcOptions, formFieldsSearchValue]);
|
|
101
|
+
|
|
60
102
|
const formIdsReferencedInFormula = useMemo(() => {
|
|
61
103
|
const ids = new Set<number>();
|
|
62
104
|
for (const path of selectedPaths) {
|
|
@@ -75,7 +117,14 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
|
|
|
75
117
|
className={`dte-formula-sidebar-slide-track${selectedFormId ? ' --detail' : ''}`}
|
|
76
118
|
>
|
|
77
119
|
<div className="dte-formula-sidebar-slide-panel">
|
|
78
|
-
|
|
120
|
+
<SearchField
|
|
121
|
+
placeholder="Search"
|
|
122
|
+
size="small"
|
|
123
|
+
style={{ marginLeft: '4px', marginTop: '18px' }}
|
|
124
|
+
value={topSearchValue}
|
|
125
|
+
onChange={e => setTopSearchValue(e.target.value ?? '')}
|
|
126
|
+
/>
|
|
127
|
+
{filteredForms.length > 0 && (
|
|
79
128
|
<div className="dte-formula-sidebar-forms-heading">
|
|
80
129
|
<Text variant="headline" el="h6" size="small">
|
|
81
130
|
Forms
|
|
@@ -85,7 +134,7 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
|
|
|
85
134
|
role="listbox"
|
|
86
135
|
aria-label="Forms"
|
|
87
136
|
>
|
|
88
|
-
{
|
|
137
|
+
{filteredForms.map(form => (
|
|
89
138
|
<li
|
|
90
139
|
key={form.id}
|
|
91
140
|
role="option"
|
|
@@ -122,7 +171,7 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
|
|
|
122
171
|
/>
|
|
123
172
|
<FieldOptionList
|
|
124
173
|
title="Merge tags"
|
|
125
|
-
options={
|
|
174
|
+
options={filteredMergeTagOptions}
|
|
126
175
|
highlightElementPath={highlightElementPath}
|
|
127
176
|
selectedPaths={selectedPaths}
|
|
128
177
|
onHover={onHover}
|
|
@@ -138,7 +187,7 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
|
|
|
138
187
|
size="small"
|
|
139
188
|
icon={IconArrowBack}
|
|
140
189
|
title={selectedForm?.name ?? 'Form fields'}
|
|
141
|
-
onClick={
|
|
190
|
+
onClick={onBackToList}
|
|
142
191
|
>
|
|
143
192
|
<Text
|
|
144
193
|
size="medium"
|
|
@@ -148,11 +197,18 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
|
|
|
148
197
|
{selectedForm?.name ?? 'Form fields'}
|
|
149
198
|
</Text>
|
|
150
199
|
</Button>
|
|
200
|
+
<SearchField
|
|
201
|
+
style={{ margin: '4px' }}
|
|
202
|
+
placeholder="Search"
|
|
203
|
+
size="small"
|
|
204
|
+
value={formFieldsSearchValue}
|
|
205
|
+
onChange={e => setFormFieldsSearchValue(e.target.value ?? '')}
|
|
206
|
+
/>
|
|
151
207
|
|
|
152
208
|
<FieldOptionList
|
|
153
209
|
title=""
|
|
154
210
|
isLoading={isFormFieldsLoading}
|
|
155
|
-
options={
|
|
211
|
+
options={filteredCalcOptions}
|
|
156
212
|
highlightElementPath={highlightElementPath}
|
|
157
213
|
selectedPaths={selectedPaths}
|
|
158
214
|
onHover={onHover}
|
|
@@ -228,6 +284,14 @@ const FieldOptionList: FC<FieldOptionListProps> = ({
|
|
|
228
284
|
<span className="dte-text-ellipsis" title={opt.label ?? opt.path}>
|
|
229
285
|
{opt.label ?? opt.path}
|
|
230
286
|
</span>
|
|
287
|
+
<Text
|
|
288
|
+
size="small"
|
|
289
|
+
subdued
|
|
290
|
+
style={{ fontSize: '12px', marginTop: '-2px' }}
|
|
291
|
+
className="ellipsis"
|
|
292
|
+
>
|
|
293
|
+
e.g. {getFieldSampleData(opt.sampleData, opt.fieldType)}
|
|
294
|
+
</Text>
|
|
231
295
|
</li>
|
|
232
296
|
))}
|
|
233
297
|
</ul>
|
|
@@ -11,13 +11,17 @@ import { useFormulaEditor } from '../../hooks';
|
|
|
11
11
|
import {
|
|
12
12
|
CalculatedFieldFormat,
|
|
13
13
|
DataModelFieldGroup,
|
|
14
|
+
DataModelValues,
|
|
14
15
|
FieldTypeOption,
|
|
15
16
|
FormFieldsByFormIdI,
|
|
16
17
|
FormInfo,
|
|
17
18
|
StructuredFormula,
|
|
18
19
|
} from '../../interface/types';
|
|
19
20
|
import {
|
|
21
|
+
evaluateFormula,
|
|
22
|
+
formatCalculatedResult,
|
|
20
23
|
formFieldInfosToCalculationOptions,
|
|
24
|
+
getFieldSampleData,
|
|
21
25
|
parseExpression,
|
|
22
26
|
tokenizeExpression,
|
|
23
27
|
tokensToExpression,
|
|
@@ -46,6 +50,20 @@ function getValidPathsSet(groups: DataModelFieldGroup[]): Set<string> {
|
|
|
46
50
|
return set;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
function setSampleValueAtPath(target: DataModelValues, path: string, value: unknown): void {
|
|
54
|
+
const keys = path.split('.');
|
|
55
|
+
let cursor: DataModelValues = target;
|
|
56
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
57
|
+
const key = keys[i];
|
|
58
|
+
const next = cursor[key];
|
|
59
|
+
if (next === null || typeof next !== 'object') {
|
|
60
|
+
cursor[key] = {};
|
|
61
|
+
}
|
|
62
|
+
cursor = cursor[key];
|
|
63
|
+
}
|
|
64
|
+
cursor[keys[keys.length - 1]] = value;
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
export interface FormulaModalProps {
|
|
50
68
|
initialFormula: StructuredFormula | undefined;
|
|
51
69
|
initialFormat?: CalculatedFieldFormat;
|
|
@@ -197,6 +215,48 @@ export const FormulaModal: FC<FormulaModalProps> = ({
|
|
|
197
215
|
return errors;
|
|
198
216
|
}, [formulaEditor.draftExpression, validPaths]);
|
|
199
217
|
|
|
218
|
+
const optionByPath = useMemo(() => {
|
|
219
|
+
const map = new Map<string, FieldTypeOption>();
|
|
220
|
+
for (const opt of allFields) {
|
|
221
|
+
if (opt.path) {
|
|
222
|
+
map.set(opt.path, opt);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const opt of fillableFieldsFromDocument) {
|
|
226
|
+
if (opt.path) {
|
|
227
|
+
map.set(opt.path, opt);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const opt of formCalculationOptions) {
|
|
231
|
+
if (opt.path) {
|
|
232
|
+
map.set(opt.path, opt);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return map;
|
|
236
|
+
}, [allFields, fillableFieldsFromDocument, formCalculationOptions]);
|
|
237
|
+
|
|
238
|
+
const previewValue = useMemo(() => {
|
|
239
|
+
if (parsedTokens.length === 0 || !formulaValidation.valid) {
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
const sampleData: DataModelValues = {};
|
|
243
|
+
for (const token of parsedTokens) {
|
|
244
|
+
if (token.type !== 'field') {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const option = optionByPath.get(token.path);
|
|
248
|
+
const sample = getFieldSampleData(option?.sampleData, token.fieldType);
|
|
249
|
+
if (sample !== null) {
|
|
250
|
+
setSampleValueAtPath(sampleData, token.path, sample);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const computed = evaluateFormula({ tokens: parsedTokens }, sampleData);
|
|
254
|
+
if (computed === null) {
|
|
255
|
+
return '';
|
|
256
|
+
}
|
|
257
|
+
return formatCalculatedResult(computed, format);
|
|
258
|
+
}, [parsedTokens, formulaValidation.valid, optionByPath, format]);
|
|
259
|
+
|
|
200
260
|
const handleClose = useCallback(() => {
|
|
201
261
|
onClose();
|
|
202
262
|
}, [onClose]);
|
|
@@ -243,6 +303,7 @@ export const FormulaModal: FC<FormulaModalProps> = ({
|
|
|
243
303
|
isInvalid={isInvalid && formulaEditor.isDirty}
|
|
244
304
|
validationError={validationError}
|
|
245
305
|
format={format}
|
|
306
|
+
previewValue={previewValue}
|
|
246
307
|
onResultTypeChange={nextType => {
|
|
247
308
|
if (nextType === 'date') {
|
|
248
309
|
setFormat(DEFAULT_DATE_CALCULATED_FORMAT);
|
|
@@ -30,6 +30,7 @@ interface FormulaWorkspaceProps {
|
|
|
30
30
|
onRemoveField: (fieldIndex: number) => void;
|
|
31
31
|
onResultTypeChange: (nextType: CalculatedFieldFormat['resultType']) => void;
|
|
32
32
|
onToggleAdvanced: () => void;
|
|
33
|
+
previewValue: string;
|
|
33
34
|
setFormat: Dispatch<SetStateAction<CalculatedFieldFormat>>;
|
|
34
35
|
validationError: string;
|
|
35
36
|
}
|
|
@@ -50,6 +51,7 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
|
|
|
50
51
|
onRemoveField,
|
|
51
52
|
onResultTypeChange,
|
|
52
53
|
onToggleAdvanced,
|
|
54
|
+
previewValue,
|
|
53
55
|
setFormat,
|
|
54
56
|
validationError,
|
|
55
57
|
}) => {
|
|
@@ -74,6 +76,12 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
|
|
|
74
76
|
|
|
75
77
|
return (
|
|
76
78
|
<Flex direction="column" flex={1} gap={2} style={{ padding: '12px' }}>
|
|
79
|
+
<Flex direction="column" gap={1}>
|
|
80
|
+
<Text variant="body" size="small">
|
|
81
|
+
Preview (uses demo sample data)
|
|
82
|
+
</Text>
|
|
83
|
+
<div className="dte-formula-preview">{previewValue ?? ' '}</div>
|
|
84
|
+
</Flex>
|
|
77
85
|
<Text variant="body" size="small">
|
|
78
86
|
Click a field on the left to add it. Use +, -,{' '}
|
|
79
87
|
{disabledOperators?.has('*') ? '' : '*, /, and '}parentheses to build formulas.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flex, SearchField, Text } from '@servicetitan/anvil2';
|
|
2
2
|
import { FC, Fragment, useMemo, useState } from 'react';
|
|
3
3
|
import { DataModelFieldGroup, FieldTypeOption } from '../../interface/types';
|
|
4
|
+
import { getFieldSampleData } from '../../utils';
|
|
4
5
|
import { FieldType } from './field-type';
|
|
5
6
|
|
|
6
7
|
interface DataModelFieldTypeListProps {
|
|
@@ -51,9 +52,13 @@ export const DataModelFieldTypeList: FC<DataModelFieldTypeListProps> = ({
|
|
|
51
52
|
return (
|
|
52
53
|
<FieldType
|
|
53
54
|
key={key}
|
|
54
|
-
label={fieldOption.label}
|
|
55
55
|
onDragEnd={onDragEnd}
|
|
56
56
|
onDragStart={() => onDragStart(fieldOption)}
|
|
57
|
+
label={fieldOption.label}
|
|
58
|
+
sampleData={getFieldSampleData(
|
|
59
|
+
fieldOption.sampleData,
|
|
60
|
+
fieldOption.fieldType,
|
|
61
|
+
)}
|
|
57
62
|
/>
|
|
58
63
|
);
|
|
59
64
|
})}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { Icon, Text } from '@servicetitan/anvil2';
|
|
1
|
+
import { Flex, Icon, Text } from '@servicetitan/anvil2';
|
|
2
2
|
import IconDragIndicator from '@servicetitan/anvil2/assets/icons/material/round/drag_indicator.svg';
|
|
3
3
|
import { FC } from 'react';
|
|
4
4
|
|
|
5
5
|
interface FieldTypeProps {
|
|
6
6
|
label: string;
|
|
7
|
+
sampleData?: string | number | null;
|
|
7
8
|
onDragEnd(): void;
|
|
8
9
|
onDragStart(): void;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export const FieldType: FC<FieldTypeProps> = ({ label, onDragEnd, onDragStart }) => {
|
|
12
|
+
export const FieldType: FC<FieldTypeProps> = ({ label, onDragEnd, onDragStart, sampleData }) => {
|
|
12
13
|
return (
|
|
13
14
|
<div
|
|
14
15
|
draggable
|
|
@@ -22,7 +23,19 @@ export const FieldType: FC<FieldTypeProps> = ({ label, onDragEnd, onDragStart })
|
|
|
22
23
|
className="dte-field-type-item"
|
|
23
24
|
>
|
|
24
25
|
<Icon svg={IconDragIndicator} size="medium" />
|
|
25
|
-
<
|
|
26
|
+
<Flex direction="column" style={{ overflow: 'hidden' }}>
|
|
27
|
+
<Text size="small">{label}</Text>
|
|
28
|
+
{sampleData && (
|
|
29
|
+
<Text
|
|
30
|
+
size="small"
|
|
31
|
+
subdued
|
|
32
|
+
style={{ fontSize: '12px', marginTop: '-2px' }}
|
|
33
|
+
className="ellipsis"
|
|
34
|
+
>
|
|
35
|
+
e.g. {sampleData}
|
|
36
|
+
</Text>
|
|
37
|
+
)}
|
|
38
|
+
</Flex>
|
|
26
39
|
</div>
|
|
27
40
|
);
|
|
28
41
|
};
|
|
@@ -4,6 +4,7 @@ import IconArrowForward from '@servicetitan/anvil2/assets/icons/material/round/k
|
|
|
4
4
|
import { Stack } from '@servicetitan/design-system';
|
|
5
5
|
import { FC, Fragment } from 'react';
|
|
6
6
|
import { FieldTypeOption, FormInfo } from '../../interface/types';
|
|
7
|
+
import { getFieldSampleData } from '../../utils';
|
|
7
8
|
import { FieldType } from './field-type';
|
|
8
9
|
|
|
9
10
|
export interface FormFieldsTypeListProps {
|
|
@@ -97,6 +98,10 @@ export const FormFieldsTypeList: FC<FormFieldsTypeListProps> = ({
|
|
|
97
98
|
<FieldType
|
|
98
99
|
key={option.path}
|
|
99
100
|
label={option.label}
|
|
101
|
+
sampleData={getFieldSampleData(
|
|
102
|
+
option.sampleData,
|
|
103
|
+
option.fieldType,
|
|
104
|
+
)}
|
|
100
105
|
onDragEnd={onDragEnd}
|
|
101
106
|
onDragStart={() => onDragStart(option)}
|
|
102
107
|
/>
|
|
@@ -2,7 +2,12 @@ import { FC, MouseEvent, RefObject } from 'react';
|
|
|
2
2
|
import { DISABLE_FIELD_RESIZE } from '../../constants';
|
|
3
3
|
import { useFieldDrag, useFieldResize } from '../../hooks';
|
|
4
4
|
import { FieldTypeEnum, PdfField } from '../../interface/types';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getFieldBackgroundColor,
|
|
7
|
+
getFieldFontSizeInPixels,
|
|
8
|
+
getPagePosition,
|
|
9
|
+
supportsFieldFontSize,
|
|
10
|
+
} from '../../utils';
|
|
6
11
|
import { PdfOverlayFieldCalculated } from './pdf-overlay-field-calculated';
|
|
7
12
|
import { PdfOverlayFieldDataModel } from './pdf-overlay-field-data-model';
|
|
8
13
|
import { PdfOverlayFieldESign } from './pdf-overlay-field-e-sign';
|
|
@@ -64,6 +69,9 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
|
|
|
64
69
|
width: field.width,
|
|
65
70
|
height: field.height,
|
|
66
71
|
backgroundColor: getFieldBackgroundColor(field.recipient, recipientsColors),
|
|
72
|
+
...(supportsFieldFontSize(field)
|
|
73
|
+
? { fontSize: `${getFieldFontSizeInPixels(field)}px` }
|
|
74
|
+
: {}),
|
|
67
75
|
};
|
|
68
76
|
|
|
69
77
|
return (
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { FC, PropsWithChildren, RefObject } from 'react';
|
|
2
2
|
import { DataModelValues, PdfField } from '../../interface/types';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
evaluateDisplayCondition,
|
|
5
|
+
getFieldBackgroundColor,
|
|
6
|
+
getFieldFontSizeInPixels,
|
|
7
|
+
getPagePosition,
|
|
8
|
+
supportsFieldFontSize,
|
|
9
|
+
} from '../../utils';
|
|
4
10
|
|
|
5
11
|
interface PdfViewFieldContainer {
|
|
6
12
|
pdfWrapperRef: RefObject<HTMLDivElement>;
|
|
@@ -39,6 +45,9 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
|
|
|
39
45
|
top: `${pagePos.top + field.y}px`,
|
|
40
46
|
width: `${field.width}px`,
|
|
41
47
|
height: `${field.height}px`,
|
|
48
|
+
...(supportsFieldFontSize(field)
|
|
49
|
+
? { fontSize: `${getFieldFontSizeInPixels(field)}px` }
|
|
50
|
+
: {}),
|
|
42
51
|
borderColor: error ? 'red' : undefined,
|
|
43
52
|
}}
|
|
44
53
|
>
|
|
@@ -22,6 +22,7 @@ const fillableTextStyles = {
|
|
|
22
22
|
width: 'inherit',
|
|
23
23
|
height: 'inherit',
|
|
24
24
|
borderColor: 'inherit',
|
|
25
|
+
fontSize: 'inherit',
|
|
25
26
|
} as const;
|
|
26
27
|
|
|
27
28
|
export const FillableTextInput: FC<FillableTextInputProps> = ({
|
|
@@ -149,6 +150,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
149
150
|
width: 'inherit',
|
|
150
151
|
height: 'inherit',
|
|
151
152
|
borderColor: 'inherit',
|
|
153
|
+
fontSize: 'inherit',
|
|
152
154
|
}}
|
|
153
155
|
disabled={!isFillable}
|
|
154
156
|
id={field.path}
|
|
@@ -172,6 +174,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
|
|
|
172
174
|
width: 'inherit',
|
|
173
175
|
height: 'inherit',
|
|
174
176
|
borderColor: 'inherit',
|
|
177
|
+
fontSize: 'inherit',
|
|
175
178
|
}}
|
|
176
179
|
disabled={!isFillable}
|
|
177
180
|
id={field.path}
|
|
@@ -18,6 +18,12 @@ export const FIELD_CONSTANTS = {
|
|
|
18
18
|
dropOffsetY: 15,
|
|
19
19
|
} as const;
|
|
20
20
|
|
|
21
|
+
export const DEFAULT_FIELD_FONT_SIZE = 8;
|
|
22
|
+
|
|
23
|
+
export const FIELD_FONT_SIZE_OPTIONS = Array.from({ length: 17 }, (_, index) => {
|
|
24
|
+
return 4 + index;
|
|
25
|
+
});
|
|
26
|
+
|
|
21
27
|
export const FILLABLE_FIELD_DEFAULT_SIZES = {
|
|
22
28
|
text: { width: 200, height: 25 },
|
|
23
29
|
date: { width: 200, height: 25 },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DragEvent, RefObject, useCallback, useRef, useState } from 'react';
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
3
|
import {
|
|
4
|
+
DEFAULT_FIELD_FONT_SIZE,
|
|
4
5
|
FIELD_CONSTANTS,
|
|
5
6
|
FILLABLE_FIELD_DEFAULT_SIZES,
|
|
6
7
|
GENERIC_FIELD_DEFAULT_SIZES,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
generateESignPath,
|
|
19
20
|
generateFillablePath,
|
|
20
21
|
isDragOverCanvas,
|
|
22
|
+
supportsFieldFontSize,
|
|
21
23
|
} from '../utils';
|
|
22
24
|
|
|
23
25
|
const DRAG_OVER_THROTTLE_MS = 32; // ~30fps for drop effect updates
|
|
@@ -39,7 +41,7 @@ const initializeDroppedFieldByType: Record<FieldTypeEnum, InitializeDroppedField
|
|
|
39
41
|
return {
|
|
40
42
|
...base,
|
|
41
43
|
recipient: recipientName,
|
|
42
|
-
required:
|
|
44
|
+
required: false,
|
|
43
45
|
path: generateFillablePath(recipientName, uuidv4()),
|
|
44
46
|
...(defaultSize && {
|
|
45
47
|
width: defaultSize.width,
|
|
@@ -192,6 +194,13 @@ export const usePdfFieldDnD = ({
|
|
|
192
194
|
width: FIELD_CONSTANTS.defaultWidth,
|
|
193
195
|
height: FIELD_CONSTANTS.defaultHeight,
|
|
194
196
|
path: draggedFieldOption.path,
|
|
197
|
+
...(supportsFieldFontSize(draggedFieldOption)
|
|
198
|
+
? {
|
|
199
|
+
styles: {
|
|
200
|
+
fontSize: DEFAULT_FIELD_FONT_SIZE,
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
: {}),
|
|
195
204
|
};
|
|
196
205
|
|
|
197
206
|
const newField = initializeDroppedFieldByType[baseField.type](
|
package/src/interface/types.ts
CHANGED
|
@@ -77,6 +77,10 @@ export interface DisplayConditionSingle {
|
|
|
77
77
|
formSnapshot?: FormulaFieldFormSnapshot;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
export interface PdfFieldStyles {
|
|
81
|
+
fontSize?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
export interface PdfField {
|
|
81
85
|
id: string;
|
|
82
86
|
type: FieldTypeEnum;
|
|
@@ -97,6 +101,7 @@ export interface PdfField {
|
|
|
97
101
|
formulaFormat?: CalculatedFieldFormat;
|
|
98
102
|
displayCondition?: DisplayConditionState | null;
|
|
99
103
|
multiline?: boolean;
|
|
104
|
+
styles?: PdfFieldStyles;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
export type SchemaFieldType = 'number' | 'date';
|
|
@@ -108,6 +113,7 @@ export interface FieldTypeOption {
|
|
|
108
113
|
path?: string;
|
|
109
114
|
fieldType?: SchemaFieldType;
|
|
110
115
|
formSnapshot?: FormulaFieldFormSnapshot;
|
|
116
|
+
sampleData?: unknown;
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
export interface DataModelFieldGroup {
|
|
@@ -116,6 +116,16 @@
|
|
|
116
116
|
white-space: pre-wrap;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
.dte-formula-preview {
|
|
120
|
+
padding: var(--spacing-1) var(--spacing-2);
|
|
121
|
+
border: 1px solid var(--border-color);
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
height: 20px;
|
|
124
|
+
background-color: var(--color-neutral-10, #f5f6f7);
|
|
125
|
+
font-size: var(--typescale-2);
|
|
126
|
+
line-height: var(--base-line-height);
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
.dte-formula-editor:focus {
|
|
120
130
|
border-color: var(--border-color-active);
|
|
121
131
|
box-shadow: 0 0 0 1px var(--border-color-active);
|
|
@@ -91,6 +91,7 @@ export const extractGroupedFieldsFromDataModel = (
|
|
|
91
91
|
type: FieldTypeEnum.dataModel,
|
|
92
92
|
path: key,
|
|
93
93
|
fieldType: getSchemaFieldType(property),
|
|
94
|
+
sampleData: property?.options?.sampleData,
|
|
94
95
|
});
|
|
95
96
|
}
|
|
96
97
|
}
|
|
@@ -132,6 +133,7 @@ const extractFieldsRecursive = (
|
|
|
132
133
|
type: FieldTypeEnum.dataModel,
|
|
133
134
|
path: currentPath,
|
|
134
135
|
fieldType: getSchemaFieldType(fieldProperty),
|
|
136
|
+
sampleData: fieldProperty.options?.sampleData,
|
|
135
137
|
});
|
|
136
138
|
} else if (fieldProperty.type === 'object' && fieldProperty.properties) {
|
|
137
139
|
extractFieldsRecursive(
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DEFAULT_FIELD_FONT_SIZE } from '../../constants';
|
|
2
|
+
import { FieldTypeEnum, PdfField } from '../../interface/types';
|
|
3
|
+
import { pointsToPixels } from '../pdf';
|
|
4
|
+
|
|
5
|
+
export const supportsFieldFontSize = (field: Pick<PdfField, 'type' | 'subType'>) => {
|
|
6
|
+
if (field.type === FieldTypeEnum.eSign) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (field.type === FieldTypeEnum.generic && field.subType === 'table') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return !(
|
|
15
|
+
field.type === FieldTypeEnum.fillable &&
|
|
16
|
+
(field.subType === 'checkbox' || field.subType === 'radio')
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Returns the field font size in points, as stored and rendered by the backend. */
|
|
21
|
+
export const getFieldFontSize = (field: PdfField) => {
|
|
22
|
+
return field.styles?.fontSize ?? DEFAULT_FIELD_FONT_SIZE;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Converts the field font size from points to editor pixels for CSS rendering. */
|
|
26
|
+
export const getFieldFontSizeInPixels = (field: PdfField) => {
|
|
27
|
+
return pointsToPixels(getFieldFontSize(field));
|
|
28
|
+
};
|
package/src/utils/field/index.ts
CHANGED