@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.
Files changed (93) hide show
  1. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  2. package/dist/components/field-config-panel/field-config-panel.js +15 -4
  3. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  4. package/dist/components/field-config-panel/field-sidebar.d.ts.map +1 -1
  5. package/dist/components/field-config-panel/field-sidebar.js +42 -6
  6. package/dist/components/field-config-panel/field-sidebar.js.map +1 -1
  7. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/formula-modal.js +55 -2
  9. package/dist/components/field-config-panel/formula-modal.js.map +1 -1
  10. package/dist/components/field-config-panel/formula-workspace.d.ts +1 -0
  11. package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
  12. package/dist/components/field-config-panel/formula-workspace.js +3 -3
  13. package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
  14. package/dist/components/field-sidebar/data-model-field-type-list.d.ts.map +1 -1
  15. package/dist/components/field-sidebar/data-model-field-type-list.js +2 -1
  16. package/dist/components/field-sidebar/data-model-field-type-list.js.map +1 -1
  17. package/dist/components/field-sidebar/field-type.d.ts +1 -0
  18. package/dist/components/field-sidebar/field-type.d.ts.map +1 -1
  19. package/dist/components/field-sidebar/field-type.js +3 -3
  20. package/dist/components/field-sidebar/field-type.js.map +1 -1
  21. package/dist/components/field-sidebar/form-fields-type-list.d.ts.map +1 -1
  22. package/dist/components/field-sidebar/form-fields-type-list.js +2 -1
  23. package/dist/components/field-sidebar/form-fields-type-list.js.map +1 -1
  24. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  25. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +4 -1
  26. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  27. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  28. package/dist/components/pdf-view/pdf-view-field-container.js +4 -1
  29. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  30. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  31. package/dist/components/pdf-view/pdf-view-fillable.js +3 -0
  32. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  33. package/dist/constants/field.constants.d.ts +2 -0
  34. package/dist/constants/field.constants.d.ts.map +1 -1
  35. package/dist/constants/field.constants.js +4 -0
  36. package/dist/constants/field.constants.js.map +1 -1
  37. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  38. package/dist/hooks/usePdfFieldDnD.js +10 -3
  39. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  40. package/dist/interface/types.d.ts +5 -0
  41. package/dist/interface/types.d.ts.map +1 -1
  42. package/dist/interface/types.js.map +1 -1
  43. package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
  44. package/dist/utils/data-model/extract-fields.utils.js +4 -2
  45. package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
  46. package/dist/utils/field/get-field-font-size.utils.d.ts +7 -0
  47. package/dist/utils/field/get-field-font-size.utils.d.ts.map +1 -0
  48. package/dist/utils/field/get-field-font-size.utils.js +23 -0
  49. package/dist/utils/field/get-field-font-size.utils.js.map +1 -0
  50. package/dist/utils/field/index.d.ts +1 -0
  51. package/dist/utils/field/index.d.ts.map +1 -1
  52. package/dist/utils/field/index.js +1 -0
  53. package/dist/utils/field/index.js.map +1 -1
  54. package/dist/utils/pdf/index.d.ts +1 -0
  55. package/dist/utils/pdf/index.d.ts.map +1 -1
  56. package/dist/utils/pdf/index.js +1 -0
  57. package/dist/utils/pdf/index.js.map +1 -1
  58. package/dist/utils/pdf/unit-conversion.utils.d.ts +22 -0
  59. package/dist/utils/pdf/unit-conversion.utils.d.ts.map +1 -0
  60. package/dist/utils/pdf/unit-conversion.utils.js +27 -0
  61. package/dist/utils/pdf/unit-conversion.utils.js.map +1 -0
  62. package/dist/utils/shared/index.d.ts +1 -0
  63. package/dist/utils/shared/index.d.ts.map +1 -1
  64. package/dist/utils/shared/index.js +1 -0
  65. package/dist/utils/shared/index.js.map +1 -1
  66. package/dist/utils/shared/sample-data.utils.d.ts +3 -0
  67. package/dist/utils/shared/sample-data.utils.d.ts.map +1 -0
  68. package/dist/utils/shared/sample-data.utils.js +20 -0
  69. package/dist/utils/shared/sample-data.utils.js.map +1 -0
  70. package/package.json +1 -1
  71. package/src/components/field-config-panel/field-config-panel.tsx +43 -3
  72. package/src/components/field-config-panel/field-sidebar.tsx +71 -7
  73. package/src/components/field-config-panel/formula-modal.tsx +61 -0
  74. package/src/components/field-config-panel/formula-workspace.tsx +8 -0
  75. package/src/components/field-sidebar/data-model-field-type-list.tsx +6 -1
  76. package/src/components/field-sidebar/field-type.tsx +16 -3
  77. package/src/components/field-sidebar/form-fields-type-list.tsx +5 -0
  78. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +9 -1
  79. package/src/components/pdf-view/pdf-view-field-container.tsx +10 -1
  80. package/src/components/pdf-view/pdf-view-fillable.tsx +3 -0
  81. package/src/constants/field.constants.ts +6 -0
  82. package/src/hooks/usePdfFieldDnD.ts +10 -1
  83. package/src/interface/types.ts +6 -0
  84. package/src/styles/formula-modal.css +10 -0
  85. package/src/styles/pdf-field-overlay.css +1 -0
  86. package/src/utils/data-model/extract-fields.utils.ts +2 -0
  87. package/src/utils/field/get-field-font-size.utils.ts +28 -0
  88. package/src/utils/field/index.ts +1 -0
  89. package/src/utils/pdf/__tests__/unit-conversion.utils.test.ts +35 -0
  90. package/src/utils/pdf/index.ts +1 -0
  91. package/src/utils/pdf/unit-conversion.utils.ts +31 -0
  92. package/src/utils/shared/index.ts +1 -0
  93. 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 { generateESignPath, generateFillablePath } from '../../utils';
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 && field.subType !== 'checkbox' && (
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 { formFieldInfosToCalculationOptions, parseFormFieldKey } from '../../utils';
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
- {forms.length > 0 && (
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
- {forms.map(form => (
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={mergeTagOptions}
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={() => setSelectedFormId(null)}
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={calcOptions}
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
- <Text size="small">{label}</Text>
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 { getFieldBackgroundColor, getPagePosition } from '../../utils';
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 { evaluateDisplayCondition, getFieldBackgroundColor, getPagePosition } from '../../utils';
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: base.subType === 'checkbox',
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](
@@ -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);
@@ -111,6 +111,7 @@
111
111
  border: none;
112
112
  pointer-events: none;
113
113
  margin: var(--spacing-0, 0);
114
+ font-size: inherit;
114
115
  }
115
116
 
116
117
  .dte-pdf-field-generic-table {
@@ -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
+ };
@@ -4,3 +4,4 @@ export * from './field-coordinates.utils';
4
4
  export * from './field-drag.utils';
5
5
  export * from './field-placeholder-text.utils';
6
6
  export * from './field-resize.utils';
7
+ export * from './get-field-font-size.utils';