@servicetitan/dte-unlayer 0.158.0 → 0.159.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.
Files changed (147) hide show
  1. package/dist/api-core.js +22 -29
  2. package/dist/api-core.js.map +1 -1
  3. package/dist/api-custom-tools.js +23 -29
  4. package/dist/api-custom-tools.js.map +1 -1
  5. package/dist/business-calendar/BusinessCalendarDrawer.d.ts +19 -0
  6. package/dist/business-calendar/BusinessCalendarDrawer.d.ts.map +1 -0
  7. package/dist/business-calendar/BusinessCalendarDrawer.js +41 -0
  8. package/dist/business-calendar/BusinessCalendarDrawer.js.map +1 -0
  9. package/dist/business-calendar/business-calendar-requests.d.ts +25 -0
  10. package/dist/business-calendar/business-calendar-requests.d.ts.map +1 -0
  11. package/dist/business-calendar/business-calendar-requests.js +46 -0
  12. package/dist/business-calendar/business-calendar-requests.js.map +1 -0
  13. package/dist/business-calendar/business-calendar-state.d.ts +22 -0
  14. package/dist/business-calendar/business-calendar-state.d.ts.map +1 -0
  15. package/dist/business-calendar/business-calendar-state.js +74 -0
  16. package/dist/business-calendar/business-calendar-state.js.map +1 -0
  17. package/dist/business-calendar/useBusinessCalendarRequests.d.ts +15 -0
  18. package/dist/business-calendar/useBusinessCalendarRequests.d.ts.map +1 -0
  19. package/dist/business-calendar/useBusinessCalendarRequests.js +30 -0
  20. package/dist/business-calendar/useBusinessCalendarRequests.js.map +1 -0
  21. package/dist/display-conditions/ConditionGroup.js +15 -89
  22. package/dist/display-conditions/ConditionGroup.js.map +1 -1
  23. package/dist/display-conditions/ConditionGroupsSection.js +2 -70
  24. package/dist/display-conditions/ConditionGroupsSection.js.map +1 -1
  25. package/dist/display-conditions/ConditionRow.js +63 -205
  26. package/dist/display-conditions/ConditionRow.js.map +1 -1
  27. package/dist/display-conditions/DisplayConditionModal.js +40 -190
  28. package/dist/display-conditions/DisplayConditionModal.js.map +1 -1
  29. package/dist/display-conditions/LookupValueControl.js +42 -105
  30. package/dist/display-conditions/LookupValueControl.js.map +1 -1
  31. package/dist/display-conditions/SeparatorWithChip.js +1 -10
  32. package/dist/display-conditions/SeparatorWithChip.js.map +1 -1
  33. package/dist/display-conditions/constants.js +15 -20
  34. package/dist/display-conditions/constants.js.map +1 -1
  35. package/dist/display-conditions/displayConditionController.js +5 -8
  36. package/dist/display-conditions/displayConditionController.js.map +1 -1
  37. package/dist/display-conditions/lookupOptionsController.js +16 -16
  38. package/dist/display-conditions/lookupOptionsController.js.map +1 -1
  39. package/dist/display-conditions/nunjucks.js +98 -116
  40. package/dist/display-conditions/nunjucks.js.map +1 -1
  41. package/dist/display-conditions/schemaDataPoints.js +13 -11
  42. package/dist/display-conditions/schemaDataPoints.js.map +1 -1
  43. package/dist/display-conditions/types.js +23 -67
  44. package/dist/display-conditions/types.js.map +1 -1
  45. package/dist/display-conditions/useConditionForms.js +38 -56
  46. package/dist/display-conditions/useConditionForms.js.map +1 -1
  47. package/dist/display-conditions/useLookupOptions.js +24 -44
  48. package/dist/display-conditions/useLookupOptions.js.map +1 -1
  49. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js +37 -150
  50. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js.map +1 -1
  51. package/dist/e-sign-offset/ESignOffsetRuleRow.js +12 -73
  52. package/dist/e-sign-offset/ESignOffsetRuleRow.js.map +1 -1
  53. package/dist/e-sign-offset/eSignOffsetController.js +5 -8
  54. package/dist/e-sign-offset/eSignOffsetController.js.map +1 -1
  55. package/dist/editor-core-source.d.ts +2 -2
  56. package/dist/editor-core-source.d.ts.map +1 -1
  57. package/dist/editor-core-source.js +3 -3
  58. package/dist/editor-core-source.js.map +1 -1
  59. package/dist/editor-core.js +2 -2
  60. package/dist/editor-core.js.map +1 -1
  61. package/dist/editor.d.ts.map +1 -1
  62. package/dist/editor.js +56 -118
  63. package/dist/editor.js.map +1 -1
  64. package/dist/image-upload-error.js +3 -3
  65. package/dist/image-upload-error.js.map +1 -1
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +0 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/loadScript.js +7 -8
  71. package/dist/loadScript.js.map +1 -1
  72. package/dist/log.js +2 -3
  73. package/dist/log.js.map +1 -1
  74. package/dist/merge-tags.js +18 -17
  75. package/dist/merge-tags.js.map +1 -1
  76. package/dist/shared/configs.js +1 -2
  77. package/dist/shared/configs.js.map +1 -1
  78. package/dist/shared/const.d.ts +70 -0
  79. package/dist/shared/const.d.ts.map +1 -1
  80. package/dist/shared/const.js +13 -4
  81. package/dist/shared/const.js.map +1 -1
  82. package/dist/shared/date-calc.js +47 -38
  83. package/dist/shared/date-calc.js.map +1 -1
  84. package/dist/shared/e-sign-offset.js +46 -63
  85. package/dist/shared/e-sign-offset.js.map +1 -1
  86. package/dist/shared/edit-icon.js +0 -1
  87. package/dist/shared/edit-icon.js.map +1 -1
  88. package/dist/shared/fonts.js +16 -17
  89. package/dist/shared/fonts.js.map +1 -1
  90. package/dist/shared/forms.js +16 -38
  91. package/dist/shared/forms.js.map +1 -1
  92. package/dist/shared/instance-id.js +6 -9
  93. package/dist/shared/instance-id.js.map +1 -1
  94. package/dist/shared/nunjucks-helpers.js +2 -5
  95. package/dist/shared/nunjucks-helpers.js.map +1 -1
  96. package/dist/shared/schema.js +52 -96
  97. package/dist/shared/schema.js.map +1 -1
  98. package/dist/shared/tools.js +6 -13
  99. package/dist/shared/tools.js.map +1 -1
  100. package/dist/store.d.ts +15 -1
  101. package/dist/store.d.ts.map +1 -1
  102. package/dist/store.js +851 -548
  103. package/dist/store.js.map +1 -1
  104. package/dist/tools.js +133 -143
  105. package/dist/tools.js.map +1 -1
  106. package/dist/unlayer-interface.d.ts +15 -4
  107. package/dist/unlayer-interface.d.ts.map +1 -1
  108. package/dist/unlayer-interface.js +1 -2
  109. package/dist/unlayer-interface.js.map +1 -1
  110. package/dist/unlayer.js +86 -113
  111. package/dist/unlayer.js.map +1 -1
  112. package/dist/vendor/dte-shared/business-calendar-validation.utils.d.ts +25 -0
  113. package/dist/vendor/dte-shared/business-calendar-validation.utils.js +80 -0
  114. package/dist/vendor/dte-shared/components/business-calendar-section.d.ts +15 -0
  115. package/dist/vendor/dte-shared/components/business-calendar-section.js +14 -0
  116. package/dist/vendor/dte-shared/components/schedule-drawer.d.ts +18 -0
  117. package/dist/vendor/dte-shared/components/schedule-drawer.js +119 -0
  118. package/dist/vendor/dte-shared/components/schedule-picker.d.ts +8 -0
  119. package/dist/vendor/dte-shared/components/schedule-picker.js +88 -0
  120. package/dist/vendor/dte-shared/constants.d.ts +31 -0
  121. package/dist/vendor/dte-shared/constants.js +34 -0
  122. package/dist/vendor/dte-shared/field-schedule-editor-context.d.ts +31 -0
  123. package/dist/vendor/dte-shared/field-schedule-editor-context.js +110 -0
  124. package/dist/vendor/dte-shared/index.d.ts +8 -0
  125. package/dist/vendor/dte-shared/index.js +7 -0
  126. package/dist/vendor/dte-shared/schedule-bindings.utils.d.ts +22 -0
  127. package/dist/vendor/dte-shared/schedule-bindings.utils.js +31 -0
  128. package/dist/vendor/dte-shared/styles/schedule.css +19 -0
  129. package/dist/vendor/dte-shared/types.d.ts +129 -0
  130. package/dist/vendor/dte-shared/types.js +1 -0
  131. package/package.json +7 -1
  132. package/src/__tests__/business-calendar-drawer.test.tsx +183 -0
  133. package/src/__tests__/business-calendar-requests.test.ts +166 -0
  134. package/src/__tests__/business-calendar-state.test.tsx +142 -0
  135. package/src/__tests__/store.business-calendar.test.ts +185 -0
  136. package/src/__tests__/unlayer.e-sign-types.test.ts +30 -0
  137. package/src/business-calendar/BusinessCalendarDrawer.tsx +76 -0
  138. package/src/business-calendar/business-calendar-requests.ts +80 -0
  139. package/src/business-calendar/business-calendar-state.ts +113 -0
  140. package/src/business-calendar/useBusinessCalendarRequests.ts +49 -0
  141. package/src/editor-core-source.ts +2 -2
  142. package/src/editor.tsx +15 -0
  143. package/src/index.ts +10 -0
  144. package/src/shared/const.ts +59 -0
  145. package/src/store.ts +67 -5
  146. package/src/unlayer-interface.tsx +15 -4
  147. package/src/unlayer.tsx +5 -5
@@ -0,0 +1,185 @@
1
+ import type { UnlayerEventBusinessCalendarState } from '../shared/const';
2
+ import { UnlayerStore } from '../store';
3
+
4
+ const editorMock = {
5
+ addEventListener: jest.fn(),
6
+ removeEventListener: jest.fn(),
7
+ registerCallback: jest.fn(),
8
+ unregisterCallback: jest.fn(),
9
+ setMergeTags: jest.fn(),
10
+ loadDesign: jest.fn(),
11
+ setBodyValues: jest.fn(),
12
+ };
13
+
14
+ jest.mock('../loadScript', () => ({ loadScript: jest.fn().mockResolvedValue(undefined) }));
15
+ jest.mock('../unlayer', () => ({
16
+ ...jest.requireActual('../unlayer'),
17
+ createUnlayerEditor: jest.fn(() => editorMock),
18
+ }));
19
+
20
+ const offState: UnlayerEventBusinessCalendarState = {
21
+ enabled: true,
22
+ bindings: {},
23
+ schedules: [],
24
+ canDeleteSchedule: true,
25
+ fieldCalendars: {},
26
+ };
27
+
28
+ const onState: UnlayerEventBusinessCalendarState = {
29
+ enabled: true,
30
+ bindings: { 'field-a': { scheduleId: 7 } },
31
+ schedules: [
32
+ {
33
+ id: 7,
34
+ name: 'Sundays only',
35
+ isDefault: false,
36
+ calendar: { weekendDays: [0], knownHolidays: [], customDates: [] },
37
+ },
38
+ ],
39
+ canDeleteSchedule: true,
40
+ fieldCalendars: {
41
+ 'field-a': { weekendDays: [0], holidays: [], firstYear: 2025, lastYear: 2027 },
42
+ },
43
+ holidays: ['2026-07-03'],
44
+ };
45
+
46
+ /** A message posted to the parent window: by the tools core in Unlayer's iframe, by default. */
47
+ const postFromIframe = (
48
+ type: string,
49
+ data?: unknown,
50
+ source: Window | null = document.querySelector('iframe')?.contentWindow ?? null,
51
+ ) =>
52
+ window.dispatchEvent(
53
+ new MessageEvent('message', { data: { '-dte-message-from': { type, data } }, source }),
54
+ );
55
+
56
+ describe('UnlayerStore business-day schedules', () => {
57
+ let store: UnlayerStore;
58
+ let container: HTMLDivElement;
59
+ let postMessage: jest.SpyInstance;
60
+
61
+ /** Types of the messages sent to the iframe, in order. */
62
+ const sentTypes = () =>
63
+ postMessage.mock.calls.map(([message]) => message['-dte-message-to'].type);
64
+ const sentData = (type: string) =>
65
+ postMessage.mock.calls
66
+ .map(([message]) => message['-dte-message-to'])
67
+ .filter(message => message.type === type)
68
+ .map(message => message.data);
69
+
70
+ beforeEach(async () => {
71
+ jest.useFakeTimers();
72
+ jest.clearAllMocks();
73
+ (window as any).unlayer = {};
74
+ container = document.createElement('div');
75
+ container.appendChild(document.createElement('iframe'));
76
+ document.body.appendChild(container);
77
+
78
+ store = new UnlayerStore({ tools: [] });
79
+ await store.init(container);
80
+ // The window message listener is added on the next tick (not the tools-ready watchdog).
81
+ jest.advanceTimersByTime(0);
82
+ postMessage = jest.spyOn(container.querySelector('iframe')!.contentWindow!, 'postMessage');
83
+ });
84
+
85
+ afterEach(() => {
86
+ store.destroy();
87
+ container.remove();
88
+ jest.useRealTimers();
89
+ });
90
+
91
+ test('keeps the state until the tools core has loaded, then sends it right after --config', () => {
92
+ store.setBusinessCalendarState(onState);
93
+ expect(sentTypes()).toEqual([]);
94
+
95
+ postFromIframe('--core-loaded');
96
+
97
+ expect(sentTypes()).toEqual(['--config', '--business-calendar-state']);
98
+ expect(sentData('--business-calendar-state')).toEqual([onState]);
99
+ });
100
+
101
+ test('sends a changed state at once, and skips an unchanged one', () => {
102
+ postFromIframe('--core-loaded');
103
+ postMessage.mockClear();
104
+
105
+ store.setBusinessCalendarState(offState);
106
+ store.setBusinessCalendarState({ ...offState });
107
+ store.setBusinessCalendarState(onState);
108
+
109
+ expect(sentData('--business-calendar-state')).toEqual([offState, onState]);
110
+ });
111
+
112
+ test('sends the state again when the tools core loads again', () => {
113
+ store.setBusinessCalendarState(onState);
114
+ postFromIframe('--core-loaded');
115
+ postMessage.mockClear();
116
+
117
+ postFromIframe('--core-loaded');
118
+
119
+ expect(sentData('--business-calendar-state')).toEqual([onState]);
120
+ });
121
+
122
+ test('sends a plain copy, not the object it was given', () => {
123
+ postFromIframe('--core-loaded');
124
+ store.setBusinessCalendarState(onState);
125
+
126
+ const [sent] = sentData('--business-calendar-state');
127
+ expect(sent).toEqual(onState);
128
+ expect(sent).not.toBe(onState);
129
+ });
130
+
131
+ test('passes schedule requests to the host only when they name an action and a field', () => {
132
+ const onRequest = jest.fn();
133
+ store.setOnBusinessCalendarRequest(onRequest);
134
+
135
+ postFromIframe('--business-calendar-request', { action: 'toggle', enabled: true });
136
+ postFromIframe('--business-calendar-request', { fieldKey: 'field-a' });
137
+ postFromIframe('--business-calendar-request', {
138
+ action: 'toggle',
139
+ fieldKey: 'field-a',
140
+ enabled: true,
141
+ });
142
+ postFromIframe('--business-calendar-request', {
143
+ action: 'select',
144
+ fieldKey: 'esign-a',
145
+ scheduleId: 7,
146
+ });
147
+
148
+ expect(onRequest.mock.calls).toEqual([
149
+ [{ action: 'toggle', fieldKey: 'field-a', enabled: true }],
150
+ [{ action: 'select', fieldKey: 'esign-a', scheduleId: 7 }],
151
+ ]);
152
+ });
153
+
154
+ test("ignores schedule requests that don't come from the editor's iframe", () => {
155
+ const onRequest = jest.fn();
156
+ store.setOnBusinessCalendarRequest(onRequest);
157
+ const otherFrame = document.createElement('iframe');
158
+ document.body.appendChild(otherFrame);
159
+
160
+ const request = { action: 'delete', fieldKey: 'field-a', scheduleId: 7, requestId: 'r1' };
161
+ postFromIframe('--business-calendar-request', request, otherFrame.contentWindow);
162
+ postFromIframe('--business-calendar-request', request, window);
163
+ postFromIframe('--business-calendar-request', request, null);
164
+
165
+ expect(onRequest).not.toHaveBeenCalled();
166
+ });
167
+
168
+ test('sends the outcome of a delete request to the iframe', () => {
169
+ store.sendBusinessCalendarResult({ requestId: 'r1', error: 'in use' });
170
+
171
+ expect(sentData('--business-calendar-result')).toEqual([
172
+ { requestId: 'r1', error: 'in use' },
173
+ ]);
174
+ });
175
+
176
+ test('registers the e-sign type labels with the tools', () => {
177
+ postFromIframe('--data-loaded');
178
+
179
+ const [register] = sentData('--register');
180
+ expect(register.eSignFieldTypes).toContain('Date Signed Offset');
181
+ expect(register.eSignFieldTypeLabels).toEqual({
182
+ dateSignedOffset: 'Calculated Signing Date',
183
+ });
184
+ });
185
+ });
@@ -0,0 +1,30 @@
1
+ import { E_SIGN_DATE_SIGNED_OFFSET_TYPE } from '../shared/const';
2
+ import { createUnlayerEditor } from '../unlayer';
3
+
4
+ describe('createUnlayerEditor e-sign field types', () => {
5
+ const createEditor = jest.fn(() => ({ registerCallback: jest.fn() }));
6
+
7
+ beforeEach(() => {
8
+ createEditor.mockClear();
9
+ (window as any).unlayer = { createEditor };
10
+ });
11
+
12
+ const eSignTypeOptions = (): { label: string; value: string }[] => {
13
+ const container = document.createElement('div');
14
+ container.id = 'editor';
15
+ createUnlayerEditor(container, {
16
+ tools: [],
17
+ eSignFieldTypes: ['Signature', 'Date Signed', 'Date Signed Offset'],
18
+ });
19
+ const [[config]] = createEditor.mock.calls as unknown as [[any]];
20
+ return config.tools['custom#e-sign'].properties.eSignFieldType.editor.data.options;
21
+ };
22
+
23
+ test('shows "Calculated Signing Date" for the date signed offset type, keeping its value', () => {
24
+ expect(eSignTypeOptions()).toEqual([
25
+ { label: 'Signature', value: 'signature' },
26
+ { label: 'Date Signed', value: 'dateSigned' },
27
+ { label: 'Calculated Signing Date', value: E_SIGN_DATE_SIGNED_OFFSET_TYPE },
28
+ ]);
29
+ });
30
+ });
@@ -0,0 +1,76 @@
1
+ import { ThemeProvider } from '@servicetitan/anvil2';
2
+ // eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
3
+ import {
4
+ type BusinessCalendarHostProps,
5
+ type BusinessDaySchedule,
6
+ findSelectedSchedule,
7
+ ScheduleDrawer,
8
+ useFieldScheduleEditorContext,
9
+ } from '@servicetitan/dte-shared';
10
+ import { useEffect } from 'react';
11
+ import { createPortal } from 'react-dom';
12
+ import type { BusinessCalendarDrawerRequest } from './business-calendar-requests';
13
+
14
+ export interface BusinessCalendarDrawerProps {
15
+ /** The field and the schedule to edit (none: a new schedule), asked for from the iframe. */
16
+ request: BusinessCalendarDrawerRequest;
17
+ /** The editor's schedule props (`CreateUnlayerEditorProps`). */
18
+ host: BusinessCalendarHostProps;
19
+ /** Same contract as `DisplayConditionModalProps.portalContainer`. */
20
+ portalContainer?: Element | DocumentFragment | null;
21
+ onClose(): void;
22
+ }
23
+
24
+ /**
25
+ * The shared "Create New Schedule" / "Edit Schedule" drawer, opened in the parent window from a
26
+ * date field's inline schedule picker (a calculated field's, in the Formula Builder, or an e-sign
27
+ * "Date Signed Offset" field's, in its property panel), which runs inside the Unlayer iframe
28
+ * without the design system. As BYOD's picker does, a saved schedule is selected for the field.
29
+ */
30
+ export function BusinessCalendarDrawer({
31
+ host,
32
+ onClose,
33
+ portalContainer,
34
+ request,
35
+ }: Readonly<BusinessCalendarDrawerProps>) {
36
+ const businessCalendar = useFieldScheduleEditorContext(request.fieldKey, host);
37
+ const editingSchedule =
38
+ request.scheduleId === undefined
39
+ ? undefined
40
+ : findSelectedSchedule(host.schedules, request.scheduleId);
41
+
42
+ /*
43
+ * The schedule to edit was deleted meanwhile: close the request instead of turning the edit
44
+ * into a create, or of leaving it pending to open the drawer on its own later.
45
+ */
46
+ const scheduleMissing = request.scheduleId !== undefined && !editingSchedule;
47
+ useEffect(() => {
48
+ if (scheduleMissing) {
49
+ onClose();
50
+ }
51
+ }, [onClose, scheduleMissing]);
52
+
53
+ if (scheduleMissing) {
54
+ return null;
55
+ }
56
+
57
+ const handleSaved = (schedule: BusinessDaySchedule) => {
58
+ businessCalendar.onSelectSchedule(schedule.id);
59
+ onClose();
60
+ };
61
+
62
+ const drawer = (
63
+ <ScheduleDrawer
64
+ businessCalendar={businessCalendar}
65
+ editingSchedule={editingSchedule}
66
+ onClose={onClose}
67
+ onSaved={handleSaved}
68
+ />
69
+ );
70
+
71
+ if (portalContainer) {
72
+ return createPortal(<ThemeProvider>{drawer}</ThemeProvider>, portalContainer);
73
+ }
74
+
75
+ return drawer;
76
+ }
@@ -0,0 +1,80 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
2
+ import {
3
+ buildFieldScheduleEditorContext,
4
+ type BusinessCalendarHostProps,
5
+ type FieldScheduleBindingsAccess,
6
+ SCHEDULE_PICKER_TEXT,
7
+ } from '@servicetitan/dte-shared';
8
+ import type {
9
+ UnlayerEventBusinessCalendarRequest,
10
+ UnlayerEventBusinessCalendarResult,
11
+ } from '../shared/const';
12
+
13
+ /** The schedule drawer to show for a field: a new schedule, or `scheduleId` to edit. */
14
+ export interface BusinessCalendarDrawerRequest {
15
+ fieldKey: string;
16
+ scheduleId?: number;
17
+ }
18
+
19
+ export interface BusinessCalendarRequestHandlerDeps {
20
+ bindings: FieldScheduleBindingsAccess;
21
+ /** The editor's current schedule props. */
22
+ getHost(): BusinessCalendarHostProps;
23
+ openDrawer(request: BusinessCalendarDrawerRequest): void;
24
+ sendResult(result: UnlayerEventBusinessCalendarResult): void;
25
+ }
26
+
27
+ /**
28
+ * Carries out the iframe's schedule requests (`--business-calendar-request`) through the field's
29
+ * `ScheduleEditorContext`, the one BYOD's toggle and picker use, so a toggle, a pick or a delete
30
+ * changes the bindings exactly as there. A delete then reports back. Unlike `SchedulePicker` it
31
+ * doesn't select the default schedule for the field afterwards: the context's delete has already
32
+ * moved every field still on that schedule to the default one, and selecting it again would turn
33
+ * the field's toggle back on if it was switched off while the delete was running. A pick or a
34
+ * delete of a schedule the host doesn't list as one of the tenant's own is ignored or refused.
35
+ */
36
+ export const createBusinessCalendarRequestHandler =
37
+ ({ bindings, getHost, openDrawer, sendResult }: BusinessCalendarRequestHandlerDeps) =>
38
+ (request: UnlayerEventBusinessCalendarRequest) => {
39
+ const host = getHost();
40
+ const context = buildFieldScheduleEditorContext(request.fieldKey, host, bindings);
41
+ const isListedTenantSchedule = (scheduleId: number) =>
42
+ !!host.schedules?.some(schedule => schedule.id === scheduleId && !schedule.isDefault);
43
+
44
+ switch (request.action) {
45
+ case 'toggle':
46
+ context.onToggle(request.enabled);
47
+ return;
48
+ case 'select':
49
+ // No id is the default schedule.
50
+ if (
51
+ request.scheduleId === undefined ||
52
+ isListedTenantSchedule(request.scheduleId)
53
+ ) {
54
+ context.onSelectSchedule(request.scheduleId);
55
+ }
56
+ return;
57
+ case 'create':
58
+ openDrawer({ fieldKey: request.fieldKey });
59
+ return;
60
+ case 'edit':
61
+ openDrawer({ fieldKey: request.fieldKey, scheduleId: request.scheduleId });
62
+ return;
63
+ case 'delete': {
64
+ const { requestId, scheduleId } = request;
65
+ if (!context.onDeleteSchedule || !isListedTenantSchedule(scheduleId)) {
66
+ sendResult({ requestId, error: SCHEDULE_PICKER_TEXT.deleteFailed });
67
+ return;
68
+ }
69
+ context.onDeleteSchedule(scheduleId).then(
70
+ () => sendResult({ requestId }),
71
+ (e: unknown) =>
72
+ sendResult({
73
+ requestId,
74
+ error:
75
+ e instanceof Error ? e.message : SCHEDULE_PICKER_TEXT.deleteFailed,
76
+ }),
77
+ );
78
+ }
79
+ }
80
+ };
@@ -0,0 +1,113 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import type { UnlayerEventBusinessCalendarState } from '../shared/const';
3
+ import type { BusinessCalendar } from '../shared/date-calc';
4
+ import type { UnlayerStore } from '../store';
5
+ import type { CreateUnlayerEditorProps } from '../unlayer-interface';
6
+
7
+ type BusinessCalendarStateSource = Pick<
8
+ CreateUnlayerEditorProps,
9
+ | 'fieldCalendars'
10
+ | 'holidayCatalog'
11
+ | 'holidayCatalogError'
12
+ | 'holidayCatalogLoading'
13
+ | 'holidays'
14
+ | 'scheduleBindings'
15
+ | 'schedules'
16
+ | 'schedulesError'
17
+ | 'schedulesLoading'
18
+ > &
19
+ Pick<UnlayerEventBusinessCalendarState, 'canDeleteSchedule' | 'enabled'>;
20
+
21
+ /**
22
+ * The host's schedule props as the iframe needs them (`--business-calendar-state`): whether the
23
+ * schedule UI is on, each bound field's schedule, the schedules and holidays the inline picker
24
+ * shows, and the calendars the previews use.
25
+ *
26
+ * While the host edits schedules, only bound fields keep their calendar: a field whose toggle was
27
+ * just turned off uses the default calendar right away, without waiting for the host to drop its
28
+ * resolved calendar.
29
+ */
30
+ export const buildBusinessCalendarState = (
31
+ source: BusinessCalendarStateSource,
32
+ ): UnlayerEventBusinessCalendarState => {
33
+ const bindings: UnlayerEventBusinessCalendarState['bindings'] = {};
34
+ const boundCalendars: Record<string, BusinessCalendar> = {};
35
+
36
+ for (const { fieldKey, scheduleId } of source.scheduleBindings ?? []) {
37
+ bindings[fieldKey] = { scheduleId };
38
+
39
+ const calendar = source.fieldCalendars?.[fieldKey];
40
+ if (calendar) {
41
+ boundCalendars[fieldKey] = calendar;
42
+ }
43
+ }
44
+
45
+ return {
46
+ enabled: source.enabled,
47
+ bindings,
48
+ schedules: source.schedules ?? [],
49
+ schedulesLoading: source.schedulesLoading,
50
+ schedulesError: source.schedulesError,
51
+ holidayCatalog: source.holidayCatalog,
52
+ holidayCatalogLoading: source.holidayCatalogLoading,
53
+ holidayCatalogError: source.holidayCatalogError,
54
+ canDeleteSchedule: source.canDeleteSchedule,
55
+ fieldCalendars: source.enabled ? boundCalendars : (source.fieldCalendars ?? {}),
56
+ holidays: source.holidays,
57
+ };
58
+ };
59
+
60
+ /**
61
+ * Keeps the iframe's schedule state in step with the editor's props: rebuilt only when one of
62
+ * them changes (the callbacks count only for whether they are passed), and sent by the store only
63
+ * when the result differs from what the iframe has.
64
+ */
65
+ export function useBusinessCalendarState(store: UnlayerStore, opts: CreateUnlayerEditorProps) {
66
+ const {
67
+ fieldCalendars,
68
+ holidayCatalog,
69
+ holidayCatalogError,
70
+ holidayCatalogLoading,
71
+ holidays,
72
+ scheduleBindings,
73
+ schedules,
74
+ schedulesError,
75
+ schedulesLoading,
76
+ } = opts;
77
+ const enabled = !!opts.onScheduleBindingsChange;
78
+ const canDeleteSchedule = !!opts.onDeleteSchedule;
79
+
80
+ const state = useMemo(
81
+ () =>
82
+ buildBusinessCalendarState({
83
+ canDeleteSchedule,
84
+ enabled,
85
+ fieldCalendars,
86
+ holidayCatalog,
87
+ holidayCatalogError,
88
+ holidayCatalogLoading,
89
+ holidays,
90
+ scheduleBindings,
91
+ schedules,
92
+ schedulesError,
93
+ schedulesLoading,
94
+ }),
95
+ [
96
+ canDeleteSchedule,
97
+ enabled,
98
+ fieldCalendars,
99
+ holidayCatalog,
100
+ holidayCatalogError,
101
+ holidayCatalogLoading,
102
+ holidays,
103
+ scheduleBindings,
104
+ schedules,
105
+ schedulesError,
106
+ schedulesLoading,
107
+ ],
108
+ );
109
+
110
+ useEffect(() => {
111
+ store.setBusinessCalendarState(state);
112
+ }, [state, store]);
113
+ }
@@ -0,0 +1,49 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
2
+ import {
3
+ type BusinessCalendarHostProps,
4
+ useFieldScheduleBindingsAccess,
5
+ } from '@servicetitan/dte-shared';
6
+ import { useCallback, useEffect, useRef, useState } from 'react';
7
+ import type { UnlayerStore } from '../store';
8
+ import {
9
+ type BusinessCalendarDrawerRequest,
10
+ createBusinessCalendarRequestHandler,
11
+ } from './business-calendar-requests';
12
+
13
+ /**
14
+ * Handles the iframe's schedule requests for `UnlayerEditor`: changes the bindings through the
15
+ * host's callbacks, and returns the schedule drawer to show. Each drawer request carries a new
16
+ * `id`, to remount the drawer with its own starting values.
17
+ */
18
+ export function useBusinessCalendarRequests(store: UnlayerStore, host: BusinessCalendarHostProps) {
19
+ const [drawer, setDrawer] = useState<(BusinessCalendarDrawerRequest & { id: number }) | null>(
20
+ null,
21
+ );
22
+ const bindings = useFieldScheduleBindingsAccess(
23
+ host.scheduleBindings,
24
+ host.onScheduleBindingsChange,
25
+ );
26
+ const latestHost = useRef(host);
27
+ useEffect(() => {
28
+ latestHost.current = host;
29
+ });
30
+
31
+ useEffect(() => {
32
+ let drawerId = 0;
33
+
34
+ store.setOnBusinessCalendarRequest(
35
+ createBusinessCalendarRequestHandler({
36
+ bindings,
37
+ getHost: () => latestHost.current,
38
+ openDrawer: request => setDrawer({ ...request, id: ++drawerId }),
39
+ sendResult: store.sendBusinessCalendarResult,
40
+ }),
41
+ );
42
+
43
+ return () => store.setOnBusinessCalendarRequest();
44
+ }, [bindings, store]);
45
+
46
+ const closeDrawer = useCallback(() => setDrawer(null), []);
47
+
48
+ return { closeDrawer, drawer };
49
+ }