@servicetitan/dte-pdf-editor 1.17.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 (184) 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 +2 -1
  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/useFormulaEditor.d.ts +22 -0
  83. package/dist/hooks/useFormulaEditor.d.ts.map +1 -0
  84. package/dist/hooks/useFormulaEditor.js +290 -0
  85. package/dist/hooks/useFormulaEditor.js.map +1 -0
  86. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  87. package/dist/hooks/usePdfFieldDnD.js +3 -0
  88. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  89. package/dist/interface/types.d.ts +45 -3
  90. package/dist/interface/types.d.ts.map +1 -1
  91. package/dist/interface/types.js +3 -0
  92. package/dist/interface/types.js.map +1 -1
  93. package/dist/utils/data-model/extract-fields.utils.d.ts +5 -5
  94. package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
  95. package/dist/utils/data-model/extract-fields.utils.js +42 -8
  96. package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
  97. package/dist/utils/formula/caret.utils.d.ts +3 -0
  98. package/dist/utils/formula/caret.utils.d.ts.map +1 -0
  99. package/dist/utils/formula/caret.utils.js +123 -0
  100. package/dist/utils/formula/caret.utils.js.map +1 -0
  101. package/dist/utils/formula/dom.utils.d.ts +4 -0
  102. package/dist/utils/formula/dom.utils.d.ts.map +1 -0
  103. package/dist/utils/formula/dom.utils.js +34 -0
  104. package/dist/utils/formula/dom.utils.js.map +1 -0
  105. package/dist/utils/formula/evaluate-formula.utils.d.ts +13 -0
  106. package/dist/utils/formula/evaluate-formula.utils.d.ts.map +1 -0
  107. package/dist/utils/formula/evaluate-formula.utils.js +134 -0
  108. package/dist/utils/formula/evaluate-formula.utils.js.map +1 -0
  109. package/dist/utils/formula/expression.utils.d.ts +18 -0
  110. package/dist/utils/formula/expression.utils.d.ts.map +1 -0
  111. package/dist/utils/formula/expression.utils.js +84 -0
  112. package/dist/utils/formula/expression.utils.js.map +1 -0
  113. package/dist/utils/formula/format-calculated-result.utils.d.ts +7 -0
  114. package/dist/utils/formula/format-calculated-result.utils.d.ts.map +1 -0
  115. package/dist/utils/formula/format-calculated-result.utils.js +50 -0
  116. package/dist/utils/formula/format-calculated-result.utils.js.map +1 -0
  117. package/dist/utils/formula/formula-types.d.ts +3 -0
  118. package/dist/utils/formula/formula-types.d.ts.map +1 -0
  119. package/dist/utils/formula/formula-types.js +2 -0
  120. package/dist/utils/formula/formula-types.js.map +1 -0
  121. package/dist/utils/formula/index.d.ts +11 -0
  122. package/dist/utils/formula/index.d.ts.map +1 -0
  123. package/dist/utils/formula/index.js +11 -0
  124. package/dist/utils/formula/index.js.map +1 -0
  125. package/dist/utils/formula/referenced-paths.utils.d.ts +7 -0
  126. package/dist/utils/formula/referenced-paths.utils.d.ts.map +1 -0
  127. package/dist/utils/formula/referenced-paths.utils.js +18 -0
  128. package/dist/utils/formula/referenced-paths.utils.js.map +1 -0
  129. package/dist/utils/formula/render-formula.utils.d.ts +8 -0
  130. package/dist/utils/formula/render-formula.utils.d.ts.map +1 -0
  131. package/dist/utils/formula/render-formula.utils.js +39 -0
  132. package/dist/utils/formula/render-formula.utils.js.map +1 -0
  133. package/dist/utils/formula/serialize-formula.utils.d.ts +14 -0
  134. package/dist/utils/formula/serialize-formula.utils.d.ts.map +1 -0
  135. package/dist/utils/formula/serialize-formula.utils.js +33 -0
  136. package/dist/utils/formula/serialize-formula.utils.js.map +1 -0
  137. package/dist/utils/formula/validate-formula.utils.d.ts +11 -0
  138. package/dist/utils/formula/validate-formula.utils.d.ts.map +1 -0
  139. package/dist/utils/formula/validate-formula.utils.js +79 -0
  140. package/dist/utils/formula/validate-formula.utils.js.map +1 -0
  141. package/dist/utils/index.d.ts +1 -0
  142. package/dist/utils/index.d.ts.map +1 -1
  143. package/dist/utils/index.js +1 -0
  144. package/dist/utils/index.js.map +1 -1
  145. package/package.json +2 -2
  146. package/src/components/field-config-panel/advanced-settings.tsx +113 -0
  147. package/src/components/field-config-panel/field-config-panel-overlay.tsx +8 -1
  148. package/src/components/field-config-panel/field-config-panel.tsx +43 -15
  149. package/src/components/field-config-panel/field-sidebar.tsx +91 -0
  150. package/src/components/field-config-panel/formula-generator.tsx +122 -0
  151. package/src/components/field-config-panel/formula-modal.tsx +229 -0
  152. package/src/components/field-config-panel/formula-workspace.tsx +116 -0
  153. package/src/components/field-config-panel/result-type-selector.tsx +34 -0
  154. package/src/components/field-sidebar/calculated-field-type-list.tsx +29 -0
  155. package/src/components/field-sidebar/data-model-field-type-list.tsx +10 -4
  156. package/src/components/field-sidebar/field-menu-group.tsx +36 -0
  157. package/src/components/field-sidebar/field-sidebar.tsx +14 -55
  158. package/src/components/field-sidebar/fillable-field-type-list.tsx +11 -9
  159. package/src/components/pdf-editor/pdf-editor.tsx +2 -0
  160. package/src/components/pdf-fields-overlay/pdf-overlay-field-calculated.tsx +15 -0
  161. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +2 -0
  162. package/src/components/pdf-view/pdf-view-calculated.tsx +23 -0
  163. package/src/components/pdf-view/pdf-view.tsx +4 -0
  164. package/src/constants/field.constants.ts +9 -2
  165. package/src/constants/menu-group.ts +26 -0
  166. package/src/hooks/index.ts +1 -0
  167. package/src/hooks/useFormulaEditor.ts +336 -0
  168. package/src/hooks/usePdfFieldDnD.ts +4 -0
  169. package/src/interface/types.ts +38 -2
  170. package/src/styles/formula-modal.css +307 -0
  171. package/src/styles/index.css +1 -0
  172. package/src/utils/data-model/extract-fields.utils.ts +65 -7
  173. package/src/utils/formula/caret.utils.ts +125 -0
  174. package/src/utils/formula/dom.utils.ts +35 -0
  175. package/src/utils/formula/evaluate-formula.utils.ts +159 -0
  176. package/src/utils/formula/expression.utils.ts +99 -0
  177. package/src/utils/formula/format-calculated-result.utils.ts +79 -0
  178. package/src/utils/formula/formula-types.ts +2 -0
  179. package/src/utils/formula/index.ts +10 -0
  180. package/src/utils/formula/referenced-paths.utils.ts +18 -0
  181. package/src/utils/formula/render-formula.utils.ts +40 -0
  182. package/src/utils/formula/serialize-formula.utils.ts +40 -0
  183. package/src/utils/formula/validate-formula.utils.ts +94 -0
  184. package/src/utils/index.ts +1 -0
@@ -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';
@@ -65,6 +66,7 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
65
66
  className={`dte-pdf-field ${isSelected ? '--selected' : '--unselected'} ${isDragging ? '--dragging' : ''} ${isSameGroup ? '--colored' : ''}`}
66
67
  style={style}
67
68
  >
69
+ {field.type === FieldTypeEnum.calculated && <PdfOverlayFieldCalculated field={field} />}
68
70
  {field.type === FieldTypeEnum.eSign && <PdfOverlayFieldESign field={field} />}
69
71
  {field.type === FieldTypeEnum.dataModel && <PdfOverlayFieldDataModel field={field} />}
70
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';
@@ -0,0 +1,336 @@
1
+ import {
2
+ KeyboardEvent as ReactKeyboardEvent,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import {
10
+ getCaretExpressionIndex,
11
+ placeCaretAtEnd,
12
+ readExpressionFromEditor,
13
+ renderFormulaHtml,
14
+ setCaretByExpressionIndex,
15
+ tokenizeExpression,
16
+ } from '../utils';
17
+
18
+ export const useFormulaEditor = (params: {
19
+ currentExpression: string;
20
+ opened: boolean;
21
+ pathToLabel: Map<string, string>;
22
+ }) => {
23
+ const { currentExpression, opened, pathToLabel } = params;
24
+ const [draftExpression, setDraftExpression] = useState(currentExpression);
25
+ const [history, setHistory] = useState<string[]>([currentExpression]);
26
+ const [historyIndex, setHistoryIndex] = useState(0);
27
+ const [isDirty, setIsDirty] = useState(false);
28
+ const editorRef = useRef<HTMLDivElement | null>(null);
29
+ const updateSourceRef = useRef<'editor' | 'state' | null>(null);
30
+ const pendingCaretRef = useRef<number | null>(null);
31
+ const lastCaretRef = useRef(0);
32
+ const historyIndexRef = useRef(0);
33
+ const historyRef = useRef<string[]>([currentExpression]);
34
+
35
+ useEffect(() => {
36
+ if (opened) {
37
+ setDraftExpression(currentExpression);
38
+ setIsDirty(false);
39
+ setHistory([currentExpression]);
40
+ setHistoryIndex(0);
41
+ historyIndexRef.current = 0;
42
+ historyRef.current = [currentExpression];
43
+ const editor = editorRef.current;
44
+ if (editor) {
45
+ editor.innerHTML = renderFormulaHtml(currentExpression, pathToLabel);
46
+ placeCaretAtEnd(editor);
47
+ }
48
+ }
49
+ }, [opened, currentExpression, pathToLabel]);
50
+
51
+ useEffect(() => {
52
+ const editor = editorRef.current;
53
+ if (!editor) {
54
+ return;
55
+ }
56
+ if (updateSourceRef.current === 'editor') {
57
+ updateSourceRef.current = null;
58
+ return;
59
+ }
60
+ editor.innerHTML = renderFormulaHtml(draftExpression, pathToLabel);
61
+ if (pendingCaretRef.current !== null) {
62
+ setCaretByExpressionIndex(editor, pendingCaretRef.current);
63
+ pendingCaretRef.current = null;
64
+ } else {
65
+ placeCaretAtEnd(editor);
66
+ }
67
+ }, [draftExpression, pathToLabel]);
68
+
69
+ useEffect(() => {
70
+ historyIndexRef.current = historyIndex;
71
+ historyRef.current = history;
72
+ }, [historyIndex, history]);
73
+
74
+ const handleEditorInput = useCallback(() => {
75
+ const editor = editorRef.current;
76
+ if (!editor) {
77
+ return;
78
+ }
79
+ const nextValue = readExpressionFromEditor(editor);
80
+ const caretIndex = getCaretExpressionIndex(editor);
81
+ pendingCaretRef.current = caretIndex;
82
+ lastCaretRef.current = caretIndex;
83
+ updateSourceRef.current = 'editor';
84
+ setDraftExpression(nextValue);
85
+ setIsDirty(true);
86
+ const cur = historyIndexRef.current;
87
+ const hist = historyRef.current;
88
+ const base = hist.slice(0, cur + 1);
89
+ if (base[base.length - 1] !== nextValue) {
90
+ setHistory([...base, nextValue]);
91
+ setHistoryIndex(base.length);
92
+ }
93
+ }, []);
94
+
95
+ const getCaretIndexForInsert = useCallback((): number => {
96
+ const editor = editorRef.current;
97
+ if (!editor) {
98
+ return draftExpression.length;
99
+ }
100
+ const sel = window.getSelection();
101
+ if (sel?.rangeCount && editor.contains(sel.getRangeAt(0).startContainer)) {
102
+ const idx = getCaretExpressionIndex(editor);
103
+ lastCaretRef.current = idx;
104
+ return idx;
105
+ }
106
+ return lastCaretRef.current;
107
+ }, [draftExpression.length]);
108
+
109
+ const pushHistory = useCallback((nextValue: string, nextCaret: number) => {
110
+ pendingCaretRef.current = nextCaret;
111
+ lastCaretRef.current = nextCaret;
112
+ setDraftExpression(nextValue);
113
+ setIsDirty(true);
114
+ const cur = historyIndexRef.current;
115
+ const hist = historyRef.current;
116
+ const base = hist.slice(0, cur + 1);
117
+ if (base[base.length - 1] !== nextValue) {
118
+ setHistory([...base, nextValue]);
119
+ setHistoryIndex(base.length);
120
+ }
121
+ requestAnimationFrame(() => editorRef.current?.focus());
122
+ }, []);
123
+
124
+ const insertField = useCallback(
125
+ (path: string) => {
126
+ const editor = editorRef.current;
127
+ if (!editor) {
128
+ return;
129
+ }
130
+ const inserted = `${path} `;
131
+ const caretIndex = getCaretIndexForInsert();
132
+ const before = draftExpression.slice(0, caretIndex);
133
+ const after = draftExpression.slice(caretIndex);
134
+ const nextValue = `${before}${inserted}${after}`;
135
+ updateSourceRef.current = 'state';
136
+ pushHistory(nextValue, caretIndex + inserted.length);
137
+ },
138
+ [draftExpression, getCaretIndexForInsert, pushHistory],
139
+ );
140
+
141
+ const insertOperator = useCallback(
142
+ (op: string) => {
143
+ const editor = editorRef.current;
144
+ if (!editor) {
145
+ return;
146
+ }
147
+ const caretIndex = getCaretIndexForInsert();
148
+ const before = draftExpression.slice(0, caretIndex);
149
+ const after = draftExpression.slice(caretIndex);
150
+ const inserted = ` ${op} `;
151
+ const nextValue = `${before}${inserted}${after}`;
152
+ updateSourceRef.current = 'state';
153
+ pushHistory(nextValue, caretIndex + inserted.length);
154
+ },
155
+ [draftExpression, getCaretIndexForInsert, pushHistory],
156
+ );
157
+
158
+ const removeFieldAtIndex = useCallback(
159
+ (fieldIndex: number) => {
160
+ const parts = tokenizeExpression(draftExpression);
161
+ let currentFieldIndex = -1;
162
+ let index = 0;
163
+ let removed = false;
164
+ const nextValue = parts
165
+ .map(part => {
166
+ if (part.type === 'field') {
167
+ currentFieldIndex += 1;
168
+ if (currentFieldIndex === fieldIndex) {
169
+ removed = true;
170
+ index += part.value.length;
171
+ return '';
172
+ }
173
+ }
174
+ index += part.value.length;
175
+ return part.value;
176
+ })
177
+ .join('');
178
+ if (!removed) {
179
+ return;
180
+ }
181
+ updateSourceRef.current = 'state';
182
+ const nextCaret = Math.min(index, nextValue.length);
183
+ pendingCaretRef.current = nextCaret;
184
+ lastCaretRef.current = nextCaret;
185
+ setDraftExpression(nextValue);
186
+ setIsDirty(true);
187
+ const cur = historyIndexRef.current;
188
+ const hist = historyRef.current;
189
+ const base = hist.slice(0, cur + 1);
190
+ if (base[base.length - 1] !== nextValue) {
191
+ setHistory([...base, nextValue]);
192
+ setHistoryIndex(base.length);
193
+ }
194
+ requestAnimationFrame(() => editorRef.current?.focus());
195
+ },
196
+ [draftExpression],
197
+ );
198
+
199
+ const handleKeyDown = useCallback((e: ReactKeyboardEvent<HTMLDivElement>) => {
200
+ if (e.ctrlKey || e.metaKey) {
201
+ const key = e.key.toLowerCase();
202
+ if (key === 'z') {
203
+ e.preventDefault();
204
+ if (historyIndexRef.current > 0) {
205
+ const next = historyRef.current[historyIndexRef.current - 1];
206
+ updateSourceRef.current = 'state';
207
+ pendingCaretRef.current = next.length;
208
+ setDraftExpression(next);
209
+ setHistoryIndex(historyIndexRef.current - 1);
210
+ setIsDirty(true);
211
+ }
212
+ return;
213
+ }
214
+ if (key === 'y' || (e.shiftKey && key === 'z')) {
215
+ e.preventDefault();
216
+ const cur = historyIndexRef.current;
217
+ const hist = historyRef.current;
218
+ if (cur < hist.length - 1) {
219
+ const next = hist[cur + 1];
220
+ updateSourceRef.current = 'state';
221
+ pendingCaretRef.current = next.length;
222
+ setDraftExpression(next);
223
+ setHistoryIndex(cur + 1);
224
+ setIsDirty(true);
225
+ }
226
+ return;
227
+ }
228
+ }
229
+ const allowed = new Set([
230
+ '+',
231
+ '-',
232
+ '*',
233
+ '/',
234
+ '(',
235
+ ')',
236
+ '0',
237
+ '1',
238
+ '2',
239
+ '3',
240
+ '4',
241
+ '5',
242
+ '6',
243
+ '7',
244
+ '8',
245
+ '9',
246
+ '.',
247
+ 'Backspace',
248
+ 'Delete',
249
+ 'ArrowLeft',
250
+ 'ArrowRight',
251
+ 'ArrowUp',
252
+ 'ArrowDown',
253
+ 'Home',
254
+ 'End',
255
+ 'Tab',
256
+ ' ',
257
+ ]);
258
+ if (!allowed.has(e.key)) {
259
+ e.preventDefault();
260
+ }
261
+ }, []);
262
+
263
+ const handlePaste = useCallback(
264
+ (text: string) => {
265
+ if (!text?.trim()) {
266
+ return;
267
+ }
268
+ const parts = tokenizeExpression(text).filter(
269
+ p => p.type !== 'text' || p.value.trim() !== '',
270
+ );
271
+ const sanitized = parts.map(p => p.value).join('');
272
+ if (!sanitized) {
273
+ return;
274
+ }
275
+ const editor = editorRef.current;
276
+ if (!editor) {
277
+ return;
278
+ }
279
+ const caretIndex = getCaretIndexForInsert();
280
+ const before = draftExpression.slice(0, caretIndex);
281
+ const after = draftExpression.slice(caretIndex);
282
+ const nextValue = `${before}${sanitized}${after}`;
283
+ updateSourceRef.current = 'state';
284
+ pushHistory(nextValue, caretIndex + sanitized.length);
285
+ },
286
+ [draftExpression, getCaretIndexForInsert, pushHistory],
287
+ );
288
+
289
+ const undo = useCallback(() => {
290
+ if (historyIndexRef.current > 0) {
291
+ const next = historyRef.current[historyIndexRef.current - 1];
292
+ updateSourceRef.current = 'state';
293
+ pendingCaretRef.current = next.length;
294
+ setDraftExpression(next);
295
+ setHistoryIndex(historyIndexRef.current - 1);
296
+ setIsDirty(true);
297
+ requestAnimationFrame(() => editorRef.current?.focus());
298
+ }
299
+ }, []);
300
+
301
+ const redo = useCallback(() => {
302
+ const cur = historyIndexRef.current;
303
+ const hist = historyRef.current;
304
+ if (cur < hist.length - 1) {
305
+ const next = hist[cur + 1];
306
+ updateSourceRef.current = 'state';
307
+ pendingCaretRef.current = next.length;
308
+ setDraftExpression(next);
309
+ setHistoryIndex(cur + 1);
310
+ setIsDirty(true);
311
+ requestAnimationFrame(() => editorRef.current?.focus());
312
+ }
313
+ }, []);
314
+
315
+ const selectedFieldPaths = useMemo(() => {
316
+ const parts = tokenizeExpression(draftExpression);
317
+ return new Set(parts.filter(p => p.type === 'field').map(p => p.value));
318
+ }, [draftExpression]);
319
+
320
+ return {
321
+ draftExpression,
322
+ editorRef,
323
+ isDirty,
324
+ canUndo: historyIndex > 0,
325
+ canRedo: historyIndex < history.length - 1,
326
+ undo,
327
+ redo,
328
+ handleEditorInput,
329
+ handleKeyDown,
330
+ handlePaste,
331
+ insertField,
332
+ insertOperator,
333
+ removeFieldAtIndex,
334
+ selectedFieldPaths,
335
+ };
336
+ };
@@ -122,6 +122,10 @@ export const usePdfFieldDnD = ({
122
122
  }
123
123
  }
124
124
 
125
+ if (newField.type === FieldTypeEnum.calculated) {
126
+ newField.path = newField.id;
127
+ }
128
+
125
129
  onFieldsChange([...fields, newField]);
126
130
  onSelectField(newField.id);
127
131
  setDraggedFieldOption(null);