@servicetitan/dte-pdf-editor 1.16.0 → 1.18.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 (190) hide show
  1. package/README.md +35 -7
  2. package/dist/components/field-config-panel/advanced-settings.d.ts +9 -0
  3. package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -0
  4. package/dist/components/field-config-panel/advanced-settings.js +17 -0
  5. package/dist/components/field-config-panel/advanced-settings.js.map +1 -0
  6. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +4 -1
  7. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/field-config-panel-overlay.js +2 -2
  9. package/dist/components/field-config-panel/field-config-panel-overlay.js.map +1 -1
  10. package/dist/components/field-config-panel/field-config-panel.d.ts +4 -1
  11. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  12. package/dist/components/field-config-panel/field-config-panel.js +11 -5
  13. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  14. package/dist/components/field-config-panel/field-sidebar.d.ts +13 -0
  15. package/dist/components/field-config-panel/field-sidebar.d.ts.map +1 -0
  16. package/dist/components/field-config-panel/field-sidebar.js +20 -0
  17. package/dist/components/field-config-panel/field-sidebar.js.map +1 -0
  18. package/dist/components/field-config-panel/formula-generator.d.ts +11 -0
  19. package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -0
  20. package/dist/components/field-config-panel/formula-generator.js +51 -0
  21. package/dist/components/field-config-panel/formula-generator.js.map +1 -0
  22. package/dist/components/field-config-panel/formula-modal.d.ts +12 -0
  23. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -0
  24. package/dist/components/field-config-panel/formula-modal.js +99 -0
  25. package/dist/components/field-config-panel/formula-modal.js.map +1 -0
  26. package/dist/components/field-config-panel/formula-workspace.d.ts +23 -0
  27. package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -0
  28. package/dist/components/field-config-panel/formula-workspace.js +28 -0
  29. package/dist/components/field-config-panel/formula-workspace.js.map +1 -0
  30. package/dist/components/field-config-panel/result-type-selector.d.ts +9 -0
  31. package/dist/components/field-config-panel/result-type-selector.d.ts.map +1 -0
  32. package/dist/components/field-config-panel/result-type-selector.js +10 -0
  33. package/dist/components/field-config-panel/result-type-selector.js.map +1 -0
  34. package/dist/components/field-sidebar/calculated-field-type-list.d.ts +9 -0
  35. package/dist/components/field-sidebar/calculated-field-type-list.d.ts.map +1 -0
  36. package/dist/components/field-sidebar/calculated-field-type-list.js +12 -0
  37. package/dist/components/field-sidebar/calculated-field-type-list.js.map +1 -0
  38. package/dist/components/field-sidebar/data-model-field-type-list.d.ts +0 -1
  39. package/dist/components/field-sidebar/data-model-field-type-list.d.ts.map +1 -1
  40. package/dist/components/field-sidebar/data-model-field-type-list.js +8 -7
  41. package/dist/components/field-sidebar/data-model-field-type-list.js.map +1 -1
  42. package/dist/components/field-sidebar/field-menu-group.d.ts +11 -0
  43. package/dist/components/field-sidebar/field-menu-group.d.ts.map +1 -0
  44. package/dist/components/field-sidebar/field-menu-group.js +6 -0
  45. package/dist/components/field-sidebar/field-menu-group.js.map +1 -0
  46. package/dist/components/field-sidebar/field-sidebar.d.ts.map +1 -1
  47. package/dist/components/field-sidebar/field-sidebar.js +6 -15
  48. package/dist/components/field-sidebar/field-sidebar.js.map +1 -1
  49. package/dist/components/field-sidebar/fillable-field-type-list.d.ts +0 -1
  50. package/dist/components/field-sidebar/fillable-field-type-list.d.ts.map +1 -1
  51. package/dist/components/field-sidebar/fillable-field-type-list.js +8 -9
  52. package/dist/components/field-sidebar/fillable-field-type-list.js.map +1 -1
  53. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  54. package/dist/components/pdf-editor/pdf-editor.js +1 -1
  55. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  56. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts +8 -0
  57. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts.map +1 -0
  58. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js +5 -0
  59. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js.map +1 -0
  60. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  61. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +11 -6
  62. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  63. package/dist/components/pdf-view/pdf-view-calculated.d.ts +9 -0
  64. package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -0
  65. package/dist/components/pdf-view/pdf-view-calculated.js +18 -0
  66. package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -0
  67. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  68. package/dist/components/pdf-view/pdf-view.js +2 -1
  69. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  70. package/dist/constants/field.constants.d.ts +3 -2
  71. package/dist/constants/field.constants.d.ts.map +1 -1
  72. package/dist/constants/field.constants.js +6 -0
  73. package/dist/constants/field.constants.js.map +1 -1
  74. package/dist/constants/menu-group.d.ts +8 -0
  75. package/dist/constants/menu-group.d.ts.map +1 -0
  76. package/dist/constants/menu-group.js +20 -0
  77. package/dist/constants/menu-group.js.map +1 -0
  78. package/dist/hooks/index.d.ts +1 -0
  79. package/dist/hooks/index.d.ts.map +1 -1
  80. package/dist/hooks/index.js +1 -0
  81. package/dist/hooks/index.js.map +1 -1
  82. package/dist/hooks/useFieldDrag.d.ts +9 -2
  83. package/dist/hooks/useFieldDrag.d.ts.map +1 -1
  84. package/dist/hooks/useFieldDrag.js +60 -8
  85. package/dist/hooks/useFieldDrag.js.map +1 -1
  86. package/dist/hooks/useFormulaEditor.d.ts +22 -0
  87. package/dist/hooks/useFormulaEditor.d.ts.map +1 -0
  88. package/dist/hooks/useFormulaEditor.js +290 -0
  89. package/dist/hooks/useFormulaEditor.js.map +1 -0
  90. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  91. package/dist/hooks/usePdfFieldDnD.js +19 -3
  92. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  93. package/dist/interface/types.d.ts +45 -3
  94. package/dist/interface/types.d.ts.map +1 -1
  95. package/dist/interface/types.js +3 -0
  96. package/dist/interface/types.js.map +1 -1
  97. package/dist/utils/data-model/extract-fields.utils.d.ts +5 -5
  98. package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
  99. package/dist/utils/data-model/extract-fields.utils.js +42 -8
  100. package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
  101. package/dist/utils/formula/caret.utils.d.ts +3 -0
  102. package/dist/utils/formula/caret.utils.d.ts.map +1 -0
  103. package/dist/utils/formula/caret.utils.js +123 -0
  104. package/dist/utils/formula/caret.utils.js.map +1 -0
  105. package/dist/utils/formula/dom.utils.d.ts +4 -0
  106. package/dist/utils/formula/dom.utils.d.ts.map +1 -0
  107. package/dist/utils/formula/dom.utils.js +34 -0
  108. package/dist/utils/formula/dom.utils.js.map +1 -0
  109. package/dist/utils/formula/evaluate-formula.utils.d.ts +13 -0
  110. package/dist/utils/formula/evaluate-formula.utils.d.ts.map +1 -0
  111. package/dist/utils/formula/evaluate-formula.utils.js +134 -0
  112. package/dist/utils/formula/evaluate-formula.utils.js.map +1 -0
  113. package/dist/utils/formula/expression.utils.d.ts +18 -0
  114. package/dist/utils/formula/expression.utils.d.ts.map +1 -0
  115. package/dist/utils/formula/expression.utils.js +84 -0
  116. package/dist/utils/formula/expression.utils.js.map +1 -0
  117. package/dist/utils/formula/format-calculated-result.utils.d.ts +7 -0
  118. package/dist/utils/formula/format-calculated-result.utils.d.ts.map +1 -0
  119. package/dist/utils/formula/format-calculated-result.utils.js +50 -0
  120. package/dist/utils/formula/format-calculated-result.utils.js.map +1 -0
  121. package/dist/utils/formula/formula-types.d.ts +3 -0
  122. package/dist/utils/formula/formula-types.d.ts.map +1 -0
  123. package/dist/utils/formula/formula-types.js +2 -0
  124. package/dist/utils/formula/formula-types.js.map +1 -0
  125. package/dist/utils/formula/index.d.ts +11 -0
  126. package/dist/utils/formula/index.d.ts.map +1 -0
  127. package/dist/utils/formula/index.js +11 -0
  128. package/dist/utils/formula/index.js.map +1 -0
  129. package/dist/utils/formula/referenced-paths.utils.d.ts +7 -0
  130. package/dist/utils/formula/referenced-paths.utils.d.ts.map +1 -0
  131. package/dist/utils/formula/referenced-paths.utils.js +18 -0
  132. package/dist/utils/formula/referenced-paths.utils.js.map +1 -0
  133. package/dist/utils/formula/render-formula.utils.d.ts +8 -0
  134. package/dist/utils/formula/render-formula.utils.d.ts.map +1 -0
  135. package/dist/utils/formula/render-formula.utils.js +39 -0
  136. package/dist/utils/formula/render-formula.utils.js.map +1 -0
  137. package/dist/utils/formula/serialize-formula.utils.d.ts +14 -0
  138. package/dist/utils/formula/serialize-formula.utils.d.ts.map +1 -0
  139. package/dist/utils/formula/serialize-formula.utils.js +33 -0
  140. package/dist/utils/formula/serialize-formula.utils.js.map +1 -0
  141. package/dist/utils/formula/validate-formula.utils.d.ts +11 -0
  142. package/dist/utils/formula/validate-formula.utils.d.ts.map +1 -0
  143. package/dist/utils/formula/validate-formula.utils.js +79 -0
  144. package/dist/utils/formula/validate-formula.utils.js.map +1 -0
  145. package/dist/utils/index.d.ts +1 -0
  146. package/dist/utils/index.d.ts.map +1 -1
  147. package/dist/utils/index.js +1 -0
  148. package/dist/utils/index.js.map +1 -1
  149. package/package.json +2 -2
  150. package/src/components/field-config-panel/advanced-settings.tsx +113 -0
  151. package/src/components/field-config-panel/field-config-panel-overlay.tsx +8 -1
  152. package/src/components/field-config-panel/field-config-panel.tsx +43 -15
  153. package/src/components/field-config-panel/field-sidebar.tsx +91 -0
  154. package/src/components/field-config-panel/formula-generator.tsx +122 -0
  155. package/src/components/field-config-panel/formula-modal.tsx +229 -0
  156. package/src/components/field-config-panel/formula-workspace.tsx +116 -0
  157. package/src/components/field-config-panel/result-type-selector.tsx +34 -0
  158. package/src/components/field-sidebar/calculated-field-type-list.tsx +29 -0
  159. package/src/components/field-sidebar/data-model-field-type-list.tsx +10 -4
  160. package/src/components/field-sidebar/field-menu-group.tsx +36 -0
  161. package/src/components/field-sidebar/field-sidebar.tsx +14 -55
  162. package/src/components/field-sidebar/fillable-field-type-list.tsx +11 -9
  163. package/src/components/pdf-editor/pdf-editor.tsx +2 -0
  164. package/src/components/pdf-fields-overlay/pdf-overlay-field-calculated.tsx +15 -0
  165. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +10 -5
  166. package/src/components/pdf-view/pdf-view-calculated.tsx +23 -0
  167. package/src/components/pdf-view/pdf-view.tsx +4 -0
  168. package/src/constants/field.constants.ts +9 -2
  169. package/src/constants/menu-group.ts +26 -0
  170. package/src/hooks/index.ts +1 -0
  171. package/src/hooks/useFieldDrag.ts +84 -8
  172. package/src/hooks/useFormulaEditor.ts +336 -0
  173. package/src/hooks/usePdfFieldDnD.ts +36 -14
  174. package/src/interface/types.ts +38 -2
  175. package/src/styles/formula-modal.css +307 -0
  176. package/src/styles/index.css +1 -0
  177. package/src/styles/pdf-field-overlay.css +1 -0
  178. package/src/utils/data-model/extract-fields.utils.ts +65 -7
  179. package/src/utils/formula/caret.utils.ts +125 -0
  180. package/src/utils/formula/dom.utils.ts +35 -0
  181. package/src/utils/formula/evaluate-formula.utils.ts +159 -0
  182. package/src/utils/formula/expression.utils.ts +99 -0
  183. package/src/utils/formula/format-calculated-result.utils.ts +79 -0
  184. package/src/utils/formula/formula-types.ts +2 -0
  185. package/src/utils/formula/index.ts +10 -0
  186. package/src/utils/formula/referenced-paths.utils.ts +18 -0
  187. package/src/utils/formula/render-formula.utils.ts +40 -0
  188. package/src/utils/formula/serialize-formula.utils.ts +40 -0
  189. package/src/utils/formula/validate-formula.utils.ts +94 -0
  190. package/src/utils/index.ts +1 -0
@@ -0,0 +1,34 @@
1
+ import { Button, Flex, Text } from '@servicetitan/anvil2';
2
+ import { FC, Fragment } from 'react';
3
+ import { CalculatedFieldFormat } from '../../interface/types';
4
+
5
+ interface ResultTypeSelectorProps {
6
+ format: CalculatedFieldFormat;
7
+ onChange: (nextType: CalculatedFieldFormat['resultType']) => void;
8
+ }
9
+
10
+ const RESULT_TYPE_OPTIONS: { label: string; value: CalculatedFieldFormat['resultType'] }[] = [
11
+ { label: 'Number', value: 'number' },
12
+ { label: 'Currency', value: 'currency' },
13
+ { label: 'Percent', value: 'percent' },
14
+ ];
15
+
16
+ export const ResultTypeSelector: FC<ResultTypeSelectorProps> = ({ format, onChange }) => (
17
+ <Fragment>
18
+ <Text variant="body" size="small">
19
+ Result type
20
+ </Text>
21
+ <Flex gap={2}>
22
+ {RESULT_TYPE_OPTIONS.map(({ label, value }) => (
23
+ <Button
24
+ key={value}
25
+ appearance={format.resultType === value ? 'primary' : 'secondary'}
26
+ size="small"
27
+ onClick={() => onChange(value)}
28
+ >
29
+ {label}
30
+ </Button>
31
+ ))}
32
+ </Flex>
33
+ </Fragment>
34
+ );
@@ -0,0 +1,29 @@
1
+ import { FC, Fragment } from 'react';
2
+ import { CALCULATED_FIELD_TYPES } from '../../constants';
3
+ import { FieldTypeOption } from '../../interface/types';
4
+ import { FieldType } from './field-type';
5
+
6
+ interface CalculatedFieldTypeListProps {
7
+ onDragStart(fieldOption: FieldTypeOption): void;
8
+ onDragEnd(): void;
9
+ }
10
+ export const CalculatedFieldTypeList: FC<CalculatedFieldTypeListProps> = ({
11
+ onDragEnd,
12
+ onDragStart,
13
+ }) => {
14
+ return (
15
+ <Fragment>
16
+ {CALCULATED_FIELD_TYPES.map(fieldOption => {
17
+ const key = `${fieldOption.type}-${fieldOption.path ?? fieldOption.label}`;
18
+ return (
19
+ <FieldType
20
+ key={key}
21
+ label={fieldOption.label}
22
+ onDragEnd={onDragEnd}
23
+ onDragStart={() => onDragStart(fieldOption)}
24
+ />
25
+ );
26
+ })}
27
+ </Fragment>
28
+ );
29
+ };
@@ -1,10 +1,9 @@
1
- import { Flex, Text } from '@servicetitan/anvil2';
2
- import { FC, Fragment, useMemo } from 'react';
1
+ import { Flex, SearchField, Text } from '@servicetitan/anvil2';
2
+ import { FC, Fragment, useMemo, useState } from 'react';
3
3
  import { DataModelFieldGroup, FieldTypeOption } from '../../interface/types';
4
4
  import { FieldType } from './field-type';
5
5
 
6
6
  interface DataModelFieldTypeListProps {
7
- searchText?: string;
8
7
  dataModelGroups?: DataModelFieldGroup[];
9
8
  onDragStart(fieldOption: FieldTypeOption): void;
10
9
  onDragEnd(): void;
@@ -14,8 +13,9 @@ export const DataModelFieldTypeList: FC<DataModelFieldTypeListProps> = ({
14
13
  dataModelGroups = [],
15
14
  onDragEnd,
16
15
  onDragStart,
17
- searchText,
18
16
  }) => {
17
+ const [searchText, setSearchText] = useState('');
18
+
19
19
  const searchedFieldGroups = useMemo(() => {
20
20
  if (!searchText) {
21
21
  return dataModelGroups;
@@ -33,6 +33,12 @@ export const DataModelFieldTypeList: FC<DataModelFieldTypeListProps> = ({
33
33
  }, [dataModelGroups, searchText]);
34
34
  return (
35
35
  <Fragment>
36
+ <SearchField
37
+ placeholder="Search"
38
+ size="small"
39
+ className="dte-field-sidebar-search"
40
+ onChange={e => setSearchText(e.target.value ?? '')}
41
+ />
36
42
  {searchedFieldGroups.map(group => (
37
43
  <div key={group.groupName} className="dte-field-type-group">
38
44
  <Flex alignItems="center" gap={1} className="dte-field-type-group-header">
@@ -0,0 +1,36 @@
1
+ import { Flex, Icon, Text } from '@servicetitan/anvil2';
2
+ import { FC } from 'react';
3
+ import { MenuGroupModel } from '../../constants/menu-group';
4
+ import { FieldTypeEnum } from '../../interface/types';
5
+
6
+ interface FieldMenuGroupProps {
7
+ menuGroups: MenuGroupModel[];
8
+ activeFieldKey: FieldTypeEnum;
9
+ onClickMenu(key: FieldTypeEnum): void;
10
+ }
11
+
12
+ export const FieldMenuGroup: FC<FieldMenuGroupProps> = ({
13
+ activeFieldKey,
14
+ menuGroups,
15
+ onClickMenu,
16
+ }) => {
17
+ return (
18
+ <Flex className="dte-field-sidebar-menu" shrink={0} direction="column" gap="3">
19
+ {menuGroups.map(({ key, label, svgIcon }) => (
20
+ <Flex
21
+ key={key}
22
+ alignItems="center"
23
+ justifyContent="center"
24
+ direction="column"
25
+ className={`dte-field-sidebar-menu-item cursor-pointer ${key === activeFieldKey ? '--active' : ''}`}
26
+ onClick={() => onClickMenu(key)}
27
+ >
28
+ <Icon svg={svgIcon} size="large" className="dte-field-sidebar-menu-item-icon" />
29
+ <Text variant="body" size="small" className="dte-field-sidebar-menu-item-text">
30
+ {label}
31
+ </Text>
32
+ </Flex>
33
+ ))}
34
+ </Flex>
35
+ );
36
+ };
@@ -1,14 +1,14 @@
1
- import { Button, Flex, Icon, SearchField, Text } from '@servicetitan/anvil2';
2
- import IconBorderColor from '@servicetitan/anvil2/assets/icons/material/round/border_color.svg';
1
+ import { Button, Flex, Icon, Text } from '@servicetitan/anvil2';
3
2
  import IconMenuOpen from '@servicetitan/anvil2/assets/icons/material/round/menu_open.svg';
4
- import IconPhotoSizeSelectSmall from '@servicetitan/anvil2/assets/icons/material/round/photo_size_select_small.svg';
5
- import IconEstimate from '@servicetitan/anvil2/assets/icons/st/estimate.svg';
6
3
 
7
4
  import { FC, useState } from 'react';
5
+ import { menuGroups } from '../../constants/menu-group';
8
6
  import { useToggle } from '../../hooks';
9
7
  import { DataModelFieldGroup, FieldTypeEnum, FieldTypeOption } from '../../interface/types';
8
+ import { CalculatedFieldTypeList } from './calculated-field-type-list';
10
9
  import { DataModelFieldTypeList } from './data-model-field-type-list';
11
10
  import { ESignFieldTypeList } from './e-sign-field-type-list';
11
+ import { FieldMenuGroup } from './field-menu-group';
12
12
  import { FillableFieldTypeList } from './fillable-field-type-list';
13
13
 
14
14
  interface FieldSidebarProps {
@@ -17,23 +17,12 @@ interface FieldSidebarProps {
17
17
  onDragStart(fieldOption: FieldTypeOption): void;
18
18
  }
19
19
 
20
- const menuGroups: { svgIcon: any; label: string; key: FieldTypeEnum }[] = [
21
- { svgIcon: IconEstimate, label: 'Merge Tags', key: FieldTypeEnum.dataModel },
22
- { svgIcon: IconBorderColor, label: 'E-Sign', key: FieldTypeEnum.eSign },
23
- {
24
- svgIcon: IconPhotoSizeSelectSmall,
25
- label: 'Fillable Fields',
26
- key: FieldTypeEnum.fillable,
27
- },
28
- ];
29
-
30
20
  export const FieldSidebar: FC<FieldSidebarProps> = ({
31
21
  dataModelGroups = [],
32
22
  onDragEnd,
33
23
  onDragStart,
34
24
  }) => {
35
25
  const [activeFieldType, setActiveFieldType] = useState<FieldTypeEnum>(FieldTypeEnum.dataModel);
36
- const [searchText, setSearchText] = useState('');
37
26
  const { isOpen, open, toggle } = useToggle(true);
38
27
 
39
28
  const handleActiveFieldTypeChange = (activeFieldType: FieldTypeEnum) => {
@@ -43,31 +32,11 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
43
32
 
44
33
  return (
45
34
  <Flex className="dte-field-sidebar-container skeleton-item">
46
- <Flex className="dte-field-sidebar-menu" shrink={0} direction="column" gap="3">
47
- {menuGroups.map(({ key, label, svgIcon }) => (
48
- <Flex
49
- key={key}
50
- alignItems="center"
51
- justifyContent="center"
52
- direction="column"
53
- className={`dte-field-sidebar-menu-item cursor-pointer ${key === activeFieldType ? '--active' : ''}`}
54
- onClick={() => handleActiveFieldTypeChange(key)}
55
- >
56
- <Icon
57
- svg={svgIcon}
58
- size="large"
59
- className="dte-field-sidebar-menu-item-icon"
60
- />
61
- <Text
62
- variant="body"
63
- size="small"
64
- className="dte-field-sidebar-menu-item-text"
65
- >
66
- {label}
67
- </Text>
68
- </Flex>
69
- ))}
70
- </Flex>
35
+ <FieldMenuGroup
36
+ menuGroups={menuGroups}
37
+ onClickMenu={handleActiveFieldTypeChange}
38
+ activeFieldKey={activeFieldType}
39
+ />
71
40
 
72
41
  <Flex
73
42
  className={`dte-field-sidebar-content skeleton-item ${isOpen ? '--open' : ''}`}
@@ -75,35 +44,25 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
75
44
  >
76
45
  <Flex className="dte-field-sidebar-content-inner">
77
46
  <Text variant="headline" el="h1" size="medium">
78
- {menuGroups.find(group => group.key === activeFieldType)!.label}
47
+ {menuGroups.find(group => group.key === activeFieldType)?.label}
79
48
  </Text>
80
49
 
81
- {activeFieldType !== FieldTypeEnum.eSign && (
82
- <SearchField
83
- placeholder="Search"
84
- size="small"
85
- className="dte-field-sidebar-search"
86
- onChange={e => setSearchText(e.target.value ?? '')}
87
- />
88
- )}
89
50
  {activeFieldType === FieldTypeEnum.fillable && (
90
- <FillableFieldTypeList
91
- searchText={searchText}
92
- onDragStart={onDragStart}
93
- onDragEnd={onDragEnd}
94
- />
51
+ <FillableFieldTypeList onDragStart={onDragStart} onDragEnd={onDragEnd} />
95
52
  )}
96
53
  {activeFieldType === FieldTypeEnum.eSign && (
97
54
  <ESignFieldTypeList onDragEnd={onDragEnd} onDragStart={onDragStart} />
98
55
  )}
99
56
  {activeFieldType === FieldTypeEnum.dataModel && (
100
57
  <DataModelFieldTypeList
101
- searchText={searchText}
102
58
  dataModelGroups={dataModelGroups}
103
59
  onDragStart={onDragStart}
104
60
  onDragEnd={onDragEnd}
105
61
  />
106
62
  )}
63
+ {activeFieldType === FieldTypeEnum.calculated && (
64
+ <CalculatedFieldTypeList onDragStart={onDragStart} onDragEnd={onDragEnd} />
65
+ )}
107
66
  </Flex>
108
67
  </Flex>
109
68
  <Button
@@ -1,10 +1,10 @@
1
- import { FC, Fragment, useMemo } from 'react';
1
+ import { SearchField } from '@servicetitan/anvil2';
2
+ import { FC, Fragment, useMemo, useState } from 'react';
2
3
  import { FILLABLE_FIELD_TYPES } from '../../constants';
3
4
  import { FieldTypeOption } from '../../interface/types';
4
5
  import { FieldType } from './field-type';
5
6
 
6
7
  interface FillableFieldTypeListProps {
7
- searchText?: string;
8
8
  onDragStart(fieldOption: FieldTypeOption): void;
9
9
  onDragEnd(): void;
10
10
  }
@@ -12,8 +12,9 @@ interface FillableFieldTypeListProps {
12
12
  export const FillableFieldTypeList: FC<FillableFieldTypeListProps> = ({
13
13
  onDragEnd,
14
14
  onDragStart,
15
- searchText,
16
15
  }) => {
16
+ const [searchText, setSearchText] = useState('');
17
+
17
18
  const searchedFieldTypes = useMemo(() => {
18
19
  if (!searchText) {
19
20
  return FILLABLE_FIELD_TYPES;
@@ -27,18 +28,19 @@ export const FillableFieldTypeList: FC<FillableFieldTypeListProps> = ({
27
28
 
28
29
  return (
29
30
  <Fragment>
31
+ <SearchField
32
+ placeholder="Search"
33
+ size="small"
34
+ className="dte-field-sidebar-search"
35
+ onChange={e => setSearchText(e.target.value ?? '')}
36
+ />
30
37
  {searchedFieldTypes.map(fieldOption => {
31
38
  return (
32
39
  <FieldType
33
40
  key={fieldOption.subType}
34
41
  label={fieldOption.label}
35
42
  onDragEnd={onDragEnd}
36
- onDragStart={() =>
37
- onDragStart({
38
- ...fieldOption,
39
- label: '',
40
- })
41
- }
43
+ onDragStart={() => onDragStart(fieldOption)}
42
44
  />
43
45
  );
44
46
  })}
@@ -69,6 +69,8 @@ export const PdfEditor: FC<PdfEditorProps> = ({
69
69
  <Flex flex={1} className={`dte-pdf-editor ${loading ? 'skeleton' : ''}`}>
70
70
  {selectedField && (
71
71
  <FieldConfigPanelOverlay
72
+ dataModel={dataModel}
73
+ documentFields={fields}
72
74
  selectedField={selectedField}
73
75
  onFieldConfigChange={updateField}
74
76
  onDeleteField={deleteSelectedField}
@@ -0,0 +1,15 @@
1
+ import { FC } from 'react';
2
+ import { PdfField } from '../../interface/types';
3
+
4
+ interface PdfOverlayFieldCalculatedProps {
5
+ field: PdfField;
6
+ }
7
+
8
+ export const PdfOverlayFieldCalculated: FC<PdfOverlayFieldCalculatedProps> = ({ field }) => {
9
+ return (
10
+ <span>
11
+ {field.label ? field.label : field.path}
12
+ {field.required ? '*' : ''}
13
+ </span>
14
+ );
15
+ };
@@ -2,6 +2,7 @@ import { FC, MouseEvent, RefObject } from 'react';
2
2
  import { useFieldDrag, useFieldResize } from '../../hooks';
3
3
  import { FieldTypeEnum, PdfField } from '../../interface/types';
4
4
  import { getFieldBackgroundColor, getPagePosition } from '../../utils';
5
+ import { PdfOverlayFieldCalculated } from './pdf-overlay-field-calculated';
5
6
  import { PdfOverlayFieldDataModel } from './pdf-overlay-field-data-model';
6
7
  import { PdfOverlayFieldESign } from './pdf-overlay-field-e-sign';
7
8
  import { PdfOverlayFieldFillable } from './pdf-overlay-field-fillable';
@@ -30,22 +31,25 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
30
31
  pdfWrapperRef,
31
32
  recipientsColors,
32
33
  }) => {
33
- const { draggingFieldIdRef, handleDrag, handleDragEnd, handleDragStart } = useFieldDrag({
34
+ const { dragVisualPosition, handleDrag, handleDragEnd, handleDragStart } = useFieldDrag({
34
35
  pdfWrapperRef,
35
36
  onFieldMove,
36
37
  });
37
- const isDragging = draggingFieldIdRef.current === field.id;
38
+ const isDragging = dragVisualPosition !== null;
38
39
 
39
40
  const { handleResizeStart } = useFieldResize({
40
41
  pdfWrapperRef,
41
42
  onFieldResize,
42
43
  });
43
44
 
44
- const pagePos = getPagePosition(field.page, pdfWrapperRef);
45
+ const page = dragVisualPosition?.page ?? field.page;
46
+ const x = dragVisualPosition?.x ?? field.x;
47
+ const y = dragVisualPosition?.y ?? field.y;
48
+ const pagePos = getPagePosition(page, pdfWrapperRef);
45
49
 
46
50
  const style = {
47
- left: pagePos.left + field.x,
48
- top: pagePos.top + field.y,
51
+ left: pagePos.left + x,
52
+ top: pagePos.top + y,
49
53
  width: field.width,
50
54
  height: field.height,
51
55
  backgroundColor: getFieldBackgroundColor(field.recipient, recipientsColors),
@@ -62,6 +66,7 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
62
66
  className={`dte-pdf-field ${isSelected ? '--selected' : '--unselected'} ${isDragging ? '--dragging' : ''} ${isSameGroup ? '--colored' : ''}`}
63
67
  style={style}
64
68
  >
69
+ {field.type === FieldTypeEnum.calculated && <PdfOverlayFieldCalculated field={field} />}
65
70
  {field.type === FieldTypeEnum.eSign && <PdfOverlayFieldESign field={field} />}
66
71
  {field.type === FieldTypeEnum.dataModel && <PdfOverlayFieldDataModel field={field} />}
67
72
  {field.type === FieldTypeEnum.fillable && (
@@ -0,0 +1,23 @@
1
+ import { FC, useMemo } from 'react';
2
+ import { DataModelValues, PdfField } from '../../interface/types';
3
+ import { evaluateFormula, formatCalculatedResult, resolvePdfDataValues } from '../../utils';
4
+
5
+ interface PdfViewCalculatedProps {
6
+ field: PdfField;
7
+ data?: DataModelValues;
8
+ }
9
+
10
+ export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({ data, field }) => {
11
+ const displayValue = useMemo(() => {
12
+ if (field.formula?.tokens?.length) {
13
+ const value = evaluateFormula(field.formula, data);
14
+ return formatCalculatedResult(value, field.formulaFormat);
15
+ }
16
+ if (field.path) {
17
+ return resolvePdfDataValues(data, field.path) ?? '';
18
+ }
19
+ return field.label ?? '';
20
+ }, [data, field.formula, field.formulaFormat, field.label, field.path]);
21
+
22
+ return <div className="dte-pdf-field-value">{displayValue}</div>;
23
+ };
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../interface/types';
12
12
  import { mapColorsToRecipients } from '../../utils';
13
13
  import { PdfDocumentRenderer } from '../pdf-canvas/pdf-document-renderer';
14
+ import { PdfViewCalculated } from './pdf-view-calculated';
14
15
  import { PdfViewDataModel } from './pdf-view-data-model';
15
16
  import { PdfViewESign } from './pdf-view-e-sign';
16
17
  import { PdfViewFieldContainer } from './pdf-view-field-container';
@@ -94,6 +95,9 @@ export const PdfView: FC<PdfViewProps> = ({
94
95
  {field.type === FieldTypeEnum.dataModel && (
95
96
  <PdfViewDataModel field={field} data={data} />
96
97
  )}
98
+ {field.type === FieldTypeEnum.calculated && (
99
+ <PdfViewCalculated field={field} data={data} />
100
+ )}
97
101
  {field.type === FieldTypeEnum.eSign && (
98
102
  <PdfViewESign field={field} />
99
103
  )}
@@ -2,7 +2,7 @@ import {
2
2
  ESignFieldType,
3
3
  FieldTypeEnum,
4
4
  FieldTypeOption,
5
- FillableFieldType,
5
+ PdfFieldSubType,
6
6
  } from '../interface/types';
7
7
 
8
8
  export const FIELD_CONSTANTS = {
@@ -19,7 +19,7 @@ export const FILLABLE_FIELD_DEFAULT_SIZES = {
19
19
  date: { width: 200, height: 25 },
20
20
  radio: { width: 20, height: 20 },
21
21
  checkbox: { width: 20, height: 20 },
22
- } as Record<FillableFieldType | ESignFieldType, { width: number; height: number }>;
22
+ } as Record<PdfFieldSubType, { width: number; height: number }>;
23
23
 
24
24
  export const E_SIGN_FIELD_TYPE_OPTIONS: { name: string; id: ESignFieldType }[] = [
25
25
  { name: 'Signature', id: ESignFieldType.signature },
@@ -43,3 +43,10 @@ export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
43
43
  subType: ESignFieldType.signature,
44
44
  },
45
45
  ];
46
+
47
+ export const CALCULATED_FIELD_TYPES: FieldTypeOption[] = [
48
+ {
49
+ label: 'Calculated Field',
50
+ type: FieldTypeEnum.calculated,
51
+ },
52
+ ];
@@ -0,0 +1,26 @@
1
+ import IconBorderColor from '@servicetitan/anvil2/assets/icons/material/round/border_color.svg';
2
+ import IconCalculate from '@servicetitan/anvil2/assets/icons/material/round/calculate.svg';
3
+ import IconPhotoSizeSelectSmall from '@servicetitan/anvil2/assets/icons/material/round/photo_size_select_small.svg';
4
+ import IconEstimate from '@servicetitan/anvil2/assets/icons/st/estimate.svg';
5
+ import { FieldTypeEnum } from '../interface/types';
6
+
7
+ export interface MenuGroupModel {
8
+ svgIcon: any;
9
+ label: string;
10
+ key: FieldTypeEnum;
11
+ }
12
+
13
+ export const menuGroups: MenuGroupModel[] = [
14
+ { svgIcon: IconEstimate, label: 'Merge Tags', key: FieldTypeEnum.dataModel },
15
+ { svgIcon: IconBorderColor, label: 'E-Sign', key: FieldTypeEnum.eSign },
16
+ {
17
+ svgIcon: IconPhotoSizeSelectSmall,
18
+ label: 'Fillable Fields',
19
+ key: FieldTypeEnum.fillable,
20
+ },
21
+ {
22
+ svgIcon: IconCalculate,
23
+ label: 'Calculated Fields',
24
+ key: FieldTypeEnum.calculated,
25
+ },
26
+ ];
@@ -1,5 +1,6 @@
1
1
  export * from './useFieldDrag';
2
2
  export * from './useFieldResize';
3
+ export * from './useFormulaEditor';
3
4
  export * from './usePdfDocumentRenderer';
4
5
  export * from './usePdfFieldDnD';
5
6
  export * from './usePdfFieldSelection';
@@ -1,6 +1,17 @@
1
- import { DragEvent, MutableRefObject, RefObject, useRef } from 'react';
1
+ import { DragEvent, MutableRefObject, RefObject, useCallback, useRef, useState } from 'react';
2
2
  import { PdfField } from '../interface/types';
3
- import { handleFieldDrag, handleFieldDragStart } from '../utils';
3
+ import {
4
+ getPageDimensions,
5
+ getPageNumberFromClientY,
6
+ getPagePosition,
7
+ handleFieldDragStart,
8
+ } from '../utils';
9
+
10
+ export interface DragVisualPosition {
11
+ x: number;
12
+ y: number;
13
+ page: number;
14
+ }
4
15
 
5
16
  interface UseFieldDragOptions {
6
17
  pdfWrapperRef: RefObject<HTMLDivElement>;
@@ -10,14 +21,22 @@ interface UseFieldDragOptions {
10
21
  interface UseFieldDragReturn {
11
22
  dragOffsetRef: MutableRefObject<{ x: number; y: number; page: number } | null>;
12
23
  draggingFieldIdRef: MutableRefObject<string | null>;
24
+ dragVisualPosition: DragVisualPosition | null;
13
25
  handleDragStart: (e: DragEvent<HTMLDivElement>, field: PdfField) => void;
14
26
  handleDrag: (e: DragEvent<HTMLDivElement>, field: PdfField) => void;
15
27
  handleDragEnd: () => void;
16
28
  }
17
29
 
30
+ interface PendingMove {
31
+ x: number;
32
+ y: number;
33
+ page: number;
34
+ }
35
+
18
36
  /**
19
- * Custom hook for handling field drag operations
20
- * Manages drag state and coordinates field movement
37
+ * Custom hook for handling field drag operations.
38
+ * During drag, position is kept in local state (dragVisualPosition) so the rest of the tree
39
+ * (config panel, other fields) does not re-render. onFieldMove is called only once on drag end.
21
40
  */
22
41
  export const useFieldDrag = ({
23
42
  onFieldMove,
@@ -25,9 +44,29 @@ export const useFieldDrag = ({
25
44
  }: UseFieldDragOptions): UseFieldDragReturn => {
26
45
  const dragOffsetRef = useRef<{ x: number; y: number; page: number } | null>(null);
27
46
  const draggingFieldIdRef = useRef<string | null>(null);
47
+ const pendingMoveRef = useRef<PendingMove | null>(null);
48
+ const rafIdRef = useRef<number | null>(null);
49
+ const fieldIdRef = useRef<string | null>(null);
50
+
51
+ const [dragVisualPosition, setDragVisualPosition] = useState<DragVisualPosition | null>(null);
52
+
53
+ const flushPendingVisualUpdate = useCallback(() => {
54
+ if (pendingMoveRef.current) {
55
+ setDragVisualPosition({ ...pendingMoveRef.current });
56
+ pendingMoveRef.current = null;
57
+ }
58
+ rafIdRef.current = null;
59
+ }, []);
28
60
 
29
61
  const handleDragStart = (e: DragEvent<HTMLDivElement>, field: PdfField) => {
30
62
  draggingFieldIdRef.current = field.id;
63
+ fieldIdRef.current = field.id;
64
+ pendingMoveRef.current = null;
65
+ if (rafIdRef.current !== null) {
66
+ cancelAnimationFrame(rafIdRef.current);
67
+ rafIdRef.current = null;
68
+ }
69
+ setDragVisualPosition({ x: field.x, y: field.y, page: field.page });
31
70
 
32
71
  const dragOffset = handleFieldDragStart(e, field, pdfWrapperRef);
33
72
  if (dragOffset) {
@@ -36,19 +75,56 @@ export const useFieldDrag = ({
36
75
  };
37
76
 
38
77
  const handleDrag = (e: DragEvent<HTMLDivElement>, field: PdfField) => {
39
- if (dragOffsetRef.current) {
40
- handleFieldDrag(e, field, dragOffsetRef.current, pdfWrapperRef, onFieldMove);
78
+ if (!dragOffsetRef.current) {
79
+ return;
80
+ }
81
+ if (!pdfWrapperRef.current || e.clientX === 0 || e.clientY === 0) {
82
+ return;
41
83
  }
84
+
85
+ const currentPage = getPageNumberFromClientY(e.clientY, pdfWrapperRef);
86
+ const currentPagePos = getPagePosition(currentPage, pdfWrapperRef);
87
+ const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
88
+ const pageDimensions = getPageDimensions(currentPage, pdfWrapperRef);
89
+
90
+ const x = e.clientX - wrapperRect.left - currentPagePos.left - dragOffsetRef.current.x;
91
+ const y = e.clientY - wrapperRect.top - currentPagePos.top - dragOffsetRef.current.y;
92
+
93
+ const minX = 0;
94
+ const minY = 0;
95
+ const maxX = Math.max(0, pageDimensions.width - field.width);
96
+ const maxY = Math.max(0, pageDimensions.height - field.height);
97
+
98
+ const clampedX = Math.max(minX, Math.min(maxX, x));
99
+ const clampedY = Math.max(minY, Math.min(maxY, y));
100
+
101
+ pendingMoveRef.current = { x: clampedX, y: clampedY, page: currentPage };
102
+
103
+ rafIdRef.current ??= requestAnimationFrame(flushPendingVisualUpdate);
42
104
  };
43
105
 
44
- const handleDragEnd = () => {
106
+ const handleDragEnd = useCallback(() => {
107
+ if (rafIdRef.current !== null) {
108
+ cancelAnimationFrame(rafIdRef.current);
109
+ rafIdRef.current = null;
110
+ }
111
+ // Prefer pending (latest) position; fallback to current visual state
112
+ const final = pendingMoveRef.current ?? dragVisualPosition;
113
+ const fid = fieldIdRef.current;
114
+ pendingMoveRef.current = null;
115
+ setDragVisualPosition(null);
45
116
  dragOffsetRef.current = null;
46
117
  draggingFieldIdRef.current = null;
47
- };
118
+ fieldIdRef.current = null;
119
+ if (fid && final) {
120
+ onFieldMove(fid, final.x, final.y, final.page);
121
+ }
122
+ }, [onFieldMove, dragVisualPosition]);
48
123
 
49
124
  return {
50
125
  dragOffsetRef,
51
126
  draggingFieldIdRef,
127
+ dragVisualPosition,
52
128
  handleDragStart,
53
129
  handleDrag,
54
130
  handleDragEnd,