@servicetitan/dte-pdf-editor 1.41.0 → 1.43.0

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