@servicetitan/dte-pdf-editor 1.92.1 → 1.92.2

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 (153) hide show
  1. package/dist/components/field-config-panel/field-sidebar.d.ts.map +1 -1
  2. package/dist/components/field-config-panel/field-sidebar.js +8 -4
  3. package/dist/components/field-config-panel/field-sidebar.js.map +1 -1
  4. package/dist/components/field-sidebar/calculated-field-type-list.d.ts.map +1 -1
  5. package/dist/components/field-sidebar/calculated-field-type-list.js +1 -1
  6. package/dist/components/field-sidebar/calculated-field-type-list.js.map +1 -1
  7. package/dist/components/field-sidebar/data-model-field-type-list.d.ts +15 -1
  8. package/dist/components/field-sidebar/data-model-field-type-list.d.ts.map +1 -1
  9. package/dist/components/field-sidebar/data-model-field-type-list.js +54 -10
  10. package/dist/components/field-sidebar/data-model-field-type-list.js.map +1 -1
  11. package/dist/components/field-sidebar/e-sign-field-type-list.d.ts.map +1 -1
  12. package/dist/components/field-sidebar/e-sign-field-type-list.js +1 -1
  13. package/dist/components/field-sidebar/e-sign-field-type-list.js.map +1 -1
  14. package/dist/components/field-sidebar/field-sidebar.d.ts +7 -0
  15. package/dist/components/field-sidebar/field-sidebar.d.ts.map +1 -1
  16. package/dist/components/field-sidebar/field-sidebar.js +8 -11
  17. package/dist/components/field-sidebar/field-sidebar.js.map +1 -1
  18. package/dist/components/field-sidebar/field-type.d.ts +0 -2
  19. package/dist/components/field-sidebar/field-type.d.ts.map +1 -1
  20. package/dist/components/field-sidebar/field-type.js +2 -2
  21. package/dist/components/field-sidebar/field-type.js.map +1 -1
  22. package/dist/components/field-sidebar/fillable-field-type-list.d.ts.map +1 -1
  23. package/dist/components/field-sidebar/fillable-field-type-list.js +1 -1
  24. package/dist/components/field-sidebar/fillable-field-type-list.js.map +1 -1
  25. package/dist/components/field-sidebar/form-fields-type-list.d.ts +28 -5
  26. package/dist/components/field-sidebar/form-fields-type-list.d.ts.map +1 -1
  27. package/dist/components/field-sidebar/form-fields-type-list.js +27 -6
  28. package/dist/components/field-sidebar/form-fields-type-list.js.map +1 -1
  29. package/dist/components/field-sidebar/generic-field-type-list.d.ts.map +1 -1
  30. package/dist/components/field-sidebar/generic-field-type-list.js +1 -1
  31. package/dist/components/field-sidebar/generic-field-type-list.js.map +1 -1
  32. package/dist/components/pdf-canvas/canvas-controls.d.ts +15 -9
  33. package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -1
  34. package/dist/components/pdf-canvas/canvas-controls.js +14 -4
  35. package/dist/components/pdf-canvas/canvas-controls.js.map +1 -1
  36. package/dist/components/pdf-canvas/pdf-canvas.d.ts +2 -6
  37. package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
  38. package/dist/components/pdf-canvas/pdf-canvas.js +5 -11
  39. package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
  40. package/dist/components/pdf-editor/pdf-editor.d.ts +0 -2
  41. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  42. package/dist/components/pdf-editor/pdf-editor.js +4 -3
  43. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  44. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +1 -4
  45. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  46. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.d.ts.map +1 -1
  47. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js +2 -3
  48. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js.map +1 -1
  49. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  50. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +2 -4
  51. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  52. package/dist/components/pdf-view/pdf-view-e-sign.d.ts.map +1 -1
  53. package/dist/components/pdf-view/pdf-view-e-sign.js +2 -3
  54. package/dist/components/pdf-view/pdf-view-e-sign.js.map +1 -1
  55. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  56. package/dist/components/pdf-view/pdf-view-field-container.js +3 -10
  57. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  58. package/dist/components/pdf-view/pdf-view-fillable.js +1 -1
  59. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  60. package/dist/components/pdf-view/pdf-view.d.ts +0 -5
  61. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  62. package/dist/components/pdf-view/pdf-view.js +24 -51
  63. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  64. package/dist/constants/field.constants.d.ts.map +1 -1
  65. package/dist/constants/field.constants.js +6 -27
  66. package/dist/constants/field.constants.js.map +1 -1
  67. package/dist/constants/menu-group.d.ts +5 -0
  68. package/dist/constants/menu-group.d.ts.map +1 -1
  69. package/dist/constants/menu-group.js +5 -2
  70. package/dist/constants/menu-group.js.map +1 -1
  71. package/dist/hooks/index.d.ts +0 -1
  72. package/dist/hooks/index.d.ts.map +1 -1
  73. package/dist/hooks/index.js +0 -1
  74. package/dist/hooks/index.js.map +1 -1
  75. package/dist/hooks/useFormFields.d.ts +3 -1
  76. package/dist/hooks/useFormFields.d.ts.map +1 -1
  77. package/dist/hooks/useFormFields.js +20 -3
  78. package/dist/hooks/useFormFields.js.map +1 -1
  79. package/dist/hooks/useZoom.d.ts +0 -2
  80. package/dist/hooks/useZoom.d.ts.map +1 -1
  81. package/dist/hooks/useZoom.js +6 -18
  82. package/dist/hooks/useZoom.js.map +1 -1
  83. package/dist/interface/types.d.ts +0 -2
  84. package/dist/interface/types.d.ts.map +1 -1
  85. package/dist/interface/types.js.map +1 -1
  86. package/dist/styles/field-type.css +5 -0
  87. package/dist/styles/formula-modal.css +17 -0
  88. package/dist/styles/pdf-canvas.css +15 -91
  89. package/dist/styles/pdf-editor.css +0 -1
  90. package/dist/styles/pdf-field-overlay.css +78 -109
  91. package/dist/styles/variables.css +0 -3
  92. package/dist/utils/field/get-navigable-fields.utils.js +1 -1
  93. package/dist/utils/field/get-navigable-fields.utils.js.map +1 -1
  94. package/dist/utils/field/index.d.ts +0 -1
  95. package/dist/utils/field/index.d.ts.map +1 -1
  96. package/dist/utils/field/index.js +0 -1
  97. package/dist/utils/field/index.js.map +1 -1
  98. package/dist/utils/pdf/index.d.ts +0 -1
  99. package/dist/utils/pdf/index.d.ts.map +1 -1
  100. package/dist/utils/pdf/index.js +0 -1
  101. package/dist/utils/pdf/index.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/components/field-config-panel/field-sidebar.tsx +24 -8
  104. package/src/components/field-sidebar/calculated-field-type-list.tsx +0 -1
  105. package/src/components/field-sidebar/data-model-field-type-list.tsx +143 -29
  106. package/src/components/field-sidebar/e-sign-field-type-list.tsx +0 -1
  107. package/src/components/field-sidebar/field-sidebar.tsx +19 -18
  108. package/src/components/field-sidebar/field-type.tsx +3 -13
  109. package/src/components/field-sidebar/fillable-field-type-list.tsx +0 -1
  110. package/src/components/field-sidebar/form-fields-type-list.tsx +137 -95
  111. package/src/components/field-sidebar/generic-field-type-list.tsx +0 -1
  112. package/src/components/pdf-canvas/canvas-controls.tsx +128 -30
  113. package/src/components/pdf-canvas/pdf-canvas.tsx +8 -18
  114. package/src/components/pdf-editor/pdf-editor.tsx +5 -7
  115. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +1 -1
  116. package/src/components/pdf-fields-overlay/pdf-overlay-field-e-sign.tsx +6 -8
  117. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +1 -17
  118. package/src/components/pdf-view/pdf-view-e-sign.tsx +6 -8
  119. package/src/components/pdf-view/pdf-view-field-container.tsx +1 -21
  120. package/src/components/pdf-view/pdf-view-fillable.tsx +1 -1
  121. package/src/components/pdf-view/pdf-view.tsx +95 -156
  122. package/src/constants/field.constants.ts +6 -28
  123. package/src/constants/menu-group.ts +5 -2
  124. package/src/hooks/index.ts +0 -1
  125. package/src/hooks/useFormFields.ts +24 -2
  126. package/src/hooks/useZoom.ts +8 -20
  127. package/src/interface/types.ts +0 -3
  128. package/src/styles/field-type.css +5 -0
  129. package/src/styles/formula-modal.css +17 -0
  130. package/src/styles/pdf-canvas.css +15 -91
  131. package/src/styles/pdf-editor.css +0 -1
  132. package/src/styles/pdf-field-overlay.css +78 -109
  133. package/src/styles/variables.css +0 -3
  134. package/src/utils/field/get-navigable-fields.utils.ts +1 -1
  135. package/src/utils/field/index.ts +0 -1
  136. package/src/utils/pdf/index.ts +0 -1
  137. package/dist/hooks/usePinchZoom.d.ts +0 -14
  138. package/dist/hooks/usePinchZoom.d.ts.map +0 -1
  139. package/dist/hooks/usePinchZoom.js +0 -58
  140. package/dist/hooks/usePinchZoom.js.map +0 -1
  141. package/dist/utils/field/get-field-type-option.utils.d.ts +0 -3
  142. package/dist/utils/field/get-field-type-option.utils.d.ts.map +0 -1
  143. package/dist/utils/field/get-field-type-option.utils.js +0 -10
  144. package/dist/utils/field/get-field-type-option.utils.js.map +0 -1
  145. package/dist/utils/pdf/pinch-zoom.utils.d.ts +0 -6
  146. package/dist/utils/pdf/pinch-zoom.utils.d.ts.map +0 -1
  147. package/dist/utils/pdf/pinch-zoom.utils.js +0 -18
  148. package/dist/utils/pdf/pinch-zoom.utils.js.map +0 -1
  149. package/src/hooks/usePinchZoom.ts +0 -86
  150. package/src/utils/field/__tests__/get-field-type-option.utils.test.ts +0 -53
  151. package/src/utils/field/get-field-type-option.utils.ts +0 -16
  152. package/src/utils/pdf/__tests__/pinch-zoom.utils.test.ts +0 -58
  153. package/src/utils/pdf/pinch-zoom.utils.ts +0 -30
@@ -1,37 +1,111 @@
1
1
  import { Flex, SearchField, Text } from '@servicetitan/anvil2';
2
- import { FC, Fragment, useMemo, useState } from 'react';
3
- import { type DataModelFieldGroup, type FieldTypeOption } from '../../interface/types';
2
+ import { FC, Fragment, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import {
4
+ type DataModelFieldGroup,
5
+ type FieldTypeOption,
6
+ type FormInfo,
7
+ } from '../../interface/types';
4
8
  import { getFieldSampleData } from '../../utils';
5
9
  import { FieldType } from './field-type';
10
+ import { FormFieldsPanel, FormsList } from './form-fields-type-list';
6
11
 
7
12
  interface DataModelFieldTypeListProps {
8
13
  dataModelGroups?: DataModelFieldGroup[];
14
+ /**
15
+ * Forms offered as a group after the data model groups, already narrowed to the ones that have
16
+ * fields. Omitted (or empty, with nothing loading) hides the group; `onSelectForm` must be
17
+ * provided for the group to render.
18
+ */
19
+ forms?: FormInfo[];
20
+ /** The form whose fields are shown in place of the group list; null shows the group list. */
21
+ selectedForm?: FormInfo | null;
22
+ /** Draggable options for `selectedForm`. */
23
+ formFieldOptions?: FieldTypeOption[];
24
+ /** Placeholder rows to draw while it is still unknown which forms have fields. */
25
+ loadingFormsCount?: number;
26
+ isLoadingFormFields?: boolean;
27
+ onSelectForm?(form: FormInfo | null): void;
9
28
  onDragStart(fieldOption: FieldTypeOption): void;
10
29
  onDragEnd(): void;
11
30
  }
12
31
 
32
+ const matchesSearch = (label: string, searchLower: string) =>
33
+ label.toLowerCase().includes(searchLower);
34
+
13
35
  export const DataModelFieldTypeList: FC<DataModelFieldTypeListProps> = ({
14
36
  dataModelGroups = [],
37
+ formFieldOptions = [],
38
+ forms,
39
+ isLoadingFormFields = false,
40
+ loadingFormsCount = 0,
15
41
  onDragEnd,
16
42
  onDragStart,
43
+ onSelectForm,
44
+ selectedForm = null,
17
45
  }) => {
18
46
  const [searchText, setSearchText] = useState('');
47
+ const searchLower = searchText.trim().toLowerCase();
48
+ const showForms = (!!forms?.length || loadingFormsCount > 0) && !!onSelectForm;
19
49
 
20
50
  const searchedFieldGroups = useMemo(() => {
21
- if (!searchText) {
51
+ if (!searchLower) {
22
52
  return dataModelGroups;
23
53
  }
24
54
 
25
- const searchLower = searchText.toLowerCase();
26
55
  return dataModelGroups
27
56
  .map(group => ({
28
57
  ...group,
29
- fields: group.fields.filter(({ label }) =>
30
- label.toLowerCase().includes(searchLower),
31
- ),
58
+ fields: group.fields.filter(({ label }) => matchesSearch(label, searchLower)),
32
59
  }))
33
60
  .filter(group => group.fields.length > 0);
34
- }, [dataModelGroups, searchText]);
61
+ }, [dataModelGroups, searchLower]);
62
+
63
+ const searchedForms = useMemo(() => {
64
+ if (!forms?.length) {
65
+ return [];
66
+ }
67
+ if (!searchLower) {
68
+ return forms;
69
+ }
70
+ return forms.filter(form => matchesSearch(form.name, searchLower));
71
+ }, [forms, searchLower]);
72
+
73
+ const searchedFormFieldOptions = useMemo(() => {
74
+ if (!searchLower) {
75
+ return formFieldOptions;
76
+ }
77
+ return formFieldOptions.filter(option => matchesSearch(option.label, searchLower));
78
+ }, [formFieldOptions, searchLower]);
79
+
80
+ const isFormDetail = showForms && !!selectedForm;
81
+
82
+ /*
83
+ * Both slide panels share one scroller. The Forms group sits below every data model group, so
84
+ * a form is usually picked with the list scrolled far down — and the fields panel, being much
85
+ * shorter, would then be scrolled clean past its own content. Start the fields panel at the
86
+ * top and hand the list back the offset it was left at.
87
+ *
88
+ * The offset is tracked as the user scrolls rather than read when the panel switches: by then
89
+ * the list has already collapsed (see formula-modal.css) and the browser has clamped the
90
+ * scroller back to 0.
91
+ */
92
+ const scrollerRef = useRef<HTMLDivElement>(null);
93
+ const listScrollTopRef = useRef(0);
94
+
95
+ useLayoutEffect(() => {
96
+ const scroller = scrollerRef.current;
97
+ if (!scroller) {
98
+ return;
99
+ }
100
+ scroller.scrollTop = isFormDetail ? 0 : listScrollTopRef.current;
101
+ }, [isFormDetail]);
102
+
103
+ const handleScroll = () => {
104
+ if (!isFormDetail && scrollerRef.current) {
105
+ listScrollTopRef.current = scrollerRef.current.scrollTop;
106
+ }
107
+ };
108
+
35
109
  return (
36
110
  <Fragment>
37
111
  <SearchField
@@ -40,30 +114,70 @@ export const DataModelFieldTypeList: FC<DataModelFieldTypeListProps> = ({
40
114
  className="dte-field-sidebar-search"
41
115
  onChange={e => setSearchText(e.target.value ?? '')}
42
116
  />
43
- {searchedFieldGroups.map(group => (
44
- <div key={group.groupName} className="dte-field-type-group">
45
- <Flex alignItems="center" gap={1} className="dte-field-type-group-header">
46
- <Text variant="headline" el="h6" size="small">
47
- {group.groupName}
48
- </Text>
49
- </Flex>
50
- {group.fields.map(fieldOption => {
51
- const key = `${fieldOption.type}-${group.groupName}-${fieldOption.path}`;
52
- return (
53
- <FieldType
54
- key={key}
117
+ <div
118
+ className="dte-formula-sidebar-slide-outer"
119
+ ref={scrollerRef}
120
+ onScroll={handleScroll}
121
+ >
122
+ <div
123
+ className={`dte-formula-sidebar-slide-track${isFormDetail ? ' --detail' : ''}`}
124
+ >
125
+ <div className="dte-formula-sidebar-slide-panel">
126
+ {searchedFieldGroups.map(group => (
127
+ <div key={group.groupName} className="dte-field-type-group">
128
+ <Flex
129
+ alignItems="center"
130
+ gap={1}
131
+ className="dte-field-type-group-header"
132
+ >
133
+ <Text variant="headline" el="h6" size="small">
134
+ {group.groupName}
135
+ </Text>
136
+ </Flex>
137
+ {group.fields.map(fieldOption => {
138
+ const key = `${fieldOption.type}-${group.groupName}-${fieldOption.path}`;
139
+ return (
140
+ <FieldType
141
+ key={key}
142
+ onDragEnd={onDragEnd}
143
+ onDragStart={() => onDragStart(fieldOption)}
144
+ label={fieldOption.label}
145
+ sampleData={getFieldSampleData(
146
+ fieldOption.sampleData,
147
+ fieldOption.fieldType,
148
+ )}
149
+ />
150
+ );
151
+ })}
152
+ </div>
153
+ ))}
154
+ {showForms && (searchedForms.length > 0 || !searchLower) && (
155
+ <FormsList
156
+ forms={searchedForms}
157
+ loadingCount={loadingFormsCount}
158
+ onSelectForm={onSelectForm}
159
+ />
160
+ )}
161
+ </div>
162
+
163
+ <div className="dte-formula-sidebar-slide-panel --form-fields">
164
+ {isFormDetail && (
165
+ <FormFieldsPanel
166
+ form={selectedForm}
167
+ fieldOptions={searchedFormFieldOptions}
168
+ hasHiddenFields={
169
+ formFieldOptions.length > 0 &&
170
+ searchedFormFieldOptions.length === 0
171
+ }
172
+ isLoading={isLoadingFormFields}
173
+ onBack={() => onSelectForm(null)}
55
174
  onDragEnd={onDragEnd}
56
- onDragStart={() => onDragStart(fieldOption)}
57
- label={fieldOption.label}
58
- sampleData={getFieldSampleData(
59
- fieldOption.sampleData,
60
- fieldOption.fieldType,
61
- )}
175
+ onDragStart={onDragStart}
62
176
  />
63
- );
64
- })}
177
+ )}
178
+ </div>
65
179
  </div>
66
- ))}
180
+ </div>
67
181
  </Fragment>
68
182
  );
69
183
  };
@@ -13,7 +13,6 @@ export const ESignFieldTypeList: FC<ESignFieldTypeListProps> = ({ onDragEnd, onD
13
13
  <div className="dte-field-type-items-wrapper">
14
14
  {E_SIGN_FIELD_TYPES.map(fieldOption => (
15
15
  <FieldType
16
- icon={fieldOption.icon}
17
16
  key={fieldOption.subType}
18
17
  label={fieldOption.label}
19
18
  onDragEnd={onDragEnd}
@@ -14,15 +14,21 @@ import { DataModelFieldTypeList } from './data-model-field-type-list';
14
14
  import { ESignFieldTypeList } from './e-sign-field-type-list';
15
15
  import { FieldMenuGroup } from './field-menu-group';
16
16
  import { FillableFieldTypeList } from './fillable-field-type-list';
17
- import { FormFieldsTypeList } from './form-fields-type-list';
18
17
  import { GenericFieldTypeList } from './generic-field-type-list';
19
18
 
20
19
  interface FieldSidebarProps {
21
20
  dataModelGroups?: DataModelFieldGroup[];
21
+ /**
22
+ * Forms listed as a group inside the Merge Tags tab (alongside the data model groups).
23
+ * Hidden when empty, when `onSelectForm` is missing, or when `hideFields` names
24
+ * `FieldTypeEnum.forms`.
25
+ */
22
26
  forms?: FormInfo[];
23
27
  formFieldOptions?: FieldTypeOption[];
24
28
  hideFields?: FieldTypeEnum[];
25
29
  selectedForm?: FormInfo | null;
30
+ /** Placeholder rows to draw while it is still unknown which forms have fields. */
31
+ loadingFormsCount?: number;
26
32
  isLoadingFormFields?: boolean;
27
33
  onDragEnd(): void;
28
34
  onDragStart(fieldOption: FieldTypeOption): void;
@@ -35,6 +41,7 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
35
41
  forms,
36
42
  hideFields,
37
43
  isLoadingFormFields = false,
44
+ loadingFormsCount = 0,
38
45
  onDragEnd,
39
46
  onDragStart,
40
47
  onSelectForm,
@@ -49,15 +56,14 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
49
56
  };
50
57
 
51
58
  const filteredMenuGroups = useMemo(() => {
52
- let groups = menuGroups;
53
- if (hideFields?.length) {
54
- groups = groups.filter(menuGroup => !hideFields.includes(menuGroup.key));
59
+ if (!hideFields?.length) {
60
+ return menuGroups;
55
61
  }
56
- if (!forms?.length || !onSelectForm) {
57
- groups = groups.filter(menuGroup => menuGroup.key !== FieldTypeEnum.forms);
58
- }
59
- return groups;
60
- }, [forms?.length, hideFields, onSelectForm]);
62
+ return menuGroups.filter(menuGroup => !hideFields.includes(menuGroup.key));
63
+ }, [hideFields]);
64
+
65
+ // The list itself hides the group when there are no forms or no selection handler.
66
+ const visibleForms = hideFields?.includes(FieldTypeEnum.forms) ? undefined : forms;
61
67
 
62
68
  return (
63
69
  <Flex className="dte-field-sidebar-container skeleton-item">
@@ -85,17 +91,12 @@ export const FieldSidebar: FC<FieldSidebarProps> = ({
85
91
  {activeFieldType === FieldTypeEnum.dataModel && (
86
92
  <DataModelFieldTypeList
87
93
  dataModelGroups={dataModelGroups}
88
- onDragStart={onDragStart}
89
- onDragEnd={onDragEnd}
90
- />
91
- )}
92
- {activeFieldType === FieldTypeEnum.forms && forms && onSelectForm && (
93
- <FormFieldsTypeList
94
- fieldOptions={formFieldOptions}
95
- forms={forms}
94
+ forms={visibleForms}
95
+ formFieldOptions={formFieldOptions}
96
+ loadingFormsCount={visibleForms ? loadingFormsCount : 0}
96
97
  isLoadingFormFields={isLoadingFormFields}
97
- onDragEnd={onDragEnd}
98
98
  onDragStart={onDragStart}
99
+ onDragEnd={onDragEnd}
99
100
  onSelectForm={onSelectForm}
100
101
  selectedForm={selectedForm}
101
102
  />
@@ -1,22 +1,15 @@
1
- import { Flex, Icon, Svg, Text } from '@servicetitan/anvil2';
1
+ import { Flex, Icon, Text } from '@servicetitan/anvil2';
2
2
  import IconDragIndicator from '@servicetitan/anvil2/assets/icons/material/round/drag_indicator.svg';
3
3
  import { FC } from 'react';
4
4
 
5
5
  interface FieldTypeProps {
6
- icon?: Svg;
7
6
  label: string;
8
7
  sampleData?: string | number | null;
9
8
  onDragEnd(): void;
10
9
  onDragStart(): void;
11
10
  }
12
11
 
13
- export const FieldType: FC<FieldTypeProps> = ({
14
- icon,
15
- label,
16
- onDragEnd,
17
- onDragStart,
18
- sampleData,
19
- }) => {
12
+ export const FieldType: FC<FieldTypeProps> = ({ label, onDragEnd, onDragStart, sampleData }) => {
20
13
  return (
21
14
  <div
22
15
  draggable
@@ -31,10 +24,7 @@ export const FieldType: FC<FieldTypeProps> = ({
31
24
  >
32
25
  <Icon svg={IconDragIndicator} size="medium" />
33
26
  <Flex direction="column" style={{ overflow: 'hidden' }}>
34
- <Flex alignItems="center" gap="2">
35
- {icon && <Icon svg={icon} size="medium" />}
36
- <Text size="small">{label}</Text>
37
- </Flex>
27
+ <Text size="small">{label}</Text>
38
28
  {sampleData && (
39
29
  <Text
40
30
  size="small"
@@ -37,7 +37,6 @@ export const FillableFieldTypeList: FC<FillableFieldTypeListProps> = ({
37
37
  {searchedFieldTypes.map(fieldOption => {
38
38
  return (
39
39
  <FieldType
40
- icon={fieldOption.icon}
41
40
  key={fieldOption.subType}
42
41
  label={fieldOption.label}
43
42
  onDragEnd={onDragEnd}
@@ -1,4 +1,4 @@
1
- import { Button, Flex, Text } from '@servicetitan/anvil2';
1
+ import { Button, Flex, Skeleton, Text } from '@servicetitan/anvil2';
2
2
  import IconArrowBack from '@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_left.svg';
3
3
  import IconArrowForward from '@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_right.svg';
4
4
  import { FC, Fragment } from 'react';
@@ -6,112 +6,154 @@ import { type FieldTypeOption, type FormInfo } from '../../interface/types';
6
6
  import { getFieldSampleData } from '../../utils';
7
7
  import { FieldType } from './field-type';
8
8
 
9
- export interface FormFieldsTypeListProps {
9
+ /** Heading of the Forms group inside the Merge Tags tab. */
10
+ export const FORMS_GROUP_NAME = 'Forms';
11
+
12
+ /** A form's field count is unknown until it answers, so the placeholder settles on a short list. */
13
+ const FIELD_SKELETON_ROWS = 4;
14
+
15
+ /** Placeholder rows, shaped like the real ones, for a list whose contents are still in flight. */
16
+ const SkeletonRows: FC<{ count: number; className: string }> = ({ className, count }) => {
17
+ return (
18
+ <Fragment>
19
+ {Array.from({ length: count }, (_, index) => (
20
+ <div key={index} className={`${className} --skeleton`} aria-hidden>
21
+ <Skeleton.Text />
22
+ </div>
23
+ ))}
24
+ </Fragment>
25
+ );
26
+ };
27
+
28
+ export interface FormsListProps {
10
29
  forms: FormInfo[];
11
- selectedForm: FormInfo | null;
30
+ /**
31
+ * Rows to draw as placeholders instead of the list. Forms are only listed once they are known
32
+ * to have fields, so until the fields arrive there is nothing truthful to show.
33
+ */
34
+ loadingCount?: number;
35
+ /** Rendered when the (possibly search-filtered) list is empty. */
36
+ emptyText?: string;
37
+ onSelectForm(form: FormInfo): void;
38
+ }
39
+
40
+ /**
41
+ * Root level of the Forms group: one clickable row per form. Selecting a form hands off to
42
+ * `FormFieldsPanel`, which lists that form's draggable fields.
43
+ */
44
+ export const FormsList: FC<FormsListProps> = ({
45
+ emptyText = 'No forms configured.',
46
+ forms,
47
+ loadingCount = 0,
48
+ onSelectForm,
49
+ }) => {
50
+ return (
51
+ <div className="dte-field-type-group">
52
+ <Flex alignItems="center" gap={1} className="dte-field-type-group-header">
53
+ <Text variant="headline" el="h6" size="small">
54
+ {FORMS_GROUP_NAME}
55
+ </Text>
56
+ </Flex>
57
+ {loadingCount > 0 ? (
58
+ <SkeletonRows count={loadingCount} className="dte-formula-field-list-item" />
59
+ ) : forms.length > 0 ? (
60
+ <ul className="dte-formula-field-list" role="listbox" aria-label={FORMS_GROUP_NAME}>
61
+ {forms.map(form => (
62
+ <li
63
+ key={form.id}
64
+ role="option"
65
+ className="dte-formula-field-list-item"
66
+ onMouseDown={e => {
67
+ e.preventDefault();
68
+ onSelectForm(form);
69
+ }}
70
+ >
71
+ <Flex justifyContent="space-between" alignItems="center">
72
+ {/*
73
+ * Same Text as the data model rows use (via FieldType): a bare
74
+ * span would inherit the host page's font instead of Anvil2's,
75
+ * so the Forms rows would not match the groups above them.
76
+ */}
77
+ <Text size="small" className="dte-text-ellipsis" title={form.name}>
78
+ {form.name}
79
+ </Text>
80
+ <IconArrowForward />
81
+ </Flex>
82
+ </li>
83
+ ))}
84
+ </ul>
85
+ ) : (
86
+ <Text size="small" subdued variant="body">
87
+ {emptyText}
88
+ </Text>
89
+ )}
90
+ </div>
91
+ );
92
+ };
93
+
94
+ export interface FormFieldsPanelProps {
95
+ form: FormInfo;
12
96
  fieldOptions: FieldTypeOption[];
13
- isLoadingFormFields?: boolean;
14
- onSelectForm(form: FormInfo | null): void;
97
+ isLoading?: boolean;
98
+ /** Rendered when the form has fields but none match the current search. */
99
+ hasHiddenFields?: boolean;
100
+ onBack(): void;
15
101
  onDragEnd(): void;
16
102
  onDragStart(fieldOption: FieldTypeOption): void;
17
103
  }
18
104
 
19
- export const FormFieldsTypeList: FC<FormFieldsTypeListProps> = ({
105
+ /**
106
+ * Detail level of the Forms group: a back button naming the form, then its fields as the same
107
+ * draggable rows the data model groups use, so a form field drops onto the PDF like any merge tag.
108
+ */
109
+ export const FormFieldsPanel: FC<FormFieldsPanelProps> = ({
20
110
  fieldOptions,
21
- forms,
22
- isLoadingFormFields = false,
111
+ form,
112
+ hasHiddenFields = false,
113
+ isLoading = false,
114
+ onBack,
23
115
  onDragEnd,
24
116
  onDragStart,
25
- onSelectForm,
26
- selectedForm,
27
117
  }) => {
28
118
  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' : ''}`}
119
+ <Fragment>
120
+ <Button
121
+ style={{ width: '100%' }}
122
+ appearance="ghost"
123
+ size="small"
124
+ icon={IconArrowBack}
125
+ title={form.name}
126
+ onClick={onBack}
127
+ >
128
+ <Text
129
+ size="medium"
130
+ className="dte-text-ellipsis"
131
+ style={{ fontWeight: 'bold', width: '100%' }}
33
132
  >
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
- <Flex justifyContent="space-between" alignItems="center">
52
- <span className="dte-text-ellipsis" title={form.name}>
53
- {form.name}
54
- </span>
55
- <IconArrowForward />
56
- </Flex>
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
- sampleData={getFieldSampleData(
101
- option.sampleData,
102
- option.fieldType,
103
- )}
104
- onDragEnd={onDragEnd}
105
- onDragStart={() => onDragStart(option)}
106
- />
107
- ))
108
- )}
109
- </div>
110
- </Fragment>
111
- ) : null}
112
- </div>
113
- </div>
133
+ {form.name}
134
+ </Text>
135
+ </Button>
136
+ <div className="dte-field-type-group">
137
+ {isLoading ? (
138
+ <SkeletonRows count={FIELD_SKELETON_ROWS} className="dte-field-type-item" />
139
+ ) : fieldOptions.length === 0 ? (
140
+ <Text size="small" subdued variant="body">
141
+ {hasHiddenFields
142
+ ? 'No fields match the search.'
143
+ : 'No fields on this form.'}
144
+ </Text>
145
+ ) : (
146
+ fieldOptions.map(option => (
147
+ <FieldType
148
+ key={option.path}
149
+ label={option.label}
150
+ sampleData={getFieldSampleData(option.sampleData, option.fieldType)}
151
+ onDragEnd={onDragEnd}
152
+ onDragStart={() => onDragStart(option)}
153
+ />
154
+ ))
155
+ )}
114
156
  </div>
115
- </Flex>
157
+ </Fragment>
116
158
  );
117
159
  };
@@ -16,7 +16,6 @@ export const GenericFieldTypeList: FC<GenericFieldTypeListProps> = ({ onDragEnd,
16
16
  return (
17
17
  <FieldType
18
18
  key={key}
19
- icon={fieldOption.icon}
20
19
  label={fieldOption.label}
21
20
  onDragEnd={onDragEnd}
22
21
  onDragStart={() => onDragStart(fieldOption)}