@servicetitan/dte-pdf-editor 1.92.4 → 1.93.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 (209) hide show
  1. package/README.md +0 -2
  2. package/dist/components/field-config-panel/dropdown-field-items.d.ts +9 -0
  3. package/dist/components/field-config-panel/dropdown-field-items.d.ts.map +1 -0
  4. package/dist/components/field-config-panel/dropdown-field-items.js +60 -0
  5. package/dist/components/field-config-panel/dropdown-field-items.js.map +1 -0
  6. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +1 -1
  7. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/field-config-panel.d.ts +1 -1
  9. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  10. package/dist/components/field-config-panel/field-config-panel.js +5 -1
  11. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  12. package/dist/components/field-config-panel/fillable-default-config.d.ts +19 -0
  13. package/dist/components/field-config-panel/fillable-default-config.d.ts.map +1 -0
  14. package/dist/components/field-config-panel/fillable-default-config.js +77 -0
  15. package/dist/components/field-config-panel/fillable-default-config.js.map +1 -0
  16. package/dist/components/field-sidebar/calculated-field-type-list.d.ts.map +1 -1
  17. package/dist/components/field-sidebar/calculated-field-type-list.js +1 -1
  18. package/dist/components/field-sidebar/calculated-field-type-list.js.map +1 -1
  19. package/dist/components/field-sidebar/e-sign-field-type-list.d.ts.map +1 -1
  20. package/dist/components/field-sidebar/e-sign-field-type-list.js +1 -1
  21. package/dist/components/field-sidebar/e-sign-field-type-list.js.map +1 -1
  22. package/dist/components/field-sidebar/field-type.d.ts +2 -0
  23. package/dist/components/field-sidebar/field-type.d.ts.map +1 -1
  24. package/dist/components/field-sidebar/field-type.js +2 -2
  25. package/dist/components/field-sidebar/field-type.js.map +1 -1
  26. package/dist/components/field-sidebar/fillable-field-type-list.d.ts.map +1 -1
  27. package/dist/components/field-sidebar/fillable-field-type-list.js +1 -1
  28. package/dist/components/field-sidebar/fillable-field-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 +9 -15
  33. package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -1
  34. package/dist/components/pdf-canvas/canvas-controls.js +4 -14
  35. package/dist/components/pdf-canvas/canvas-controls.js.map +1 -1
  36. package/dist/components/pdf-canvas/pdf-canvas.d.ts +6 -2
  37. package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
  38. package/dist/components/pdf-canvas/pdf-canvas.js +11 -5
  39. package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
  40. package/dist/components/pdf-editor/pdf-editor.d.ts +2 -0
  41. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  42. package/dist/components/pdf-editor/pdf-editor.js +2 -2
  43. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  44. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +4 -1
  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 +3 -2
  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-fillable.d.ts +9 -0
  50. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
  51. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +39 -12
  52. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
  53. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  54. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +6 -4
  55. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  56. package/dist/components/pdf-view/pdf-view-e-sign.d.ts.map +1 -1
  57. package/dist/components/pdf-view/pdf-view-e-sign.js +3 -2
  58. package/dist/components/pdf-view/pdf-view-e-sign.js.map +1 -1
  59. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  60. package/dist/components/pdf-view/pdf-view-field-container.js +9 -15
  61. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  62. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  63. package/dist/components/pdf-view/pdf-view-fillable.js +7 -3
  64. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  65. package/dist/components/pdf-view/pdf-view.d.ts +5 -0
  66. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  67. package/dist/components/pdf-view/pdf-view.js +58 -24
  68. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  69. package/dist/constants/field.constants.d.ts +1 -0
  70. package/dist/constants/field.constants.d.ts.map +1 -1
  71. package/dist/constants/field.constants.js +36 -6
  72. package/dist/constants/field.constants.js.map +1 -1
  73. package/dist/hooks/index.d.ts +1 -0
  74. package/dist/hooks/index.d.ts.map +1 -1
  75. package/dist/hooks/index.js +1 -0
  76. package/dist/hooks/index.js.map +1 -1
  77. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  78. package/dist/hooks/usePdfFieldDnD.js +6 -0
  79. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  80. package/dist/hooks/usePdfFieldSelection.d.ts +1 -1
  81. package/dist/hooks/usePdfFieldSelection.d.ts.map +1 -1
  82. package/dist/hooks/usePdfFieldSelection.js +3 -1
  83. package/dist/hooks/usePdfFieldSelection.js.map +1 -1
  84. package/dist/hooks/usePinchZoom.d.ts +14 -0
  85. package/dist/hooks/usePinchZoom.d.ts.map +1 -0
  86. package/dist/hooks/usePinchZoom.js +58 -0
  87. package/dist/hooks/usePinchZoom.js.map +1 -0
  88. package/dist/hooks/useZoom.d.ts +2 -0
  89. package/dist/hooks/useZoom.d.ts.map +1 -1
  90. package/dist/hooks/useZoom.js +18 -6
  91. package/dist/hooks/useZoom.js.map +1 -1
  92. package/dist/index.d.ts +1 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +1 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/interface/types.d.ts +48 -1
  97. package/dist/interface/types.d.ts.map +1 -1
  98. package/dist/interface/types.js.map +1 -1
  99. package/dist/styles/index.css +1 -0
  100. package/dist/styles/inline-editable.css +2 -0
  101. package/dist/styles/pdf-canvas.css +92 -15
  102. package/dist/styles/pdf-editor.css +1 -0
  103. package/dist/styles/pdf-field-overlay.css +40 -118
  104. package/dist/styles/pdf-field.css +111 -0
  105. package/dist/styles/variables.css +4 -0
  106. package/dist/utils/data-model/index.d.ts +1 -0
  107. package/dist/utils/data-model/index.d.ts.map +1 -1
  108. package/dist/utils/data-model/index.js +1 -0
  109. package/dist/utils/data-model/index.js.map +1 -1
  110. package/dist/utils/data-model/resolve-fields-default-values.d.ts +3 -0
  111. package/dist/utils/data-model/resolve-fields-default-values.d.ts.map +1 -0
  112. package/dist/utils/data-model/resolve-fields-default-values.js +19 -0
  113. package/dist/utils/data-model/resolve-fields-default-values.js.map +1 -0
  114. package/dist/utils/exposes-utils.d.ts +2 -0
  115. package/dist/utils/exposes-utils.d.ts.map +1 -0
  116. package/dist/utils/exposes-utils.js +2 -0
  117. package/dist/utils/exposes-utils.js.map +1 -0
  118. package/dist/utils/field/field-background-color.utils.d.ts +1 -1
  119. package/dist/utils/field/field-background-color.utils.d.ts.map +1 -1
  120. package/dist/utils/field/field-background-color.utils.js +1 -1
  121. package/dist/utils/field/field-background-color.utils.js.map +1 -1
  122. package/dist/utils/field/get-dropdown-field-items.utils.d.ts +3 -0
  123. package/dist/utils/field/get-dropdown-field-items.utils.d.ts.map +1 -0
  124. package/dist/utils/field/get-dropdown-field-items.utils.js +7 -0
  125. package/dist/utils/field/get-dropdown-field-items.utils.js.map +1 -0
  126. package/dist/utils/field/get-field-bounds-style.utils.d.ts +1 -2
  127. package/dist/utils/field/get-field-bounds-style.utils.d.ts.map +1 -1
  128. package/dist/utils/field/get-field-bounds-style.utils.js +9 -4
  129. package/dist/utils/field/get-field-bounds-style.utils.js.map +1 -1
  130. package/dist/utils/field/get-field-type-option.utils.d.ts +3 -0
  131. package/dist/utils/field/get-field-type-option.utils.d.ts.map +1 -0
  132. package/dist/utils/field/get-field-type-option.utils.js +10 -0
  133. package/dist/utils/field/get-field-type-option.utils.js.map +1 -0
  134. package/dist/utils/field/get-fillable-default-value.utils.d.ts +29 -0
  135. package/dist/utils/field/get-fillable-default-value.utils.d.ts.map +1 -0
  136. package/dist/utils/field/get-fillable-default-value.utils.js +59 -0
  137. package/dist/utils/field/get-fillable-default-value.utils.js.map +1 -0
  138. package/dist/utils/field/get-navigable-fields.utils.js +1 -1
  139. package/dist/utils/field/get-navigable-fields.utils.js.map +1 -1
  140. package/dist/utils/field/index.d.ts +3 -0
  141. package/dist/utils/field/index.d.ts.map +1 -1
  142. package/dist/utils/field/index.js +3 -0
  143. package/dist/utils/field/index.js.map +1 -1
  144. package/dist/utils/pdf/index.d.ts +1 -0
  145. package/dist/utils/pdf/index.d.ts.map +1 -1
  146. package/dist/utils/pdf/index.js +1 -0
  147. package/dist/utils/pdf/index.js.map +1 -1
  148. package/dist/utils/pdf/pinch-zoom.utils.d.ts +6 -0
  149. package/dist/utils/pdf/pinch-zoom.utils.d.ts.map +1 -0
  150. package/dist/utils/pdf/pinch-zoom.utils.js +18 -0
  151. package/dist/utils/pdf/pinch-zoom.utils.js.map +1 -0
  152. package/dist/utils/recipients/map-colors.js +23 -23
  153. package/dist/utils/recipients/map-colors.js.map +1 -1
  154. package/package.json +1 -1
  155. package/src/__tests__/field-types/e-sign-fields.test.ts +3 -3
  156. package/src/__tests__/field-types/fillable-fields.test.ts +142 -6
  157. package/src/components/field-config-panel/dropdown-field-items.tsx +138 -0
  158. package/src/components/field-config-panel/field-config-panel-overlay.tsx +1 -1
  159. package/src/components/field-config-panel/field-config-panel.tsx +16 -1
  160. package/src/components/field-config-panel/fillable-default-config.tsx +155 -0
  161. package/src/components/field-sidebar/calculated-field-type-list.tsx +1 -0
  162. package/src/components/field-sidebar/e-sign-field-type-list.tsx +1 -0
  163. package/src/components/field-sidebar/field-type.tsx +13 -3
  164. package/src/components/field-sidebar/fillable-field-type-list.tsx +1 -0
  165. package/src/components/field-sidebar/generic-field-type-list.tsx +1 -0
  166. package/src/components/pdf-canvas/canvas-controls.tsx +30 -128
  167. package/src/components/pdf-canvas/pdf-canvas.tsx +18 -8
  168. package/src/components/pdf-editor/pdf-editor.tsx +5 -2
  169. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +2 -2
  170. package/src/components/pdf-fields-overlay/pdf-overlay-field-e-sign.tsx +8 -6
  171. package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +66 -3
  172. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +22 -3
  173. package/src/components/pdf-view/pdf-view-e-sign.tsx +8 -6
  174. package/src/components/pdf-view/pdf-view-field-container.tsx +31 -31
  175. package/src/components/pdf-view/pdf-view-fillable.tsx +26 -3
  176. package/src/components/pdf-view/pdf-view.tsx +164 -95
  177. package/src/constants/field.constants.ts +38 -6
  178. package/src/hooks/index.ts +1 -0
  179. package/src/hooks/usePdfFieldDnD.ts +6 -0
  180. package/src/hooks/usePdfFieldSelection.ts +8 -2
  181. package/src/hooks/usePinchZoom.ts +86 -0
  182. package/src/hooks/useZoom.ts +20 -8
  183. package/src/index.ts +1 -0
  184. package/src/interface/types.ts +39 -1
  185. package/src/styles/index.css +1 -0
  186. package/src/styles/inline-editable.css +2 -0
  187. package/src/styles/pdf-canvas.css +92 -15
  188. package/src/styles/pdf-editor.css +1 -0
  189. package/src/styles/pdf-field-overlay.css +40 -118
  190. package/src/styles/pdf-field.css +111 -0
  191. package/src/styles/variables.css +4 -0
  192. package/src/utils/data-model/index.ts +1 -0
  193. package/src/utils/data-model/resolve-fields-default-values.ts +19 -0
  194. package/src/utils/exposes-utils.ts +1 -0
  195. package/src/utils/field/__tests__/field-background-color.utils.test.ts +3 -3
  196. package/src/utils/field/__tests__/get-field-bounds-style.utils.test.ts +0 -1
  197. package/src/utils/field/__tests__/get-field-type-option.utils.test.ts +53 -0
  198. package/src/utils/field/__tests__/get-fillable-default-value.utils.test.ts +137 -0
  199. package/src/utils/field/field-background-color.utils.ts +1 -1
  200. package/src/utils/field/get-dropdown-field-items.utils.ts +14 -0
  201. package/src/utils/field/get-field-bounds-style.utils.ts +11 -11
  202. package/src/utils/field/get-field-type-option.utils.ts +16 -0
  203. package/src/utils/field/get-fillable-default-value.utils.ts +70 -0
  204. package/src/utils/field/get-navigable-fields.utils.ts +1 -1
  205. package/src/utils/field/index.ts +3 -0
  206. package/src/utils/pdf/__tests__/pinch-zoom.utils.test.ts +58 -0
  207. package/src/utils/pdf/index.ts +1 -0
  208. package/src/utils/pdf/pinch-zoom.utils.ts +30 -0
  209. package/src/utils/recipients/map-colors.ts +23 -23
@@ -2,16 +2,21 @@ import { FieldTypeEnum, type PdfField, type RecipientInfo } from '../../interfac
2
2
  import {
3
3
  generateFillablePath,
4
4
  getDocumentFieldsDataPointOptions,
5
- getFieldBackgroundColor,
5
+ getDropdownFieldItems,
6
+ getFieldColor,
6
7
  getFieldPlaceholderText,
8
+ getFillableEffectiveValue,
9
+ getTodayDateInputValue,
10
+ isRadioDefaultSelected,
11
+ isRadioOptionChecked,
7
12
  mapColorsToRecipients,
8
- parseFillablePathName,
13
+ parseFillablePathName
9
14
  } from '../../utils';
10
15
 
11
16
  /**
12
17
  * Scenario tests for fillable fields.
13
18
  *
14
- * Fillable fields support five sub-types: text, number, date, checkbox, radio.
19
+ * Fillable fields support six sub-types: text, number, date, checkbox, radio, dropdown.
15
20
  * These tests validate the full flow: path generation -> parsing -> placeholder
16
21
  * -> color resolution -> data-point option export for downstream conditions.
17
22
  */
@@ -149,19 +154,150 @@ describe('fillable field scenarios', () => {
149
154
  });
150
155
  });
151
156
 
157
+ describe('with default value (DTE-4668)', () => {
158
+ test('a field with a configured default shows it when nothing has been filled yet', () => {
159
+ const field = buildFillableField({ default: { type: 'text', value: 'Acme Inc.' } });
160
+ expect(getFillableEffectiveValue(field, undefined)).toBe('Acme Inc.');
161
+ });
162
+
163
+ test('a field with no default round-trips without one and still starts blank', () => {
164
+ const field = buildFillableField();
165
+ expect(field.default).toBeUndefined();
166
+ expect(getFillableEffectiveValue(field, undefined)).toBeUndefined();
167
+ });
168
+
169
+ test('an explicit filled-in value always overrides the default', () => {
170
+ const field = buildFillableField({ default: { type: 'text', value: 'Acme Inc.' } });
171
+ expect(getFillableEffectiveValue(field, 'Contoso Ltd.')).toBe('Contoso Ltd.');
172
+ });
173
+
174
+ test('an explicit blank is a stored answer and is not re-defaulted on reload', () => {
175
+ const field = buildFillableField({
176
+ subType: 'checkbox',
177
+ default: { type: 'checkbox', value: true },
178
+ });
179
+ // The recipient unchecked it - `false` is present, not a missing path.
180
+ expect(getFillableEffectiveValue(field, false)).toBe(false);
181
+ });
182
+
183
+ test('a date field can default to a fixed calendar date', () => {
184
+ const field = buildFillableField({
185
+ subType: 'date',
186
+ default: { type: 'date', value: '2026-01-01' },
187
+ });
188
+ expect(getFillableEffectiveValue(field, undefined)).toBe('2026-01-01');
189
+ });
190
+
191
+ test('a date field can default to today, resolved at render time rather than frozen at authoring time', () => {
192
+ const field = buildFillableField({ subType: 'date', default: { type: 'today' } });
193
+ expect(getFillableEffectiveValue(field, undefined)).toBe(getTodayDateInputValue());
194
+ });
195
+
196
+ test('a radio option default only flags that specific option, not a scalar on the group', () => {
197
+ const path = generateFillablePath('signer1', 'Choice');
198
+ const defaultOption = buildFillableField({
199
+ id: 'opt-yes',
200
+ subType: 'radio',
201
+ path,
202
+ default: { type: 'radio' },
203
+ });
204
+ const otherOption = buildFillableField({ id: 'opt-no', subType: 'radio', path });
205
+
206
+ expect(isRadioDefaultSelected(defaultOption)).toBe(true);
207
+ expect(isRadioDefaultSelected(otherOption)).toBe(false);
208
+ // Nothing chosen yet for the group's own path -> the flagged option reads as checked.
209
+ expect(isRadioOptionChecked(defaultOption, undefined)).toBe(true);
210
+ expect(isRadioOptionChecked(otherOption, undefined)).toBe(false);
211
+ });
212
+
213
+ test('a chosen radio option in the same group overrides the default flag on reload', () => {
214
+ const path = generateFillablePath('signer1', 'Choice');
215
+ const defaultOption = buildFillableField({
216
+ id: 'opt-yes',
217
+ subType: 'radio',
218
+ path,
219
+ default: { type: 'radio' },
220
+ });
221
+ const otherOption = buildFillableField({ id: 'opt-no', subType: 'radio', path });
222
+
223
+ // The recipient picked "opt-no" - that id is now the group's stored value at `path`.
224
+ expect(isRadioOptionChecked(defaultOption, 'opt-no')).toBe(false);
225
+ expect(isRadioOptionChecked(otherOption, 'opt-no')).toBe(true);
226
+ });
227
+ });
228
+
229
+ describe('with dropdown sub-type', () => {
230
+ const field = buildFillableField({
231
+ subType: 'dropdown',
232
+ label: 'Preferred Contact',
233
+ path: generateFillablePath('signer1', 'PreferredContact'),
234
+ dropdownItems: [
235
+ { value: 'email', label: 'Email' },
236
+ { value: 'phone', label: 'Phone' },
237
+ ],
238
+ });
239
+
240
+ test('exports as string data point option', () => {
241
+ const options = getDocumentFieldsDataPointOptions([field]);
242
+ expect(options[0].fieldType).toBe('string');
243
+ });
244
+
245
+ test('reads back the configured items', () => {
246
+ expect(getDropdownFieldItems(field)).toEqual([
247
+ { value: 'email', label: 'Email' },
248
+ { value: 'phone', label: 'Phone' },
249
+ ]);
250
+ });
251
+
252
+ test('returns an empty list when items are missing (pre-existing documents)', () => {
253
+ expect(getDropdownFieldItems({ ...field, dropdownItems: undefined })).toEqual([]);
254
+ });
255
+
256
+ test('returns an empty list when items are malformed', () => {
257
+ expect(
258
+ getDropdownFieldItems({ ...field, dropdownItems: 'not-an-array' as any }),
259
+ ).toEqual([]);
260
+ expect(
261
+ getDropdownFieldItems({
262
+ ...field,
263
+ dropdownItems: [{ value: 'email' }] as any,
264
+ }),
265
+ ).toEqual([]);
266
+ });
267
+
268
+ describe('with a default item (FE-only `dropdown` variant, no backend equivalent)', () => {
269
+ const fieldWithDefault = buildFillableField({
270
+ ...field,
271
+ default: { type: 'dropdown', value: 'phone' },
272
+ });
273
+
274
+ test('shows the defaulted item value when nothing has been filled yet', () => {
275
+ expect(getFillableEffectiveValue(fieldWithDefault, undefined)).toBe('phone');
276
+ });
277
+
278
+ test('an explicit stored value overrides the default', () => {
279
+ expect(getFillableEffectiveValue(fieldWithDefault, 'email')).toBe('email');
280
+ });
281
+
282
+ test('starts blank when no item is flagged as default', () => {
283
+ expect(getFillableEffectiveValue(field, undefined)).toBeUndefined();
284
+ });
285
+ });
286
+ });
287
+
152
288
  describe('with multi-recipient color resolution', () => {
153
289
  const colors = mapColorsToRecipients(recipients);
154
290
  const signer1Field = buildFillableField({ recipient: 'signer1' });
155
291
  const signer2Field = buildFillableField({ id: 'f2', recipient: 'signer2' });
156
292
 
157
293
  test('returns distinct background colors per recipient', () => {
158
- expect(getFieldBackgroundColor(signer1Field.recipient, colors)).not.toBe(
159
- getFieldBackgroundColor(signer2Field.recipient, colors),
294
+ expect(getFieldColor(signer1Field.recipient, colors)).not.toBe(
295
+ getFieldColor(signer2Field.recipient, colors),
160
296
  );
161
297
  });
162
298
 
163
299
  test('returns "none" when field has no recipient', () => {
164
- expect(getFieldBackgroundColor(undefined, colors)).toBe('none');
300
+ expect(getFieldColor(undefined, colors)).toBe('none');
165
301
  });
166
302
  });
167
303
 
@@ -0,0 +1,138 @@
1
+ import { Button, Flex, SelectFieldSync, Text, TextField } from '@servicetitan/anvil2';
2
+ import AddCircleIcon from '@servicetitan/anvil2/assets/icons/material/round/add_circle.svg';
3
+ import DeleteIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
4
+ import { FC } from 'react';
5
+ import { DROPDOWN_FIELD_MAX_ITEMS } from '../../constants';
6
+ import { type FillableDropdownItem, type PdfField } from '../../interface/types';
7
+ import { getDropdownFieldItems } from '../../utils';
8
+
9
+ interface DropdownFieldItemsProps {
10
+ field: PdfField;
11
+ onFieldConfigChange(updates: Partial<PdfField>): void;
12
+ }
13
+
14
+ export const DropdownFieldItems: FC<DropdownFieldItemsProps> = ({ field, onFieldConfigChange }) => {
15
+ const items = getDropdownFieldItems(field);
16
+ const defaultValue = field.default?.type === 'dropdown' ? field.default.value : undefined;
17
+
18
+ const setItems = (nextItems: FillableDropdownItem[]) => {
19
+ onFieldConfigChange({ dropdownItems: nextItems });
20
+ };
21
+
22
+ const handleAddItem = () => {
23
+ setItems([...items, { value: '', label: '' }]);
24
+ };
25
+
26
+ const handleRemoveItem = (index: number) => {
27
+ const nextItems = items.filter((_, i) => i !== index);
28
+ onFieldConfigChange({
29
+ dropdownItems: nextItems,
30
+ ...(items[index].value === defaultValue ? { default: undefined } : {}),
31
+ });
32
+ };
33
+
34
+ const handleUpdateItem = (index: number, updates: Partial<FillableDropdownItem>) => {
35
+ const isDefaultItem = items[index].value === defaultValue;
36
+ const nextItems = items.map((item, i) => (i === index ? { ...item, ...updates } : item));
37
+ onFieldConfigChange({
38
+ dropdownItems: nextItems,
39
+ ...(isDefaultItem
40
+ ? { default: { type: 'dropdown', value: nextItems[index].value } }
41
+ : {}),
42
+ });
43
+ };
44
+
45
+ const getDuplicateFlags = (values: string[]): boolean[] => {
46
+ const counts = new Map<string, number>();
47
+ values.forEach(value => {
48
+ if (value) {
49
+ counts.set(value, (counts.get(value) ?? 0) + 1);
50
+ }
51
+ });
52
+ return values.map(value => !!value && (counts.get(value) ?? 0) > 1);
53
+ };
54
+
55
+ const isDuplicateValue = getDuplicateFlags(items.map(item => item.value));
56
+ const isDuplicateLabel = getDuplicateFlags(items.map(item => item.label));
57
+
58
+ const itemOptions = items.map((item, index) => ({
59
+ id: index,
60
+ label: item.label || item.value || `Item ${index + 1}`,
61
+ }));
62
+ const selectedDefaultIndex = items.findIndex(item => item.value === defaultValue);
63
+ const selectedDefaultOption =
64
+ selectedDefaultIndex === -1 ? null : itemOptions[selectedDefaultIndex];
65
+
66
+ const handleDefaultChange = (option: { id: number; label: string } | null) => {
67
+ const selectedItem = option ? items[option.id] : undefined;
68
+ onFieldConfigChange({
69
+ default: selectedItem ? { type: 'dropdown', value: selectedItem.value } : undefined,
70
+ });
71
+ };
72
+
73
+ return (
74
+ <Flex direction="column" gap="2">
75
+ <Flex justifyContent="space-between" alignItems="center">
76
+ <Text variant="body" size="small">
77
+ Items
78
+ </Text>
79
+ <Button
80
+ size="small"
81
+ appearance="ghost"
82
+ icon={{ before: AddCircleIcon }}
83
+ disabled={items.length >= DROPDOWN_FIELD_MAX_ITEMS}
84
+ onClick={handleAddItem}
85
+ >
86
+ Add Item
87
+ </Button>
88
+ </Flex>
89
+
90
+ {items.map((item, index) => (
91
+ /*
92
+ * items are plain {value, label} pairs with no separate id, and value/label are
93
+ * exactly what's being edited and validated for uniqueness here, so neither is a
94
+ * safe list key.
95
+ */
96
+ // eslint-disable-next-line react/no-array-index-key
97
+ <Flex key={index} direction="row" gap="1" className="dte-rule-card">
98
+ <TextField
99
+ size="small"
100
+ aria-label="Item value"
101
+ placeholder="Value"
102
+ error={(isDuplicateValue[index] && 'Must be unique') || !item.value}
103
+ value={item.value}
104
+ style={{ flex: 1, minWidth: 0 }}
105
+ onChange={e => handleUpdateItem(index, { value: e.target.value })}
106
+ />
107
+ <TextField
108
+ size="small"
109
+ aria-label="Item label"
110
+ placeholder="Label"
111
+ error={(isDuplicateLabel[index] && 'Must be unique') || !item.label}
112
+ value={item.label}
113
+ style={{ flex: 1, minWidth: 0 }}
114
+ onChange={e => handleUpdateItem(index, { label: e.target.value })}
115
+ />
116
+ <Flex justifyContent="flex-end" gap="1">
117
+ <Button
118
+ size="small"
119
+ disabled={items.length <= 1}
120
+ appearance="secondary"
121
+ aria-label="Remove item"
122
+ icon={{ before: DeleteIcon }}
123
+ onClick={() => handleRemoveItem(index)}
124
+ />
125
+ </Flex>
126
+ </Flex>
127
+ ))}
128
+
129
+ <SelectFieldSync
130
+ label="Default Selected Item"
131
+ options={itemOptions}
132
+ value={selectedDefaultOption}
133
+ onSelectedOptionChange={handleDefaultChange}
134
+ filter={{ keys: ['label'] }}
135
+ />
136
+ </Flex>
137
+ );
138
+ };
@@ -22,7 +22,7 @@ interface FieldConfigPanelOverlayProps {
22
22
  onFieldConfigChange(updates: Partial<PdfField>): void;
23
23
  onMatchingFieldsConfigChange?(
24
24
  predicate: (field: PdfField) => boolean,
25
- updates: Partial<PdfField>,
25
+ updates: Partial<PdfField> | ((field: PdfField) => Partial<PdfField>),
26
26
  ): void;
27
27
  onDeleteField(): void;
28
28
  onDeselectField?(): void;
@@ -20,7 +20,9 @@ import {
20
20
  supportsFieldFontSize,
21
21
  } from '../../utils';
22
22
  import { DisplayCondition } from '../display-conditions/display-condition';
23
+ import { DropdownFieldItems } from './dropdown-field-items';
23
24
  import { ESignOptionsModal } from './e-sign-options-modal';
25
+ import { FillableDefaultConfig } from './fillable-default-config';
24
26
  import { FormulaGenerator } from './formula-generator';
25
27
  import { TableConfigs } from './table-configs';
26
28
 
@@ -37,7 +39,7 @@ interface FieldConfigPanelProps {
37
39
  onFieldConfigChange(updates: Partial<PdfField>): void;
38
40
  onMatchingFieldsConfigChange?(
39
41
  predicate: (field: PdfField) => boolean,
40
- updates: Partial<PdfField>,
42
+ updates: Partial<PdfField> | ((field: PdfField) => Partial<PdfField>),
41
43
  ): void;
42
44
  onRequestFormFields(formId: number): void;
43
45
  }
@@ -228,6 +230,19 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
228
230
  }
229
231
  />
230
232
  )}
233
+ {field.type === FieldTypeEnum.fillable && field.subType === 'dropdown' && (
234
+ <DropdownFieldItems field={field} onFieldConfigChange={onFieldConfigChange} />
235
+ )}
236
+ {field.type === FieldTypeEnum.fillable &&
237
+ field.subType &&
238
+ ['text', 'number', 'checkbox', 'date', 'radio'].includes(field.subType) && (
239
+ <FillableDefaultConfig
240
+ field={field}
241
+ documentFields={documentFields}
242
+ onFieldConfigChange={onFieldConfigChange}
243
+ onMatchingFieldsConfigChange={onMatchingFieldsConfigChange}
244
+ />
245
+ )}
231
246
  {field.type === FieldTypeEnum.calculated && (
232
247
  <FormulaGenerator
233
248
  dataModel={dataModel}
@@ -0,0 +1,155 @@
1
+ import {
2
+ Checkbox,
3
+ DateFieldSingle,
4
+ Flex,
5
+ NumberField,
6
+ SelectFieldSync,
7
+ TextField,
8
+ } from '@servicetitan/anvil2';
9
+ import { FC } from 'react';
10
+ import { type PdfField } from '../../interface/types';
11
+
12
+ interface FillableDefaultConfigProps {
13
+ field: PdfField;
14
+ documentFields?: PdfField[];
15
+ onFieldConfigChange(updates: Partial<PdfField>): void;
16
+ onMatchingFieldsConfigChange?(
17
+ predicate: (field: PdfField) => boolean,
18
+ updates: Partial<PdfField> | ((field: PdfField) => Partial<PdfField>),
19
+ ): void;
20
+ }
21
+
22
+ /**
23
+ * Configures `field.default` (DTE-4668) - an optional author-configured starting value that the
24
+ * fillable views merge in for display only when the recipient hasn't stored a value at this
25
+ * field's path yet. Mirrors the backend's `FillableDefault`/`ValidateDefault` shape and
26
+ * constraints one-for-one (`ByodField.cs`) so a template saved here never fails backend
27
+ * validation: a blank/whitespace text default is never authored (it's just "no default", same as
28
+ * omitting it), and a radio group can never end up with more than one option flagged as default.
29
+ */
30
+ export const FillableDefaultConfig: FC<FillableDefaultConfigProps> = ({
31
+ documentFields = [],
32
+ field,
33
+ onFieldConfigChange,
34
+ onMatchingFieldsConfigChange,
35
+ }) => {
36
+ const isRadioGroupSibling = (documentField: PdfField) =>
37
+ documentField.subType === 'radio' && documentField.path === field.path;
38
+
39
+ const radioGroupSiblings = documentFields.filter(isRadioGroupSibling);
40
+ const radioGroupOptions = radioGroupSiblings.map(sibling => ({
41
+ id: sibling.id,
42
+ label: sibling.label || sibling.id,
43
+ }));
44
+ const selectedRadioDefault =
45
+ radioGroupOptions.find(
46
+ option =>
47
+ radioGroupSiblings.find(sibling => sibling.id === option.id)?.default?.type ===
48
+ 'radio',
49
+ ) ?? null;
50
+
51
+ switch (field.subType) {
52
+ case 'text':
53
+ return (
54
+ <TextField
55
+ label="Default Value"
56
+ value={field.default?.type === 'text' ? field.default.value : ''}
57
+ onChange={e => {
58
+ const value = e.target.value;
59
+ onFieldConfigChange({
60
+ default: value.trim() ? { type: 'text', value } : undefined,
61
+ });
62
+ }}
63
+ />
64
+ );
65
+
66
+ case 'number':
67
+ return (
68
+ <NumberField
69
+ label="Default Value"
70
+ value={field.default?.type === 'number' ? field.default.value : null}
71
+ onChange={value =>
72
+ onFieldConfigChange({
73
+ default: value === null ? undefined : { type: 'number', value },
74
+ })
75
+ }
76
+ />
77
+ );
78
+
79
+ case 'checkbox':
80
+ return (
81
+ <Checkbox
82
+ label="Checked by default"
83
+ checked={field.default?.type === 'checkbox' ? field.default.value : false}
84
+ onChange={() =>
85
+ onFieldConfigChange({
86
+ default:
87
+ field.default?.type === 'checkbox'
88
+ ? undefined
89
+ : { type: 'checkbox', value: true },
90
+ })
91
+ }
92
+ />
93
+ );
94
+
95
+ case 'date':
96
+ return (
97
+ <Flex direction="column" gap="2">
98
+ <Checkbox
99
+ label="Default to today"
100
+ checked={field.default?.type === 'today'}
101
+ onChange={() =>
102
+ onFieldConfigChange({
103
+ default:
104
+ field.default?.type === 'today' ? undefined : { type: 'today' },
105
+ })
106
+ }
107
+ />
108
+ {field.default?.type !== 'today' && (
109
+ <DateFieldSingle
110
+ label="Default Value"
111
+ value={field.default?.type === 'date' ? field.default.value : null}
112
+ onChange={({ date }) =>
113
+ onFieldConfigChange({
114
+ default: date ? { type: 'date', value: date } : undefined,
115
+ })
116
+ }
117
+ />
118
+ )}
119
+ </Flex>
120
+ );
121
+
122
+ case 'radio':
123
+ return (
124
+ <SelectFieldSync
125
+ label="Default Selected Option"
126
+ options={radioGroupOptions}
127
+ value={selectedRadioDefault}
128
+ onSelectedOptionChange={option => {
129
+ if (field.path && onMatchingFieldsConfigChange) {
130
+ /*
131
+ * One update, matched against every option in the group, so the "only
132
+ * one default per radio group" constraint is applied atomically - a
133
+ * separate call per option would each compute its next-fields array
134
+ * from the same stale `fields` snapshot and clobber the others instead
135
+ * of composing.
136
+ */
137
+ onMatchingFieldsConfigChange(isRadioGroupSibling, siblingField => ({
138
+ default:
139
+ siblingField.id === option?.id ? { type: 'radio' } : undefined,
140
+ }));
141
+ return;
142
+ }
143
+
144
+ onFieldConfigChange({
145
+ default: option?.id === field.id ? { type: 'radio' } : undefined,
146
+ });
147
+ }}
148
+ filter={{ keys: ['label'] }}
149
+ />
150
+ );
151
+
152
+ default:
153
+ return null;
154
+ }
155
+ };
@@ -18,6 +18,7 @@ export const CalculatedFieldTypeList: FC<CalculatedFieldTypeListProps> = ({
18
18
  return (
19
19
  <FieldType
20
20
  key={key}
21
+ icon={fieldOption.icon}
21
22
  label={fieldOption.label}
22
23
  onDragEnd={onDragEnd}
23
24
  onDragStart={() => onDragStart(fieldOption)}
@@ -13,6 +13,7 @@ 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}
16
17
  key={fieldOption.subType}
17
18
  label={fieldOption.label}
18
19
  onDragEnd={onDragEnd}
@@ -1,15 +1,22 @@
1
- import { Flex, Icon, Text } from '@servicetitan/anvil2';
1
+ import { Flex, Icon, Svg, 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;
6
7
  label: string;
7
8
  sampleData?: string | number | null;
8
9
  onDragEnd(): void;
9
10
  onDragStart(): void;
10
11
  }
11
12
 
12
- export const FieldType: FC<FieldTypeProps> = ({ label, onDragEnd, onDragStart, sampleData }) => {
13
+ export const FieldType: FC<FieldTypeProps> = ({
14
+ icon,
15
+ label,
16
+ onDragEnd,
17
+ onDragStart,
18
+ sampleData,
19
+ }) => {
13
20
  return (
14
21
  <div
15
22
  draggable
@@ -24,7 +31,10 @@ export const FieldType: FC<FieldTypeProps> = ({ label, onDragEnd, onDragStart, s
24
31
  >
25
32
  <Icon svg={IconDragIndicator} size="medium" />
26
33
  <Flex direction="column" style={{ overflow: 'hidden' }}>
27
- <Text size="small">{label}</Text>
34
+ <Flex alignItems="center" gap="2">
35
+ {icon && <Icon svg={icon} size="medium" />}
36
+ <Text size="small">{label}</Text>
37
+ </Flex>
28
38
  {sampleData && (
29
39
  <Text
30
40
  size="small"
@@ -37,6 +37,7 @@ export const FillableFieldTypeList: FC<FillableFieldTypeListProps> = ({
37
37
  {searchedFieldTypes.map(fieldOption => {
38
38
  return (
39
39
  <FieldType
40
+ icon={fieldOption.icon}
40
41
  key={fieldOption.subType}
41
42
  label={fieldOption.label}
42
43
  onDragEnd={onDragEnd}
@@ -16,6 +16,7 @@ export const GenericFieldTypeList: FC<GenericFieldTypeListProps> = ({ onDragEnd,
16
16
  return (
17
17
  <FieldType
18
18
  key={key}
19
+ icon={fieldOption.icon}
19
20
  label={fieldOption.label}
20
21
  onDragEnd={onDragEnd}
21
22
  onDragStart={() => onDragStart(fieldOption)}