@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.
Files changed (69) hide show
  1. package/README.md +26 -8
  2. package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
  3. package/dist/components/field-config-panel/advanced-settings.js +1 -0
  4. package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
  5. package/dist/components/field-config-panel/e-sign-offset-rule-row.js +1 -1
  6. package/dist/components/field-config-panel/e-sign-offset-rule-row.js.map +1 -1
  7. package/dist/components/field-config-panel/e-sign-options-modal.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/e-sign-options-modal.js +1 -0
  9. package/dist/components/field-config-panel/e-sign-options-modal.js.map +1 -1
  10. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  11. package/dist/components/field-config-panel/field-config-panel-overlay.js +23 -33
  12. package/dist/components/field-config-panel/field-config-panel-overlay.js.map +1 -1
  13. package/dist/components/field-config-panel/field-config-panel.d.ts +5 -0
  14. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  15. package/dist/components/field-config-panel/field-config-panel.js +5 -2
  16. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  17. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
  18. package/dist/components/field-config-panel/formula-modal.js +4 -3
  19. package/dist/components/field-config-panel/formula-modal.js.map +1 -1
  20. package/dist/components/pdf-editor/pdf-editor.d.ts +12 -9
  21. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  22. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  23. package/dist/constants/field.constants.js +1 -1
  24. package/dist/interface/types.d.ts +4 -8
  25. package/dist/interface/types.d.ts.map +1 -1
  26. package/dist/interface/types.js.map +1 -1
  27. package/dist/styles/field-config-panel-overlay.css +24 -0
  28. package/dist/utils/formula/index.d.ts +0 -1
  29. package/dist/utils/formula/index.d.ts.map +1 -1
  30. package/dist/utils/formula/index.js +0 -1
  31. package/dist/utils/formula/index.js.map +1 -1
  32. package/dist/vendor/dte-shared/business-calendar-validation.utils.d.ts +3 -0
  33. package/dist/vendor/dte-shared/business-calendar-validation.utils.js +4 -0
  34. package/dist/vendor/dte-shared/components/business-calendar-section.d.ts +1 -1
  35. package/dist/vendor/dte-shared/components/business-calendar-section.js +3 -2
  36. package/dist/vendor/dte-shared/components/schedule-drawer.d.ts +5 -3
  37. package/dist/vendor/dte-shared/components/schedule-drawer.js +19 -12
  38. package/dist/vendor/dte-shared/components/schedule-picker.js +10 -10
  39. package/dist/vendor/dte-shared/constants.d.ts +21 -0
  40. package/dist/vendor/dte-shared/constants.js +24 -0
  41. package/dist/vendor/dte-shared/field-schedule-editor-context.d.ts +31 -0
  42. package/dist/vendor/dte-shared/field-schedule-editor-context.js +110 -0
  43. package/dist/vendor/dte-shared/index.d.ts +2 -0
  44. package/dist/vendor/dte-shared/index.js +2 -0
  45. package/dist/vendor/dte-shared/schedule-bindings.utils.d.ts +22 -0
  46. package/dist/vendor/dte-shared/schedule-bindings.utils.js +31 -0
  47. package/dist/vendor/dte-shared/styles/schedule.css +2 -2
  48. package/dist/vendor/dte-shared/types.d.ts +62 -0
  49. package/package.json +2 -2
  50. package/src/components/field-config-panel/__tests__/field-config-panel-overlay.test.tsx +200 -0
  51. package/src/components/field-config-panel/__tests__/schedule-editor-placement.test.tsx +289 -0
  52. package/src/components/field-config-panel/__tests__/schedule-switch-visibility.test.tsx +206 -0
  53. package/src/components/field-config-panel/advanced-settings.tsx +1 -0
  54. package/src/components/field-config-panel/e-sign-offset-rule-row.tsx +1 -1
  55. package/src/components/field-config-panel/e-sign-options-modal.tsx +1 -0
  56. package/src/components/field-config-panel/field-config-panel-overlay.tsx +69 -91
  57. package/src/components/field-config-panel/field-config-panel.tsx +10 -1
  58. package/src/components/field-config-panel/formula-modal.tsx +5 -8
  59. package/src/components/pdf-editor/pdf-editor.tsx +12 -9
  60. package/src/constants/field.constants.ts +1 -1
  61. package/src/interface/types.ts +4 -8
  62. package/src/styles/field-config-panel-overlay.css +24 -0
  63. package/src/utils/formula/index.ts +0 -1
  64. package/dist/utils/formula/schedule-bindings.utils.d.ts +0 -12
  65. package/dist/utils/formula/schedule-bindings.utils.d.ts.map +0 -1
  66. package/dist/utils/formula/schedule-bindings.utils.js +0 -17
  67. package/dist/utils/formula/schedule-bindings.utils.js.map +0 -1
  68. package/src/utils/formula/__tests__/schedule-bindings.utils.test.ts +0 -73
  69. 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
- import { type ScheduleEditorContext } from '@servicetitan/dte-shared';
4
- import { FC, useCallback, useMemo } from 'react';
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, setFieldSchedule, setFieldScheduleEnabled } from '../../utils';
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 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
- );
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
- <Flex
144
- className="dte-field-config-panel-header"
145
- alignItems="center"
146
- justifyContent="space-between"
147
- >
148
- <Text variant="headline" el="h6" size="small">
149
- Field Configuration
150
- </Text>
151
- <Button
152
- appearance="ghost"
153
- size="small"
154
- onClick={onDeselectField}
155
- aria-label="Close"
156
- icon={IconClose}
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
- height: MODAL_CONTENT_MAX_HEIGHT,
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. 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).
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}`). 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.
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 Singing Date',
98
+ label: 'Calculated Signing Date',
99
99
  type: FieldTypeEnum.eSign,
100
100
  subType: ESignFieldType.dateSignedOffset,
101
101
  icon: IconAppointmentsIcon,
@@ -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`). A field with
287
- * no entry here uses the default calendar with the toggle off (plain calendar-day arithmetic).
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 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
- }
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"}