@servicetitan/dte-unlayer 0.157.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.
- package/dist/api-core.js +22 -29
- package/dist/api-core.js.map +1 -1
- package/dist/api-custom-tools.js +23 -29
- package/dist/api-custom-tools.js.map +1 -1
- package/dist/business-calendar/BusinessCalendarDrawer.d.ts +19 -0
- package/dist/business-calendar/BusinessCalendarDrawer.d.ts.map +1 -0
- package/dist/business-calendar/BusinessCalendarDrawer.js +41 -0
- package/dist/business-calendar/BusinessCalendarDrawer.js.map +1 -0
- package/dist/business-calendar/business-calendar-requests.d.ts +25 -0
- package/dist/business-calendar/business-calendar-requests.d.ts.map +1 -0
- package/dist/business-calendar/business-calendar-requests.js +46 -0
- package/dist/business-calendar/business-calendar-requests.js.map +1 -0
- package/dist/business-calendar/business-calendar-state.d.ts +22 -0
- package/dist/business-calendar/business-calendar-state.d.ts.map +1 -0
- package/dist/business-calendar/business-calendar-state.js +74 -0
- package/dist/business-calendar/business-calendar-state.js.map +1 -0
- package/dist/business-calendar/useBusinessCalendarRequests.d.ts +15 -0
- package/dist/business-calendar/useBusinessCalendarRequests.d.ts.map +1 -0
- package/dist/business-calendar/useBusinessCalendarRequests.js +30 -0
- package/dist/business-calendar/useBusinessCalendarRequests.js.map +1 -0
- package/dist/display-conditions/ConditionGroup.js +15 -89
- package/dist/display-conditions/ConditionGroup.js.map +1 -1
- package/dist/display-conditions/ConditionGroupsSection.js +2 -70
- package/dist/display-conditions/ConditionGroupsSection.js.map +1 -1
- package/dist/display-conditions/ConditionRow.d.ts.map +1 -1
- package/dist/display-conditions/ConditionRow.js +71 -296
- package/dist/display-conditions/ConditionRow.js.map +1 -1
- package/dist/display-conditions/DisplayConditionModal.js +40 -190
- package/dist/display-conditions/DisplayConditionModal.js.map +1 -1
- package/dist/display-conditions/LookupValueControl.d.ts.map +1 -1
- package/dist/display-conditions/LookupValueControl.js +42 -109
- package/dist/display-conditions/LookupValueControl.js.map +1 -1
- package/dist/display-conditions/SeparatorWithChip.js +1 -10
- package/dist/display-conditions/SeparatorWithChip.js.map +1 -1
- package/dist/display-conditions/constants.js +15 -20
- package/dist/display-conditions/constants.js.map +1 -1
- package/dist/display-conditions/displayConditionController.js +5 -8
- package/dist/display-conditions/displayConditionController.js.map +1 -1
- package/dist/display-conditions/lookupOptionsController.js +16 -16
- package/dist/display-conditions/lookupOptionsController.js.map +1 -1
- package/dist/display-conditions/nunjucks.js +98 -116
- package/dist/display-conditions/nunjucks.js.map +1 -1
- package/dist/display-conditions/schemaDataPoints.js +13 -11
- package/dist/display-conditions/schemaDataPoints.js.map +1 -1
- package/dist/display-conditions/types.js +23 -67
- package/dist/display-conditions/types.js.map +1 -1
- package/dist/display-conditions/useConditionForms.js +38 -56
- package/dist/display-conditions/useConditionForms.js.map +1 -1
- package/dist/display-conditions/useLookupOptions.js +24 -44
- package/dist/display-conditions/useLookupOptions.js.map +1 -1
- package/dist/e-sign-offset/ESignDateSignedOffsetModal.d.ts.map +1 -1
- package/dist/e-sign-offset/ESignDateSignedOffsetModal.js +48 -156
- package/dist/e-sign-offset/ESignDateSignedOffsetModal.js.map +1 -1
- package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts +3 -1
- package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts.map +1 -1
- package/dist/e-sign-offset/ESignOffsetRuleRow.js +12 -71
- package/dist/e-sign-offset/ESignOffsetRuleRow.js.map +1 -1
- package/dist/e-sign-offset/eSignOffsetController.js +5 -8
- package/dist/e-sign-offset/eSignOffsetController.js.map +1 -1
- package/dist/editor-core-source.d.ts +2 -2
- package/dist/editor-core-source.d.ts.map +1 -1
- package/dist/editor-core-source.js +3 -3
- package/dist/editor-core-source.js.map +1 -1
- package/dist/editor-core.js +2 -2
- package/dist/editor-core.js.map +1 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +56 -118
- package/dist/editor.js.map +1 -1
- package/dist/image-upload-error.js +3 -3
- package/dist/image-upload-error.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/loadScript.js +7 -8
- package/dist/loadScript.js.map +1 -1
- package/dist/log.js +2 -3
- package/dist/log.js.map +1 -1
- package/dist/merge-tags.js +18 -17
- package/dist/merge-tags.js.map +1 -1
- package/dist/shared/configs.js +1 -2
- package/dist/shared/configs.js.map +1 -1
- package/dist/shared/const.d.ts +72 -0
- package/dist/shared/const.d.ts.map +1 -1
- package/dist/shared/const.js +13 -4
- package/dist/shared/const.js.map +1 -1
- package/dist/shared/date-calc.d.ts +32 -13
- package/dist/shared/date-calc.d.ts.map +1 -1
- package/dist/shared/date-calc.js +130 -76
- package/dist/shared/date-calc.js.map +1 -1
- package/dist/shared/e-sign-offset.js +46 -63
- package/dist/shared/e-sign-offset.js.map +1 -1
- package/dist/shared/edit-icon.js +0 -1
- package/dist/shared/edit-icon.js.map +1 -1
- package/dist/shared/fonts.js +16 -17
- package/dist/shared/fonts.js.map +1 -1
- package/dist/shared/forms.js +16 -38
- package/dist/shared/forms.js.map +1 -1
- package/dist/shared/instance-id.d.ts +3 -0
- package/dist/shared/instance-id.d.ts.map +1 -1
- package/dist/shared/instance-id.js +10 -7
- package/dist/shared/instance-id.js.map +1 -1
- package/dist/shared/nunjucks-helpers.js +2 -5
- package/dist/shared/nunjucks-helpers.js.map +1 -1
- package/dist/shared/schema.js +52 -96
- package/dist/shared/schema.js.map +1 -1
- package/dist/shared/tools.js +6 -13
- package/dist/shared/tools.js.map +1 -1
- package/dist/store.d.ts +15 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +851 -547
- package/dist/store.js.map +1 -1
- package/dist/tools.js +133 -143
- package/dist/tools.js.map +1 -1
- package/dist/unlayer-interface.d.ts +25 -1
- package/dist/unlayer-interface.d.ts.map +1 -1
- package/dist/unlayer-interface.js +1 -2
- package/dist/unlayer-interface.js.map +1 -1
- package/dist/unlayer.js +86 -113
- package/dist/unlayer.js.map +1 -1
- package/dist/vendor/dte-shared/business-calendar-validation.utils.d.ts +25 -0
- package/dist/vendor/dte-shared/business-calendar-validation.utils.js +80 -0
- package/dist/vendor/dte-shared/components/business-calendar-section.d.ts +15 -0
- package/dist/vendor/dte-shared/components/business-calendar-section.js +14 -0
- package/dist/vendor/dte-shared/components/schedule-drawer.d.ts +18 -0
- package/dist/vendor/dte-shared/components/schedule-drawer.js +119 -0
- package/dist/vendor/dte-shared/components/schedule-picker.d.ts +8 -0
- package/dist/vendor/dte-shared/components/schedule-picker.js +88 -0
- package/dist/vendor/dte-shared/constants.d.ts +31 -0
- package/dist/vendor/dte-shared/constants.js +34 -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 +8 -0
- package/dist/vendor/dte-shared/index.js +7 -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 +19 -0
- package/dist/vendor/dte-shared/types.d.ts +129 -0
- package/dist/vendor/dte-shared/types.js +1 -0
- package/package.json +7 -1
- package/src/__tests__/business-calendar-drawer.test.tsx +183 -0
- package/src/__tests__/business-calendar-requests.test.ts +166 -0
- package/src/__tests__/business-calendar-state.test.tsx +142 -0
- package/src/__tests__/condition-row.test.tsx +131 -0
- package/src/__tests__/date-calc.test.ts +145 -0
- package/src/__tests__/e-sign-offset-modal.test.tsx +124 -0
- package/src/__tests__/instance-id.test.ts +49 -0
- package/src/__tests__/store.business-calendar.test.ts +185 -0
- package/src/__tests__/unlayer.e-sign-types.test.ts +30 -0
- package/src/business-calendar/BusinessCalendarDrawer.tsx +76 -0
- package/src/business-calendar/business-calendar-requests.ts +80 -0
- package/src/business-calendar/business-calendar-state.ts +113 -0
- package/src/business-calendar/useBusinessCalendarRequests.ts +49 -0
- package/src/display-conditions/ConditionRow.tsx +102 -175
- package/src/display-conditions/LookupValueControl.tsx +1 -12
- package/src/e-sign-offset/ESignDateSignedOffsetModal.tsx +20 -12
- package/src/e-sign-offset/ESignOffsetRuleRow.tsx +7 -2
- package/src/editor-core-source.ts +2 -2
- package/src/editor.tsx +15 -0
- package/src/index.ts +10 -0
- package/src/shared/const.ts +61 -0
- package/src/shared/date-calc.ts +155 -78
- package/src/shared/instance-id.ts +6 -1
- package/src/store.ts +68 -5
- package/src/unlayer-interface.tsx +25 -1
- package/src/unlayer.tsx +5 -5
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.dte-business-calendar-section {
|
|
2
|
+
margin-top: var(--spacing-2);
|
|
3
|
+
padding-top: var(--spacing-2);
|
|
4
|
+
border-top: 1px solid var(--border-color);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.dte-business-calendar-preview {
|
|
8
|
+
padding: var(--spacing-1);
|
|
9
|
+
border: 1px solid var(--border-color);
|
|
10
|
+
border-radius: 8px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.dte-business-calendar-preview-header {
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.dte-business-calendar-preview-content {
|
|
18
|
+
margin-top: var(--spacing-1);
|
|
19
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** A weekend day excluded by a business-day schedule, as `Date.getUTCDay()` numbers them. */
|
|
2
|
+
export type WeekendDay = 0 | 6;
|
|
3
|
+
/** A known holiday a tenant can exclude, from DTE's catalog (`GET api/holidays/catalog`). */
|
|
4
|
+
export interface KnownHoliday {
|
|
5
|
+
/** Stable code stored in a schedule's `knownHolidays`, e.g. `JULY4`. */
|
|
6
|
+
code: string;
|
|
7
|
+
/** Name to show, e.g. "Independence Day". */
|
|
8
|
+
displayName: string;
|
|
9
|
+
/** When it falls, e.g. "July 4" or "4th Thursday of November". */
|
|
10
|
+
rule: string;
|
|
11
|
+
}
|
|
12
|
+
/** The known holidays a tenant can exclude, for the schedule editor's checklist. */
|
|
13
|
+
export interface HolidayCatalog {
|
|
14
|
+
/** The jurisdiction the holidays belong to, e.g. "US" or a US state code. */
|
|
15
|
+
jurisdiction: string;
|
|
16
|
+
holidays: KnownHoliday[];
|
|
17
|
+
}
|
|
18
|
+
/** The rules of a business-day schedule: which days a date calculation skips. */
|
|
19
|
+
export interface BusinessCalendarRules {
|
|
20
|
+
weekendDays: WeekendDay[];
|
|
21
|
+
/** Codes of excluded holidays from the jurisdiction's catalog (`KnownHoliday.code`). */
|
|
22
|
+
knownHolidays: string[];
|
|
23
|
+
/** Extra excluded days as `MM-DD`, repeating every year, e.g. `12-24`. */
|
|
24
|
+
customDates: string[];
|
|
25
|
+
}
|
|
26
|
+
/** A schedule a tenant can pick for a business-day calculation (DTE's `GET api/holidays/schedules`). */
|
|
27
|
+
export interface BusinessDaySchedule {
|
|
28
|
+
/** Identifier of a tenant schedule; undefined for the read-only default schedule. */
|
|
29
|
+
id?: number;
|
|
30
|
+
name: string;
|
|
31
|
+
/** Whether this is the read-only default schedule of the tenant's jurisdiction. */
|
|
32
|
+
isDefault: boolean;
|
|
33
|
+
calendar: BusinessCalendarRules;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Request to create a tenant business-day schedule (DTE's `POST api/holidays/schedules`). Always
|
|
37
|
+
* creates a new schedule — editing a selected one does not overwrite it.
|
|
38
|
+
*/
|
|
39
|
+
export interface SaveScheduleRequest extends BusinessCalendarRules {
|
|
40
|
+
/** Display name, unique per tenant ignoring case, max 255 characters, and not "Default". */
|
|
41
|
+
name: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A date calculated field whose custom business-day exclusion toggle is on, and the schedule it
|
|
45
|
+
* uses (DTE's bindings, full-replace via `PUT api/holidays/templates/{id}/bindings`). A field with
|
|
46
|
+
* no entry has the toggle off; what that means for its date arithmetic is up to the editor.
|
|
47
|
+
*/
|
|
48
|
+
export interface FieldScheduleBinding {
|
|
49
|
+
/** The field's key: `PdfField.id` in BYOD, the tool's instance id (`values.id`) in Unlayer. */
|
|
50
|
+
fieldKey: string;
|
|
51
|
+
/** One of the tenant's schedules, or undefined for the default schedule. */
|
|
52
|
+
scheduleId?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The business-day schedule props an editor (`PdfEditor`, `UnlayerEditor`) takes from its host,
|
|
56
|
+
* with the same names and meaning in every editor. The editor builds each field's
|
|
57
|
+
* `ScheduleEditorContext` from them (`useFieldScheduleEditorContext`).
|
|
58
|
+
*/
|
|
59
|
+
export interface BusinessCalendarHostProps {
|
|
60
|
+
/**
|
|
61
|
+
* Date calculated fields whose custom business-day exclusion toggle is on, and the schedule
|
|
62
|
+
* each uses. Also drives the toggle/picker of the field being edited.
|
|
63
|
+
*/
|
|
64
|
+
scheduleBindings?: FieldScheduleBinding[];
|
|
65
|
+
/** The tenant's known holidays, for the schedule editor's checklist. */
|
|
66
|
+
holidayCatalog?: HolidayCatalog;
|
|
67
|
+
holidayCatalogLoading?: boolean;
|
|
68
|
+
holidayCatalogError?: string;
|
|
69
|
+
/** The schedules the tenant can pick from the schedule picker. */
|
|
70
|
+
schedules?: BusinessDaySchedule[];
|
|
71
|
+
schedulesLoading?: boolean;
|
|
72
|
+
schedulesError?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Called with the complete desired `scheduleBindings` whenever the toggle, the schedule picker
|
|
75
|
+
* or a schedule delete changes them (full replacement, mirroring DTE's
|
|
76
|
+
* `PUT api/holidays/templates/{id}/bindings`). The editor does not persist them itself.
|
|
77
|
+
*/
|
|
78
|
+
onScheduleBindingsChange?(bindings: FieldScheduleBinding[]): void;
|
|
79
|
+
/**
|
|
80
|
+
* Creates a tenant schedule from the "Create New Schedule" drawer. Always creates a new
|
|
81
|
+
* schedule — it never overwrites a selected one. On success the created schedule is selected
|
|
82
|
+
* in the picker. A rejected promise's `message` is shown as the drawer's error (for example a
|
|
83
|
+
* duplicate-name conflict).
|
|
84
|
+
*/
|
|
85
|
+
onCreateSchedule?(request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
86
|
+
/**
|
|
87
|
+
* Replaces a tenant schedule's name and rules in place, from the picker's "Edit" action (DTE's
|
|
88
|
+
* `PUT api/holidays/schedules/{id}`). On success the updated schedule stays selected.
|
|
89
|
+
*/
|
|
90
|
+
onUpdateSchedule?(id: number, request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
91
|
+
/**
|
|
92
|
+
* Permanently deletes a tenant schedule, from the picker's "Delete" action (DTE's
|
|
93
|
+
* `DELETE api/holidays/schedules/{id}`). On success every field bound to it moves to the
|
|
94
|
+
* default schedule (toggle still on) through `onScheduleBindingsChange`, as DTE falls back to
|
|
95
|
+
* the default schedule for a deleted one.
|
|
96
|
+
*/
|
|
97
|
+
onDeleteSchedule?(id: number): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Prop bundle for the business-calendar toggle/picker/drawer, built by a host package (currently
|
|
101
|
+
* `@servicetitan/dte-pdf-editor` and `@servicetitan/dte-unlayer`) from whatever it uses to track
|
|
102
|
+
* which of its own fields have the toggle on and which schedule each uses — this package has no
|
|
103
|
+
* opinion on that part, only on the schedule CRUD and the UI built from it.
|
|
104
|
+
*/
|
|
105
|
+
export interface ScheduleEditorContext {
|
|
106
|
+
/** Whether the current field's custom business-day exclusion toggle is on. */
|
|
107
|
+
enabled: boolean;
|
|
108
|
+
/** The current field's bound schedule id, or undefined for the default schedule. */
|
|
109
|
+
selectedScheduleId?: number;
|
|
110
|
+
holidayCatalog?: HolidayCatalog;
|
|
111
|
+
holidayCatalogLoading?: boolean;
|
|
112
|
+
holidayCatalogError?: string;
|
|
113
|
+
schedules?: BusinessDaySchedule[];
|
|
114
|
+
schedulesLoading?: boolean;
|
|
115
|
+
schedulesError?: string;
|
|
116
|
+
/**
|
|
117
|
+
* When set, the schedule form ("Create New Schedule" / "Edit Schedule") is rendered into this
|
|
118
|
+
* element through a portal instead of opening in a drawer. The host owns the element and decides
|
|
119
|
+
* how it is shown; leave it unset (or null, before the element exists) for the drawer.
|
|
120
|
+
*/
|
|
121
|
+
editorContainer?: HTMLElement | null;
|
|
122
|
+
onToggle(enabled: boolean): void;
|
|
123
|
+
onSelectSchedule(scheduleId: number | undefined): void;
|
|
124
|
+
onCreateSchedule?(request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
125
|
+
/** Replaces a tenant schedule's name and rules in place (DTE's `PUT api/holidays/schedules/{id}`). */
|
|
126
|
+
onUpdateSchedule?(id: number, request: SaveScheduleRequest): Promise<BusinessDaySchedule>;
|
|
127
|
+
/** Permanently deletes a tenant schedule; fields bound to it fall back to the default schedule. */
|
|
128
|
+
onDeleteSchedule?(id: number): Promise<void>;
|
|
129
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/dte-unlayer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.159.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"typings": "./dist/index.d.ts",
|
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
"access": "public",
|
|
13
13
|
"registry": "https://registry.npmjs.org"
|
|
14
14
|
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "npm run build --prefix ../shared && tsc --build && startup prepare-package && node ../shared/scripts/bundle-shared.mjs"
|
|
17
|
+
},
|
|
15
18
|
"cli": {
|
|
16
19
|
"webpack": false
|
|
17
20
|
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@servicetitan/dte-shared": "^0.0.1"
|
|
23
|
+
},
|
|
18
24
|
"peerDependencies": {
|
|
19
25
|
"@servicetitan/anvil2": "^3.7.1"
|
|
20
26
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { BusinessCalendarHostProps, BusinessDaySchedule } from '@servicetitan/dte-shared';
|
|
2
|
+
import { act } from 'react';
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
4
|
+
import { TextDecoder, TextEncoder } from 'util';
|
|
5
|
+
|
|
6
|
+
// Anvil2 needs these in jsdom: text encoding at import time, the rest when it renders.
|
|
7
|
+
Object.assign(globalThis, { TextDecoder, TextEncoder });
|
|
8
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
globalThis.ResizeObserver ??= class {
|
|
10
|
+
observe() {}
|
|
11
|
+
unobserve() {}
|
|
12
|
+
disconnect() {}
|
|
13
|
+
} as unknown as typeof ResizeObserver;
|
|
14
|
+
window.matchMedia ??= ((query: string) => ({
|
|
15
|
+
matches: false,
|
|
16
|
+
media: query,
|
|
17
|
+
onchange: null,
|
|
18
|
+
addListener: () => undefined,
|
|
19
|
+
removeListener: () => undefined,
|
|
20
|
+
addEventListener: () => undefined,
|
|
21
|
+
removeEventListener: () => undefined,
|
|
22
|
+
dispatchEvent: () => false,
|
|
23
|
+
})) as typeof window.matchMedia;
|
|
24
|
+
Object.assign(HTMLElement.prototype, {
|
|
25
|
+
showPopover() {},
|
|
26
|
+
hidePopover() {},
|
|
27
|
+
togglePopover() {},
|
|
28
|
+
});
|
|
29
|
+
Object.assign(HTMLDialogElement.prototype, {
|
|
30
|
+
showModal(this: HTMLDialogElement) {
|
|
31
|
+
this.setAttribute('open', '');
|
|
32
|
+
},
|
|
33
|
+
close(this: HTMLDialogElement) {
|
|
34
|
+
this.removeAttribute('open');
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
+
const { BusinessCalendarDrawer } =
|
|
40
|
+
require('../business-calendar/BusinessCalendarDrawer') as typeof import('../business-calendar/BusinessCalendarDrawer');
|
|
41
|
+
|
|
42
|
+
const defaultSchedule: BusinessDaySchedule = {
|
|
43
|
+
name: 'Weekends and US Federal Holidays',
|
|
44
|
+
isDefault: true,
|
|
45
|
+
calendar: { weekendDays: [0, 6], knownHolidays: ['JULY4'], customDates: [] },
|
|
46
|
+
};
|
|
47
|
+
const sundaysOnly: BusinessDaySchedule = {
|
|
48
|
+
id: 7,
|
|
49
|
+
name: 'Sundays only',
|
|
50
|
+
isDefault: false,
|
|
51
|
+
calendar: { weekendDays: [0], knownHolidays: [], customDates: [] },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe('BusinessCalendarDrawer (the drawer Unlayer opens from a field)', () => {
|
|
55
|
+
let container: HTMLDivElement;
|
|
56
|
+
let root: Root;
|
|
57
|
+
|
|
58
|
+
const host = (
|
|
59
|
+
overrides: Partial<BusinessCalendarHostProps> = {},
|
|
60
|
+
): BusinessCalendarHostProps => ({
|
|
61
|
+
holidayCatalog: {
|
|
62
|
+
jurisdiction: 'US',
|
|
63
|
+
holidays: [{ code: 'JULY4', displayName: 'Independence Day', rule: 'July 4' }],
|
|
64
|
+
},
|
|
65
|
+
schedules: [defaultSchedule, sundaysOnly],
|
|
66
|
+
scheduleBindings: [{ fieldKey: 'field-a' }],
|
|
67
|
+
onScheduleBindingsChange: jest.fn(),
|
|
68
|
+
onCreateSchedule: jest.fn(async request => ({
|
|
69
|
+
id: 8,
|
|
70
|
+
name: request.name,
|
|
71
|
+
isDefault: false,
|
|
72
|
+
calendar: request,
|
|
73
|
+
})),
|
|
74
|
+
onUpdateSchedule: jest.fn(async (id, request) => ({
|
|
75
|
+
id,
|
|
76
|
+
name: request.name,
|
|
77
|
+
isDefault: false,
|
|
78
|
+
calendar: request,
|
|
79
|
+
})),
|
|
80
|
+
...overrides,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const typeInto = (input: HTMLInputElement, value: string) =>
|
|
84
|
+
act(() => {
|
|
85
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(
|
|
86
|
+
input,
|
|
87
|
+
value,
|
|
88
|
+
);
|
|
89
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
90
|
+
});
|
|
91
|
+
const button = (text: string) =>
|
|
92
|
+
Array.from(document.body.querySelectorAll('button')).find(
|
|
93
|
+
b => b.textContent?.trim() === text,
|
|
94
|
+
)!;
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
container = document.createElement('div');
|
|
98
|
+
document.body.appendChild(container);
|
|
99
|
+
root = createRoot(container);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
act(() => root.unmount());
|
|
104
|
+
document.body.innerHTML = '';
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('HD-01 creates a schedule from the default rules and selects it for the field', async () => {
|
|
108
|
+
const props = host();
|
|
109
|
+
const onClose = jest.fn();
|
|
110
|
+
act(() =>
|
|
111
|
+
root.render(
|
|
112
|
+
<BusinessCalendarDrawer
|
|
113
|
+
request={{ fieldKey: 'field-a' }}
|
|
114
|
+
host={props}
|
|
115
|
+
onClose={onClose}
|
|
116
|
+
/>,
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(document.body.textContent).toContain('Create New Schedule');
|
|
121
|
+
typeInto(document.body.querySelector<HTMLInputElement>('input')!, 'Sundays and July 4');
|
|
122
|
+
act(() => button('Saturday').click());
|
|
123
|
+
await act(async () => {
|
|
124
|
+
button('Create').click();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(props.onCreateSchedule).toHaveBeenCalledWith({
|
|
128
|
+
name: 'Sundays and July 4',
|
|
129
|
+
weekendDays: [0],
|
|
130
|
+
knownHolidays: ['JULY4'],
|
|
131
|
+
customDates: [],
|
|
132
|
+
});
|
|
133
|
+
expect(props.onScheduleBindingsChange).toHaveBeenLastCalledWith([
|
|
134
|
+
{ fieldKey: 'field-a', scheduleId: 8 },
|
|
135
|
+
]);
|
|
136
|
+
expect(onClose).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('HD-02 edits the requested schedule in place and keeps it selected', async () => {
|
|
140
|
+
const props = host({ scheduleBindings: [{ fieldKey: 'field-a', scheduleId: 7 }] });
|
|
141
|
+
act(() =>
|
|
142
|
+
root.render(
|
|
143
|
+
<BusinessCalendarDrawer
|
|
144
|
+
request={{ fieldKey: 'field-a', scheduleId: 7 }}
|
|
145
|
+
host={props}
|
|
146
|
+
onClose={jest.fn()}
|
|
147
|
+
/>,
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(document.body.textContent).toContain('Edit Schedule');
|
|
152
|
+
expect(document.body.querySelector<HTMLInputElement>('input')!.value).toBe('Sundays only');
|
|
153
|
+
await act(async () => {
|
|
154
|
+
button('Save').click();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(props.onUpdateSchedule).toHaveBeenCalledWith(7, {
|
|
158
|
+
name: 'Sundays only',
|
|
159
|
+
weekendDays: [0],
|
|
160
|
+
knownHolidays: [],
|
|
161
|
+
customDates: [],
|
|
162
|
+
});
|
|
163
|
+
// Still the field's schedule, so its binding stays as it is: nothing to resend.
|
|
164
|
+
expect(props.onScheduleBindingsChange).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('HD-03 closes without showing anything when the schedule to edit was deleted meanwhile', () => {
|
|
168
|
+
const onClose = jest.fn();
|
|
169
|
+
act(() =>
|
|
170
|
+
root.render(
|
|
171
|
+
<BusinessCalendarDrawer
|
|
172
|
+
request={{ fieldKey: 'field-a', scheduleId: 99 }}
|
|
173
|
+
host={host()}
|
|
174
|
+
onClose={onClose}
|
|
175
|
+
/>,
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(document.body.textContent).not.toContain('Create New Schedule');
|
|
180
|
+
expect(document.body.textContent).not.toContain('Edit Schedule');
|
|
181
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { BusinessCalendarHostProps, FieldScheduleBinding } from '@servicetitan/dte-shared';
|
|
2
|
+
import { TextDecoder, TextEncoder } from 'util';
|
|
3
|
+
import type { BusinessCalendarRequestHandlerDeps } from '../business-calendar/business-calendar-requests';
|
|
4
|
+
|
|
5
|
+
// `@servicetitan/dte-shared` loads Anvil2, which needs these at import time (jsdom has neither).
|
|
6
|
+
Object.assign(globalThis, { TextDecoder, TextEncoder });
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
9
|
+
const { createBusinessCalendarRequestHandler } =
|
|
10
|
+
require('../business-calendar/business-calendar-requests') as typeof import('../business-calendar/business-calendar-requests');
|
|
11
|
+
|
|
12
|
+
const SUNDAYS_ONLY = {
|
|
13
|
+
id: 7,
|
|
14
|
+
name: 'Sundays only',
|
|
15
|
+
isDefault: false,
|
|
16
|
+
calendar: { weekendDays: [0 as const], knownHolidays: [], customDates: [] },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A handler over bindings kept the way a host keeps them, plus what it asked the editor to do.
|
|
21
|
+
* The host lists schedule 7 unless `host` says otherwise.
|
|
22
|
+
*/
|
|
23
|
+
const setup = (initial: FieldScheduleBinding[], host: BusinessCalendarHostProps = {}) => {
|
|
24
|
+
let current = initial;
|
|
25
|
+
const deps: BusinessCalendarRequestHandlerDeps = {
|
|
26
|
+
getHost: () => ({ schedules: [SUNDAYS_ONLY], ...host, scheduleBindings: current }),
|
|
27
|
+
bindings: {
|
|
28
|
+
get: () => current,
|
|
29
|
+
set: next => {
|
|
30
|
+
current = next;
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
openDrawer: jest.fn(),
|
|
34
|
+
sendResult: jest.fn(),
|
|
35
|
+
};
|
|
36
|
+
return { deps, handle: createBusinessCalendarRequestHandler(deps), current: () => current };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Lets the delete's promise callbacks run. */
|
|
40
|
+
const flush = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
41
|
+
|
|
42
|
+
describe('createBusinessCalendarRequestHandler', () => {
|
|
43
|
+
test('toggling on and off changes only that field, as in BYOD', () => {
|
|
44
|
+
const { current, handle } = setup([{ fieldKey: 'other', scheduleId: 3 }]);
|
|
45
|
+
|
|
46
|
+
handle({ action: 'toggle', fieldKey: 'field-a', enabled: true });
|
|
47
|
+
expect(current()).toEqual([{ fieldKey: 'other', scheduleId: 3 }, { fieldKey: 'field-a' }]);
|
|
48
|
+
|
|
49
|
+
handle({ action: 'toggle', fieldKey: 'field-a', enabled: false });
|
|
50
|
+
expect(current()).toEqual([{ fieldKey: 'other', scheduleId: 3 }]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("selecting a schedule sets the field's binding", () => {
|
|
54
|
+
const { current, handle } = setup([{ fieldKey: 'field-a' }]);
|
|
55
|
+
|
|
56
|
+
handle({ action: 'select', fieldKey: 'field-a', scheduleId: 7 });
|
|
57
|
+
|
|
58
|
+
expect(current()).toEqual([{ fieldKey: 'field-a', scheduleId: 7 }]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('opens the drawer for a new schedule or the one to edit', () => {
|
|
62
|
+
const { deps, handle } = setup([]);
|
|
63
|
+
|
|
64
|
+
handle({ action: 'create', fieldKey: 'field-a' });
|
|
65
|
+
handle({ action: 'edit', fieldKey: 'field-a', scheduleId: 7 });
|
|
66
|
+
|
|
67
|
+
expect(deps.openDrawer).toHaveBeenNthCalledWith(1, { fieldKey: 'field-a' });
|
|
68
|
+
expect(deps.openDrawer).toHaveBeenNthCalledWith(2, { fieldKey: 'field-a', scheduleId: 7 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a delete moves every field on the schedule to the default one and reports success', async () => {
|
|
72
|
+
const onDeleteSchedule = jest.fn().mockResolvedValue(undefined);
|
|
73
|
+
const { current, deps, handle } = setup(
|
|
74
|
+
[
|
|
75
|
+
{ fieldKey: 'field-a', scheduleId: 7 },
|
|
76
|
+
{ fieldKey: 'field-b', scheduleId: 7 },
|
|
77
|
+
],
|
|
78
|
+
{ onDeleteSchedule },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
handle({ action: 'delete', fieldKey: 'field-a', scheduleId: 7, requestId: 'r1' });
|
|
82
|
+
await flush();
|
|
83
|
+
|
|
84
|
+
expect(onDeleteSchedule).toHaveBeenCalledWith(7);
|
|
85
|
+
expect(current()).toEqual([{ fieldKey: 'field-a' }, { fieldKey: 'field-b' }]);
|
|
86
|
+
expect(deps.sendResult).toHaveBeenCalledWith({ requestId: 'r1' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a field's toggle turned off while its schedule is deleted stays off", async () => {
|
|
90
|
+
let resolveDelete!: () => void;
|
|
91
|
+
const onDeleteSchedule = jest.fn(
|
|
92
|
+
() => new Promise<void>(resolve => (resolveDelete = resolve)),
|
|
93
|
+
);
|
|
94
|
+
const { current, deps, handle } = setup([{ fieldKey: 'field-a', scheduleId: 7 }], {
|
|
95
|
+
onDeleteSchedule,
|
|
96
|
+
});
|
|
97
|
+
const set = jest.spyOn(deps.bindings, 'set');
|
|
98
|
+
|
|
99
|
+
handle({ action: 'delete', fieldKey: 'field-a', scheduleId: 7, requestId: 'r1' });
|
|
100
|
+
handle({ action: 'toggle', fieldKey: 'field-a', enabled: false });
|
|
101
|
+
resolveDelete();
|
|
102
|
+
await flush();
|
|
103
|
+
|
|
104
|
+
expect(current()).toEqual([]);
|
|
105
|
+
// The toggle's change only: the delete had no field left on the schedule to move.
|
|
106
|
+
expect(set).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(deps.sendResult).toHaveBeenCalledWith({ requestId: 'r1' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a failed delete keeps the bindings and reports the host's message", async () => {
|
|
111
|
+
const onDeleteSchedule = jest.fn().mockRejectedValue(new Error('in use'));
|
|
112
|
+
const { current, deps, handle } = setup([{ fieldKey: 'field-a', scheduleId: 7 }], {
|
|
113
|
+
onDeleteSchedule,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
handle({ action: 'delete', fieldKey: 'field-a', scheduleId: 7, requestId: 'r1' });
|
|
117
|
+
await flush();
|
|
118
|
+
|
|
119
|
+
expect(current()).toEqual([{ fieldKey: 'field-a', scheduleId: 7 }]);
|
|
120
|
+
expect(deps.sendResult).toHaveBeenCalledWith({ requestId: 'r1', error: 'in use' });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("ignores picking a schedule the host doesn't list, but not the default one", () => {
|
|
124
|
+
const { current, handle } = setup([{ fieldKey: 'field-a', scheduleId: 7 }]);
|
|
125
|
+
|
|
126
|
+
handle({ action: 'select', fieldKey: 'field-a', scheduleId: 99 });
|
|
127
|
+
expect(current()).toEqual([{ fieldKey: 'field-a', scheduleId: 7 }]);
|
|
128
|
+
|
|
129
|
+
handle({ action: 'select', fieldKey: 'field-a' });
|
|
130
|
+
expect(current()).toEqual([{ fieldKey: 'field-a' }]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("refuses to delete a schedule the host doesn't list as the tenant's own", async () => {
|
|
134
|
+
const onDeleteSchedule = jest.fn().mockResolvedValue(undefined);
|
|
135
|
+
const { current, deps, handle } = setup([{ fieldKey: 'field-a', scheduleId: 7 }], {
|
|
136
|
+
onDeleteSchedule,
|
|
137
|
+
schedules: [{ ...SUNDAYS_ONLY, id: 1, isDefault: true }, SUNDAYS_ONLY],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
handle({ action: 'delete', fieldKey: 'field-a', scheduleId: 99, requestId: 'r1' });
|
|
141
|
+
handle({ action: 'delete', fieldKey: 'field-a', scheduleId: 1, requestId: 'r2' });
|
|
142
|
+
await flush();
|
|
143
|
+
|
|
144
|
+
expect(onDeleteSchedule).not.toHaveBeenCalled();
|
|
145
|
+
expect(current()).toEqual([{ fieldKey: 'field-a', scheduleId: 7 }]);
|
|
146
|
+
expect(deps.sendResult).toHaveBeenCalledWith({
|
|
147
|
+
requestId: 'r1',
|
|
148
|
+
error: 'Failed to delete schedule',
|
|
149
|
+
});
|
|
150
|
+
expect(deps.sendResult).toHaveBeenCalledWith({
|
|
151
|
+
requestId: 'r2',
|
|
152
|
+
error: 'Failed to delete schedule',
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('a delete without a host delete reports a failure', () => {
|
|
157
|
+
const { deps, handle } = setup([{ fieldKey: 'field-a', scheduleId: 7 }]);
|
|
158
|
+
|
|
159
|
+
handle({ action: 'delete', fieldKey: 'field-a', scheduleId: 7, requestId: 'r1' });
|
|
160
|
+
|
|
161
|
+
expect(deps.sendResult).toHaveBeenCalledWith({
|
|
162
|
+
requestId: 'r1',
|
|
163
|
+
error: 'Failed to delete schedule',
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { BusinessDaySchedule } from '@servicetitan/dte-shared';
|
|
2
|
+
import { act } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import {
|
|
5
|
+
buildBusinessCalendarState,
|
|
6
|
+
useBusinessCalendarState,
|
|
7
|
+
} from '../business-calendar/business-calendar-state';
|
|
8
|
+
import type { BusinessCalendar } from '../shared/date-calc';
|
|
9
|
+
import type { UnlayerStore } from '../store';
|
|
10
|
+
import type { CreateUnlayerEditorProps } from '../unlayer-interface';
|
|
11
|
+
|
|
12
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
|
+
|
|
14
|
+
const schedules: BusinessDaySchedule[] = [
|
|
15
|
+
{
|
|
16
|
+
name: 'Weekends and US Federal Holidays',
|
|
17
|
+
isDefault: true,
|
|
18
|
+
calendar: { weekendDays: [0, 6], knownHolidays: ['JULY4'], customDates: [] },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 7,
|
|
22
|
+
name: 'Sundays only',
|
|
23
|
+
isDefault: false,
|
|
24
|
+
calendar: { weekendDays: [0], knownHolidays: [], customDates: [] },
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const sundaysOnly: BusinessCalendar = {
|
|
29
|
+
weekendDays: [0],
|
|
30
|
+
holidays: [],
|
|
31
|
+
firstYear: 2025,
|
|
32
|
+
lastYear: 2027,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('buildBusinessCalendarState', () => {
|
|
36
|
+
test('when disabled, passes every calendar through', () => {
|
|
37
|
+
expect(
|
|
38
|
+
buildBusinessCalendarState({
|
|
39
|
+
enabled: false,
|
|
40
|
+
canDeleteSchedule: false,
|
|
41
|
+
fieldCalendars: { 'field-a': sundaysOnly },
|
|
42
|
+
holidays: ['2026-07-03'],
|
|
43
|
+
}),
|
|
44
|
+
).toEqual({
|
|
45
|
+
enabled: false,
|
|
46
|
+
bindings: {},
|
|
47
|
+
schedules: [],
|
|
48
|
+
canDeleteSchedule: false,
|
|
49
|
+
fieldCalendars: { 'field-a': sundaysOnly },
|
|
50
|
+
holidays: ['2026-07-03'],
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('passes the schedules, the holiday catalog and their states through for the inline picker', () => {
|
|
55
|
+
const catalog = {
|
|
56
|
+
jurisdiction: 'US',
|
|
57
|
+
holidays: [{ code: 'JULY4', displayName: 'Independence Day', rule: 'July 4' }],
|
|
58
|
+
};
|
|
59
|
+
const state = buildBusinessCalendarState({
|
|
60
|
+
enabled: true,
|
|
61
|
+
canDeleteSchedule: true,
|
|
62
|
+
schedules,
|
|
63
|
+
schedulesLoading: true,
|
|
64
|
+
schedulesError: 'schedules failed',
|
|
65
|
+
holidayCatalog: catalog,
|
|
66
|
+
holidayCatalogLoading: true,
|
|
67
|
+
holidayCatalogError: 'catalog failed',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(state).toMatchObject({
|
|
71
|
+
schedules,
|
|
72
|
+
schedulesLoading: true,
|
|
73
|
+
schedulesError: 'schedules failed',
|
|
74
|
+
holidayCatalog: catalog,
|
|
75
|
+
holidayCatalogLoading: true,
|
|
76
|
+
holidayCatalogError: 'catalog failed',
|
|
77
|
+
canDeleteSchedule: true,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('lists each bound field with its schedule, none for the default one', () => {
|
|
82
|
+
const state = buildBusinessCalendarState({
|
|
83
|
+
enabled: true,
|
|
84
|
+
canDeleteSchedule: false,
|
|
85
|
+
schedules,
|
|
86
|
+
scheduleBindings: [{ fieldKey: 'field-a', scheduleId: 7 }, { fieldKey: 'field-b' }],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(state.enabled).toBe(true);
|
|
90
|
+
expect(state.bindings).toEqual({
|
|
91
|
+
'field-a': { scheduleId: 7 },
|
|
92
|
+
'field-b': { scheduleId: undefined },
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('keeps only the calendars of bound fields while schedules are edited', () => {
|
|
97
|
+
const state = buildBusinessCalendarState({
|
|
98
|
+
enabled: true,
|
|
99
|
+
canDeleteSchedule: false,
|
|
100
|
+
schedules,
|
|
101
|
+
scheduleBindings: [{ fieldKey: 'field-a', scheduleId: 7 }],
|
|
102
|
+
// field-b's toggle was just turned off; the host hasn't dropped its calendar yet.
|
|
103
|
+
fieldCalendars: { 'field-a': sundaysOnly, 'field-b': sundaysOnly },
|
|
104
|
+
holidays: ['2026-07-03'],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(state.fieldCalendars).toEqual({ 'field-a': sundaysOnly });
|
|
108
|
+
expect(state.holidays).toEqual(['2026-07-03']);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('useBusinessCalendarState', () => {
|
|
113
|
+
test('rebuilds the state only when a schedule prop changes, not on every render', () => {
|
|
114
|
+
const store = { setBusinessCalendarState: jest.fn() };
|
|
115
|
+
const Editor = ({ opts }: { opts: CreateUnlayerEditorProps }) => {
|
|
116
|
+
useBusinessCalendarState(store as unknown as UnlayerStore, opts);
|
|
117
|
+
return null;
|
|
118
|
+
};
|
|
119
|
+
const container = document.createElement('div');
|
|
120
|
+
const root = createRoot(container);
|
|
121
|
+
// A host that rebuilds its options, callbacks included, on every render.
|
|
122
|
+
const opts = (next: Partial<CreateUnlayerEditorProps> = {}): CreateUnlayerEditorProps => ({
|
|
123
|
+
tools: [],
|
|
124
|
+
schedules,
|
|
125
|
+
onScheduleBindingsChange: () => undefined,
|
|
126
|
+
...next,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
act(() => root.render(<Editor opts={opts()} />));
|
|
130
|
+
act(() => root.render(<Editor opts={opts()} />));
|
|
131
|
+
expect(store.setBusinessCalendarState).toHaveBeenCalledTimes(1);
|
|
132
|
+
|
|
133
|
+
act(() => root.render(<Editor opts={opts({ schedules: [...schedules] })} />));
|
|
134
|
+
act(() => root.render(<Editor opts={opts({ onDeleteSchedule: async () => undefined })} />));
|
|
135
|
+
expect(store.setBusinessCalendarState).toHaveBeenCalledTimes(3);
|
|
136
|
+
expect(store.setBusinessCalendarState).toHaveBeenLastCalledWith(
|
|
137
|
+
expect.objectContaining({ enabled: true, canDeleteSchedule: true }),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
act(() => root.unmount());
|
|
141
|
+
});
|
|
142
|
+
});
|