@servicetitan/dte-pdf-editor 1.96.1 → 1.96.3
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 +26 -8
- package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
- package/dist/components/field-config-panel/advanced-settings.js +1 -0
- package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
- package/dist/components/field-config-panel/e-sign-offset-rule-row.js +1 -1
- package/dist/components/field-config-panel/e-sign-offset-rule-row.js.map +1 -1
- 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 +1 -0
- 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.map +1 -1
- package/dist/components/field-config-panel/field-config-panel-overlay.js +23 -33
- 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 +5 -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 +5 -2
- package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
- package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
- package/dist/components/field-config-panel/formula-modal.js +4 -3
- package/dist/components/field-config-panel/formula-modal.js.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.d.ts +12 -9
- package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
- package/dist/constants/field.constants.js +1 -1
- package/dist/interface/types.d.ts +4 -8
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js.map +1 -1
- package/dist/styles/field-config-panel-overlay.css +24 -0
- package/dist/utils/formula/index.d.ts +0 -1
- package/dist/utils/formula/index.d.ts.map +1 -1
- package/dist/utils/formula/index.js +0 -1
- package/dist/utils/formula/index.js.map +1 -1
- package/dist/vendor/dte-shared/business-calendar-validation.utils.d.ts +3 -0
- package/dist/vendor/dte-shared/business-calendar-validation.utils.js +4 -0
- package/dist/vendor/dte-shared/components/business-calendar-section.d.ts +1 -1
- package/dist/vendor/dte-shared/components/business-calendar-section.js +3 -2
- package/dist/vendor/dte-shared/components/schedule-drawer.d.ts +5 -3
- package/dist/vendor/dte-shared/components/schedule-drawer.js +19 -12
- package/dist/vendor/dte-shared/components/schedule-picker.js +10 -10
- package/dist/vendor/dte-shared/constants.d.ts +21 -0
- package/dist/vendor/dte-shared/constants.js +24 -0
- package/dist/vendor/dte-shared/field-schedule-editor-context.d.ts +31 -0
- package/dist/vendor/dte-shared/field-schedule-editor-context.js +110 -0
- package/dist/vendor/dte-shared/index.d.ts +2 -0
- package/dist/vendor/dte-shared/index.js +2 -0
- package/dist/vendor/dte-shared/schedule-bindings.utils.d.ts +22 -0
- package/dist/vendor/dte-shared/schedule-bindings.utils.js +31 -0
- package/dist/vendor/dte-shared/styles/schedule.css +2 -2
- package/dist/vendor/dte-shared/types.d.ts +62 -0
- package/package.json +2 -2
- package/src/components/field-config-panel/__tests__/field-config-panel-overlay.test.tsx +200 -0
- package/src/components/field-config-panel/__tests__/schedule-editor-placement.test.tsx +289 -0
- package/src/components/field-config-panel/__tests__/schedule-switch-visibility.test.tsx +206 -0
- package/src/components/field-config-panel/advanced-settings.tsx +1 -0
- package/src/components/field-config-panel/e-sign-offset-rule-row.tsx +1 -1
- package/src/components/field-config-panel/e-sign-options-modal.tsx +1 -0
- package/src/components/field-config-panel/field-config-panel-overlay.tsx +69 -91
- package/src/components/field-config-panel/field-config-panel.tsx +10 -1
- package/src/components/field-config-panel/formula-modal.tsx +5 -8
- package/src/components/pdf-editor/pdf-editor.tsx +12 -9
- package/src/constants/field.constants.ts +1 -1
- package/src/interface/types.ts +4 -8
- package/src/styles/field-config-panel-overlay.css +24 -0
- package/src/utils/formula/index.ts +0 -1
- package/dist/utils/formula/schedule-bindings.utils.d.ts +0 -12
- package/dist/utils/formula/schedule-bindings.utils.d.ts.map +0 -1
- package/dist/utils/formula/schedule-bindings.utils.js +0 -17
- package/dist/utils/formula/schedule-bindings.utils.js.map +0 -1
- package/src/utils/formula/__tests__/schedule-bindings.utils.test.ts +0 -73
- package/src/utils/formula/schedule-bindings.utils.ts +0 -27
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The overlay gives the panel no schedule context when the host cannot save bindings. This checks
|
|
3
|
+
* the other half: the places that draw the schedule switch show it only with a context.
|
|
4
|
+
*/
|
|
5
|
+
import { TextDecoder, TextEncoder } from 'util';
|
|
6
|
+
|
|
7
|
+
// jsdom has no TextEncoder; anvil2 reads it when it is loaded, so this must run before it is imported.
|
|
8
|
+
Object.assign(globalThis, { TextEncoder, TextDecoder });
|
|
9
|
+
|
|
10
|
+
import { act, useState } from 'react';
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
12
|
+
import {
|
|
13
|
+
type CalculatedFieldFormat,
|
|
14
|
+
ESignFieldType,
|
|
15
|
+
FieldTypeEnum,
|
|
16
|
+
type PdfField,
|
|
17
|
+
} from '../../../interface/types';
|
|
18
|
+
import { AdvancedSettings } from '../advanced-settings';
|
|
19
|
+
import { ESignOptionsModal } from '../e-sign-options-modal';
|
|
20
|
+
import { FieldConfigPanel } from '../field-config-panel';
|
|
21
|
+
|
|
22
|
+
// jsdom has no matchMedia, ResizeObserver, IntersectionObserver or popover API; anvil2 uses them.
|
|
23
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
24
|
+
writable: true,
|
|
25
|
+
value: (query: string) => ({
|
|
26
|
+
matches: false,
|
|
27
|
+
media: query,
|
|
28
|
+
onchange: null,
|
|
29
|
+
addListener: () => {},
|
|
30
|
+
removeListener: () => {},
|
|
31
|
+
addEventListener: () => {},
|
|
32
|
+
removeEventListener: () => {},
|
|
33
|
+
dispatchEvent: () => false,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
class ObserverMock {
|
|
37
|
+
observe() {}
|
|
38
|
+
unobserve() {}
|
|
39
|
+
disconnect() {}
|
|
40
|
+
}
|
|
41
|
+
global.ResizeObserver = ObserverMock as unknown as typeof ResizeObserver;
|
|
42
|
+
global.IntersectionObserver = ObserverMock as unknown as typeof IntersectionObserver;
|
|
43
|
+
HTMLElement.prototype.showPopover ??= () => {};
|
|
44
|
+
HTMLElement.prototype.hidePopover ??= () => {};
|
|
45
|
+
HTMLElement.prototype.togglePopover ??= () => false;
|
|
46
|
+
|
|
47
|
+
const SWITCH_LABEL = 'Exclude weekends & holidays from the calculation';
|
|
48
|
+
const noOp = () => {};
|
|
49
|
+
const businessCalendar = { enabled: false, onToggle: noOp, onSelectSchedule: noOp };
|
|
50
|
+
const dateFormat: CalculatedFieldFormat = { resultType: 'date', dateFormat: 'MM/DD/YYYY' };
|
|
51
|
+
const numberFormat: CalculatedFieldFormat = {
|
|
52
|
+
resultType: 'number',
|
|
53
|
+
thousandsSeparator: false,
|
|
54
|
+
decimals: 2,
|
|
55
|
+
roundingMode: 'round',
|
|
56
|
+
decimalSeparatorEnabled: false,
|
|
57
|
+
decimalSeparator: '.',
|
|
58
|
+
prefixText: '',
|
|
59
|
+
postfixText: '',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const AdvancedSettingsSubject = ({
|
|
63
|
+
format,
|
|
64
|
+
withSchedules,
|
|
65
|
+
}: {
|
|
66
|
+
format: CalculatedFieldFormat;
|
|
67
|
+
withSchedules: boolean;
|
|
68
|
+
}) => {
|
|
69
|
+
const [current, setCurrent] = useState(format);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<AdvancedSettings
|
|
73
|
+
format={current}
|
|
74
|
+
setFormat={setCurrent}
|
|
75
|
+
businessCalendar={withSchedules ? businessCalendar : undefined}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
describe('the schedule switch', () => {
|
|
81
|
+
let container: HTMLDivElement;
|
|
82
|
+
let root: Root;
|
|
83
|
+
|
|
84
|
+
const render = (element: JSX.Element) =>
|
|
85
|
+
act(async () => {
|
|
86
|
+
root.render(element);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
beforeAll(() => {
|
|
90
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
container = document.createElement('div');
|
|
95
|
+
document.body.appendChild(container);
|
|
96
|
+
root = createRoot(container);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(async () => {
|
|
100
|
+
await act(async () => root.unmount());
|
|
101
|
+
container.remove();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('in the advanced settings of a calculated field', () => {
|
|
105
|
+
test('is shown for a date field with a schedule context', async () => {
|
|
106
|
+
await render(<AdvancedSettingsSubject format={dateFormat} withSchedules />);
|
|
107
|
+
|
|
108
|
+
expect(container.textContent).toContain('Date format');
|
|
109
|
+
expect(container.textContent).toContain(SWITCH_LABEL);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('is not shown for a date field without a schedule context', async () => {
|
|
113
|
+
await render(<AdvancedSettingsSubject format={dateFormat} withSchedules={false} />);
|
|
114
|
+
|
|
115
|
+
expect(container.textContent).toContain('Date format');
|
|
116
|
+
expect(container.textContent).not.toContain(SWITCH_LABEL);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('is not shown for a number field', async () => {
|
|
120
|
+
await render(<AdvancedSettingsSubject format={numberFormat} withSchedules />);
|
|
121
|
+
|
|
122
|
+
expect(container.textContent).not.toContain(SWITCH_LABEL);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('in the options of an e-sign date signed offset', () => {
|
|
127
|
+
const renderOptions = (withSchedules: boolean) =>
|
|
128
|
+
render(
|
|
129
|
+
<ESignOptionsModal
|
|
130
|
+
initialState={{ rules: [{ days: 3 }] }}
|
|
131
|
+
onSave={() => {}}
|
|
132
|
+
businessCalendar={withSchedules ? businessCalendar : undefined}
|
|
133
|
+
/>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
test('is shown with a schedule context', async () => {
|
|
137
|
+
await renderOptions(true);
|
|
138
|
+
|
|
139
|
+
expect(container.textContent).toContain(SWITCH_LABEL);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('is not shown without a schedule context', async () => {
|
|
143
|
+
await renderOptions(false);
|
|
144
|
+
|
|
145
|
+
expect(container.textContent).not.toContain(SWITCH_LABEL);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('in the config panel of an e-sign date signed offset field', () => {
|
|
150
|
+
const field = {
|
|
151
|
+
id: 'e1',
|
|
152
|
+
type: FieldTypeEnum.eSign,
|
|
153
|
+
subType: ESignFieldType.dateSignedOffset,
|
|
154
|
+
recipient: 'customer',
|
|
155
|
+
path: 'esign.customer.dateSignedOffset',
|
|
156
|
+
data: { rules: [{ days: 3 }] },
|
|
157
|
+
} as unknown as PdfField;
|
|
158
|
+
const onFieldConfigChange = jest.fn();
|
|
159
|
+
|
|
160
|
+
const renderPanel = (context?: typeof businessCalendar) =>
|
|
161
|
+
render(
|
|
162
|
+
<FieldConfigPanel
|
|
163
|
+
field={field}
|
|
164
|
+
recipients={[{ id: 1, name: 'customer', displayName: 'Customer' }]}
|
|
165
|
+
forms={[]}
|
|
166
|
+
hideConditionalLogic
|
|
167
|
+
businessCalendar={context}
|
|
168
|
+
onDeleteField={noOp}
|
|
169
|
+
onFieldConfigChange={onFieldConfigChange}
|
|
170
|
+
onRequestFormFields={noOp}
|
|
171
|
+
/>,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const findSwitch = () =>
|
|
175
|
+
[...container.querySelectorAll('input')].find(input =>
|
|
176
|
+
input.closest('label')?.textContent?.includes(SWITCH_LABEL),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
beforeEach(() => onFieldConfigChange.mockClear());
|
|
180
|
+
|
|
181
|
+
test('is not shown without a schedule context', async () => {
|
|
182
|
+
await renderPanel();
|
|
183
|
+
|
|
184
|
+
expect(container.textContent).toContain('Number of days');
|
|
185
|
+
expect(container.textContent).not.toContain(SWITCH_LABEL);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('is shown with a schedule context', async () => {
|
|
189
|
+
await renderPanel(businessCalendar);
|
|
190
|
+
|
|
191
|
+
expect(findSwitch()).toBeDefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('stores the choice in the field data and does not call the context', async () => {
|
|
195
|
+
const onToggle = jest.fn();
|
|
196
|
+
await renderPanel({ ...businessCalendar, onToggle });
|
|
197
|
+
|
|
198
|
+
await act(async () => findSwitch()?.click());
|
|
199
|
+
|
|
200
|
+
expect(onFieldConfigChange).toHaveBeenCalledWith({
|
|
201
|
+
data: { rules: [{ days: 3 }], businessCalendarEnabled: true },
|
|
202
|
+
});
|
|
203
|
+
expect(onToggle).not.toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Flex, SegmentedControl, Switch, Text } from '@servicetitan/anvil2';
|
|
2
|
+
// eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
|
|
2
3
|
import { BusinessCalendarSection, type ScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
3
4
|
import { Dispatch, FC, SetStateAction } from 'react';
|
|
4
5
|
import { DEFAULT_DATE_FORMAT, ROUNDING_OPTIONS } from '../../constants';
|
|
@@ -35,7 +35,7 @@ export const ESignOffsetRuleRow: FC<ESignOffsetRuleRowProps> = ({
|
|
|
35
35
|
rule,
|
|
36
36
|
}) => {
|
|
37
37
|
const handleDaysChange = (days: number | null) => {
|
|
38
|
-
onChange({ ...rule, days: days ?? 0 });
|
|
38
|
+
onChange({ ...rule, days: Math.max(Math.min(365, days ?? 0), 0) });
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
const group = rule.condition.groups[0];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Button, Flex, NumberField } from '@servicetitan/anvil2';
|
|
2
2
|
import AddIcon from '@servicetitan/anvil2/assets/icons/material/round/add_circle.svg';
|
|
3
|
+
// eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
|
|
3
4
|
import { BusinessCalendarSection, type ScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
4
5
|
import { FC, Fragment, useState } from 'react';
|
|
5
6
|
import {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Button, Flex, Text } from '@servicetitan/anvil2';
|
|
2
2
|
import IconClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
|
|
3
|
-
|
|
4
|
-
import {
|
|
3
|
+
// eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
|
|
4
|
+
import { useFieldScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
5
|
+
import { FC, useMemo, useState } from 'react';
|
|
5
6
|
import {
|
|
6
7
|
type BusinessCalendar,
|
|
7
8
|
type BusinessDaySchedule,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
type SaveScheduleRequest,
|
|
15
16
|
type SchemaObject,
|
|
16
17
|
} from '../../interface/types';
|
|
17
|
-
import { getFieldDateCalcOptions
|
|
18
|
+
import { getFieldDateCalcOptions } from '../../utils';
|
|
18
19
|
import { FieldConfigPanel } from './field-config-panel';
|
|
19
20
|
|
|
20
21
|
interface FieldConfigPanelOverlayProps {
|
|
@@ -77,103 +78,80 @@ export const FieldConfigPanelOverlay: FC<FieldConfigPanelOverlayProps> = ({
|
|
|
77
78
|
schedulesLoading,
|
|
78
79
|
selectedField,
|
|
79
80
|
}) => {
|
|
81
|
+
/*
|
|
82
|
+
* Where an e-sign date offset's schedule form is portaled to. A state, not a ref, so the panel
|
|
83
|
+
* re-renders with the element once it exists.
|
|
84
|
+
*/
|
|
85
|
+
const [scheduleEditorContainer, setScheduleEditorContainer] = useState<HTMLElement | null>(
|
|
86
|
+
null,
|
|
87
|
+
);
|
|
88
|
+
|
|
80
89
|
const dateCalcOptions = useMemo(
|
|
81
90
|
() => getFieldDateCalcOptions(selectedField.id, scheduleBindings, holidays, fieldCalendars),
|
|
82
91
|
[selectedField.id, scheduleBindings, holidays, fieldCalendars],
|
|
83
92
|
);
|
|
84
93
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
);
|
|
94
|
+
const fieldBusinessCalendar = useFieldScheduleEditorContext(selectedField.id, {
|
|
95
|
+
holidayCatalog,
|
|
96
|
+
holidayCatalogError,
|
|
97
|
+
holidayCatalogLoading,
|
|
98
|
+
onCreateSchedule,
|
|
99
|
+
onDeleteSchedule,
|
|
100
|
+
onScheduleBindingsChange,
|
|
101
|
+
onUpdateSchedule,
|
|
102
|
+
scheduleBindings,
|
|
103
|
+
schedules,
|
|
104
|
+
schedulesError,
|
|
105
|
+
schedulesLoading,
|
|
106
|
+
});
|
|
107
|
+
/*
|
|
108
|
+
* The schedule UI (the Formula Builder's switch and the e-sign day offset's schedule) is offered
|
|
109
|
+
* only to a host that passes `onScheduleBindingsChange`. Without it there is no context, and
|
|
110
|
+
* everything that renders the schedule UI checks for one.
|
|
111
|
+
*/
|
|
112
|
+
const businessCalendar = onScheduleBindingsChange ? fieldBusinessCalendar : undefined;
|
|
140
113
|
|
|
141
114
|
return (
|
|
142
115
|
<Flex className="dte-field-config-panel-overlay" onClick={e => e.stopPropagation()}>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
116
|
+
{/* Before the inner panel, so a filled slot can hide it with a sibling selector. */}
|
|
117
|
+
<div ref={setScheduleEditorContainer} className="dte-field-config-panel-editor-slot" />
|
|
118
|
+
<Flex className="dte-field-config-panel-overlay-inner" direction="column">
|
|
119
|
+
<Flex
|
|
120
|
+
className="dte-field-config-panel-header"
|
|
121
|
+
alignItems="center"
|
|
122
|
+
justifyContent="space-between"
|
|
123
|
+
>
|
|
124
|
+
<Text variant="headline" el="h6" size="small">
|
|
125
|
+
Field Configuration
|
|
126
|
+
</Text>
|
|
127
|
+
<Button
|
|
128
|
+
appearance="ghost"
|
|
129
|
+
size="small"
|
|
130
|
+
onClick={onDeselectField}
|
|
131
|
+
aria-label="Close"
|
|
132
|
+
icon={IconClose}
|
|
133
|
+
/>
|
|
134
|
+
</Flex>
|
|
135
|
+
<div className="dte-field-config-panel-content">
|
|
136
|
+
<FieldConfigPanel
|
|
137
|
+
dataModel={dataModel}
|
|
138
|
+
documentFields={documentFields}
|
|
139
|
+
field={selectedField}
|
|
140
|
+
formFieldsByFormId={formFieldsByFormId}
|
|
141
|
+
forms={forms}
|
|
142
|
+
formulaLoadingFormId={formulaLoadingFormId}
|
|
143
|
+
recipients={recipients}
|
|
144
|
+
onFieldConfigChange={onFieldConfigChange}
|
|
145
|
+
onMatchingFieldsConfigChange={onMatchingFieldsConfigChange}
|
|
146
|
+
onDeleteField={onDeleteField}
|
|
147
|
+
hideConditionalLogic={hideConditionalLogic}
|
|
148
|
+
dateCalcOptions={dateCalcOptions}
|
|
149
|
+
businessCalendar={businessCalendar}
|
|
150
|
+
scheduleEditorContainer={scheduleEditorContainer}
|
|
151
|
+
onRequestFormFields={onRequestFormFields}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
158
154
|
</Flex>
|
|
159
|
-
<div className="dte-field-config-panel-content">
|
|
160
|
-
<FieldConfigPanel
|
|
161
|
-
dataModel={dataModel}
|
|
162
|
-
documentFields={documentFields}
|
|
163
|
-
field={selectedField}
|
|
164
|
-
formFieldsByFormId={formFieldsByFormId}
|
|
165
|
-
forms={forms}
|
|
166
|
-
formulaLoadingFormId={formulaLoadingFormId}
|
|
167
|
-
recipients={recipients}
|
|
168
|
-
onFieldConfigChange={onFieldConfigChange}
|
|
169
|
-
onMatchingFieldsConfigChange={onMatchingFieldsConfigChange}
|
|
170
|
-
onDeleteField={onDeleteField}
|
|
171
|
-
hideConditionalLogic={hideConditionalLogic}
|
|
172
|
-
dateCalcOptions={dateCalcOptions}
|
|
173
|
-
businessCalendar={businessCalendar}
|
|
174
|
-
onRequestFormFields={onRequestFormFields}
|
|
175
|
-
/>
|
|
176
|
-
</div>
|
|
177
155
|
</Flex>
|
|
178
156
|
);
|
|
179
157
|
};
|
|
@@ -41,6 +41,11 @@ interface FieldConfigPanelProps {
|
|
|
41
41
|
dateCalcOptions?: DateCalcOptions;
|
|
42
42
|
/** The business-calendar toggle/picker/drawer state and callbacks for this field. */
|
|
43
43
|
businessCalendar?: ScheduleEditorContext;
|
|
44
|
+
/**
|
|
45
|
+
* Where an e-sign `dateSignedOffset` field's schedule form is portaled to instead of a drawer.
|
|
46
|
+
* A calculated field's schedule form always uses the drawer.
|
|
47
|
+
*/
|
|
48
|
+
scheduleEditorContainer?: HTMLElement | null;
|
|
44
49
|
onDeleteField(): void;
|
|
45
50
|
onFieldConfigChange(updates: Partial<PdfField>): void;
|
|
46
51
|
onMatchingFieldsConfigChange?(
|
|
@@ -65,6 +70,7 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
65
70
|
onMatchingFieldsConfigChange,
|
|
66
71
|
onRequestFormFields,
|
|
67
72
|
recipients,
|
|
73
|
+
scheduleEditorContainer,
|
|
68
74
|
}) => {
|
|
69
75
|
const recipientOptions = useMemo(
|
|
70
76
|
() =>
|
|
@@ -100,11 +106,14 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
|
|
|
100
106
|
* Reuses the catalog/CRUD half of `businessCalendar` (shared tenant schedules, same as a
|
|
101
107
|
* calculated field's), but overrides the enabled/selected-schedule half to read and write
|
|
102
108
|
* `ESignDateSignedOffsetSettings` instead of a `scheduleBindings` entry, since the toggle here
|
|
103
|
-
* applies to every `dateSignedOffset` field in the document, same as `rules` does.
|
|
109
|
+
* applies to every `dateSignedOffset` field in the document, same as `rules` does. It is also
|
|
110
|
+
* the only context given `scheduleEditorContainer`, so only this field's schedule form leaves
|
|
111
|
+
* the drawer.
|
|
104
112
|
*/
|
|
105
113
|
const dateSignedOffsetData = field.data as ESignDateSignedOffsetSettings;
|
|
106
114
|
const eSignBusinessCalendar: ScheduleEditorContext | undefined = businessCalendar && {
|
|
107
115
|
...businessCalendar,
|
|
116
|
+
editorContainer: scheduleEditorContainer,
|
|
108
117
|
enabled: !!dateSignedOffsetData?.businessCalendarEnabled,
|
|
109
118
|
selectedScheduleId: dateSignedOffsetData?.scheduleId,
|
|
110
119
|
onToggle: businessCalendarEnabled =>
|
|
@@ -3,11 +3,7 @@ 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
4
|
import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
|
|
5
5
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_DATE_CALCULATED_FORMAT,
|
|
8
|
-
DEFAULT_NUMBER_FORMAT,
|
|
9
|
-
MODAL_CONTENT_MAX_HEIGHT,
|
|
10
|
-
} from '../../constants';
|
|
6
|
+
import { DEFAULT_DATE_CALCULATED_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../constants';
|
|
11
7
|
import { useFormulaEditor } from '../../hooks';
|
|
12
8
|
import {
|
|
13
9
|
type CalculatedFieldFormat,
|
|
@@ -305,16 +301,17 @@ export const FormulaModal: FC<FormulaModalProps> = ({
|
|
|
305
301
|
parsedTokens.length > 0;
|
|
306
302
|
|
|
307
303
|
return (
|
|
308
|
-
<Dialog open onClose={handleClose} size="xlarge">
|
|
304
|
+
<Dialog open onClose={handleClose} size="xlarge" fullScreen>
|
|
309
305
|
<Dialog.Header>Formula Builder</Dialog.Header>
|
|
310
306
|
<Divider />
|
|
311
|
-
<Dialog.Content>
|
|
307
|
+
<Dialog.Content style={{ overflowY: 'auto', paddingBlock: 0 }}>
|
|
312
308
|
<Flex
|
|
313
309
|
flex={1}
|
|
314
310
|
alignItems="stretch"
|
|
315
311
|
gap={2}
|
|
316
312
|
style={{
|
|
317
|
-
|
|
313
|
+
flexGrow: 1,
|
|
314
|
+
minHeight: 0,
|
|
318
315
|
overflowY: 'auto',
|
|
319
316
|
}}
|
|
320
317
|
>
|
|
@@ -46,7 +46,7 @@ export interface PdfEditorProps {
|
|
|
46
46
|
/**
|
|
47
47
|
* Date calculated fields whose custom business-day exclusion toggle is on, and the schedule
|
|
48
48
|
* each uses (`PdfField.id` as `fieldKey`), as `PdfView`'s. Also drives the Formula Builder's
|
|
49
|
-
* toggle/picker for the selected field
|
|
49
|
+
* toggle/picker for the selected field, which are shown only with `onScheduleBindingsChange`.
|
|
50
50
|
*/
|
|
51
51
|
scheduleBindings?: FieldScheduleBinding[];
|
|
52
52
|
/** The tenant's known holidays, for the schedule editor's checklist. */
|
|
@@ -67,13 +67,18 @@ export interface PdfEditorProps {
|
|
|
67
67
|
* `PUT api/holidays/templates/{id}/bindings`). `PdfEditor` does not persist this itself —
|
|
68
68
|
* the host is expected to save it (and the bindings are typically saved separately from the
|
|
69
69
|
* template's own fields).
|
|
70
|
+
*
|
|
71
|
+
* Passing it also turns the schedule UI on: without it the Formula Builder has no "Exclude
|
|
72
|
+
* weekends & holidays" switch and the e-sign day offset has no schedule. A host that cannot save
|
|
73
|
+
* bindings (for example one without a tenant) leaves it out. It is not called for the e-sign
|
|
74
|
+
* schedule, which is stored in the field's `data`.
|
|
70
75
|
*/
|
|
71
76
|
onScheduleBindingsChange?(bindings: FieldScheduleBinding[]): void;
|
|
72
77
|
/**
|
|
73
|
-
* Creates a tenant schedule from the "Create New Schedule" drawer
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* (for example a duplicate-name conflict).
|
|
78
|
+
* Creates a tenant schedule from the "Create New Schedule" form (a drawer, or inside the Field
|
|
79
|
+
* Configuration panel for an e-sign `dateSignedOffset` field). Always creates a new schedule — it never overwrites a
|
|
80
|
+
* selected one. On success the created schedule is auto-selected in the picker. A rejected
|
|
81
|
+
* promise's `message` is shown as the form's error (for example a duplicate-name conflict).
|
|
77
82
|
*/
|
|
78
83
|
onCreateSchedule?(request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
79
84
|
/**
|
|
@@ -84,10 +89,8 @@ export interface PdfEditorProps {
|
|
|
84
89
|
onUpdateSchedule?(id: number, request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
85
90
|
/**
|
|
86
91
|
* Permanently deletes a tenant schedule, from the picker's "Delete" action (DTE's
|
|
87
|
-
* `DELETE api/holidays/schedules/{id}`).
|
|
88
|
-
*
|
|
89
|
-
* does not itself update other fields' `scheduleBindings`; the host should drop bindings to
|
|
90
|
-
* a deleted schedule when it next saves them.
|
|
92
|
+
* `DELETE api/holidays/schedules/{id}`). On success every field bound to it, the current one
|
|
93
|
+
* included, moves to the default schedule (toggle still on) through `onScheduleBindingsChange`.
|
|
91
94
|
*/
|
|
92
95
|
onDeleteSchedule?(id: number): Promise<void>;
|
|
93
96
|
onLoadSuccess?(numPages: number): void;
|
|
@@ -95,7 +95,7 @@ export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
|
|
|
95
95
|
icon: EventAvailableIcon,
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
|
-
label: 'Calculated
|
|
98
|
+
label: 'Calculated Signing Date',
|
|
99
99
|
type: FieldTypeEnum.eSign,
|
|
100
100
|
subType: ESignFieldType.dateSignedOffset,
|
|
101
101
|
icon: IconAppointmentsIcon,
|
package/src/interface/types.ts
CHANGED
|
@@ -283,15 +283,11 @@ export type {
|
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
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`)
|
|
287
|
-
*
|
|
286
|
+
* uses (DTE's bindings, full-replace via `PUT api/holidays/templates/{id}/bindings`), shared with
|
|
287
|
+
* `@servicetitan/dte-unlayer`. Here `fieldKey` is `PdfField.id`, and a field with no entry uses
|
|
288
|
+
* plain calendar-day arithmetic.
|
|
288
289
|
*/
|
|
289
|
-
export
|
|
290
|
-
/** `PdfField.id`. */
|
|
291
|
-
fieldKey: string;
|
|
292
|
-
/** One of the tenant's schedules, or undefined for the default schedule. */
|
|
293
|
-
scheduleId?: number;
|
|
294
|
-
}
|
|
290
|
+
export type { FieldScheduleBinding } from '@servicetitan/dte-shared';
|
|
295
291
|
|
|
296
292
|
/** Format options for a calculated field with a numeric result (number, currency, percent) */
|
|
297
293
|
export interface CalculatedFormatForNumber {
|
|
@@ -33,3 +33,27 @@
|
|
|
33
33
|
padding: var(--spacing-2);
|
|
34
34
|
flex: 1;
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* The schedule form of an e-sign date offset is portaled into this slot. While it has content it
|
|
39
|
+
* takes the place of the panel (the `:empty` and `~` selectors need the slot to come first and to
|
|
40
|
+
* hold nothing, not even whitespace, when it is closed).
|
|
41
|
+
*/
|
|
42
|
+
.dte-field-config-panel-editor-slot {
|
|
43
|
+
flex: 1;
|
|
44
|
+
padding: var(--spacing-2);
|
|
45
|
+
height: 100%;
|
|
46
|
+
overflow: auto;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.dte-field-config-panel-editor-slot:empty {
|
|
50
|
+
display: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dte-field-config-panel-overlay-inner {
|
|
54
|
+
flex: 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.dte-field-config-panel-editor-slot:not(:empty) ~ .dte-field-config-panel-overlay-inner {
|
|
58
|
+
display: none;
|
|
59
|
+
}
|
|
@@ -5,6 +5,5 @@ export * from './expression.utils';
|
|
|
5
5
|
export * from './format-calculated-result.utils';
|
|
6
6
|
export * from './formula-types';
|
|
7
7
|
export * from './render-formula.utils';
|
|
8
|
-
export * from './schedule-bindings.utils';
|
|
9
8
|
export * from './validate-formula.utils';
|
|
10
9
|
export * from './form-fields.utils';
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type FieldScheduleBinding } from '../../interface/types';
|
|
2
|
-
/**
|
|
3
|
-
* Replaces `fieldKey`'s binding: enabling adds it (default schedule, `scheduleId` undefined);
|
|
4
|
-
* disabling removes it. Every other field's binding is returned unchanged.
|
|
5
|
-
*/
|
|
6
|
-
export declare function setFieldScheduleEnabled(bindings: FieldScheduleBinding[], fieldKey: string, enabled: boolean): FieldScheduleBinding[];
|
|
7
|
-
/**
|
|
8
|
-
* Sets `fieldKey`'s selected schedule (`scheduleId` undefined for the default schedule),
|
|
9
|
-
* enabling its toggle if it wasn't already on. Every other field's binding is returned unchanged.
|
|
10
|
-
*/
|
|
11
|
-
export declare function setFieldSchedule(bindings: FieldScheduleBinding[], fieldKey: string, scheduleId: number | undefined): FieldScheduleBinding[];
|
|
12
|
-
//# sourceMappingURL=schedule-bindings.utils.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schedule-bindings.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/formula/schedule-bindings.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAElE;;;GAGG;AACH,wBAAgB,uBAAuB,CACnC,QAAQ,EAAE,oBAAoB,EAAE,EAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,GACjB,oBAAoB,EAAE,CAGxB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC5B,QAAQ,EAAE,oBAAoB,EAAE,EAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC/B,oBAAoB,EAAE,CAGxB"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Replaces `fieldKey`'s binding: enabling adds it (default schedule, `scheduleId` undefined);
|
|
3
|
-
* disabling removes it. Every other field's binding is returned unchanged.
|
|
4
|
-
*/
|
|
5
|
-
export function setFieldScheduleEnabled(bindings, fieldKey, enabled) {
|
|
6
|
-
const withoutField = bindings.filter(b => b.fieldKey !== fieldKey);
|
|
7
|
-
return enabled ? [...withoutField, { fieldKey }] : withoutField;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Sets `fieldKey`'s selected schedule (`scheduleId` undefined for the default schedule),
|
|
11
|
-
* enabling its toggle if it wasn't already on. Every other field's binding is returned unchanged.
|
|
12
|
-
*/
|
|
13
|
-
export function setFieldSchedule(bindings, fieldKey, scheduleId) {
|
|
14
|
-
const withoutField = bindings.filter(b => b.fieldKey !== fieldKey);
|
|
15
|
-
return [...withoutField, { fieldKey, scheduleId }];
|
|
16
|
-
}
|
|
17
|
-
//# sourceMappingURL=schedule-bindings.utils.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schedule-bindings.utils.js","sourceRoot":"","sources":["../../../src/utils/formula/schedule-bindings.utils.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACnC,QAAgC,EAChC,QAAgB,EAChB,OAAgB;IAEhB,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;IACnE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC5B,QAAgC,EAChC,QAAgB,EAChB,UAA8B;IAE9B,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,YAAY,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;AACvD,CAAC"}
|