@servicetitan/dte-pdf-editor 1.95.0 → 1.96.1
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 +87 -2
- package/dist/components/field-config-panel/advanced-settings.d.ts +2 -0
- package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
- package/dist/components/field-config-panel/advanced-settings.js +5 -4
- package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
- package/dist/components/field-config-panel/e-sign-options-modal.d.ts +3 -0
- package/dist/components/field-config-panel/e-sign-options-modal.d.ts.map +1 -1
- package/dist/components/field-config-panel/e-sign-options-modal.js +6 -4
- package/dist/components/field-config-panel/e-sign-options-modal.js.map +1 -1
- package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +14 -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-overlay.js +41 -2
- package/dist/components/field-config-panel/field-config-panel-overlay.js.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.d.ts +6 -0
- package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
- package/dist/components/field-config-panel/field-config-panel.js +17 -3
- package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
- package/dist/components/field-config-panel/formula-generator.d.ts +4 -0
- package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-generator.js +2 -2
- package/dist/components/field-config-panel/formula-generator.js.map +1 -1
- package/dist/components/field-config-panel/formula-modal.d.ts +5 -0
- package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-modal.js +13 -6
- package/dist/components/field-config-panel/formula-modal.js.map +1 -1
- package/dist/components/field-config-panel/formula-workspace.d.ts +2 -0
- package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-workspace.js +6 -4
- package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.d.ts +52 -1
- 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-view/pdf-view-calculated.d.ts +3 -1
- package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-calculated.js +14 -4
- package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -1
- package/dist/components/pdf-view/pdf-view.d.ts +18 -4
- package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view.js +2 -2
- package/dist/components/pdf-view/pdf-view.js.map +1 -1
- package/dist/constants/calculated.constants.d.ts +6 -0
- package/dist/constants/calculated.constants.d.ts.map +1 -1
- package/dist/constants/calculated.constants.js +6 -0
- package/dist/constants/calculated.constants.js.map +1 -1
- package/dist/hooks/usePinchZoom.d.ts.map +1 -1
- package/dist/hooks/usePinchZoom.js +1 -1
- package/dist/hooks/usePinchZoom.js.map +1 -1
- package/dist/interface/types.d.ts +37 -1
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js.map +1 -1
- package/dist/utils/formula/business-days.utils.d.ts +32 -0
- package/dist/utils/formula/business-days.utils.d.ts.map +1 -0
- package/dist/utils/formula/business-days.utils.js +148 -0
- package/dist/utils/formula/business-days.utils.js.map +1 -0
- package/dist/utils/formula/caret.utils.js +1 -1
- package/dist/utils/formula/caret.utils.js.map +1 -1
- package/dist/utils/formula/evaluate-formula.utils.d.ts +25 -6
- package/dist/utils/formula/evaluate-formula.utils.d.ts.map +1 -1
- package/dist/utils/formula/evaluate-formula.utils.js +61 -101
- package/dist/utils/formula/evaluate-formula.utils.js.map +1 -1
- package/dist/utils/formula/index.d.ts +1 -0
- package/dist/utils/formula/index.d.ts.map +1 -1
- package/dist/utils/formula/index.js +1 -0
- package/dist/utils/formula/index.js.map +1 -1
- package/dist/utils/formula/schedule-bindings.utils.d.ts +12 -0
- package/dist/utils/formula/schedule-bindings.utils.d.ts.map +1 -0
- package/dist/utils/formula/schedule-bindings.utils.js +17 -0
- package/dist/utils/formula/schedule-bindings.utils.js.map +1 -0
- package/dist/vendor/dte-shared/business-calendar-validation.utils.d.ts +22 -0
- package/dist/vendor/dte-shared/business-calendar-validation.utils.js +76 -0
- package/dist/vendor/dte-shared/components/business-calendar-section.d.ts +15 -0
- package/dist/vendor/dte-shared/components/business-calendar-section.js +13 -0
- package/dist/vendor/dte-shared/components/schedule-drawer.d.ts +16 -0
- package/dist/vendor/dte-shared/components/schedule-drawer.js +112 -0
- package/dist/vendor/dte-shared/components/schedule-picker.d.ts +8 -0
- package/dist/vendor/dte-shared/components/schedule-picker.js +88 -0
- package/dist/vendor/dte-shared/constants.d.ts +10 -0
- package/dist/vendor/dte-shared/constants.js +10 -0
- package/dist/vendor/dte-shared/index.d.ts +6 -0
- package/dist/vendor/dte-shared/index.js +5 -0
- package/dist/vendor/dte-shared/styles/schedule.css +19 -0
- package/dist/vendor/dte-shared/types.d.ts +67 -0
- package/dist/vendor/dte-shared/types.js +1 -0
- package/package.json +5 -2
- package/src/__tests__/field-types/date-fields.test.ts +29 -3
- package/src/__tests__/field-types/e-sign-fields.test.ts +2 -2
- package/src/__tests__/field-types/fillable-fields.test.ts +2 -2
- package/src/components/field-config-panel/advanced-settings.tsx +16 -3
- package/src/components/field-config-panel/e-sign-options-modal.tsx +13 -2
- package/src/components/field-config-panel/field-config-panel-overlay.tsx +97 -1
- package/src/components/field-config-panel/field-config-panel.tsx +28 -0
- package/src/components/field-config-panel/formula-generator.tsx +8 -1
- package/src/components/field-config-panel/formula-modal.tsx +19 -4
- package/src/components/field-config-panel/formula-workspace.tsx +31 -6
- package/src/components/pdf-editor/pdf-editor.tsx +82 -0
- package/src/components/pdf-view/pdf-view-calculated.tsx +28 -3
- package/src/components/pdf-view/pdf-view.tsx +23 -3
- package/src/constants/calculated.constants.ts +7 -0
- package/src/hooks/usePinchZoom.ts +1 -6
- package/src/interface/types.ts +47 -0
- package/src/utils/field/__tests__/field-background-color.utils.test.ts +8 -8
- package/src/utils/field/__tests__/get-field-bounds-style.utils.test.ts +16 -0
- package/src/utils/formula/__tests__/evaluate-formula.utils.test.ts +198 -16
- package/src/utils/formula/__tests__/schedule-bindings.utils.test.ts +73 -0
- package/src/utils/formula/business-days.utils.ts +188 -0
- package/src/utils/formula/caret.utils.ts +1 -1
- package/src/utils/formula/evaluate-formula.utils.ts +94 -114
- package/src/utils/formula/index.ts +1 -0
- package/src/utils/formula/schedule-bindings.utils.ts +27 -0
|
@@ -2,11 +2,16 @@ import { Flex } from '@servicetitan/anvil2';
|
|
|
2
2
|
import { FC, ReactNode, useCallback, useMemo, useRef } from 'react';
|
|
3
3
|
import { useFormFields, usePdfFieldDnD, usePdfFieldSelection } from '../../hooks';
|
|
4
4
|
import {
|
|
5
|
+
type BusinessCalendar,
|
|
6
|
+
type BusinessDaySchedule,
|
|
5
7
|
FieldTypeEnum,
|
|
8
|
+
type FieldScheduleBinding,
|
|
6
9
|
type FormFieldsByFormIdI,
|
|
7
10
|
type FormInfo,
|
|
11
|
+
type HolidayCatalog,
|
|
8
12
|
type PdfField,
|
|
9
13
|
type RecipientInfo,
|
|
14
|
+
type SaveScheduleRequest,
|
|
10
15
|
type SchemaObject,
|
|
11
16
|
} from '../../interface/types';
|
|
12
17
|
import { extractGroupedFieldsFromDataModel, mapColorsToRecipients } from '../../utils';
|
|
@@ -30,10 +35,61 @@ export interface PdfEditorProps {
|
|
|
30
35
|
hideConditionalLogic?: boolean;
|
|
31
36
|
forms?: FormInfo[];
|
|
32
37
|
zoomProps?: PdfCanvasProps['zoomProps'];
|
|
38
|
+
/**
|
|
39
|
+
* Holidays of the default business-day calendar, as `PdfView`'s `holidays`: the formula
|
|
40
|
+
* builder's preview of a date calculated field without its own calendar skips Saturday,
|
|
41
|
+
* Sunday and these days.
|
|
42
|
+
*/
|
|
43
|
+
holidays?: string[];
|
|
44
|
+
/** Calendars of the date calculated fields bound to a schedule, by field id, as `PdfView`'s. */
|
|
45
|
+
fieldCalendars?: Record<string, BusinessCalendar>;
|
|
46
|
+
/**
|
|
47
|
+
* Date calculated fields whose custom business-day exclusion toggle is on, and the schedule
|
|
48
|
+
* each uses (`PdfField.id` as `fieldKey`), as `PdfView`'s. Also drives the Formula Builder's
|
|
49
|
+
* toggle/picker for the selected field.
|
|
50
|
+
*/
|
|
51
|
+
scheduleBindings?: FieldScheduleBinding[];
|
|
52
|
+
/** The tenant's known holidays, for the schedule editor's checklist. */
|
|
53
|
+
holidayCatalog?: HolidayCatalog;
|
|
54
|
+
holidayCatalogLoading?: boolean;
|
|
55
|
+
holidayCatalogError?: string;
|
|
56
|
+
/** The schedules the tenant can pick from the Formula Builder's schedule picker. */
|
|
57
|
+
schedules?: BusinessDaySchedule[];
|
|
58
|
+
schedulesLoading?: boolean;
|
|
59
|
+
schedulesError?: string;
|
|
33
60
|
onFormSelect?: (
|
|
34
61
|
formIds: number[],
|
|
35
62
|
sendFormFields: (formFieldsByFormId: FormFieldsByFormIdI) => void,
|
|
36
63
|
) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Called with the complete desired `scheduleBindings` whenever the Formula Builder's toggle
|
|
66
|
+
* or schedule picker changes the selected field's binding (full replacement, mirroring DTE's
|
|
67
|
+
* `PUT api/holidays/templates/{id}/bindings`). `PdfEditor` does not persist this itself —
|
|
68
|
+
* the host is expected to save it (and the bindings are typically saved separately from the
|
|
69
|
+
* template's own fields).
|
|
70
|
+
*/
|
|
71
|
+
onScheduleBindingsChange?(bindings: FieldScheduleBinding[]): void;
|
|
72
|
+
/**
|
|
73
|
+
* Creates a tenant schedule from the "Create New Schedule" drawer. Always creates a new
|
|
74
|
+
* schedule — it never overwrites a selected one. On success the created schedule is
|
|
75
|
+
* auto-selected in the picker. A rejected promise's `message` is shown as the drawer's error
|
|
76
|
+
* (for example a duplicate-name conflict).
|
|
77
|
+
*/
|
|
78
|
+
onCreateSchedule?(request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
79
|
+
/**
|
|
80
|
+
* Replaces a tenant schedule's name and rules in place, from the picker's "Edit" action
|
|
81
|
+
* (DTE's `PUT api/holidays/schedules/{id}`). Applies from the next conversion of every
|
|
82
|
+
* template using it. On success the updated schedule stays selected.
|
|
83
|
+
*/
|
|
84
|
+
onUpdateSchedule?(id: number, request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
85
|
+
/**
|
|
86
|
+
* Permanently deletes a tenant schedule, from the picker's "Delete" action (DTE's
|
|
87
|
+
* `DELETE api/holidays/schedules/{id}`). Fields still bound to it fall back to the default
|
|
88
|
+
* schedule — `PdfEditor` selects the default schedule for the current field on success, but
|
|
89
|
+
* does not itself update other fields' `scheduleBindings`; the host should drop bindings to
|
|
90
|
+
* a deleted schedule when it next saves them.
|
|
91
|
+
*/
|
|
92
|
+
onDeleteSchedule?(id: number): Promise<void>;
|
|
37
93
|
onLoadSuccess?(numPages: number): void;
|
|
38
94
|
onFieldsChange(fields: PdfField[]): void;
|
|
39
95
|
}
|
|
@@ -42,17 +98,30 @@ export const PdfEditor: FC<PdfEditorProps> = ({
|
|
|
42
98
|
dataModel,
|
|
43
99
|
errorPlaceholder,
|
|
44
100
|
errors = {},
|
|
101
|
+
fieldCalendars,
|
|
45
102
|
fields = [],
|
|
46
103
|
forms,
|
|
47
104
|
hideConditionalLogic,
|
|
48
105
|
hideFields,
|
|
106
|
+
holidayCatalog,
|
|
107
|
+
holidayCatalogError,
|
|
108
|
+
holidayCatalogLoading,
|
|
109
|
+
holidays,
|
|
49
110
|
loading = false,
|
|
50
111
|
loadingPlaceholder,
|
|
112
|
+
onCreateSchedule,
|
|
113
|
+
onDeleteSchedule,
|
|
51
114
|
onFieldsChange,
|
|
52
115
|
onFormSelect,
|
|
53
116
|
onLoadSuccess,
|
|
117
|
+
onScheduleBindingsChange,
|
|
118
|
+
onUpdateSchedule,
|
|
54
119
|
pdfUrl,
|
|
55
120
|
recipients = [],
|
|
121
|
+
scheduleBindings,
|
|
122
|
+
schedules,
|
|
123
|
+
schedulesError,
|
|
124
|
+
schedulesLoading,
|
|
56
125
|
zoomProps,
|
|
57
126
|
zoomable = true,
|
|
58
127
|
}) => {
|
|
@@ -99,6 +168,19 @@ export const PdfEditor: FC<PdfEditorProps> = ({
|
|
|
99
168
|
recipients={recipients}
|
|
100
169
|
onDeselectField={pdfFieldSelection.deselectField}
|
|
101
170
|
hideConditionalLogic={hideConditionalLogic}
|
|
171
|
+
holidays={holidays}
|
|
172
|
+
fieldCalendars={fieldCalendars}
|
|
173
|
+
scheduleBindings={scheduleBindings}
|
|
174
|
+
onScheduleBindingsChange={onScheduleBindingsChange}
|
|
175
|
+
holidayCatalog={holidayCatalog}
|
|
176
|
+
holidayCatalogLoading={holidayCatalogLoading}
|
|
177
|
+
holidayCatalogError={holidayCatalogError}
|
|
178
|
+
schedules={schedules}
|
|
179
|
+
schedulesLoading={schedulesLoading}
|
|
180
|
+
schedulesError={schedulesError}
|
|
181
|
+
onCreateSchedule={onCreateSchedule}
|
|
182
|
+
onUpdateSchedule={onUpdateSchedule}
|
|
183
|
+
onDeleteSchedule={onDeleteSchedule}
|
|
102
184
|
onRequestFormFields={formFields.requestFormFieldsById}
|
|
103
185
|
/>
|
|
104
186
|
)}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { FC, useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type BusinessCalendar,
|
|
4
|
+
type DataModelValues,
|
|
5
|
+
type FieldScheduleBinding,
|
|
6
|
+
type PdfField,
|
|
7
|
+
type PdfViewMode,
|
|
8
|
+
} from '../../interface/types';
|
|
3
9
|
import {
|
|
4
10
|
evaluateFormula,
|
|
5
11
|
formatCalculatedResult,
|
|
12
|
+
getFieldDateCalcOptions,
|
|
6
13
|
getFieldTextClipClassName,
|
|
7
14
|
resolvePdfDataValues,
|
|
8
15
|
} from '../../utils';
|
|
@@ -11,18 +18,26 @@ interface PdfViewCalculatedProps {
|
|
|
11
18
|
field: PdfField;
|
|
12
19
|
data?: DataModelValues;
|
|
13
20
|
holidays?: string[];
|
|
21
|
+
fieldCalendars?: Record<string, BusinessCalendar>;
|
|
22
|
+
scheduleBindings?: FieldScheduleBinding[];
|
|
14
23
|
viewMode?: PdfViewMode;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({
|
|
18
27
|
data,
|
|
19
28
|
field,
|
|
29
|
+
fieldCalendars,
|
|
20
30
|
holidays,
|
|
31
|
+
scheduleBindings,
|
|
21
32
|
viewMode,
|
|
22
33
|
}) => {
|
|
23
34
|
const displayValue = useMemo(() => {
|
|
24
35
|
if (field.formula?.tokens?.length) {
|
|
25
|
-
const value = evaluateFormula(
|
|
36
|
+
const value = evaluateFormula(
|
|
37
|
+
field.formula,
|
|
38
|
+
data,
|
|
39
|
+
getFieldDateCalcOptions(field.id, scheduleBindings, holidays, fieldCalendars),
|
|
40
|
+
);
|
|
26
41
|
if (value === null) {
|
|
27
42
|
return '';
|
|
28
43
|
}
|
|
@@ -32,7 +47,17 @@ export const PdfViewCalculated: FC<PdfViewCalculatedProps> = ({
|
|
|
32
47
|
return resolvePdfDataValues(data, field.path) ?? '';
|
|
33
48
|
}
|
|
34
49
|
return field.label ?? '';
|
|
35
|
-
}, [
|
|
50
|
+
}, [
|
|
51
|
+
data,
|
|
52
|
+
field.formula,
|
|
53
|
+
field.formulaFormat,
|
|
54
|
+
field.id,
|
|
55
|
+
field.label,
|
|
56
|
+
field.path,
|
|
57
|
+
fieldCalendars,
|
|
58
|
+
holidays,
|
|
59
|
+
scheduleBindings,
|
|
60
|
+
]);
|
|
36
61
|
|
|
37
62
|
const isPreview = viewMode === 'preview';
|
|
38
63
|
const shouldShowPreviewFallback = isPreview && displayValue === '';
|
|
@@ -3,8 +3,10 @@ import { FC, ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react
|
|
|
3
3
|
import { BASE_PAGE_WIDTH } from '../../constants';
|
|
4
4
|
import { usePinchZoom, useZoom } from '../../hooks';
|
|
5
5
|
import {
|
|
6
|
+
type BusinessCalendar,
|
|
6
7
|
type DataChangePayload,
|
|
7
8
|
type DataModelValues,
|
|
9
|
+
type FieldScheduleBinding,
|
|
8
10
|
FieldTypeEnum,
|
|
9
11
|
type PdfField,
|
|
10
12
|
type PdfViewMode,
|
|
@@ -47,11 +49,25 @@ export interface PdfViewProps {
|
|
|
47
49
|
*/
|
|
48
50
|
makeFillableFieldsReadonly?: boolean;
|
|
49
51
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* when adding/subtracting days or computing date
|
|
52
|
+
* Holidays of the default business-day calendar, as date strings (`'2026-07-03'`,
|
|
53
|
+
* `'2026-03-17T00:00:00Z'`). Date calculated fields without their own calendar skip
|
|
54
|
+
* Saturday, Sunday and these days when adding/subtracting days or computing date
|
|
55
|
+
* differences, within the current year ± 1.
|
|
53
56
|
*/
|
|
54
57
|
holidays?: string[];
|
|
58
|
+
/**
|
|
59
|
+
* Calendars of the date calculated fields bound to a business-day schedule, by field id
|
|
60
|
+
* (`PdfField.id`), as DTE's `POST api/holidays/calendar` resolves them. Listed fields use
|
|
61
|
+
* only their calendar; the others use the default one (`holidays`).
|
|
62
|
+
*/
|
|
63
|
+
fieldCalendars?: Record<string, BusinessCalendar>;
|
|
64
|
+
/**
|
|
65
|
+
* Date calculated fields whose custom business-day exclusion toggle is on, and the schedule
|
|
66
|
+
* each uses (DTE's bindings, `PdfField.id` as `fieldKey`). A field with no entry here uses
|
|
67
|
+
* plain calendar-day arithmetic — no weekend or holiday skipping — regardless of `holidays`
|
|
68
|
+
* or `fieldCalendars`.
|
|
69
|
+
*/
|
|
70
|
+
scheduleBindings?: FieldScheduleBinding[];
|
|
55
71
|
/**
|
|
56
72
|
* fillingBy defines the list of recipient names
|
|
57
73
|
* who are allowed to fill this field.
|
|
@@ -71,6 +87,7 @@ export const PdfView: FC<PdfViewProps> = ({
|
|
|
71
87
|
enableInputNavigation = true,
|
|
72
88
|
errorPlaceholder,
|
|
73
89
|
errors = {},
|
|
90
|
+
fieldCalendars,
|
|
74
91
|
fields,
|
|
75
92
|
fillingBy,
|
|
76
93
|
holidays,
|
|
@@ -81,6 +98,7 @@ export const PdfView: FC<PdfViewProps> = ({
|
|
|
81
98
|
onLoadSuccess,
|
|
82
99
|
pdfUrl,
|
|
83
100
|
recipients,
|
|
101
|
+
scheduleBindings,
|
|
84
102
|
viewMode,
|
|
85
103
|
zoomProps,
|
|
86
104
|
zoomable = true,
|
|
@@ -280,6 +298,8 @@ export const PdfView: FC<PdfViewProps> = ({
|
|
|
280
298
|
field={field}
|
|
281
299
|
data={viewMode === 'preview' ? previewData : data}
|
|
282
300
|
holidays={holidays}
|
|
301
|
+
fieldCalendars={fieldCalendars}
|
|
302
|
+
scheduleBindings={scheduleBindings}
|
|
283
303
|
viewMode={viewMode}
|
|
284
304
|
/>
|
|
285
305
|
)}
|
|
@@ -4,4 +4,11 @@ export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY';
|
|
|
4
4
|
|
|
5
5
|
export const MAX_DATE_CALC_DAYS = 365;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Years either side of the current one that the default business-day calendar covers, as DTE's
|
|
9
|
+
* (`HolidayCheckService.HolidayYearsWindow`): outside them its holidays are unknown, so date
|
|
10
|
+
* arithmetic is blank.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_CALENDAR_YEARS = 1;
|
|
13
|
+
|
|
7
14
|
export const MAX_DATE_DISPLAY_RANGE_YEARS = 2;
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { RefObject, useEffect, useRef } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
clampZoom,
|
|
4
|
-
getPinchZoomValue,
|
|
5
|
-
getTouchDistance,
|
|
6
|
-
getWheelZoomFactor,
|
|
7
|
-
} from '../utils';
|
|
2
|
+
import { clampZoom, getPinchZoomValue, getTouchDistance, getWheelZoomFactor } from '../utils';
|
|
8
3
|
|
|
9
4
|
interface UsePinchZoomOptions {
|
|
10
5
|
enabled: boolean;
|
package/src/interface/types.ts
CHANGED
|
@@ -86,6 +86,14 @@ export interface ESignDateSignedOffsetSettings {
|
|
|
86
86
|
rules:
|
|
87
87
|
| [ESignDateSignedOffsetSettingsFinalRule]
|
|
88
88
|
| [...ESignDateSignedOffsetSettingsRule[], ESignDateSignedOffsetSettingsFinalRule];
|
|
89
|
+
/**
|
|
90
|
+
* Whether the day offset skips weekends/holidays per a business-day schedule, same toggle as a
|
|
91
|
+
* calculated field's (`FieldScheduleBinding`) but stored here instead, since - like `rules` -
|
|
92
|
+
* it applies to every `dateSignedOffset` field in the document, not one field at a time.
|
|
93
|
+
*/
|
|
94
|
+
businessCalendarEnabled?: boolean;
|
|
95
|
+
/** The schedule to use while `businessCalendarEnabled` is on; undefined for the default schedule. */
|
|
96
|
+
scheduleId?: number;
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
export interface FillableDropdownItem {
|
|
@@ -246,6 +254,45 @@ export interface StructuredFormula {
|
|
|
246
254
|
tokens: FormulaToken[];
|
|
247
255
|
}
|
|
248
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Days a date calculated field skips, as DTE resolves a business-day schedule
|
|
259
|
+
* (`POST api/holidays/calendar`). Date arithmetic outside `firstYear`–`lastYear` is blank.
|
|
260
|
+
*/
|
|
261
|
+
export interface BusinessCalendar {
|
|
262
|
+
/** Excluded weekend days, as `Date.getUTCDay()` numbers them: 0 = Sunday, 6 = Saturday. */
|
|
263
|
+
weekendDays: number[];
|
|
264
|
+
/** Holiday dates, already moved to their observed days, as `YYYY-MM-DD`. */
|
|
265
|
+
holidays: string[];
|
|
266
|
+
firstYear: number;
|
|
267
|
+
lastYear: number;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Re-exported from `@servicetitan/dte-shared` so existing imports from this path keep
|
|
272
|
+
* working: the schedule editor UI (`ScheduleDrawer`/`SchedulePicker`) is shared with
|
|
273
|
+
* `@servicetitan/dte-unlayer` and lives there now, these types are its domain model.
|
|
274
|
+
*/
|
|
275
|
+
export type {
|
|
276
|
+
BusinessCalendarRules,
|
|
277
|
+
BusinessDaySchedule,
|
|
278
|
+
HolidayCatalog,
|
|
279
|
+
KnownHoliday,
|
|
280
|
+
SaveScheduleRequest,
|
|
281
|
+
WeekendDay,
|
|
282
|
+
} from '@servicetitan/dte-shared';
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A date calculated field whose custom business-day exclusion toggle is on, and the schedule it
|
|
286
|
+
* uses (DTE's bindings, full-replace via `PUT api/holidays/templates/{id}/bindings`). A field with
|
|
287
|
+
* no entry here uses the default calendar with the toggle off (plain calendar-day arithmetic).
|
|
288
|
+
*/
|
|
289
|
+
export interface FieldScheduleBinding {
|
|
290
|
+
/** `PdfField.id`. */
|
|
291
|
+
fieldKey: string;
|
|
292
|
+
/** One of the tenant's schedules, or undefined for the default schedule. */
|
|
293
|
+
scheduleId?: number;
|
|
294
|
+
}
|
|
295
|
+
|
|
249
296
|
/** Format options for a calculated field with a numeric result (number, currency, percent) */
|
|
250
297
|
export interface CalculatedFormatForNumber {
|
|
251
298
|
resultType: 'number' | 'currency' | 'percent';
|
|
@@ -8,19 +8,19 @@ describe('getFieldColor', () => {
|
|
|
8
8
|
expect(subject('signer1', { signer1: '#ff0000', signer2: '#00ff00' })).toBe('#ff0000');
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
test('returns
|
|
12
|
-
expect(subject(undefined, { signer1: '#ff0000' })).
|
|
11
|
+
test('returns undefined when recipient is undefined', () => {
|
|
12
|
+
expect(subject(undefined, { signer1: '#ff0000' })).toBeUndefined();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
test('returns
|
|
16
|
-
expect(subject('', { signer1: '#ff0000' })).
|
|
15
|
+
test('returns undefined when recipient is empty string', () => {
|
|
16
|
+
expect(subject('', { signer1: '#ff0000' })).toBeUndefined();
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test('returns
|
|
20
|
-
expect(subject('unknown', { signer1: '#ff0000' })).
|
|
19
|
+
test('returns undefined when recipient is not in the color map', () => {
|
|
20
|
+
expect(subject('unknown', { signer1: '#ff0000' })).toBeUndefined();
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
test('returns
|
|
24
|
-
expect(subject('signer1', {})).
|
|
23
|
+
test('returns undefined when color map is empty', () => {
|
|
24
|
+
expect(subject('signer1', {})).toBeUndefined();
|
|
25
25
|
});
|
|
26
26
|
});
|
|
@@ -38,6 +38,22 @@ describe('getFieldBoundsStyle', () => {
|
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test('sets the recipient color variables when a color is given', () => {
|
|
42
|
+
expect(subject(buildField(), { color: '#ff0000' })).toMatchObject({
|
|
43
|
+
'--pdf-field-color': '#ff0000',
|
|
44
|
+
'--pdf-field-color-dark': '#ff0000',
|
|
45
|
+
'--pdf-field-opacity-color': '#ff00001f',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('leaves the color variables unset without a color, so the stylesheet defaults apply', () => {
|
|
50
|
+
const style = subject(buildField(), { color: undefined });
|
|
51
|
+
|
|
52
|
+
expect(style).not.toHaveProperty('--pdf-field-color');
|
|
53
|
+
expect(style).not.toHaveProperty('--pdf-field-color-dark');
|
|
54
|
+
expect(style).not.toHaveProperty('--pdf-field-opacity-color');
|
|
55
|
+
});
|
|
56
|
+
|
|
41
57
|
test('converts font size from points to editor pixels', () => {
|
|
42
58
|
const style = subject(buildField({ styles: { fontSize: 12 } }));
|
|
43
59
|
expect(style.fontSize).toBe(`${pointsToPixels(12)}px`);
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import {
|
|
4
|
+
type BusinessCalendar,
|
|
5
|
+
type DataModelValues,
|
|
6
|
+
type FieldScheduleBinding,
|
|
7
|
+
type StructuredFormula,
|
|
8
|
+
} from '../../../interface/types';
|
|
9
|
+
import {
|
|
10
|
+
evaluateFormula,
|
|
11
|
+
getFieldDateCalcOptions,
|
|
12
|
+
hasDivisionByZero,
|
|
13
|
+
valueToNumber,
|
|
14
|
+
} from '../evaluate-formula.utils';
|
|
3
15
|
|
|
4
16
|
const f = (tokens: StructuredFormula['tokens']): StructuredFormula => ({ tokens });
|
|
5
17
|
|
|
@@ -35,7 +47,7 @@ describe('evaluateFormula', () => {
|
|
|
35
47
|
formula: StructuredFormula | undefined | null,
|
|
36
48
|
data: DataModelValues | undefined,
|
|
37
49
|
holidays?: string[],
|
|
38
|
-
) => evaluateFormula(formula, data, holidays);
|
|
50
|
+
) => evaluateFormula(formula, data, { enabled: true, holidays });
|
|
39
51
|
|
|
40
52
|
describe('when formula is empty', () => {
|
|
41
53
|
test.each([
|
|
@@ -180,6 +192,18 @@ describe('evaluateFormula', () => {
|
|
|
180
192
|
});
|
|
181
193
|
|
|
182
194
|
describe('with date fields', () => {
|
|
195
|
+
// The default calendar covers the current year ± 1.
|
|
196
|
+
beforeAll(() => {
|
|
197
|
+
jest.useFakeTimers({ now: new Date('2024-06-01T00:00:00Z') });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
afterAll(() => {
|
|
201
|
+
jest.useRealTimers();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const dateKey = (result: number | null) =>
|
|
205
|
+
result === null ? null : new Date(result).toISOString().slice(0, 10);
|
|
206
|
+
|
|
183
207
|
test('adds business days to a date field (skipping weekend)', () => {
|
|
184
208
|
// Friday 2024-01-05 + 1 business day = Monday 2024-01-08
|
|
185
209
|
const formula = f([
|
|
@@ -187,22 +211,18 @@ describe('evaluateFormula', () => {
|
|
|
187
211
|
{ type: 'operator', value: '+' },
|
|
188
212
|
{ type: 'number', value: '1' },
|
|
189
213
|
]);
|
|
190
|
-
|
|
191
|
-
expect(result).not.toBeNull();
|
|
192
|
-
const date = new Date(result as number);
|
|
193
|
-
expect(date.getUTCFullYear()).toBe(2024);
|
|
194
|
-
expect(date.getUTCMonth()).toBe(0);
|
|
195
|
-
expect(date.getUTCDate()).toBe(8);
|
|
214
|
+
expect(dateKey(subject(formula, { d: '2024-01-05' }))).toBe('2024-01-08');
|
|
196
215
|
});
|
|
197
216
|
|
|
198
|
-
test('counts
|
|
199
|
-
//
|
|
217
|
+
test('counts D1 - D2 from D2 to D1, positive when D1 is later', () => {
|
|
218
|
+
// end - start traverses Mon 2024-01-01 → Fri 2024-01-05 = 4 business days
|
|
200
219
|
const formula = f([
|
|
201
|
-
{ type: 'field', path: 'start', fieldType: 'date' },
|
|
202
|
-
{ type: 'operator', value: '-' },
|
|
203
220
|
{ type: 'field', path: 'end', fieldType: 'date' },
|
|
221
|
+
{ type: 'operator', value: '-' },
|
|
222
|
+
{ type: 'field', path: 'start', fieldType: 'date' },
|
|
204
223
|
]);
|
|
205
224
|
expect(subject(formula, { start: '2024-01-01', end: '2024-01-05' })).toBe(4);
|
|
225
|
+
expect(subject(formula, { start: '2024-01-05', end: '2024-01-01' })).toBe(-4);
|
|
206
226
|
});
|
|
207
227
|
|
|
208
228
|
test('skips holidays when adding business days', () => {
|
|
@@ -212,9 +232,43 @@ describe('evaluateFormula', () => {
|
|
|
212
232
|
{ type: 'operator', value: '+' },
|
|
213
233
|
{ type: 'number', value: '1' },
|
|
214
234
|
]);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
235
|
+
expect(dateKey(subject(formula, { d: '2024-01-08' }, ['2024-01-09']))).toBe(
|
|
236
|
+
'2024-01-10',
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('applies chained offsets one step at a time', () => {
|
|
241
|
+
// D - 1 + 2 from Sat 2024-10-05: Fri 10-04, then Tue 10-08
|
|
242
|
+
const formula = f([
|
|
243
|
+
{ type: 'field', path: 'd', fieldType: 'date' },
|
|
244
|
+
{ type: 'operator', value: '-' },
|
|
245
|
+
{ type: 'number', value: '1' },
|
|
246
|
+
{ type: 'operator', value: '+' },
|
|
247
|
+
{ type: 'number', value: '2' },
|
|
248
|
+
]);
|
|
249
|
+
expect(dateKey(subject(formula, { d: '2024-10-05' }))).toBe('2024-10-08');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test.each([
|
|
253
|
+
['a fractional offset', { d: '2024-09-30', n: '2.5' }],
|
|
254
|
+
['a start outside the current year ± 1', { d: '2022-06-01', n: '1' }],
|
|
255
|
+
['a result outside the current year ± 1', { d: '2025-12-31', n: '1' }],
|
|
256
|
+
])('returns null for %s', (_, data) => {
|
|
257
|
+
const formula = f([
|
|
258
|
+
{ type: 'field', path: 'd', fieldType: 'date' },
|
|
259
|
+
{ type: 'operator', value: '+' },
|
|
260
|
+
{ type: 'field', path: 'n', fieldType: 'number' },
|
|
261
|
+
]);
|
|
262
|
+
expect(subject(formula, data)).toBeNull();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('returns null for a difference over 365 days', () => {
|
|
266
|
+
const formula = f([
|
|
267
|
+
{ type: 'field', path: 'end', fieldType: 'date' },
|
|
268
|
+
{ type: 'operator', value: '-' },
|
|
269
|
+
{ type: 'field', path: 'start', fieldType: 'date' },
|
|
270
|
+
]);
|
|
271
|
+
expect(subject(formula, { start: '2024-01-02', end: '2025-01-05' })).toBeNull();
|
|
218
272
|
});
|
|
219
273
|
|
|
220
274
|
test('returns null when date field has invalid value', () => {
|
|
@@ -225,5 +279,133 @@ describe('evaluateFormula', () => {
|
|
|
225
279
|
]);
|
|
226
280
|
expect(subject(formula, { d: 'not-a-date' })).toBeNull();
|
|
227
281
|
});
|
|
282
|
+
|
|
283
|
+
test('uses only the field calendar when one is passed', () => {
|
|
284
|
+
// Fri 2026-07-10 + 1 = Sat 2026-07-11 with a Sunday-only calendar, outside today's years.
|
|
285
|
+
const formula = f([
|
|
286
|
+
{ type: 'field', path: 'd', fieldType: 'date' },
|
|
287
|
+
{ type: 'operator', value: '+' },
|
|
288
|
+
{ type: 'number', value: '1' },
|
|
289
|
+
]);
|
|
290
|
+
const calendar: BusinessCalendar = {
|
|
291
|
+
weekendDays: [0],
|
|
292
|
+
holidays: [],
|
|
293
|
+
firstYear: 2026,
|
|
294
|
+
lastYear: 2026,
|
|
295
|
+
};
|
|
296
|
+
const result = evaluateFormula(formula, { d: '2026-07-10' }, { enabled: true, calendar });
|
|
297
|
+
expect(dateKey(result)).toBe('2026-07-11');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('uses plain calendar-day arithmetic (no skipping) when the toggle is disabled', () => {
|
|
301
|
+
// Friday 2024-01-05 + 1 day = Saturday 2024-01-06, unskipped, regardless of holidays passed.
|
|
302
|
+
const formula = f([
|
|
303
|
+
{ type: 'field', path: 'd', fieldType: 'date' },
|
|
304
|
+
{ type: 'operator', value: '+' },
|
|
305
|
+
{ type: 'number', value: '1' },
|
|
306
|
+
]);
|
|
307
|
+
const result = evaluateFormula(
|
|
308
|
+
formula,
|
|
309
|
+
{ d: '2024-01-05' },
|
|
310
|
+
{ enabled: false, holidays: ['2024-01-06'] },
|
|
311
|
+
);
|
|
312
|
+
expect(dateKey(result)).toBe('2024-01-06');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('uses plain calendar-day diff (no skipping) when the toggle is disabled', () => {
|
|
316
|
+
// Plain calendar days between Mon 2024-01-01 and Fri 2024-01-05 is 4, same as business days
|
|
317
|
+
// here, so use a span that differs: Fri 2024-01-05 -> Mon 2024-01-08 is 3 calendar days but
|
|
318
|
+
// only 1 business day.
|
|
319
|
+
const formula = f([
|
|
320
|
+
{ type: 'field', path: 'end', fieldType: 'date' },
|
|
321
|
+
{ type: 'operator', value: '-' },
|
|
322
|
+
{ type: 'field', path: 'start', fieldType: 'date' },
|
|
323
|
+
]);
|
|
324
|
+
const result = evaluateFormula(
|
|
325
|
+
formula,
|
|
326
|
+
{ start: '2024-01-05', end: '2024-01-08' },
|
|
327
|
+
{ enabled: false },
|
|
328
|
+
);
|
|
329
|
+
expect(result).toBe(3);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('getFieldDateCalcOptions', () => {
|
|
335
|
+
const calendar: BusinessCalendar = {
|
|
336
|
+
weekendDays: [0],
|
|
337
|
+
holidays: [],
|
|
338
|
+
firstYear: 2026,
|
|
339
|
+
lastYear: 2027,
|
|
340
|
+
};
|
|
341
|
+
const scheduleBindings: FieldScheduleBinding[] = [{ fieldKey: 'a' }, { fieldKey: 'b' }];
|
|
342
|
+
|
|
343
|
+
test.each([
|
|
344
|
+
['the field calendar for a bound, listed field', 'a', { enabled: true, calendar }],
|
|
345
|
+
[
|
|
346
|
+
'the default holidays for a bound field with no calendar entry',
|
|
347
|
+
'b',
|
|
348
|
+
{ enabled: true, holidays: ['2026-07-03'] },
|
|
349
|
+
],
|
|
350
|
+
['disabled for an unbound field', 'c', { enabled: false }],
|
|
351
|
+
['disabled without a field id', undefined, { enabled: false }],
|
|
352
|
+
])('returns %s', (_, fieldId, expected) => {
|
|
353
|
+
expect(
|
|
354
|
+
getFieldDateCalcOptions(fieldId, scheduleBindings, ['2026-07-03'], { a: calendar }),
|
|
355
|
+
).toEqual(expected);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('business-day parity fixture', () => {
|
|
360
|
+
// DTE's shared business-day cases; packages/unlayer's date-calc test checks the copy's hash.
|
|
361
|
+
const fixture: {
|
|
362
|
+
calendars: Record<string, BusinessCalendar>;
|
|
363
|
+
cases: {
|
|
364
|
+
name: string;
|
|
365
|
+
calendar: string;
|
|
366
|
+
start: string;
|
|
367
|
+
steps?: number[];
|
|
368
|
+
end?: string | null;
|
|
369
|
+
expected: string | number | null;
|
|
370
|
+
}[];
|
|
371
|
+
} = JSON.parse(
|
|
372
|
+
readFileSync(
|
|
373
|
+
resolve(__dirname, '../../../../../../tests/fixtures/business-day-cases.json'),
|
|
374
|
+
'utf-8',
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
type FixtureCase = (typeof fixture.cases)[number];
|
|
379
|
+
|
|
380
|
+
// The case as a BYOD formula: `start ± step ± step …`, or `end - start`.
|
|
381
|
+
const toFormula = (testCase: FixtureCase): StructuredFormula => {
|
|
382
|
+
if ('end' in testCase) {
|
|
383
|
+
return f([
|
|
384
|
+
{ type: 'field', path: 'end', fieldType: 'date' },
|
|
385
|
+
{ type: 'operator', value: '-' },
|
|
386
|
+
{ type: 'field', path: 'start', fieldType: 'date' },
|
|
387
|
+
]);
|
|
388
|
+
}
|
|
389
|
+
return f([
|
|
390
|
+
{ type: 'field', path: 'start', fieldType: 'date' },
|
|
391
|
+
...(testCase.steps ?? []).flatMap((step): StructuredFormula['tokens'] => [
|
|
392
|
+
{ type: 'operator', value: step < 0 ? '-' : '+' },
|
|
393
|
+
{ type: 'number', value: String(Math.abs(step)) },
|
|
394
|
+
]),
|
|
395
|
+
]);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
test.each(fixture.cases.map(c => [c.name, c] as const))('%s', (_, testCase) => {
|
|
399
|
+
const data = { start: testCase.start, end: testCase.end ?? '' };
|
|
400
|
+
const result = evaluateFormula(toFormula(testCase), data, {
|
|
401
|
+
enabled: true,
|
|
402
|
+
calendar: fixture.calendars[testCase.calendar],
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const actual =
|
|
406
|
+
result === null || 'end' in testCase
|
|
407
|
+
? result
|
|
408
|
+
: new Date(result).toISOString().slice(0, 10);
|
|
409
|
+
expect(actual).toBe(testCase.expected);
|
|
228
410
|
});
|
|
229
411
|
});
|