@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,7 +1,7 @@
1
- import { Flex, Text } from '@servicetitan/anvil2';
2
- import { FC, ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react';
1
+ import { Flex } from '@servicetitan/anvil2';
2
+ import { FC, ReactNode, useMemo, useRef, useState } from 'react';
3
3
  import { BASE_PAGE_WIDTH } from '../../constants';
4
- import { usePinchZoom, useZoom } from '../../hooks';
4
+ import { useZoom } from '../../hooks';
5
5
  import {
6
6
  type DataChangePayload,
7
7
  type DataModelValues,
@@ -15,11 +15,7 @@ import {
15
15
  mapColorsToRecipients,
16
16
  sortFieldsByReadingOrder,
17
17
  } from '../../utils';
18
- import {
19
- FieldNavigationControls,
20
- ZoomControls,
21
- ZoomControlsProps,
22
- } from '../pdf-canvas/canvas-controls';
18
+ import { CanvasControls } from '../pdf-canvas/canvas-controls';
23
19
  import { PdfDocumentRenderer } from '../pdf-canvas/pdf-document-renderer';
24
20
  import { PdfViewCalculated } from './pdf-view-calculated';
25
21
  import { PdfViewDataModel } from './pdf-view-data-model';
@@ -34,10 +30,6 @@ export interface PdfViewProps {
34
30
  viewMode?: PdfViewMode;
35
31
  data?: DataModelValues;
36
32
  zoomable?: boolean;
37
- zoomProps?: {
38
- vertical: ZoomControlsProps['vertical'];
39
- position: ZoomControlsProps['position'];
40
- };
41
33
  enableInputNavigation?: boolean;
42
34
  pdfUrl: string;
43
35
  loading?: boolean;
@@ -82,23 +74,14 @@ export const PdfView: FC<PdfViewProps> = ({
82
74
  pdfUrl,
83
75
  recipients,
84
76
  viewMode,
85
- zoomProps,
86
77
  zoomable = true,
87
78
  }) => {
88
79
  const [isPdfLoaded, setIsPdfLoaded] = useState<boolean>(false);
89
80
  const [previewData, setPreviewData] = useState<DataModelValues>(data ?? {});
90
- const { resetZoom, setZoomValue, zoom, zoomByFactor, zoomIn, zoomOut } = useZoom();
81
+ const { resetZoom, zoom, zoomIn, zoomOut } = useZoom();
91
82
  const activeData = viewMode === 'preview' ? previewData : data;
92
83
 
93
84
  const pdfWrapperRef = useRef<HTMLDivElement>(null);
94
- const navControlsRef = useRef<HTMLDivElement>(null);
95
- const contentContainerRef = useRef<HTMLDivElement>(null);
96
- usePinchZoom(contentContainerRef, {
97
- enabled: zoomable,
98
- setZoomValue,
99
- zoom,
100
- zoomByFactor,
101
- });
102
85
 
103
86
  const onDocumentLoadSuccess = (pageNum: number) => {
104
87
  setIsPdfLoaded(true);
@@ -128,6 +111,7 @@ export const PdfView: FC<PdfViewProps> = ({
128
111
  [navigableFields],
129
112
  );
130
113
 
114
+ // -1 means no field has been navigated to yet, so the first "next" lands on index 0.
131
115
  const [currentFieldIndex, setCurrentFieldIndex] = useState(-1);
132
116
 
133
117
  const handleFieldOverlayFocus = (fieldId: string) => {
@@ -136,160 +120,115 @@ export const PdfView: FC<PdfViewProps> = ({
136
120
  }
137
121
  };
138
122
 
139
- const getFieldElement = (fieldId: string) => {
140
- const root = pdfWrapperRef.current?.getRootNode() as Document | ShadowRoot | undefined;
141
- return root?.querySelector<HTMLElement>(`[id="dte-pdf-field-${fieldId}"]`) ?? undefined;
142
- };
143
-
144
- useLayoutEffect(() => {
145
- const field = navigableFields[currentFieldIndex];
146
- const navControlsEl = navControlsRef.current;
147
- if (!field || !navControlsEl) {
148
- return;
149
- }
150
-
151
- const fieldEl = getFieldElement(field.id);
152
- if (!fieldEl) {
153
- return;
154
- }
155
-
156
- const containerTop = contentContainerRef.current?.getBoundingClientRect().top ?? 0;
157
- navControlsEl.style.top = `${fieldEl.getBoundingClientRect().top - containerTop}px`;
158
- }, [currentFieldIndex, navigableFields]);
159
-
160
123
  const handleFocusField = (fieldId: string) => {
161
- const fieldEl = getFieldElement(fieldId);
162
- if (fieldEl) {
163
- fieldEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
164
- const focusableChild = fieldEl.querySelector<HTMLElement>('input, textarea');
165
- (focusableChild ?? fieldEl)?.focus();
166
- }
124
+ const root = pdfWrapperRef.current?.getRootNode() as Document | ShadowRoot | undefined;
125
+ const fieldEl = root?.querySelector<HTMLElement>(`[id="dte-pdf-field-${fieldId}"]`);
126
+ fieldEl?.scrollIntoView({ behavior: 'smooth', block: 'center' });
127
+ const focusableElement = fieldEl?.querySelector<HTMLElement>('input, textarea') ?? fieldEl;
128
+ focusableElement?.focus();
167
129
  };
168
130
 
169
131
  const goToField = (index: number) => {
170
- const fieldIndex = index < 0 || index >= navigableFields.length ? -1 : index;
171
- const field = navigableFields[fieldIndex];
172
-
173
- setCurrentFieldIndex(fieldIndex);
174
- if (field) {
175
- handleFocusField(field.id);
132
+ let i = index;
133
+ if (i < 0) {
134
+ i = 0;
135
+ } else if (i >= navigableFields.length) {
136
+ i = navigableFields.length - 1;
176
137
  }
177
- };
138
+ const field = navigableFields[i];
178
139
 
179
- const handleNextField = () => {
180
- goToField(currentFieldIndex + 1);
140
+ setCurrentFieldIndex(i);
141
+ handleFocusField(field.id);
181
142
  };
182
143
 
183
144
  const recipientsColors = mapColorsToRecipients(recipients);
184
- const hasNavigableFields = navigableFields.length !== 0;
185
- const shouldShowFieldNavigation = enableInputNavigation && hasNavigableFields;
186
-
145
+ const hasNavigableFields = navigableFields.length === 0;
187
146
  return (
188
147
  <Flex
148
+ flex={1}
149
+ className="dte-pdf-editor-theme dte-pdf-editor-content-container --view-mode"
189
150
  direction="column"
190
- style={{ position: 'relative', minWidth: 0 }}
191
- className="dte-pdf-editor-theme"
192
- ref={contentContainerRef}
193
151
  >
194
- {isPdfLoaded && shouldShowFieldNavigation && (
195
- <div ref={navControlsRef} className="dte-pdf-movable-next-button-wrapper">
196
- <FieldNavigationControls
197
- isStarted={currentFieldIndex !== -1}
198
- onNextField={handleNextField}
199
- />
200
- </div>
201
- )}
202
- {isPdfLoaded && zoomable && (
203
- <ZoomControls
204
- {...zoomProps}
152
+ {isPdfLoaded && (
153
+ <CanvasControls
205
154
  zoom={zoom}
206
155
  onZoomIn={zoomIn}
207
156
  onZoomOut={zoomOut}
157
+ zoomable={zoomable}
208
158
  onResetZoom={resetZoom}
159
+ enableInputNavigation={enableInputNavigation}
160
+ disabledPrev={hasNavigableFields}
161
+ disabledNext={hasNavigableFields}
162
+ disabledOnFirstField={hasNavigableFields}
163
+ disabledOnLastField={hasNavigableFields}
164
+ onPrevField={() => goToField(currentFieldIndex - 1)}
165
+ onNextField={() => goToField(currentFieldIndex + 1)}
166
+ onFirstField={() => goToField(0)}
167
+ onLastField={() => goToField(navigableFields.length - 1)}
209
168
  />
210
169
  )}
211
- {shouldShowFieldNavigation && (
212
- <Flex
213
- justifyContent="space-between"
214
- alignItems="center"
215
- gap={2}
216
- className="dte-pdf-canvas-controls"
217
- >
218
- <Text style={{ color: '#ffffff' }}>Review and complete</Text>
219
- <FieldNavigationControls
220
- isStarted={currentFieldIndex !== -1}
221
- onNextField={handleNextField}
222
- />
223
- </Flex>
224
- )}
225
- <Flex
226
- flex={1}
227
- className="dte-pdf-editor-content-container --view-mode"
228
- direction="column"
170
+ <div
171
+ ref={pdfWrapperRef}
172
+ className="dte-pdf-wrapper"
173
+ style={{ width: BASE_PAGE_WIDTH * zoom }}
229
174
  >
230
- <div
231
- ref={pdfWrapperRef}
232
- className="dte-pdf-wrapper"
233
- style={{ width: BASE_PAGE_WIDTH * zoom }}
234
- >
235
- <PdfDocumentRenderer
236
- pdfUrl={pdfUrl}
237
- loading={loading}
238
- pageWidth={BASE_PAGE_WIDTH * zoom}
239
- errorPlaceholder={errorPlaceholder}
240
- loadingPlaceholder={loadingPlaceholder}
241
- onDocumentLoadSuccess={onDocumentLoadSuccess}
242
- />
243
- {isPdfLoaded && (
244
- <div className="dte-pdf-field-overlay --view-mode">
245
- {fields.map(field => (
246
- <PdfViewFieldContainer
247
- key={field.id}
248
- field={field}
249
- data={viewMode === 'preview' ? previewData : data}
250
- error={errors[field.path!] || ''}
251
- recipientsColors={recipientsColors}
252
- pdfWrapperRef={pdfWrapperRef}
253
- zoom={zoom}
254
- onFocus={handleFieldOverlayFocus}
255
- >
256
- {(field.type === FieldTypeEnum.dataModel ||
257
- field.type === FieldTypeEnum.forms) && (
258
- <PdfViewDataModel
259
- field={field}
260
- data={viewMode === 'preview' ? previewData : data}
261
- />
262
- )}
263
- {field.type === FieldTypeEnum.eSign && (
264
- <PdfViewESign field={field} />
265
- )}
266
- {field.type === FieldTypeEnum.calculated && (
267
- <PdfViewCalculated
268
- field={field}
269
- data={viewMode === 'preview' ? previewData : data}
270
- holidays={holidays}
271
- viewMode={viewMode}
272
- />
273
- )}
274
- {field.type === FieldTypeEnum.fillable && (
275
- <PdfViewFillable
276
- data={viewMode === 'preview' ? previewData : data}
277
- field={field}
278
- readOnly={makeFillableFieldsReadonly}
279
- fillingBy={fillingBy}
280
- viewMode={viewMode}
281
- onDataChange={handleDataChange}
282
- />
283
- )}
284
- {field.type === FieldTypeEnum.generic && (
285
- <PdfViewGeneric field={field} />
286
- )}
287
- </PdfViewFieldContainer>
288
- ))}
289
- </div>
290
- )}
291
- </div>
292
- </Flex>
175
+ <PdfDocumentRenderer
176
+ pdfUrl={pdfUrl}
177
+ loading={loading}
178
+ pageWidth={BASE_PAGE_WIDTH * zoom}
179
+ errorPlaceholder={errorPlaceholder}
180
+ loadingPlaceholder={loadingPlaceholder}
181
+ onDocumentLoadSuccess={onDocumentLoadSuccess}
182
+ />
183
+ {isPdfLoaded && (
184
+ <div className="dte-pdf-field-overlay --view-mode">
185
+ {fields.map(field => (
186
+ <PdfViewFieldContainer
187
+ key={field.id}
188
+ field={field}
189
+ data={viewMode === 'preview' ? previewData : data}
190
+ error={errors[field.path!] || ''}
191
+ recipientsColors={recipientsColors}
192
+ pdfWrapperRef={pdfWrapperRef}
193
+ zoom={zoom}
194
+ onFocus={handleFieldOverlayFocus}
195
+ >
196
+ {(field.type === FieldTypeEnum.dataModel ||
197
+ field.type === FieldTypeEnum.forms) && (
198
+ <PdfViewDataModel
199
+ field={field}
200
+ data={viewMode === 'preview' ? previewData : data}
201
+ />
202
+ )}
203
+ {field.type === FieldTypeEnum.eSign && (
204
+ <PdfViewESign field={field} />
205
+ )}
206
+ {field.type === FieldTypeEnum.calculated && (
207
+ <PdfViewCalculated
208
+ field={field}
209
+ data={viewMode === 'preview' ? previewData : data}
210
+ holidays={holidays}
211
+ viewMode={viewMode}
212
+ />
213
+ )}
214
+ {field.type === FieldTypeEnum.fillable && (
215
+ <PdfViewFillable
216
+ data={viewMode === 'preview' ? previewData : data}
217
+ field={field}
218
+ readOnly={makeFillableFieldsReadonly}
219
+ fillingBy={fillingBy}
220
+ viewMode={viewMode}
221
+ onDataChange={handleDataChange}
222
+ />
223
+ )}
224
+ {field.type === FieldTypeEnum.generic && (
225
+ <PdfViewGeneric field={field} />
226
+ )}
227
+ </PdfViewFieldContainer>
228
+ ))}
229
+ </div>
230
+ )}
231
+ </div>
293
232
  </Flex>
294
233
  );
295
234
  };
@@ -1,17 +1,3 @@
1
- import NumbersIcon from '@servicetitan/anvil2/assets/icons/material/round/123.svg';
2
- import AbcIcon from '@servicetitan/anvil2/assets/icons/material/round/abc.svg';
3
- import BadgeIcon from '@servicetitan/anvil2/assets/icons/material/round/badge.svg';
4
- import CalculateIcon from '@servicetitan/anvil2/assets/icons/material/round/calculate.svg';
5
- import CalendarMonthIcon from '@servicetitan/anvil2/assets/icons/material/round/calendar_month.svg';
6
- import CheckboxIcon from '@servicetitan/anvil2/assets/icons/material/round/check_box.svg';
7
- import EventAvailableIcon from '@servicetitan/anvil2/assets/icons/material/round/event_available.svg';
8
- import GestureIcon from '@servicetitan/anvil2/assets/icons/material/round/gesture.svg';
9
- import RadioIcon from '@servicetitan/anvil2/assets/icons/material/round/radio_button_checked.svg';
10
- import ShortTextIcon from '@servicetitan/anvil2/assets/icons/material/round/short_text.svg';
11
- import TableChartIcon from '@servicetitan/anvil2/assets/icons/material/round/table_chart.svg';
12
- import TextIcon from '@servicetitan/anvil2/assets/icons/material/round/text_fields.svg';
13
- import IconAppointmentsIcon from '@servicetitan/anvil2/assets/icons/st/appointments.svg';
14
-
15
1
  import {
16
2
  type CalculatedFormatForDate,
17
3
  type CalculatedFormatForNumber,
@@ -58,11 +44,11 @@ export const GENERIC_FIELD_DEFAULT_SIZES = {
58
44
  } as Record<PdfFieldSubType, { width: number; height: number }>;
59
45
 
60
46
  export const FILLABLE_FIELD_TYPES: FieldTypeOption[] = [
61
- { label: 'Text Field', type: FieldTypeEnum.fillable, subType: 'text', icon: ShortTextIcon },
62
- { label: 'Number Field', type: FieldTypeEnum.fillable, subType: 'number', icon: NumbersIcon },
63
- { label: 'Date Field', type: FieldTypeEnum.fillable, subType: 'date', icon: CalendarMonthIcon },
64
- { label: 'Checkbox', type: FieldTypeEnum.fillable, subType: 'checkbox', icon: CheckboxIcon },
65
- { label: 'Radio', type: FieldTypeEnum.fillable, subType: 'radio', icon: RadioIcon },
47
+ { label: 'Text Field', type: FieldTypeEnum.fillable, subType: 'text' },
48
+ { label: 'Number Field', type: FieldTypeEnum.fillable, subType: 'number' },
49
+ { label: 'Date Field', type: FieldTypeEnum.fillable, subType: 'date' },
50
+ { label: 'Checkbox', type: FieldTypeEnum.fillable, subType: 'checkbox' },
51
+ { label: 'Radio', type: FieldTypeEnum.fillable, subType: 'radio' },
66
52
  ];
67
53
 
68
54
  export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
@@ -70,31 +56,26 @@ export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
70
56
  label: 'Signature',
71
57
  type: FieldTypeEnum.eSign,
72
58
  subType: ESignFieldType.signature,
73
- icon: GestureIcon,
74
59
  },
75
60
  {
76
61
  label: 'Initials',
77
62
  type: FieldTypeEnum.eSign,
78
63
  subType: ESignFieldType.initials,
79
- icon: AbcIcon,
80
64
  },
81
65
  {
82
66
  label: 'Date Signed',
83
67
  type: FieldTypeEnum.eSign,
84
68
  subType: ESignFieldType.dateSigned,
85
- icon: EventAvailableIcon,
86
69
  },
87
70
  {
88
- label: 'Calculated Singing Date',
71
+ label: 'Date Signed Offset',
89
72
  type: FieldTypeEnum.eSign,
90
73
  subType: ESignFieldType.dateSignedOffset,
91
- icon: IconAppointmentsIcon,
92
74
  },
93
75
  {
94
76
  label: 'Full Name',
95
77
  type: FieldTypeEnum.eSign,
96
78
  subType: ESignFieldType.fullName,
97
- icon: BadgeIcon,
98
79
  },
99
80
  ];
100
81
 
@@ -102,7 +83,6 @@ export const CALCULATED_FIELD_TYPES: FieldTypeOption[] = [
102
83
  {
103
84
  label: 'Calculated Field',
104
85
  type: FieldTypeEnum.calculated,
105
- icon: CalculateIcon,
106
86
  },
107
87
  ];
108
88
 
@@ -111,13 +91,11 @@ export const GENERIC_FIELD_TYPES: FieldTypeOption[] = [
111
91
  label: 'Text',
112
92
  type: FieldTypeEnum.generic,
113
93
  subType: 'text',
114
- icon: TextIcon,
115
94
  },
116
95
  {
117
96
  label: 'Table',
118
97
  type: FieldTypeEnum.generic,
119
98
  subType: 'table',
120
- icon: TableChartIcon,
121
99
  },
122
100
  ];
123
101
 
@@ -1,6 +1,5 @@
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';
4
3
  import IconPhotoSizeSelectSmall from '@servicetitan/anvil2/assets/icons/material/round/photo_size_select_small.svg';
5
4
  import IconCommercial from '@servicetitan/anvil2/assets/icons/st/commercial.svg';
6
5
  import IconEstimate from '@servicetitan/anvil2/assets/icons/st/estimate.svg';
@@ -12,6 +11,11 @@ export interface MenuGroupModel {
12
11
  key: FieldTypeEnum;
13
12
  }
14
13
 
14
+ /**
15
+ * Sidebar tabs. Forms (`FieldTypeEnum.forms`) has no tab of its own: form fields are offered as a
16
+ * group inside Merge Tags, next to the data model groups. `hideFields: [FieldTypeEnum.forms]` still
17
+ * hides that group.
18
+ */
15
19
  export const menuGroups: MenuGroupModel[] = [
16
20
  { svgIcon: IconEstimate, label: 'Merge Tags', key: FieldTypeEnum.dataModel },
17
21
  { svgIcon: IconBorderColor, label: 'E-Sign', key: FieldTypeEnum.eSign },
@@ -30,5 +34,4 @@ export const menuGroups: MenuGroupModel[] = [
30
34
  label: 'Generic Fields',
31
35
  key: FieldTypeEnum.generic,
32
36
  },
33
- { svgIcon: IconFactCheck, label: 'Forms', key: FieldTypeEnum.forms },
34
37
  ];
@@ -6,6 +6,5 @@ export * from './useFormulaEditor';
6
6
  export * from './usePdfDocumentRenderer';
7
7
  export * from './usePdfFieldDnD';
8
8
  export * from './usePdfFieldSelection';
9
- export * from './usePinchZoom';
10
9
  export * from './useToggle';
11
10
  export * from './useZoom';
@@ -10,6 +10,8 @@ import { formFieldInfoToFieldTypeOption } from '../utils';
10
10
 
11
11
  interface UseFormFieldsResult {
12
12
  isLoading: boolean;
13
+ isLoadingForms: boolean;
14
+ formsWithFields: FormInfo[];
13
15
  formFieldsByFormId: FormFieldsByFormIdI;
14
16
  formulaLoadingFormId: null | number;
15
17
  formFieldOptions: FieldTypeOption[];
@@ -18,12 +20,15 @@ interface UseFormFieldsResult {
18
20
  requestFormFieldsById: (formId: number) => void;
19
21
  }
20
22
 
23
+ const EMPTY_FORMS: FormInfo[] = [];
24
+
21
25
  export const useFormFields = (
22
26
  fields: PdfField[],
23
27
  onFormSelect?: (
24
28
  formIds: number[],
25
29
  sendFormFields: (formFieldsByFormId: FormFieldsByFormIdI) => void,
26
30
  ) => void,
31
+ forms: FormInfo[] = EMPTY_FORMS,
27
32
  ): UseFormFieldsResult => {
28
33
  const [formFieldsByFormId, setFormFieldsByFormId] = useState<FormFieldsByFormIdI>({});
29
34
  const [formulaLoadingFormId, setFormulaLoadingFormId] = useState<number | null>(null);
@@ -40,7 +45,13 @@ export const useFormFields = (
40
45
  return;
41
46
  }
42
47
 
43
- const initialFormIds = new Set<number>();
48
+ /*
49
+ * Every form is asked for up front, not just the ones a formula already references: a form
50
+ * with no fields has nothing to offer and is dropped from the list (`formsWithFields`), and
51
+ * that cannot be decided without its fields. They go out as one batched call, and the reply
52
+ * is cached, so opening a form afterwards costs nothing.
53
+ */
54
+ const initialFormIds = new Set<number>(forms.map(form => form.id));
44
55
  fields.forEach(field => {
45
56
  if (field.type === FieldTypeEnum.calculated) {
46
57
  if (field?.formula?.tokens.length) {
@@ -90,7 +101,7 @@ export const useFormFields = (
90
101
  cancelled = true;
91
102
  cancelAnimationFrame(rafId);
92
103
  };
93
- }, [fields, onFormSelect]);
104
+ }, [fields, forms, onFormSelect]);
94
105
 
95
106
  const formFieldOptions = useMemo(() => {
96
107
  if (!selectedForm) {
@@ -143,10 +154,21 @@ export const useFormFields = (
143
154
  [formFieldsByFormId, onFormSelect],
144
155
  );
145
156
 
157
+ /*
158
+ * A form is offered only once it is known to have something to drag out of it. Until its reply
159
+ * lands it is neither listed nor counted as empty — `isLoadingForms` stands in for it.
160
+ */
161
+ const formsWithFields = useMemo(() => {
162
+ return forms.filter(form => !!formFieldsByFormId[form.id]?.length);
163
+ }, [forms, formFieldsByFormId]);
164
+
146
165
  const isLoading = selectedForm != null && formFieldsLoadingFormIds[selectedForm.id];
166
+ const isLoadingForms = forms.some(form => !!formFieldsLoadingFormIds[form.id]);
147
167
 
148
168
  return {
149
169
  isLoading,
170
+ isLoadingForms,
171
+ formsWithFields,
150
172
  formFieldsByFormId,
151
173
  formulaLoadingFormId,
152
174
  formFieldOptions,
@@ -1,37 +1,25 @@
1
1
  import { useCallback, useState } from 'react';
2
- import { ZOOM_DEFAULT, ZOOM_STEP } from '../constants';
3
- import { clampZoom } from '../utils';
2
+ import { ZOOM_DEFAULT, ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from '../constants';
4
3
 
5
4
  export interface UseZoomReturn {
6
5
  zoom: number;
7
6
  zoomIn(): void;
8
7
  zoomOut(): void;
9
8
  resetZoom(): void;
10
- setZoomValue(value: number): void;
11
- zoomByFactor(factor: number): void;
12
9
  }
13
10
 
11
+ const clampZoom = (value: number) =>
12
+ Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(value * 100) / 100));
13
+
14
14
  /**
15
15
  * Manages a page zoom factor (1 = 100%), clamped to [ZOOM_MIN, ZOOM_MAX] in ZOOM_STEP increments.
16
16
  */
17
17
  export const useZoom = (initialZoom: number = ZOOM_DEFAULT): UseZoomReturn => {
18
18
  const [zoom, setZoom] = useState<number>(initialZoom);
19
19
 
20
- const zoomIn = useCallback(() => {
21
- setZoom(prev => clampZoom(prev + ZOOM_STEP));
22
- }, []);
23
- const zoomOut = useCallback(() => {
24
- setZoom(prev => clampZoom(prev - ZOOM_STEP));
25
- }, []);
26
- const resetZoom = useCallback(() => {
27
- setZoom(ZOOM_DEFAULT);
28
- }, []);
29
- const setZoomValue = useCallback((value: number) => {
30
- setZoom(clampZoom(value));
31
- }, []);
32
- const zoomByFactor = useCallback((factor: number) => {
33
- setZoom(prev => clampZoom(prev * factor));
34
- }, []);
20
+ const zoomIn = useCallback(() => setZoom(prev => clampZoom(prev + ZOOM_STEP)), []);
21
+ const zoomOut = useCallback(() => setZoom(prev => clampZoom(prev - ZOOM_STEP)), []);
22
+ const resetZoom = useCallback(() => setZoom(ZOOM_DEFAULT), []);
35
23
 
36
- return { resetZoom, setZoomValue, zoom, zoomByFactor, zoomIn, zoomOut };
24
+ return { zoom, zoomIn, zoomOut, resetZoom };
37
25
  };
@@ -1,5 +1,3 @@
1
- import { Svg } from '@servicetitan/anvil2';
2
-
3
1
  export enum FieldTypeEnum {
4
2
  dataModel = 'dataModel',
5
3
  forms = 'forms',
@@ -166,7 +164,6 @@ export interface FieldTypeOption {
166
164
  fieldType?: SchemaFieldType;
167
165
  formSnapshot?: FormulaFieldFormSnapshot;
168
166
  sampleData?: unknown;
169
- icon?: Svg;
170
167
  }
171
168
 
172
169
  export interface DataModelFieldGroup {
@@ -16,6 +16,11 @@
16
16
  box-shadow: 0 0 2px 0 var(--border-color-active);
17
17
  }
18
18
 
19
+ .dte-field-type-item.--skeleton {
20
+ cursor: default;
21
+ pointer-events: none;
22
+ }
23
+
19
24
  .dte-field-type-items-wrapper {
20
25
  &.--grid-2 {
21
26
  display: grid;
@@ -69,6 +69,18 @@
69
69
  gap: 0;
70
70
  }
71
71
 
72
+ /*
73
+ * The panels share one scroller, so the list — always the taller of the two — would otherwise keep
74
+ * driving the scroll height while the fields panel is shown, leaving the fields followed by a
75
+ * screenful of nothing. Both bounds are needed: the min-content floor above keeps a max-height on
76
+ * its own from taking effect.
77
+ */
78
+ .dte-formula-sidebar-slide-track.--detail .dte-formula-sidebar-slide-panel:not(.--form-fields) {
79
+ min-height: 0;
80
+ max-height: 0;
81
+ overflow: hidden;
82
+ }
83
+
72
84
  .dte-formula-sidebar-forms-heading {
73
85
  flex-shrink: 0;
74
86
  }
@@ -94,6 +106,11 @@
94
106
  white-space: nowrap;
95
107
  }
96
108
 
109
+ .dte-formula-field-list-item.--skeleton {
110
+ cursor: default;
111
+ pointer-events: none;
112
+ }
113
+
97
114
  .dte-formula-field-list-item:hover,
98
115
  .dte-formula-field-list-item.--highlight {
99
116
  color: var(--color-neutral-100, #e8e8e8);