@servicetitan/dte-pdf-editor 1.94.0 → 1.96.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 +121 -4
- 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-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +4 -6
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.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-fillable.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.js +27 -4
- package/dist/components/pdf-view/pdf-view-fillable.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/components/shared/char-limit-indicator.d.ts +8 -0
- package/dist/components/shared/char-limit-indicator.d.ts.map +1 -0
- package/dist/components/shared/char-limit-indicator.js +9 -0
- package/dist/components/shared/char-limit-indicator.js.map +1 -0
- package/dist/components/shared/index.d.ts +1 -0
- package/dist/components/shared/index.d.ts.map +1 -1
- package/dist/components/shared/index.js +1 -0
- package/dist/components/shared/index.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/styles/pdf-field-overlay.css +9 -2
- package/dist/styles/pdf-field.css +22 -1
- package/dist/utils/field/field-text-box.utils.d.ts +31 -0
- package/dist/utils/field/field-text-box.utils.d.ts.map +1 -0
- package/dist/utils/field/field-text-box.utils.js +102 -0
- package/dist/utils/field/field-text-box.utils.js.map +1 -0
- package/dist/utils/field/fit-field-text.utils.d.ts +13 -0
- package/dist/utils/field/fit-field-text.utils.d.ts.map +1 -0
- package/dist/utils/field/fit-field-text.utils.js +44 -0
- package/dist/utils/field/fit-field-text.utils.js.map +1 -0
- package/dist/utils/field/get-field-max-length.utils.d.ts +38 -0
- package/dist/utils/field/get-field-max-length.utils.d.ts.map +1 -0
- package/dist/utils/field/get-field-max-length.utils.js +166 -0
- package/dist/utils/field/get-field-max-length.utils.js.map +1 -0
- package/dist/utils/field/index.d.ts +2 -0
- package/dist/utils/field/index.d.ts.map +1 -1
- package/dist/utils/field/index.js +2 -0
- package/dist/utils/field/index.js.map +1 -1
- package/dist/utils/field/text-wrap.utils.d.ts +26 -0
- package/dist/utils/field/text-wrap.utils.d.ts.map +1 -0
- package/dist/utils/field/text-wrap.utils.js +53 -0
- package/dist/utils/field/text-wrap.utils.js.map +1 -0
- 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/package.json +2 -1
- 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-fields-overlay/pdf-overlay-field-fillable.tsx +41 -28
- package/src/components/pdf-view/pdf-view-calculated.tsx +28 -3
- package/src/components/pdf-view/pdf-view-fillable.tsx +82 -36
- package/src/components/pdf-view/pdf-view.tsx +23 -3
- package/src/components/shared/char-limit-indicator.tsx +23 -0
- package/src/components/shared/index.ts +1 -0
- package/src/constants/calculated.constants.ts +7 -0
- package/src/hooks/usePinchZoom.ts +1 -6
- package/src/interface/types.ts +47 -0
- package/src/styles/pdf-field-overlay.css +9 -2
- package/src/styles/pdf-field.css +22 -1
- package/src/utils/field/__tests__/field-background-color.utils.test.ts +8 -8
- package/src/utils/field/__tests__/fit-field-text.utils.test.ts +224 -0
- package/src/utils/field/__tests__/get-field-bounds-style.utils.test.ts +16 -0
- package/src/utils/field/__tests__/get-field-max-length.utils.test.ts +751 -0
- package/src/utils/field/__tests__/text-wrap.utils.test.ts +181 -0
- package/src/utils/field/field-text-box.utils.ts +143 -0
- package/src/utils/field/fit-field-text.utils.ts +61 -0
- package/src/utils/field/get-field-max-length.utils.ts +211 -0
- package/src/utils/field/index.ts +2 -0
- package/src/utils/field/text-wrap.utils.ts +78 -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
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { Button, Flex, Text } from '@servicetitan/anvil2';
|
|
2
2
|
import IconClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
|
|
3
|
-
import {
|
|
3
|
+
import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
4
|
+
import { FC, useCallback, useMemo } from 'react';
|
|
4
5
|
import {
|
|
6
|
+
type BusinessCalendar,
|
|
7
|
+
type BusinessDaySchedule,
|
|
8
|
+
type FieldScheduleBinding,
|
|
5
9
|
type FormFieldsByFormIdI,
|
|
6
10
|
type FormInfo,
|
|
11
|
+
type HolidayCatalog,
|
|
7
12
|
type PdfField,
|
|
8
13
|
type RecipientInfo,
|
|
14
|
+
type SaveScheduleRequest,
|
|
9
15
|
type SchemaObject,
|
|
10
16
|
} from '../../interface/types';
|
|
17
|
+
import { getFieldDateCalcOptions, setFieldSchedule, setFieldScheduleEnabled } from '../../utils';
|
|
11
18
|
import { FieldConfigPanel } from './field-config-panel';
|
|
12
19
|
|
|
13
20
|
interface FieldConfigPanelOverlayProps {
|
|
@@ -19,6 +26,19 @@ interface FieldConfigPanelOverlayProps {
|
|
|
19
26
|
selectedField: PdfField;
|
|
20
27
|
recipients: RecipientInfo[];
|
|
21
28
|
hideConditionalLogic?: boolean;
|
|
29
|
+
holidays?: string[];
|
|
30
|
+
fieldCalendars?: Record<string, BusinessCalendar>;
|
|
31
|
+
scheduleBindings?: FieldScheduleBinding[];
|
|
32
|
+
holidayCatalog?: HolidayCatalog;
|
|
33
|
+
holidayCatalogLoading?: boolean;
|
|
34
|
+
holidayCatalogError?: string;
|
|
35
|
+
schedules?: BusinessDaySchedule[];
|
|
36
|
+
schedulesLoading?: boolean;
|
|
37
|
+
schedulesError?: string;
|
|
38
|
+
onScheduleBindingsChange?(bindings: FieldScheduleBinding[]): void;
|
|
39
|
+
onCreateSchedule?(request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
40
|
+
onUpdateSchedule?(id: number, request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
41
|
+
onDeleteSchedule?(id: number): Promise<void>;
|
|
22
42
|
onFieldConfigChange(updates: Partial<PdfField>): void;
|
|
23
43
|
onMatchingFieldsConfigChange?(
|
|
24
44
|
predicate: (field: PdfField) => boolean,
|
|
@@ -32,18 +52,92 @@ interface FieldConfigPanelOverlayProps {
|
|
|
32
52
|
export const FieldConfigPanelOverlay: FC<FieldConfigPanelOverlayProps> = ({
|
|
33
53
|
dataModel,
|
|
34
54
|
documentFields = [],
|
|
55
|
+
fieldCalendars,
|
|
35
56
|
formFieldsByFormId,
|
|
36
57
|
forms,
|
|
37
58
|
formulaLoadingFormId,
|
|
38
59
|
hideConditionalLogic,
|
|
60
|
+
holidayCatalog,
|
|
61
|
+
holidayCatalogError,
|
|
62
|
+
holidayCatalogLoading,
|
|
63
|
+
holidays,
|
|
64
|
+
onCreateSchedule,
|
|
39
65
|
onDeleteField,
|
|
66
|
+
onDeleteSchedule,
|
|
40
67
|
onDeselectField,
|
|
41
68
|
onFieldConfigChange,
|
|
42
69
|
onMatchingFieldsConfigChange,
|
|
43
70
|
onRequestFormFields,
|
|
71
|
+
onScheduleBindingsChange,
|
|
72
|
+
onUpdateSchedule,
|
|
44
73
|
recipients,
|
|
74
|
+
scheduleBindings,
|
|
75
|
+
schedules,
|
|
76
|
+
schedulesError,
|
|
77
|
+
schedulesLoading,
|
|
45
78
|
selectedField,
|
|
46
79
|
}) => {
|
|
80
|
+
const dateCalcOptions = useMemo(
|
|
81
|
+
() => getFieldDateCalcOptions(selectedField.id, scheduleBindings, holidays, fieldCalendars),
|
|
82
|
+
[selectedField.id, scheduleBindings, holidays, fieldCalendars],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const selectedFieldId = selectedField.id;
|
|
86
|
+
const selectedBinding = useMemo(
|
|
87
|
+
() => scheduleBindings?.find(b => b.fieldKey === selectedFieldId),
|
|
88
|
+
[scheduleBindings, selectedFieldId],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const handleToggle = useCallback(
|
|
92
|
+
(enabled: boolean) => {
|
|
93
|
+
onScheduleBindingsChange?.(
|
|
94
|
+
setFieldScheduleEnabled(scheduleBindings ?? [], selectedFieldId, enabled),
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
[scheduleBindings, selectedFieldId, onScheduleBindingsChange],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const handleSelectSchedule = useCallback(
|
|
101
|
+
(scheduleId: number | undefined) => {
|
|
102
|
+
onScheduleBindingsChange?.(
|
|
103
|
+
setFieldSchedule(scheduleBindings ?? [], selectedFieldId, scheduleId),
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
[scheduleBindings, selectedFieldId, onScheduleBindingsChange],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const businessCalendar: ScheduleEditorContext = useMemo(
|
|
110
|
+
() => ({
|
|
111
|
+
enabled: !!selectedBinding,
|
|
112
|
+
selectedScheduleId: selectedBinding?.scheduleId,
|
|
113
|
+
onToggle: handleToggle,
|
|
114
|
+
onSelectSchedule: handleSelectSchedule,
|
|
115
|
+
holidayCatalog,
|
|
116
|
+
holidayCatalogLoading,
|
|
117
|
+
holidayCatalogError,
|
|
118
|
+
schedules,
|
|
119
|
+
schedulesLoading,
|
|
120
|
+
schedulesError,
|
|
121
|
+
onCreateSchedule,
|
|
122
|
+
onUpdateSchedule,
|
|
123
|
+
onDeleteSchedule,
|
|
124
|
+
}),
|
|
125
|
+
[
|
|
126
|
+
selectedBinding,
|
|
127
|
+
handleToggle,
|
|
128
|
+
handleSelectSchedule,
|
|
129
|
+
holidayCatalog,
|
|
130
|
+
holidayCatalogLoading,
|
|
131
|
+
holidayCatalogError,
|
|
132
|
+
schedules,
|
|
133
|
+
schedulesLoading,
|
|
134
|
+
schedulesError,
|
|
135
|
+
onCreateSchedule,
|
|
136
|
+
onUpdateSchedule,
|
|
137
|
+
onDeleteSchedule,
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
|
|
47
141
|
return (
|
|
48
142
|
<Flex className="dte-field-config-panel-overlay" onClick={e => e.stopPropagation()}>
|
|
49
143
|
<Flex
|
|
@@ -75,6 +169,8 @@ export const FieldConfigPanelOverlay: FC<FieldConfigPanelOverlayProps> = ({
|
|
|
75
169
|
onMatchingFieldsConfigChange={onMatchingFieldsConfigChange}
|
|
76
170
|
onDeleteField={onDeleteField}
|
|
77
171
|
hideConditionalLogic={hideConditionalLogic}
|
|
172
|
+
dateCalcOptions={dateCalcOptions}
|
|
173
|
+
businessCalendar={businessCalendar}
|
|
78
174
|
onRequestFormFields={onRequestFormFields}
|
|
79
175
|
/>
|
|
80
176
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button, Checkbox, Flex, SelectFieldSync, Textarea, TextField } from '@servicetitan/anvil2';
|
|
2
|
+
import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
2
3
|
import { FC, useMemo } from 'react';
|
|
3
4
|
import { FIELD_FONT_SIZE_OPTIONS } from '../../constants';
|
|
4
5
|
import {
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
type SchemaObject,
|
|
13
14
|
} from '../../interface/types';
|
|
14
15
|
import {
|
|
16
|
+
type DateCalcOptions,
|
|
15
17
|
generateESignPath,
|
|
16
18
|
generateFillablePath,
|
|
17
19
|
getFieldFontSize,
|
|
@@ -35,6 +37,10 @@ interface FieldConfigPanelProps {
|
|
|
35
37
|
formulaLoadingFormId?: number | null;
|
|
36
38
|
recipients: RecipientInfo[];
|
|
37
39
|
hideConditionalLogic?: boolean;
|
|
40
|
+
/** The calendar of this field's formula preview. */
|
|
41
|
+
dateCalcOptions?: DateCalcOptions;
|
|
42
|
+
/** The business-calendar toggle/picker/drawer state and callbacks for this field. */
|
|
43
|
+
businessCalendar?: ScheduleEditorContext;
|
|
38
44
|
onDeleteField(): void;
|
|
39
45
|
onFieldConfigChange(updates: Partial<PdfField>): void;
|
|
40
46
|
onMatchingFieldsConfigChange?(
|
|
@@ -45,7 +51,9 @@ interface FieldConfigPanelProps {
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
54
|
+
businessCalendar,
|
|
48
55
|
dataModel,
|
|
56
|
+
dateCalcOptions,
|
|
49
57
|
documentFields = [],
|
|
50
58
|
field,
|
|
51
59
|
formFieldsByFormId,
|
|
@@ -88,6 +96,23 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
88
96
|
onFieldConfigChange({ data });
|
|
89
97
|
};
|
|
90
98
|
|
|
99
|
+
/*
|
|
100
|
+
* Reuses the catalog/CRUD half of `businessCalendar` (shared tenant schedules, same as a
|
|
101
|
+
* calculated field's), but overrides the enabled/selected-schedule half to read and write
|
|
102
|
+
* `ESignDateSignedOffsetSettings` instead of a `scheduleBindings` entry, since the toggle here
|
|
103
|
+
* applies to every `dateSignedOffset` field in the document, same as `rules` does.
|
|
104
|
+
*/
|
|
105
|
+
const dateSignedOffsetData = field.data as ESignDateSignedOffsetSettings;
|
|
106
|
+
const eSignBusinessCalendar: ScheduleEditorContext | undefined = businessCalendar && {
|
|
107
|
+
...businessCalendar,
|
|
108
|
+
enabled: !!dateSignedOffsetData?.businessCalendarEnabled,
|
|
109
|
+
selectedScheduleId: dateSignedOffsetData?.scheduleId,
|
|
110
|
+
onToggle: businessCalendarEnabled =>
|
|
111
|
+
handleDateSignedOffsetSave({ ...dateSignedOffsetData, businessCalendarEnabled }),
|
|
112
|
+
onSelectSchedule: scheduleId =>
|
|
113
|
+
handleDateSignedOffsetSave({ ...dateSignedOffsetData, scheduleId }),
|
|
114
|
+
};
|
|
115
|
+
|
|
91
116
|
const handleRecipientChange = (item: { id: string; label: string } | null) => {
|
|
92
117
|
if (!item) {
|
|
93
118
|
return;
|
|
@@ -245,7 +270,9 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
245
270
|
)}
|
|
246
271
|
{field.type === FieldTypeEnum.calculated && (
|
|
247
272
|
<FormulaGenerator
|
|
273
|
+
businessCalendar={businessCalendar}
|
|
248
274
|
dataModel={dataModel}
|
|
275
|
+
dateCalcOptions={dateCalcOptions}
|
|
249
276
|
documentFields={documentFields}
|
|
250
277
|
formFieldsByFormId={formFieldsByFormId}
|
|
251
278
|
forms={forms}
|
|
@@ -281,6 +308,7 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
281
308
|
<ESignOptionsModal
|
|
282
309
|
initialState={field.data as ESignDateSignedOffsetSettings}
|
|
283
310
|
onSave={handleDateSignedOffsetSave}
|
|
311
|
+
businessCalendar={eSignBusinessCalendar}
|
|
284
312
|
documentFields={documentFields}
|
|
285
313
|
formFieldsByFormId={formFieldsByFormId}
|
|
286
314
|
formFieldsLoadingFormId={formulaLoadingFormId}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Button } from '@servicetitan/anvil2';
|
|
2
2
|
import EditIcon from '@servicetitan/anvil2/assets/icons/material/round/edit.svg';
|
|
3
|
+
import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
3
4
|
import { FC, Fragment, useMemo, useState } from 'react';
|
|
4
5
|
import {
|
|
5
6
|
type CalculatedFieldFormat,
|
|
@@ -12,11 +13,13 @@ import {
|
|
|
12
13
|
type SchemaObject,
|
|
13
14
|
type StructuredFormula,
|
|
14
15
|
} from '../../interface/types';
|
|
15
|
-
import { extractGroupedFieldsFromDataModel } from '../../utils';
|
|
16
|
+
import { type DateCalcOptions, extractGroupedFieldsFromDataModel } from '../../utils';
|
|
16
17
|
import { FormulaModal } from './formula-modal';
|
|
17
18
|
|
|
18
19
|
export interface FormulaGeneratorProps {
|
|
20
|
+
businessCalendar?: ScheduleEditorContext;
|
|
19
21
|
dataModel?: SchemaObject;
|
|
22
|
+
dateCalcOptions?: DateCalcOptions;
|
|
20
23
|
documentFields?: PdfField[];
|
|
21
24
|
formFieldsByFormId?: FormFieldsByFormIdI;
|
|
22
25
|
forms: FormInfo[];
|
|
@@ -43,7 +46,9 @@ function getFillableFilteredBuySubTypeFields(
|
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export const FormulaGenerator: FC<FormulaGeneratorProps> = ({
|
|
49
|
+
businessCalendar,
|
|
46
50
|
dataModel,
|
|
51
|
+
dateCalcOptions,
|
|
47
52
|
documentFields = [],
|
|
48
53
|
formFieldsByFormId,
|
|
49
54
|
forms,
|
|
@@ -87,7 +92,9 @@ export const FormulaGenerator: FC<FormulaGeneratorProps> = ({
|
|
|
87
92
|
<FormulaModal
|
|
88
93
|
initialFormula={formula}
|
|
89
94
|
initialFormat={formulaFormat}
|
|
95
|
+
businessCalendar={businessCalendar}
|
|
90
96
|
dataModelGroups={dataModelGroups}
|
|
97
|
+
dateCalcOptions={dateCalcOptions}
|
|
91
98
|
fillableFieldsFromDocument={fillableFieldsFromDocument}
|
|
92
99
|
formFieldsByFormId={formFieldsByFormId}
|
|
93
100
|
forms={forms}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Button, Dialog, Divider, Flex } from '@servicetitan/anvil2';
|
|
2
2
|
import IconRedo from '@servicetitan/anvil2/assets/icons/material/round/redo.svg';
|
|
3
3
|
import IconUndo from '@servicetitan/anvil2/assets/icons/material/round/undo.svg';
|
|
4
|
+
import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
4
5
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
6
|
import {
|
|
6
7
|
DEFAULT_DATE_CALCULATED_FORMAT,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type StructuredFormula,
|
|
19
20
|
} from '../../interface/types';
|
|
20
21
|
import {
|
|
22
|
+
type DateCalcOptions,
|
|
21
23
|
evaluateFormula,
|
|
22
24
|
formatCalculatedResult,
|
|
23
25
|
formFieldInfosToCalculationOptions,
|
|
@@ -68,7 +70,10 @@ function setSampleValueAtPath(target: DataModelValues, path: string, value: unkn
|
|
|
68
70
|
export interface FormulaModalProps {
|
|
69
71
|
initialFormula: StructuredFormula | undefined;
|
|
70
72
|
initialFormat?: CalculatedFieldFormat;
|
|
73
|
+
businessCalendar?: ScheduleEditorContext;
|
|
71
74
|
dataModelGroups: DataModelFieldGroup[];
|
|
75
|
+
/** The calendar the preview uses: the field's own, or the default one. */
|
|
76
|
+
dateCalcOptions?: DateCalcOptions;
|
|
72
77
|
fillableFieldsFromDocument?: FieldTypeOption[];
|
|
73
78
|
formFieldsByFormId?: FormFieldsByFormIdI;
|
|
74
79
|
forms: FormInfo[];
|
|
@@ -79,7 +84,9 @@ export interface FormulaModalProps {
|
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
export const FormulaModal: FC<FormulaModalProps> = ({
|
|
87
|
+
businessCalendar,
|
|
82
88
|
dataModelGroups,
|
|
89
|
+
dateCalcOptions,
|
|
83
90
|
fillableFieldsFromDocument = [],
|
|
84
91
|
formFieldsByFormId = {},
|
|
85
92
|
forms,
|
|
@@ -255,21 +262,28 @@ export const FormulaModal: FC<FormulaModalProps> = ({
|
|
|
255
262
|
if (parsedTokens.length === 0 || !formulaValidation.valid) {
|
|
256
263
|
return '';
|
|
257
264
|
}
|
|
258
|
-
return hasDivisionByZero({ tokens: parsedTokens }, sampleData)
|
|
265
|
+
return hasDivisionByZero({ tokens: parsedTokens }, sampleData, dateCalcOptions)
|
|
259
266
|
? 'Division by zero is not allowed'
|
|
260
267
|
: '';
|
|
261
|
-
}, [parsedTokens, formulaValidation.valid, sampleData]);
|
|
268
|
+
}, [parsedTokens, formulaValidation.valid, sampleData, dateCalcOptions]);
|
|
262
269
|
|
|
263
270
|
const previewValue = useMemo(() => {
|
|
264
271
|
if (parsedTokens.length === 0 || !formulaValidation.valid || divisionByZeroError) {
|
|
265
272
|
return '';
|
|
266
273
|
}
|
|
267
|
-
const computed = evaluateFormula({ tokens: parsedTokens }, sampleData);
|
|
274
|
+
const computed = evaluateFormula({ tokens: parsedTokens }, sampleData, dateCalcOptions);
|
|
268
275
|
if (computed === null) {
|
|
269
276
|
return '';
|
|
270
277
|
}
|
|
271
278
|
return formatCalculatedResult(computed, format);
|
|
272
|
-
}, [
|
|
279
|
+
}, [
|
|
280
|
+
parsedTokens,
|
|
281
|
+
formulaValidation.valid,
|
|
282
|
+
divisionByZeroError,
|
|
283
|
+
sampleData,
|
|
284
|
+
format,
|
|
285
|
+
dateCalcOptions,
|
|
286
|
+
]);
|
|
273
287
|
|
|
274
288
|
const handleClose = useCallback(() => {
|
|
275
289
|
onClose();
|
|
@@ -317,6 +331,7 @@ export const FormulaModal: FC<FormulaModalProps> = ({
|
|
|
317
331
|
selectedPaths={formulaEditor.selectedFieldPaths}
|
|
318
332
|
/>
|
|
319
333
|
<FormulaWorkspace
|
|
334
|
+
businessCalendar={businessCalendar}
|
|
320
335
|
disabledOperators={formulaEditor.disabledOperators}
|
|
321
336
|
editorRef={formulaEditor.editorRef}
|
|
322
337
|
isInvalid={isInvalid && formulaEditor.isDirty}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { Alert, Button, Flex,
|
|
1
|
+
import { Alert, Button, Flex, Text } from '@servicetitan/anvil2';
|
|
2
|
+
import IconExpandMore from '@servicetitan/anvil2/assets/icons/material/round/expand_more.svg';
|
|
3
|
+
import IconArrowForward from '@servicetitan/anvil2/assets/icons/material/round/keyboard_arrow_right.svg';
|
|
4
|
+
|
|
5
|
+
import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
2
6
|
import {
|
|
3
7
|
Dispatch,
|
|
4
8
|
FC,
|
|
@@ -17,6 +21,7 @@ import { ResultTypeSelector } from './result-type-selector';
|
|
|
17
21
|
interface FormulaWorkspaceProps {
|
|
18
22
|
actions: ReactNode;
|
|
19
23
|
advancedOpen: boolean;
|
|
24
|
+
businessCalendar?: ScheduleEditorContext;
|
|
20
25
|
disabledOperators?: Set<string>;
|
|
21
26
|
editorRef: RefObject<HTMLDivElement | null>;
|
|
22
27
|
format: CalculatedFieldFormat;
|
|
@@ -38,6 +43,7 @@ interface FormulaWorkspaceProps {
|
|
|
38
43
|
export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
|
|
39
44
|
actions,
|
|
40
45
|
advancedOpen,
|
|
46
|
+
businessCalendar,
|
|
41
47
|
disabledOperators,
|
|
42
48
|
editorRef,
|
|
43
49
|
format,
|
|
@@ -75,7 +81,7 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
|
|
|
75
81
|
);
|
|
76
82
|
|
|
77
83
|
return (
|
|
78
|
-
<Flex direction="column" flex={1} gap={2} style={{ padding: '12px' }}>
|
|
84
|
+
<Flex direction="column" flex={1} gap={2} style={{ padding: '12px', overflowY: 'auto' }}>
|
|
79
85
|
<Flex direction="column" gap={1}>
|
|
80
86
|
<Text variant="body" size="small">
|
|
81
87
|
Preview (uses demo sample data)
|
|
@@ -132,10 +138,29 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
|
|
|
132
138
|
})}
|
|
133
139
|
</Flex>
|
|
134
140
|
<ResultTypeSelector format={format} onChange={onResultTypeChange} />
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
<Flex
|
|
142
|
+
role="button"
|
|
143
|
+
onClick={onToggleAdvanced}
|
|
144
|
+
alignItems="center"
|
|
145
|
+
gap={1}
|
|
146
|
+
style={{ cursor: 'pointer', marginTop: 16 }}
|
|
147
|
+
>
|
|
148
|
+
<Text variant="eyebrow" size="small">
|
|
149
|
+
Advanced settings
|
|
150
|
+
</Text>
|
|
151
|
+
{advancedOpen ? (
|
|
152
|
+
<IconExpandMore fontSize="small" />
|
|
153
|
+
) : (
|
|
154
|
+
<IconArrowForward fontSize="small" />
|
|
155
|
+
)}
|
|
156
|
+
</Flex>
|
|
157
|
+
{advancedOpen && (
|
|
158
|
+
<AdvancedSettings
|
|
159
|
+
format={format}
|
|
160
|
+
setFormat={setFormat}
|
|
161
|
+
businessCalendar={businessCalendar}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
139
164
|
</Flex>
|
|
140
165
|
);
|
|
141
166
|
};
|
|
@@ -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
|
)}
|
|
@@ -3,12 +3,14 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
3
3
|
import { type PdfField } from '../../interface/types';
|
|
4
4
|
import {
|
|
5
5
|
getDropdownFieldItems,
|
|
6
|
+
getFieldMaxLength,
|
|
6
7
|
getFieldPlaceholderText,
|
|
7
8
|
getFieldTextClipClassName,
|
|
8
9
|
getFillablePaintedLabel,
|
|
9
10
|
getTodayDateInputValue,
|
|
10
11
|
isRadioDefaultSelected,
|
|
11
12
|
} from '../../utils';
|
|
13
|
+
import { CharLimitIndicator } from '../shared';
|
|
12
14
|
import { AddHandle } from './add-handle';
|
|
13
15
|
|
|
14
16
|
interface PdfOverlayFieldFillableProps {
|
|
@@ -130,14 +132,20 @@ export const PdfOverlayFieldFillable: FC<PdfOverlayFieldFillableProps> = ({
|
|
|
130
132
|
case 'number': {
|
|
131
133
|
const defaultNumberValue = field.default?.type === 'number' ? field.default.value : '';
|
|
132
134
|
return (
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
<Fragment>
|
|
136
|
+
<input
|
|
137
|
+
key={defaultNumberValue}
|
|
138
|
+
type="number"
|
|
139
|
+
readOnly
|
|
140
|
+
defaultValue={defaultNumberValue}
|
|
141
|
+
className={`dte-pdf-field-fillable ${clipClassName}`}
|
|
142
|
+
placeholder={placeholderText}
|
|
143
|
+
/>
|
|
144
|
+
<CharLimitIndicator
|
|
145
|
+
maxLength={getFieldMaxLength(field, String(defaultNumberValue))}
|
|
146
|
+
value={String(defaultNumberValue)}
|
|
147
|
+
/>
|
|
148
|
+
</Fragment>
|
|
141
149
|
);
|
|
142
150
|
}
|
|
143
151
|
|
|
@@ -166,27 +174,32 @@ export const PdfOverlayFieldFillable: FC<PdfOverlayFieldFillableProps> = ({
|
|
|
166
174
|
|
|
167
175
|
default: {
|
|
168
176
|
const defaultTextValue = field.default?.type === 'text' ? field.default.value : '';
|
|
169
|
-
if (field.multiline) {
|
|
170
|
-
return (
|
|
171
|
-
<textarea
|
|
172
|
-
key={defaultTextValue}
|
|
173
|
-
readOnly
|
|
174
|
-
defaultValue={defaultTextValue}
|
|
175
|
-
style={{ font: 'inherit', lineHeight: 'inherit', outline: 'none' }}
|
|
176
|
-
placeholder={placeholderText}
|
|
177
|
-
className={`dte-pdf-field-fillable ${clipClassName}`}
|
|
178
|
-
/>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
177
|
return (
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
<Fragment>
|
|
179
|
+
{field.multiline ? (
|
|
180
|
+
<textarea
|
|
181
|
+
key={defaultTextValue}
|
|
182
|
+
readOnly
|
|
183
|
+
defaultValue={defaultTextValue}
|
|
184
|
+
style={{ font: 'inherit', lineHeight: 'inherit', outline: 'none' }}
|
|
185
|
+
placeholder={placeholderText}
|
|
186
|
+
className={`dte-pdf-field-fillable ${clipClassName}`}
|
|
187
|
+
/>
|
|
188
|
+
) : (
|
|
189
|
+
<input
|
|
190
|
+
key={defaultTextValue}
|
|
191
|
+
type="text"
|
|
192
|
+
readOnly
|
|
193
|
+
defaultValue={defaultTextValue}
|
|
194
|
+
placeholder={placeholderText}
|
|
195
|
+
className={`dte-pdf-field-fillable ${clipClassName}`}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
<CharLimitIndicator
|
|
199
|
+
maxLength={getFieldMaxLength(field, defaultTextValue)}
|
|
200
|
+
value={defaultTextValue}
|
|
201
|
+
/>
|
|
202
|
+
</Fragment>
|
|
190
203
|
);
|
|
191
204
|
}
|
|
192
205
|
}
|