@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.
- package/README.md +0 -2
- package/dist/components/field-config-panel/dropdown-field-items.d.ts +9 -0
- package/dist/components/field-config-panel/dropdown-field-items.d.ts.map +1 -0
- package/dist/components/field-config-panel/dropdown-field-items.js +60 -0
- package/dist/components/field-config-panel/dropdown-field-items.js.map +1 -0
- package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +1 -1
- package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.d.ts +1 -1
- package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.js +5 -1
- package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
- package/dist/components/field-config-panel/fillable-default-config.d.ts +19 -0
- package/dist/components/field-config-panel/fillable-default-config.d.ts.map +1 -0
- package/dist/components/field-config-panel/fillable-default-config.js +77 -0
- package/dist/components/field-config-panel/fillable-default-config.js.map +1 -0
- package/dist/components/field-sidebar/calculated-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/calculated-field-type-list.js +1 -1
- package/dist/components/field-sidebar/calculated-field-type-list.js.map +1 -1
- package/dist/components/field-sidebar/e-sign-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/e-sign-field-type-list.js +1 -1
- package/dist/components/field-sidebar/e-sign-field-type-list.js.map +1 -1
- package/dist/components/field-sidebar/field-type.d.ts +2 -0
- package/dist/components/field-sidebar/field-type.d.ts.map +1 -1
- package/dist/components/field-sidebar/field-type.js +2 -2
- package/dist/components/field-sidebar/field-type.js.map +1 -1
- package/dist/components/field-sidebar/fillable-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/fillable-field-type-list.js +1 -1
- package/dist/components/field-sidebar/fillable-field-type-list.js.map +1 -1
- package/dist/components/field-sidebar/generic-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/generic-field-type-list.js +1 -1
- package/dist/components/field-sidebar/generic-field-type-list.js.map +1 -1
- package/dist/components/pdf-canvas/canvas-controls.d.ts +9 -15
- package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -1
- package/dist/components/pdf-canvas/canvas-controls.js +4 -14
- package/dist/components/pdf-canvas/canvas-controls.js.map +1 -1
- package/dist/components/pdf-canvas/pdf-canvas.d.ts +6 -2
- package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
- package/dist/components/pdf-canvas/pdf-canvas.js +11 -5
- package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.d.ts +2 -0
- package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.js +2 -2
- package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +4 -1
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js +3 -2
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts +9 -0
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +39 -12
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +6 -4
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-e-sign.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-e-sign.js +3 -2
- package/dist/components/pdf-view/pdf-view-e-sign.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.js +9 -15
- package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.js +7 -3
- package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
- package/dist/components/pdf-view/pdf-view.d.ts +5 -0
- package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view.js +58 -24
- package/dist/components/pdf-view/pdf-view.js.map +1 -1
- package/dist/constants/field.constants.d.ts +1 -0
- package/dist/constants/field.constants.d.ts.map +1 -1
- package/dist/constants/field.constants.js +36 -6
- package/dist/constants/field.constants.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
- package/dist/hooks/usePdfFieldDnD.js +6 -0
- package/dist/hooks/usePdfFieldDnD.js.map +1 -1
- package/dist/hooks/usePdfFieldSelection.d.ts +1 -1
- package/dist/hooks/usePdfFieldSelection.d.ts.map +1 -1
- package/dist/hooks/usePdfFieldSelection.js +3 -1
- package/dist/hooks/usePdfFieldSelection.js.map +1 -1
- package/dist/hooks/usePinchZoom.d.ts +14 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.js +58 -0
- package/dist/hooks/usePinchZoom.js.map +1 -0
- package/dist/hooks/useZoom.d.ts +2 -0
- package/dist/hooks/useZoom.d.ts.map +1 -1
- package/dist/hooks/useZoom.js +18 -6
- package/dist/hooks/useZoom.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interface/types.d.ts +48 -1
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js.map +1 -1
- package/dist/styles/index.css +1 -0
- package/dist/styles/inline-editable.css +2 -0
- package/dist/styles/pdf-canvas.css +92 -15
- package/dist/styles/pdf-editor.css +1 -0
- package/dist/styles/pdf-field-overlay.css +40 -118
- package/dist/styles/pdf-field.css +111 -0
- package/dist/styles/variables.css +4 -0
- package/dist/utils/data-model/index.d.ts +1 -0
- package/dist/utils/data-model/index.d.ts.map +1 -1
- package/dist/utils/data-model/index.js +1 -0
- package/dist/utils/data-model/index.js.map +1 -1
- package/dist/utils/data-model/resolve-fields-default-values.d.ts +3 -0
- package/dist/utils/data-model/resolve-fields-default-values.d.ts.map +1 -0
- package/dist/utils/data-model/resolve-fields-default-values.js +19 -0
- package/dist/utils/data-model/resolve-fields-default-values.js.map +1 -0
- package/dist/utils/exposes-utils.d.ts +2 -0
- package/dist/utils/exposes-utils.d.ts.map +1 -0
- package/dist/utils/exposes-utils.js +2 -0
- package/dist/utils/exposes-utils.js.map +1 -0
- package/dist/utils/field/field-background-color.utils.d.ts +1 -1
- package/dist/utils/field/field-background-color.utils.d.ts.map +1 -1
- package/dist/utils/field/field-background-color.utils.js +1 -1
- package/dist/utils/field/field-background-color.utils.js.map +1 -1
- package/dist/utils/field/get-dropdown-field-items.utils.d.ts +3 -0
- package/dist/utils/field/get-dropdown-field-items.utils.d.ts.map +1 -0
- package/dist/utils/field/get-dropdown-field-items.utils.js +7 -0
- package/dist/utils/field/get-dropdown-field-items.utils.js.map +1 -0
- package/dist/utils/field/get-field-bounds-style.utils.d.ts +1 -2
- package/dist/utils/field/get-field-bounds-style.utils.d.ts.map +1 -1
- package/dist/utils/field/get-field-bounds-style.utils.js +9 -4
- package/dist/utils/field/get-field-bounds-style.utils.js.map +1 -1
- package/dist/utils/field/get-field-type-option.utils.d.ts +3 -0
- package/dist/utils/field/get-field-type-option.utils.d.ts.map +1 -0
- package/dist/utils/field/get-field-type-option.utils.js +10 -0
- package/dist/utils/field/get-field-type-option.utils.js.map +1 -0
- package/dist/utils/field/get-fillable-default-value.utils.d.ts +29 -0
- package/dist/utils/field/get-fillable-default-value.utils.d.ts.map +1 -0
- package/dist/utils/field/get-fillable-default-value.utils.js +59 -0
- package/dist/utils/field/get-fillable-default-value.utils.js.map +1 -0
- package/dist/utils/field/get-navigable-fields.utils.js +1 -1
- package/dist/utils/field/get-navigable-fields.utils.js.map +1 -1
- package/dist/utils/field/index.d.ts +3 -0
- package/dist/utils/field/index.d.ts.map +1 -1
- package/dist/utils/field/index.js +3 -0
- package/dist/utils/field/index.js.map +1 -1
- package/dist/utils/pdf/index.d.ts +1 -0
- package/dist/utils/pdf/index.d.ts.map +1 -1
- package/dist/utils/pdf/index.js +1 -0
- package/dist/utils/pdf/index.js.map +1 -1
- package/dist/utils/pdf/pinch-zoom.utils.d.ts +6 -0
- package/dist/utils/pdf/pinch-zoom.utils.d.ts.map +1 -0
- package/dist/utils/pdf/pinch-zoom.utils.js +18 -0
- package/dist/utils/pdf/pinch-zoom.utils.js.map +1 -0
- package/dist/utils/recipients/map-colors.js +23 -23
- package/dist/utils/recipients/map-colors.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/field-types/e-sign-fields.test.ts +3 -3
- package/src/__tests__/field-types/fillable-fields.test.ts +142 -6
- package/src/components/field-config-panel/dropdown-field-items.tsx +138 -0
- package/src/components/field-config-panel/field-config-panel-overlay.tsx +1 -1
- package/src/components/field-config-panel/field-config-panel.tsx +16 -1
- package/src/components/field-config-panel/fillable-default-config.tsx +155 -0
- package/src/components/field-sidebar/calculated-field-type-list.tsx +1 -0
- package/src/components/field-sidebar/e-sign-field-type-list.tsx +1 -0
- package/src/components/field-sidebar/field-type.tsx +13 -3
- package/src/components/field-sidebar/fillable-field-type-list.tsx +1 -0
- package/src/components/field-sidebar/generic-field-type-list.tsx +1 -0
- package/src/components/pdf-canvas/canvas-controls.tsx +30 -128
- package/src/components/pdf-canvas/pdf-canvas.tsx +18 -8
- package/src/components/pdf-editor/pdf-editor.tsx +5 -2
- package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +2 -2
- package/src/components/pdf-fields-overlay/pdf-overlay-field-e-sign.tsx +8 -6
- package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +66 -3
- package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +22 -3
- package/src/components/pdf-view/pdf-view-e-sign.tsx +8 -6
- package/src/components/pdf-view/pdf-view-field-container.tsx +31 -31
- package/src/components/pdf-view/pdf-view-fillable.tsx +26 -3
- package/src/components/pdf-view/pdf-view.tsx +164 -95
- package/src/constants/field.constants.ts +38 -6
- package/src/hooks/index.ts +1 -0
- package/src/hooks/usePdfFieldDnD.ts +6 -0
- package/src/hooks/usePdfFieldSelection.ts +8 -2
- package/src/hooks/usePinchZoom.ts +86 -0
- package/src/hooks/useZoom.ts +20 -8
- package/src/index.ts +1 -0
- package/src/interface/types.ts +39 -1
- package/src/styles/index.css +1 -0
- package/src/styles/inline-editable.css +2 -0
- package/src/styles/pdf-canvas.css +92 -15
- package/src/styles/pdf-editor.css +1 -0
- package/src/styles/pdf-field-overlay.css +40 -118
- package/src/styles/pdf-field.css +111 -0
- package/src/styles/variables.css +4 -0
- package/src/utils/data-model/index.ts +1 -0
- package/src/utils/data-model/resolve-fields-default-values.ts +19 -0
- package/src/utils/exposes-utils.ts +1 -0
- package/src/utils/field/__tests__/field-background-color.utils.test.ts +3 -3
- package/src/utils/field/__tests__/get-field-bounds-style.utils.test.ts +0 -1
- package/src/utils/field/__tests__/get-field-type-option.utils.test.ts +53 -0
- package/src/utils/field/__tests__/get-fillable-default-value.utils.test.ts +137 -0
- package/src/utils/field/field-background-color.utils.ts +1 -1
- package/src/utils/field/get-dropdown-field-items.utils.ts +14 -0
- package/src/utils/field/get-field-bounds-style.utils.ts +11 -11
- package/src/utils/field/get-field-type-option.utils.ts +16 -0
- package/src/utils/field/get-fillable-default-value.utils.ts +70 -0
- package/src/utils/field/get-navigable-fields.utils.ts +1 -1
- package/src/utils/field/index.ts +3 -0
- package/src/utils/pdf/__tests__/pinch-zoom.utils.test.ts +58 -0
- package/src/utils/pdf/index.ts +1 -0
- package/src/utils/pdf/pinch-zoom.utils.ts +30 -0
- 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
|
-
|
|
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
|
|
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(
|
|
159
|
-
|
|
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(
|
|
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
|
+
};
|
|
@@ -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> = ({
|
|
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
|
-
<
|
|
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"
|