@servicetitan/dte-unlayer 0.158.0 → 0.160.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.d.ts.map +1 -1
- package/dist/display-conditions/ConditionGroup.js +20 -89
- package/dist/display-conditions/ConditionGroup.js.map +1 -1
- package/dist/display-conditions/ConditionGroupsSection.d.ts.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 +88 -212
- package/dist/display-conditions/ConditionRow.js.map +1 -1
- package/dist/display-conditions/DisplayConditionModal.d.ts.map +1 -1
- package/dist/display-conditions/DisplayConditionModal.js +39 -194
- package/dist/display-conditions/DisplayConditionModal.js.map +1 -1
- package/dist/display-conditions/LookupValueControl.js +42 -105
- 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.js +42 -155
- package/dist/e-sign-offset/ESignDateSignedOffsetModal.js.map +1 -1
- package/dist/e-sign-offset/ESignOffsetRuleRow.js +12 -73
- 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 +70 -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.js +47 -38
- 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.js +6 -9
- 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 -548
- 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 +15 -4
- 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 +51 -8
- package/src/__tests__/e-sign-offset-modal.test.tsx +13 -2
- 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/ConditionGroup.tsx +13 -3
- package/src/display-conditions/ConditionGroupsSection.tsx +24 -21
- package/src/display-conditions/ConditionRow.tsx +75 -32
- package/src/display-conditions/DisplayConditionModal.tsx +18 -59
- package/src/e-sign-offset/ESignDateSignedOffsetModal.tsx +6 -6
- 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 +59 -0
- package/src/store.ts +67 -5
- package/src/unlayer-interface.tsx +15 -4
- package/src/unlayer.tsx +5 -5
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { CUSTOM_DATE_MONTH_LENGTHS, CUSTOM_DATE_PATTERN, MAX_SCHEDULE_NAME_LENGTH, RESERVED_SCHEDULE_NAME, } from './constants';
|
|
2
|
+
/** Whether `value` is a valid `MM-DD` custom date (`02-29` included, as a recurring rule). */
|
|
3
|
+
export function isValidCustomDate(value) {
|
|
4
|
+
const match = CUSTOM_DATE_PATTERN.exec(value);
|
|
5
|
+
if (!match) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const month = Number(match[1]);
|
|
9
|
+
const day = Number(match[2]);
|
|
10
|
+
return day <= CUSTOM_DATE_MONTH_LENGTHS[month - 1];
|
|
11
|
+
}
|
|
12
|
+
/** Converts a complete `{ month, day }` to `MM-DD`, or null while the field is incomplete/invalid. */
|
|
13
|
+
export function yearlessDateToCustomDate(value) {
|
|
14
|
+
var _a, _b;
|
|
15
|
+
const month = (_a = value === null || value === void 0 ? void 0 : value.month) !== null && _a !== void 0 ? _a : null;
|
|
16
|
+
const day = (_b = value === null || value === void 0 ? void 0 : value.day) !== null && _b !== void 0 ? _b : null;
|
|
17
|
+
if (month === null || day === null) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const candidate = `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
|
21
|
+
return isValidCustomDate(candidate) ? candidate : null;
|
|
22
|
+
}
|
|
23
|
+
/** Converts a stored `MM-DD` custom date back to `DateFieldYearless`'s `{ month, day }` value. */
|
|
24
|
+
export function customDateToYearlessDate(value) {
|
|
25
|
+
const match = CUSTOM_DATE_PATTERN.exec(value);
|
|
26
|
+
return match ? { month: Number(match[1]), day: Number(match[2]) } : null;
|
|
27
|
+
}
|
|
28
|
+
const CUSTOM_DATE_MONTH_ABBREVIATIONS = [
|
|
29
|
+
'Jan',
|
|
30
|
+
'Feb',
|
|
31
|
+
'Mar',
|
|
32
|
+
'Apr',
|
|
33
|
+
'May',
|
|
34
|
+
'Jun',
|
|
35
|
+
'Jul',
|
|
36
|
+
'Aug',
|
|
37
|
+
'Sep',
|
|
38
|
+
'Oct',
|
|
39
|
+
'Nov',
|
|
40
|
+
'Dec',
|
|
41
|
+
];
|
|
42
|
+
/** Formats a stored `MM-DD` custom date for display, e.g. `03-03` → `Mar - 03`. */
|
|
43
|
+
export function formatCustomDateLabel(value) {
|
|
44
|
+
const match = CUSTOM_DATE_PATTERN.exec(value);
|
|
45
|
+
if (!match) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
const [, month, day] = match;
|
|
49
|
+
return `${CUSTOM_DATE_MONTH_ABBREVIATIONS[Number(month) - 1]} - ${day}`;
|
|
50
|
+
}
|
|
51
|
+
/** How the schedule picker and drawer name a known holiday, e.g. `Independence Day · July 4`. */
|
|
52
|
+
export function formatKnownHolidayLabel(holiday) {
|
|
53
|
+
return `${holiday.displayName} · ${holiday.rule}`;
|
|
54
|
+
}
|
|
55
|
+
/** Whether `name` is the reserved default-schedule name, ignoring case and surrounding whitespace. */
|
|
56
|
+
export function isReservedScheduleName(name) {
|
|
57
|
+
return name.trim().toLowerCase() === RESERVED_SCHEDULE_NAME.toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Client-side schedule name checks mirroring DTE's `SaveDateScheduleRequest.Name` contract: a
|
|
61
|
+
* required, non-reserved name up to 255 characters, unique per tenant ignoring case. `existingNames`
|
|
62
|
+
* should exclude the schedule being edited, if any. The server is still authoritative — a duplicate
|
|
63
|
+
* it rejects despite passing this check is a 409, not a bug here.
|
|
64
|
+
*/
|
|
65
|
+
export function validateScheduleName(name, existingNames) {
|
|
66
|
+
const trimmed = name.trim();
|
|
67
|
+
if (!trimmed) {
|
|
68
|
+
return 'Schedule name is required';
|
|
69
|
+
}
|
|
70
|
+
if (trimmed.length > MAX_SCHEDULE_NAME_LENGTH) {
|
|
71
|
+
return `Schedule name must be ${MAX_SCHEDULE_NAME_LENGTH} characters or fewer`;
|
|
72
|
+
}
|
|
73
|
+
if (isReservedScheduleName(trimmed)) {
|
|
74
|
+
return `"${RESERVED_SCHEDULE_NAME}" is reserved for the default schedule`;
|
|
75
|
+
}
|
|
76
|
+
if (existingNames.some(existing => existing.trim().toLowerCase() === trimmed.toLowerCase())) {
|
|
77
|
+
return 'A schedule with this name already exists';
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import '../styles/schedule.css';
|
|
3
|
+
import { type ScheduleEditorContext } from '../types';
|
|
4
|
+
interface BusinessCalendarSectionProps {
|
|
5
|
+
businessCalendar: ScheduleEditorContext;
|
|
6
|
+
/** @default "Exclude weekends & holidays from the calculation" */
|
|
7
|
+
label?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A business-day exclusion toggle, shown for a date calculated field (Formula Builder's Advanced
|
|
11
|
+
* Settings, date result type only) and an e-sign `dateSignedOffset` field's day-offset settings.
|
|
12
|
+
* On reveals the schedule picker; what off means for the date arithmetic is up to the editor.
|
|
13
|
+
*/
|
|
14
|
+
export declare const BusinessCalendarSection: FC<BusinessCalendarSectionProps>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Flex, Switch } from '@servicetitan/anvil2';
|
|
3
|
+
import { BUSINESS_CALENDAR_TOGGLE_LABEL } from '../constants';
|
|
4
|
+
import '../styles/schedule.css';
|
|
5
|
+
import { SchedulePicker } from './schedule-picker';
|
|
6
|
+
/**
|
|
7
|
+
* A business-day exclusion toggle, shown for a date calculated field (Formula Builder's Advanced
|
|
8
|
+
* Settings, date result type only) and an e-sign `dateSignedOffset` field's day-offset settings.
|
|
9
|
+
* On reveals the schedule picker; what off means for the date arithmetic is up to the editor.
|
|
10
|
+
*/
|
|
11
|
+
export const BusinessCalendarSection = ({ businessCalendar, label = BUSINESS_CALENDAR_TOGGLE_LABEL, }) => {
|
|
12
|
+
const { enabled, onToggle } = businessCalendar;
|
|
13
|
+
return (_jsx("div", { className: "dte-business-calendar-section", children: _jsxs(Flex, { direction: "column", gap: "4", children: [_jsx(Switch, { checked: enabled, onChange: (_e, state) => onToggle(state.checked), label: label, value: "business-calendar-exclusion" }), enabled && _jsx(SchedulePicker, { businessCalendar: businessCalendar })] }) }));
|
|
14
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { type BusinessDaySchedule, type ScheduleEditorContext } from '../types';
|
|
3
|
+
interface ScheduleDrawerProps {
|
|
4
|
+
businessCalendar: ScheduleEditorContext;
|
|
5
|
+
/** When set, edits this schedule in place (PUT) instead of creating a new one (POST). */
|
|
6
|
+
editingSchedule?: BusinessDaySchedule;
|
|
7
|
+
onClose(): void;
|
|
8
|
+
onSaved(schedule: BusinessDaySchedule): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* "Create New Schedule" / "Edit Schedule" drawer. With `businessCalendar.editorContainer` set, the
|
|
12
|
+
* same form is portaled into that element instead (its Close button calls `onClose`; there is no
|
|
13
|
+
* backdrop or Escape key). Creating starts from the default schedule's rules
|
|
14
|
+
* (DEFAULT_SCHEDULE_WEEKEND_DAYS, every catalog holiday); editing starts from `editingSchedule`'s
|
|
15
|
+
* own rules and replaces it in place on save.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ScheduleDrawer: FC<ScheduleDrawerProps>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Alert, Button, ButtonToggle, DateFieldYearless, Drawer, Flex, MultiSelectFieldSync, Text, TextField, } from '@servicetitan/anvil2';
|
|
3
|
+
import IconAddCircle from '@servicetitan/anvil2/assets/icons/material/round/add_circle.svg';
|
|
4
|
+
import IconClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
|
|
5
|
+
import DeleteIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
|
|
6
|
+
import { useMemo, useRef, useState } from 'react';
|
|
7
|
+
import { createPortal } from 'react-dom';
|
|
8
|
+
import { customDateToYearlessDate, formatKnownHolidayLabel, validateScheduleName, yearlessDateToCustomDate, } from '../business-calendar-validation.utils';
|
|
9
|
+
import { DEFAULT_SCHEDULE_WEEKEND_DAYS } from '../constants';
|
|
10
|
+
const EMPTY_DATE = { month: null, day: null };
|
|
11
|
+
const isEmptyDate = (value) => value.month === null && value.day === null;
|
|
12
|
+
/**
|
|
13
|
+
* "Create New Schedule" / "Edit Schedule" drawer. With `businessCalendar.editorContainer` set, the
|
|
14
|
+
* same form is portaled into that element instead (its Close button calls `onClose`; there is no
|
|
15
|
+
* backdrop or Escape key). Creating starts from the default schedule's rules
|
|
16
|
+
* (DEFAULT_SCHEDULE_WEEKEND_DAYS, every catalog holiday); editing starts from `editingSchedule`'s
|
|
17
|
+
* own rules and replaces it in place on save.
|
|
18
|
+
*/
|
|
19
|
+
export const ScheduleDrawer = ({ businessCalendar, editingSchedule, onClose, onSaved, }) => {
|
|
20
|
+
var _a;
|
|
21
|
+
const { editorContainer, holidayCatalog, holidayCatalogError, holidayCatalogLoading, onCreateSchedule, onUpdateSchedule, schedules, } = businessCalendar;
|
|
22
|
+
const [name, setName] = useState((_a = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.name) !== null && _a !== void 0 ? _a : '');
|
|
23
|
+
const [weekendDays, setWeekendDays] = useState(() => { var _a; return new Set((_a = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.calendar.weekendDays) !== null && _a !== void 0 ? _a : DEFAULT_SCHEDULE_WEEKEND_DAYS); });
|
|
24
|
+
const [knownHolidays, setKnownHolidays] = useState(() => {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
return new Set((_b = (_a = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.calendar.knownHolidays) !== null && _a !== void 0 ? _a : holidayCatalog === null || holidayCatalog === void 0 ? void 0 : holidayCatalog.holidays.map(h => h.code)) !== null && _b !== void 0 ? _b : []);
|
|
27
|
+
});
|
|
28
|
+
const [customDates, setCustomDates] = useState(() => {
|
|
29
|
+
var _a;
|
|
30
|
+
return ((_a = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.calendar.customDates) !== null && _a !== void 0 ? _a : []).map((date, index) => {
|
|
31
|
+
var _a;
|
|
32
|
+
return ({
|
|
33
|
+
id: index,
|
|
34
|
+
value: (_a = customDateToYearlessDate(date)) !== null && _a !== void 0 ? _a : EMPTY_DATE,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
const nextRowId = useRef(customDates.length);
|
|
39
|
+
const [nameTouched, setNameTouched] = useState(false);
|
|
40
|
+
const [submitError, setSubmitError] = useState();
|
|
41
|
+
const [submitting, setSubmitting] = useState(false);
|
|
42
|
+
const holidayOptions = useMemo(() => {
|
|
43
|
+
var _a;
|
|
44
|
+
return ((_a = holidayCatalog === null || holidayCatalog === void 0 ? void 0 : holidayCatalog.holidays) !== null && _a !== void 0 ? _a : []).map(h => ({
|
|
45
|
+
id: h.code,
|
|
46
|
+
label: formatKnownHolidayLabel(h),
|
|
47
|
+
}));
|
|
48
|
+
}, [holidayCatalog]);
|
|
49
|
+
const selectedHolidayOptions = useMemo(() => holidayOptions.filter(o => knownHolidays.has(String(o.id))), [holidayOptions, knownHolidays]);
|
|
50
|
+
const existingNames = useMemo(() => (schedules !== null && schedules !== void 0 ? schedules : [])
|
|
51
|
+
.filter(s => !s.isDefault && s.id !== (editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.id))
|
|
52
|
+
.map(s => s.name), [schedules, editingSchedule]);
|
|
53
|
+
const nameError = validateScheduleName(name, existingNames);
|
|
54
|
+
const toggleWeekendDay = (day) => {
|
|
55
|
+
setWeekendDays(prev => {
|
|
56
|
+
const next = new Set(prev);
|
|
57
|
+
if (next.has(day)) {
|
|
58
|
+
next.delete(day);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
next.add(day);
|
|
62
|
+
}
|
|
63
|
+
return next;
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const handleAddDate = () => setCustomDates(prev => [...prev, { id: nextRowId.current++, value: EMPTY_DATE }]);
|
|
67
|
+
const handleRemoveDate = (id) => setCustomDates(prev => prev.filter(row => row.id !== id));
|
|
68
|
+
const handleCustomDateChange = (id, value) => setCustomDates(prev => prev.map(row => (row.id === id ? { ...row, value: value !== null && value !== void 0 ? value : EMPTY_DATE } : row)));
|
|
69
|
+
/** Resolved `MM-DD` per non-empty row, or `null` for an incomplete/invalid one (blocks submit). */
|
|
70
|
+
const resolvedCustomDates = customDates.map(row => isEmptyDate(row.value) ? '' : yearlessDateToCustomDate(row.value));
|
|
71
|
+
const hasInvalidCustomDate = resolvedCustomDates.some((d, i) => d === null && !isEmptyDate(customDates[i].value));
|
|
72
|
+
/*
|
|
73
|
+
* Editing is only offered for a saved tenant schedule, which always has an id (only the
|
|
74
|
+
* read-only default schedule — never editable — lacks one).
|
|
75
|
+
*/
|
|
76
|
+
const editingId = editingSchedule === null || editingSchedule === void 0 ? void 0 : editingSchedule.id;
|
|
77
|
+
const isEditing = editingId !== undefined;
|
|
78
|
+
const canSave = isEditing ? !!onUpdateSchedule : !!onCreateSchedule;
|
|
79
|
+
const canSubmit = !nameError && !hasInvalidCustomDate && !submitting && canSave;
|
|
80
|
+
const handleSubmit = async () => {
|
|
81
|
+
setNameTouched(true);
|
|
82
|
+
if (nameError || hasInvalidCustomDate || !canSave) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const request = {
|
|
86
|
+
name: name.trim(),
|
|
87
|
+
weekendDays: Array.from(weekendDays),
|
|
88
|
+
knownHolidays: Array.from(knownHolidays),
|
|
89
|
+
customDates: Array.from(new Set(resolvedCustomDates.filter((d) => !!d))),
|
|
90
|
+
};
|
|
91
|
+
setSubmitting(true);
|
|
92
|
+
setSubmitError(undefined);
|
|
93
|
+
try {
|
|
94
|
+
if (isEditing && onUpdateSchedule) {
|
|
95
|
+
onSaved(await onUpdateSchedule(editingId, request));
|
|
96
|
+
}
|
|
97
|
+
else if (!isEditing && onCreateSchedule) {
|
|
98
|
+
onSaved(await onCreateSchedule(request));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
setSubmitError(e instanceof Error
|
|
103
|
+
? e.message
|
|
104
|
+
: `Failed to ${isEditing ? 'save' : 'create'} schedule`);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
setSubmitting(false);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const title = isEditing ? 'Edit Schedule' : 'Create New Schedule';
|
|
111
|
+
const form = (_jsxs(Flex, { direction: "column", gap: "5", style: { height: '100%', overflowY: 'auto' }, children: [_jsx(TextField, { required: true, label: "Schedule Name", placeholder: "New Schedule", value: name, onChange: e => setName(e.target.value), onBlur: () => setNameTouched(true), error: nameTouched ? nameError : undefined }), _jsxs(Flex, { direction: "column", gap: "3", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: "Exclude Weekends" }), _jsxs(Flex, { gap: "3", children: [_jsx(ButtonToggle, { checked: weekendDays.has(6), onClick: () => toggleWeekendDay(6), children: "Saturday" }), _jsx(ButtonToggle, { checked: weekendDays.has(0), onClick: () => toggleWeekendDay(0), children: "Sunday" })] })] }), _jsxs(Flex, { direction: "column", gap: "3", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: "Exclude Holidays" }), holidayCatalogLoading && _jsx(Text, { variant: "body", children: "Loading holidays\u2026" }), holidayCatalogError && _jsx(Alert, { status: "danger", title: holidayCatalogError }), _jsx(MultiSelectFieldSync, { label: holidayCatalog ? `${holidayCatalog.jurisdiction} Holidays` : 'Holidays', disabled: holidayCatalogLoading, options: holidayOptions, value: selectedHolidayOptions, onSelectedOptionsChange: options => setKnownHolidays(new Set(options.map(o => String(o.id)))), description: "Select the holidays you want to exclude", filter: { keys: ['label'] } })] }), _jsxs(Flex, { direction: "column", gap: "2", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: "Custom Dates" }), customDates.map((row, index) => {
|
|
112
|
+
const invalid = resolvedCustomDates[index] === null && !isEmptyDate(row.value);
|
|
113
|
+
return (_jsxs(Flex, { gap: "2", children: [_jsx(DateFieldYearless, { label: `Custom date ${index + 1}`, hideLabel: true, value: row.value, error: invalid ? 'Enter a valid date' : undefined, onChange: change => handleCustomDateChange(row.id, change.value), flex: 1 }), _jsx(Button, { icon: DeleteIcon, appearance: "ghost", "aria-label": "Remove date", onClick: () => handleRemoveDate(row.id) })] }, row.id));
|
|
114
|
+
}), _jsx(Button, { appearance: "ghost", icon: IconAddCircle, onClick: handleAddDate, alignSelf: "end", children: "Add Date" })] }), submitError && _jsx(Alert, { status: "danger", title: submitError })] }));
|
|
115
|
+
if (editorContainer) {
|
|
116
|
+
return createPortal(_jsxs(Flex, { direction: "column", gap: "5", style: { height: '100%' }, children: [_jsxs(Flex, { alignItems: "center", justifyContent: "space-between", children: [_jsx(Text, { variant: "headline", el: "h6", size: "small", children: title }), _jsx(Button, { appearance: "ghost", size: "small", onClick: onClose, "aria-label": "Close", icon: IconClose })] }), form, _jsx(Button, { size: "small", appearance: "primary", onClick: handleSubmit, loading: submitting, disabled: !canSubmit, style: { width: '100%' }, children: isEditing ? 'Save' : 'Create' })] }), editorContainer);
|
|
117
|
+
}
|
|
118
|
+
return (_jsxs(Drawer, { open: true, onClose: onClose, size: "medium", children: [_jsx(Drawer.Header, { children: title }), _jsx(Drawer.Content, { children: form }), _jsxs(Drawer.Footer, { sticky: true, children: [_jsx(Drawer.CancelButton, { appearance: "secondary", children: "Cancel" }), _jsx(Button, { appearance: "primary", onClick: handleSubmit, loading: submitting, disabled: !canSubmit, children: isEditing ? 'Save' : 'Create' })] })] }));
|
|
119
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { type ScheduleEditorContext } from '../types';
|
|
3
|
+
interface SchedulePickerProps {
|
|
4
|
+
businessCalendar: ScheduleEditorContext;
|
|
5
|
+
}
|
|
6
|
+
/** "Select Schedule or create new" picker plus a collapsible preview of the selected schedule's rules. */
|
|
7
|
+
export declare const SchedulePicker: FC<SchedulePickerProps>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Alert, Button, Chip, Flex, SelectFieldSync, Text } from '@servicetitan/anvil2';
|
|
3
|
+
import DeleteIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
|
|
4
|
+
import EditIcon from '@servicetitan/anvil2/assets/icons/material/round/edit.svg';
|
|
5
|
+
import IconExpandLess from '@servicetitan/anvil2/assets/icons/material/round/expand_less.svg';
|
|
6
|
+
import IconExpandMore from '@servicetitan/anvil2/assets/icons/material/round/expand_more.svg';
|
|
7
|
+
import { Fragment, useMemo, useState } from 'react';
|
|
8
|
+
import { formatCustomDateLabel, formatKnownHolidayLabel, } from '../business-calendar-validation.utils';
|
|
9
|
+
import { SCHEDULE_PICKER_TEXT, WEEKEND_DAY_LABELS } from '../constants';
|
|
10
|
+
import { findSelectedSchedule } from '../schedule-bindings.utils';
|
|
11
|
+
import { ScheduleDrawer } from './schedule-drawer';
|
|
12
|
+
const DEFAULT_OPTION_ID = 'default';
|
|
13
|
+
/** "Select Schedule or create new" picker plus a collapsible preview of the selected schedule's rules. */
|
|
14
|
+
export const SchedulePicker = ({ businessCalendar }) => {
|
|
15
|
+
var _a;
|
|
16
|
+
const { holidayCatalog, holidayCatalogError, holidayCatalogLoading, onDeleteSchedule, onSelectSchedule, schedules, schedulesError, schedulesLoading, selectedScheduleId, } = businessCalendar;
|
|
17
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
18
|
+
const [editingSchedule, setEditingSchedule] = useState();
|
|
19
|
+
const [previewOpen, setPreviewOpen] = useState(true);
|
|
20
|
+
const [deleting, setDeleting] = useState(false);
|
|
21
|
+
const [deleteError, setDeleteError] = useState();
|
|
22
|
+
const options = useMemo(() => (schedules !== null && schedules !== void 0 ? schedules : []).map(s => { var _a; return ({ id: (_a = s.id) !== null && _a !== void 0 ? _a : DEFAULT_OPTION_ID, label: s.name }); }), [schedules]);
|
|
23
|
+
const selectedOptionId = selectedScheduleId !== null && selectedScheduleId !== void 0 ? selectedScheduleId : DEFAULT_OPTION_ID;
|
|
24
|
+
const selectedOption = (_a = options.find(o => o.id === selectedOptionId)) !== null && _a !== void 0 ? _a : null;
|
|
25
|
+
const selectedSchedule = findSelectedSchedule(schedules, selectedScheduleId);
|
|
26
|
+
const holidayByCode = useMemo(() => {
|
|
27
|
+
var _a;
|
|
28
|
+
const map = new Map();
|
|
29
|
+
for (const h of (_a = holidayCatalog === null || holidayCatalog === void 0 ? void 0 : holidayCatalog.holidays) !== null && _a !== void 0 ? _a : []) {
|
|
30
|
+
map.set(h.code, h);
|
|
31
|
+
}
|
|
32
|
+
return map;
|
|
33
|
+
}, [holidayCatalog]);
|
|
34
|
+
const handleSelectedOptionChange = (option) => {
|
|
35
|
+
if (!option) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
onSelectSchedule(option.id === DEFAULT_OPTION_ID ? undefined : option.id);
|
|
39
|
+
};
|
|
40
|
+
const handleSaved = (schedule) => {
|
|
41
|
+
onSelectSchedule(schedule.id);
|
|
42
|
+
setDrawerOpen(false);
|
|
43
|
+
setEditingSchedule(undefined);
|
|
44
|
+
};
|
|
45
|
+
const handleOpenCreate = () => {
|
|
46
|
+
setEditingSchedule(undefined);
|
|
47
|
+
setDrawerOpen(true);
|
|
48
|
+
};
|
|
49
|
+
const handleOpenEdit = () => {
|
|
50
|
+
if (!selectedSchedule) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setEditingSchedule(selectedSchedule);
|
|
54
|
+
setDrawerOpen(true);
|
|
55
|
+
};
|
|
56
|
+
const handleCloseDrawer = () => {
|
|
57
|
+
setDrawerOpen(false);
|
|
58
|
+
setEditingSchedule(undefined);
|
|
59
|
+
};
|
|
60
|
+
const handleDelete = async () => {
|
|
61
|
+
if (!(selectedSchedule === null || selectedSchedule === void 0 ? void 0 : selectedSchedule.id) || !onDeleteSchedule) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
setDeleting(true);
|
|
65
|
+
setDeleteError(undefined);
|
|
66
|
+
try {
|
|
67
|
+
await onDeleteSchedule(selectedSchedule.id);
|
|
68
|
+
onSelectSchedule(undefined);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
setDeleteError(e instanceof Error ? e.message : SCHEDULE_PICKER_TEXT.deleteFailed);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
setDeleting(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return (_jsxs(Fragment, { children: [_jsx(SelectFieldSync, { label: SCHEDULE_PICKER_TEXT.selectLabel, options: options, value: selectedOption, disableClearButton: true, disabled: schedulesLoading, onSelectedOptionChange: handleSelectedOptionChange, onAddNewItem: handleOpenCreate, addItemLabel: SCHEDULE_PICKER_TEXT.createNew, filter: { keys: ['label'] } }), schedulesLoading && (_jsx(Text, { variant: "body", children: SCHEDULE_PICKER_TEXT.loadingSchedules })), schedulesError && _jsx(Alert, { status: "danger", title: schedulesError }), selectedSchedule && (_jsxs("div", { className: "dte-business-calendar-preview", children: [_jsxs(Flex, { alignItems: "center", justifyContent: "space-between", gap: "2", className: "dte-business-calendar-preview-header", children: [_jsxs(Flex, { alignItems: "center", gap: "2", onClick: () => setPreviewOpen(prev => !prev), children: [_jsx(Button, { appearance: "ghost", size: "small", icon: previewOpen ? IconExpandLess : IconExpandMore, "aria-label": previewOpen
|
|
78
|
+
? 'Collapse schedule preview'
|
|
79
|
+
: 'Expand schedule preview' }), _jsx(Text, { variant: "body", size: "small", style: { fontWeight: 'bold' }, children: selectedSchedule.name })] }), !selectedSchedule.isDefault && (_jsxs(Flex, { alignItems: "center", gap: "2", children: [_jsx(Button, { icon: EditIcon, size: "small", appearance: "ghost", onClick: handleOpenEdit, disabled: deleting }), _jsx(Button, { icon: DeleteIcon, size: "small", appearance: "ghost", "aria-label": "Delete schedule", onClick: handleDelete, loading: deleting, disabled: !onDeleteSchedule })] }))] }), deleteError && _jsx(Alert, { status: "danger", title: deleteError }), previewOpen && (_jsxs(Flex, { direction: "column", gap: "5", className: "dte-business-calendar-preview-content", children: [_jsxs(Flex, { direction: "column", gap: "4", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: SCHEDULE_PICKER_TEXT.excludedWeekends }), _jsx(Flex, { gap: "2", style: { flexWrap: 'wrap' }, children: selectedSchedule.calendar.weekendDays.map(day => {
|
|
80
|
+
var _a;
|
|
81
|
+
return (_jsx(Chip, { label: (_a = WEEKEND_DAY_LABELS[day]) !== null && _a !== void 0 ? _a : String(day), size: "medium" }, day));
|
|
82
|
+
}) })] }), _jsxs(Flex, { direction: "column", gap: "4", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: SCHEDULE_PICKER_TEXT.excludedHolidays }), holidayCatalogLoading && (_jsx(Text, { variant: "body", children: SCHEDULE_PICKER_TEXT.loadingHolidays })), holidayCatalogError && (_jsx(Alert, { status: "danger", title: holidayCatalogError })), _jsx(Flex, { gap: "2", style: { flexWrap: 'wrap' }, children: selectedSchedule.calendar.knownHolidays.map(code => {
|
|
83
|
+
const holiday = holidayByCode.get(code);
|
|
84
|
+
return (_jsx(Chip, { label: holiday
|
|
85
|
+
? formatKnownHolidayLabel(holiday)
|
|
86
|
+
: code, size: "medium" }, code));
|
|
87
|
+
}) })] }), selectedSchedule.calendar.customDates.length > 0 && (_jsxs(Flex, { direction: "column", gap: "4", children: [_jsx(Text, { variant: "eyebrow", size: "small", children: SCHEDULE_PICKER_TEXT.customDates }), _jsx(Flex, { gap: "2", style: { flexWrap: 'wrap' }, children: selectedSchedule.calendar.customDates.map(date => (_jsx(Chip, { label: formatCustomDateLabel(date), size: "medium" }, date))) })] }))] }))] })), drawerOpen && (_jsx(ScheduleDrawer, { businessCalendar: businessCalendar, editingSchedule: editingSchedule, onClose: handleCloseDrawer, onSaved: handleSaved }))] }));
|
|
88
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** DTE's `SaveDateScheduleRequest.Name` limit. */
|
|
2
|
+
export declare const MAX_SCHEDULE_NAME_LENGTH = 255;
|
|
3
|
+
/** The read-only default schedule's name; a tenant schedule may not reuse it (case-insensitive). */
|
|
4
|
+
export declare const RESERVED_SCHEDULE_NAME = "Default";
|
|
5
|
+
/** `MM-DD`, 01-12 / 01-31, as DTE's `CalendarSpecModel.customDates` (`02-29` is a valid leap day). */
|
|
6
|
+
export declare const CUSTOM_DATE_PATTERN: RegExp;
|
|
7
|
+
/** Days per month for an `MM-DD` custom date, Feb including the leap day. */
|
|
8
|
+
export declare const CUSTOM_DATE_MONTH_LENGTHS: number[];
|
|
9
|
+
/** The default schedule's weekend: Sunday and Saturday, DTE's `Date.getUTCDay()` numbering. */
|
|
10
|
+
export declare const DEFAULT_SCHEDULE_WEEKEND_DAYS: [0, 6];
|
|
11
|
+
/**
|
|
12
|
+
* `BusinessCalendarSection`'s default toggle label, used for a date calculated field and an e-sign
|
|
13
|
+
* `dateSignedOffset` field alike.
|
|
14
|
+
*/
|
|
15
|
+
export declare const BUSINESS_CALENDAR_TOGGLE_LABEL = "Exclude weekends & holidays from the calculation";
|
|
16
|
+
/** How a schedule's preview names `BusinessCalendarRules.weekendDays`. */
|
|
17
|
+
export declare const WEEKEND_DAY_LABELS: Readonly<Record<number, string>>;
|
|
18
|
+
/**
|
|
19
|
+
* `SchedulePicker`'s text, also used by an editor that renders its own copy of the picker
|
|
20
|
+
* (Unlayer's Formula Builder, inside an iframe without Anvil2), so both read the same.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SCHEDULE_PICKER_TEXT: {
|
|
23
|
+
readonly selectLabel: "Select Schedule or create new";
|
|
24
|
+
readonly createNew: "Create new schedule";
|
|
25
|
+
readonly loadingSchedules: "Loading schedules…";
|
|
26
|
+
readonly loadingHolidays: "Loading holidays…";
|
|
27
|
+
readonly excludedWeekends: "Excluded Weekends";
|
|
28
|
+
readonly excludedHolidays: "Excluded Holidays";
|
|
29
|
+
readonly customDates: "Custom Dates";
|
|
30
|
+
readonly deleteFailed: "Failed to delete schedule";
|
|
31
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** DTE's `SaveDateScheduleRequest.Name` limit. */
|
|
2
|
+
export const MAX_SCHEDULE_NAME_LENGTH = 255;
|
|
3
|
+
/** The read-only default schedule's name; a tenant schedule may not reuse it (case-insensitive). */
|
|
4
|
+
export const RESERVED_SCHEDULE_NAME = 'Default';
|
|
5
|
+
/** `MM-DD`, 01-12 / 01-31, as DTE's `CalendarSpecModel.customDates` (`02-29` is a valid leap day). */
|
|
6
|
+
export const CUSTOM_DATE_PATTERN = /^(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
|
|
7
|
+
/** Days per month for an `MM-DD` custom date, Feb including the leap day. */
|
|
8
|
+
export const CUSTOM_DATE_MONTH_LENGTHS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
9
|
+
/** The default schedule's weekend: Sunday and Saturday, DTE's `Date.getUTCDay()` numbering. */
|
|
10
|
+
export const DEFAULT_SCHEDULE_WEEKEND_DAYS = [0, 6];
|
|
11
|
+
/**
|
|
12
|
+
* `BusinessCalendarSection`'s default toggle label, used for a date calculated field and an e-sign
|
|
13
|
+
* `dateSignedOffset` field alike.
|
|
14
|
+
*/
|
|
15
|
+
export const BUSINESS_CALENDAR_TOGGLE_LABEL = 'Exclude weekends & holidays from the calculation';
|
|
16
|
+
/** How a schedule's preview names `BusinessCalendarRules.weekendDays`. */
|
|
17
|
+
export const WEEKEND_DAY_LABELS = {
|
|
18
|
+
0: 'Sundays',
|
|
19
|
+
6: 'Saturdays',
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* `SchedulePicker`'s text, also used by an editor that renders its own copy of the picker
|
|
23
|
+
* (Unlayer's Formula Builder, inside an iframe without Anvil2), so both read the same.
|
|
24
|
+
*/
|
|
25
|
+
export const SCHEDULE_PICKER_TEXT = {
|
|
26
|
+
selectLabel: 'Select Schedule or create new',
|
|
27
|
+
createNew: 'Create new schedule',
|
|
28
|
+
loadingSchedules: 'Loading schedules…',
|
|
29
|
+
loadingHolidays: 'Loading holidays…',
|
|
30
|
+
excludedWeekends: 'Excluded Weekends',
|
|
31
|
+
excludedHolidays: 'Excluded Holidays',
|
|
32
|
+
customDates: 'Custom Dates',
|
|
33
|
+
deleteFailed: 'Failed to delete schedule',
|
|
34
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type BusinessCalendarHostProps, type FieldScheduleBinding, type ScheduleEditorContext } from './types';
|
|
2
|
+
/** Where a field's `ScheduleEditorContext` reads the current bindings and sends the next ones. */
|
|
3
|
+
export interface FieldScheduleBindingsAccess {
|
|
4
|
+
get(): FieldScheduleBinding[];
|
|
5
|
+
set(bindings: FieldScheduleBinding[]): void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The `ScheduleEditorContext` of the field `fieldKey`, built from an editor's schedule props: the
|
|
9
|
+
* toggle is on while the field has a binding, and the toggle, the picker and a schedule delete
|
|
10
|
+
* replace the bindings through `bindings.set`.
|
|
11
|
+
*
|
|
12
|
+
* Every change reads the bindings through `bindings.get` when it happens, not when the context was
|
|
13
|
+
* built, so a callback kept from an earlier render can't write stale bindings back. A pick changes
|
|
14
|
+
* only a field whose toggle is on, and only to a different schedule: the picker shows only while
|
|
15
|
+
* the toggle is on, and its select of the default schedule right after a delete (which already
|
|
16
|
+
* moved the field there) must neither resend the bindings nor turn back on a toggle switched off
|
|
17
|
+
* while the delete was running. Without a `fieldKey` the toggle is off and does nothing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildFieldScheduleEditorContext(fieldKey: string | undefined, host: BusinessCalendarHostProps, bindings: FieldScheduleBindingsAccess): ScheduleEditorContext;
|
|
20
|
+
/**
|
|
21
|
+
* A stable {@link FieldScheduleBindingsAccess} over an editor's `scheduleBindings`: changes go to the
|
|
22
|
+
* latest `onScheduleBindingsChange`, even from a callback kept from an earlier render, and until the
|
|
23
|
+
* host passes them back the next change builds on the last one sent.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useFieldScheduleBindingsAccess(scheduleBindings: FieldScheduleBinding[] | undefined, onScheduleBindingsChange: ((bindings: FieldScheduleBinding[]) => void) | undefined): FieldScheduleBindingsAccess;
|
|
26
|
+
/**
|
|
27
|
+
* {@link buildFieldScheduleEditorContext} for a component: the field's context from the editor's
|
|
28
|
+
* schedule props, rebuilt only when one of them changes, with
|
|
29
|
+
* {@link useFieldScheduleBindingsAccess} for the bindings.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useFieldScheduleEditorContext(fieldKey: string | undefined, host: BusinessCalendarHostProps): ScheduleEditorContext;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { clearDeletedSchedule, setFieldSchedule, setFieldScheduleEnabled, } from './schedule-bindings.utils';
|
|
3
|
+
/**
|
|
4
|
+
* The `ScheduleEditorContext` of the field `fieldKey`, built from an editor's schedule props: the
|
|
5
|
+
* toggle is on while the field has a binding, and the toggle, the picker and a schedule delete
|
|
6
|
+
* replace the bindings through `bindings.set`.
|
|
7
|
+
*
|
|
8
|
+
* Every change reads the bindings through `bindings.get` when it happens, not when the context was
|
|
9
|
+
* built, so a callback kept from an earlier render can't write stale bindings back. A pick changes
|
|
10
|
+
* only a field whose toggle is on, and only to a different schedule: the picker shows only while
|
|
11
|
+
* the toggle is on, and its select of the default schedule right after a delete (which already
|
|
12
|
+
* moved the field there) must neither resend the bindings nor turn back on a toggle switched off
|
|
13
|
+
* while the delete was running. Without a `fieldKey` the toggle is off and does nothing.
|
|
14
|
+
*/
|
|
15
|
+
export function buildFieldScheduleEditorContext(fieldKey, host, bindings) {
|
|
16
|
+
var _a;
|
|
17
|
+
const binding = fieldKey
|
|
18
|
+
? (_a = host.scheduleBindings) === null || _a === void 0 ? void 0 : _a.find(b => b.fieldKey === fieldKey)
|
|
19
|
+
: undefined;
|
|
20
|
+
const { onDeleteSchedule } = host;
|
|
21
|
+
return {
|
|
22
|
+
enabled: !!binding,
|
|
23
|
+
selectedScheduleId: binding === null || binding === void 0 ? void 0 : binding.scheduleId,
|
|
24
|
+
onToggle: enabled => {
|
|
25
|
+
if (fieldKey) {
|
|
26
|
+
bindings.set(setFieldScheduleEnabled(bindings.get(), fieldKey, enabled));
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
onSelectSchedule: scheduleId => {
|
|
30
|
+
const current = bindings.get();
|
|
31
|
+
const fieldBinding = fieldKey ? current.find(b => b.fieldKey === fieldKey) : undefined;
|
|
32
|
+
if (fieldKey && fieldBinding && fieldBinding.scheduleId !== scheduleId) {
|
|
33
|
+
bindings.set(setFieldSchedule(current, fieldKey, scheduleId));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
holidayCatalog: host.holidayCatalog,
|
|
37
|
+
holidayCatalogLoading: host.holidayCatalogLoading,
|
|
38
|
+
holidayCatalogError: host.holidayCatalogError,
|
|
39
|
+
schedules: host.schedules,
|
|
40
|
+
schedulesLoading: host.schedulesLoading,
|
|
41
|
+
schedulesError: host.schedulesError,
|
|
42
|
+
onCreateSchedule: host.onCreateSchedule,
|
|
43
|
+
onUpdateSchedule: host.onUpdateSchedule,
|
|
44
|
+
onDeleteSchedule: onDeleteSchedule &&
|
|
45
|
+
(async (id) => {
|
|
46
|
+
await onDeleteSchedule(id);
|
|
47
|
+
const current = bindings.get();
|
|
48
|
+
if (current.some(b => b.scheduleId === id)) {
|
|
49
|
+
bindings.set(clearDeletedSchedule(current, id));
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A stable {@link FieldScheduleBindingsAccess} over an editor's `scheduleBindings`: changes go to the
|
|
56
|
+
* latest `onScheduleBindingsChange`, even from a callback kept from an earlier render, and until the
|
|
57
|
+
* host passes them back the next change builds on the last one sent.
|
|
58
|
+
*/
|
|
59
|
+
export function useFieldScheduleBindingsAccess(scheduleBindings, onScheduleBindingsChange) {
|
|
60
|
+
const latestBindings = useRef(scheduleBindings !== null && scheduleBindings !== void 0 ? scheduleBindings : []);
|
|
61
|
+
const latestOnChange = useRef(onScheduleBindingsChange);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
latestBindings.current = scheduleBindings !== null && scheduleBindings !== void 0 ? scheduleBindings : [];
|
|
64
|
+
}, [scheduleBindings]);
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
latestOnChange.current = onScheduleBindingsChange;
|
|
67
|
+
}, [onScheduleBindingsChange]);
|
|
68
|
+
return useMemo(() => ({
|
|
69
|
+
get: () => latestBindings.current,
|
|
70
|
+
set: next => {
|
|
71
|
+
var _a;
|
|
72
|
+
latestBindings.current = next;
|
|
73
|
+
(_a = latestOnChange.current) === null || _a === void 0 ? void 0 : _a.call(latestOnChange, next);
|
|
74
|
+
},
|
|
75
|
+
}), []);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* {@link buildFieldScheduleEditorContext} for a component: the field's context from the editor's
|
|
79
|
+
* schedule props, rebuilt only when one of them changes, with
|
|
80
|
+
* {@link useFieldScheduleBindingsAccess} for the bindings.
|
|
81
|
+
*/
|
|
82
|
+
export function useFieldScheduleEditorContext(fieldKey, host) {
|
|
83
|
+
const { holidayCatalog, holidayCatalogError, holidayCatalogLoading, onCreateSchedule, onDeleteSchedule, onScheduleBindingsChange, onUpdateSchedule, scheduleBindings, schedules, schedulesError, schedulesLoading, } = host;
|
|
84
|
+
const bindings = useFieldScheduleBindingsAccess(scheduleBindings, onScheduleBindingsChange);
|
|
85
|
+
return useMemo(() => buildFieldScheduleEditorContext(fieldKey, {
|
|
86
|
+
holidayCatalog,
|
|
87
|
+
holidayCatalogError,
|
|
88
|
+
holidayCatalogLoading,
|
|
89
|
+
onCreateSchedule,
|
|
90
|
+
onDeleteSchedule,
|
|
91
|
+
onUpdateSchedule,
|
|
92
|
+
scheduleBindings,
|
|
93
|
+
schedules,
|
|
94
|
+
schedulesError,
|
|
95
|
+
schedulesLoading,
|
|
96
|
+
}, bindings), [
|
|
97
|
+
bindings,
|
|
98
|
+
fieldKey,
|
|
99
|
+
holidayCatalog,
|
|
100
|
+
holidayCatalogError,
|
|
101
|
+
holidayCatalogLoading,
|
|
102
|
+
onCreateSchedule,
|
|
103
|
+
onDeleteSchedule,
|
|
104
|
+
onUpdateSchedule,
|
|
105
|
+
scheduleBindings,
|
|
106
|
+
schedules,
|
|
107
|
+
schedulesError,
|
|
108
|
+
schedulesLoading,
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { BusinessCalendarSection } from './components/business-calendar-section';
|
|
2
|
+
export { SchedulePicker } from './components/schedule-picker';
|
|
3
|
+
export { ScheduleDrawer } from './components/schedule-drawer';
|
|
4
|
+
export type * from './types';
|
|
5
|
+
export * from './constants';
|
|
6
|
+
export * from './business-calendar-validation.utils';
|
|
7
|
+
export * from './schedule-bindings.utils';
|
|
8
|
+
export * from './field-schedule-editor-context';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BusinessCalendarSection } from './components/business-calendar-section';
|
|
2
|
+
export { SchedulePicker } from './components/schedule-picker';
|
|
3
|
+
export { ScheduleDrawer } from './components/schedule-drawer';
|
|
4
|
+
export * from './constants';
|
|
5
|
+
export * from './business-calendar-validation.utils';
|
|
6
|
+
export * from './schedule-bindings.utils';
|
|
7
|
+
export * from './field-schedule-editor-context';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type BusinessDaySchedule, type FieldScheduleBinding } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The schedule a field's picker shows for its `scheduleId` (undefined for the default schedule),
|
|
4
|
+
* or undefined when it isn't listed (for example a deleted one).
|
|
5
|
+
*/
|
|
6
|
+
export declare function findSelectedSchedule(schedules: BusinessDaySchedule[] | undefined, scheduleId: number | undefined): BusinessDaySchedule | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Replaces `fieldKey`'s binding: enabling adds it (default schedule, `scheduleId` undefined);
|
|
9
|
+
* disabling removes it. Every other field's binding is returned unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export declare function setFieldScheduleEnabled(bindings: FieldScheduleBinding[], fieldKey: string, enabled: boolean): FieldScheduleBinding[];
|
|
12
|
+
/**
|
|
13
|
+
* Sets `fieldKey`'s selected schedule (`scheduleId` undefined for the default schedule),
|
|
14
|
+
* enabling its toggle if it wasn't already on. Every other field's binding is returned unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export declare function setFieldSchedule(bindings: FieldScheduleBinding[], fieldKey: string, scheduleId: number | undefined): FieldScheduleBinding[];
|
|
17
|
+
/**
|
|
18
|
+
* Moves every field bound to the deleted schedule `scheduleId` to the default schedule, keeping its
|
|
19
|
+
* toggle on, as DTE falls back for a deleted schedule (and DTE refuses to save a binding to a
|
|
20
|
+
* schedule that no longer exists). Every other binding is returned unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export declare function clearDeletedSchedule(bindings: FieldScheduleBinding[], scheduleId: number): FieldScheduleBinding[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schedule a field's picker shows for its `scheduleId` (undefined for the default schedule),
|
|
3
|
+
* or undefined when it isn't listed (for example a deleted one).
|
|
4
|
+
*/
|
|
5
|
+
export function findSelectedSchedule(schedules, scheduleId) {
|
|
6
|
+
return (schedules !== null && schedules !== void 0 ? schedules : []).find(s => scheduleId === undefined ? s.id === undefined : s.id === scheduleId);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Replaces `fieldKey`'s binding: enabling adds it (default schedule, `scheduleId` undefined);
|
|
10
|
+
* disabling removes it. Every other field's binding is returned unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export function setFieldScheduleEnabled(bindings, fieldKey, enabled) {
|
|
13
|
+
const withoutField = bindings.filter(b => b.fieldKey !== fieldKey);
|
|
14
|
+
return enabled ? [...withoutField, { fieldKey }] : withoutField;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sets `fieldKey`'s selected schedule (`scheduleId` undefined for the default schedule),
|
|
18
|
+
* enabling its toggle if it wasn't already on. Every other field's binding is returned unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export function setFieldSchedule(bindings, fieldKey, scheduleId) {
|
|
21
|
+
const withoutField = bindings.filter(b => b.fieldKey !== fieldKey);
|
|
22
|
+
return [...withoutField, { fieldKey, scheduleId }];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Moves every field bound to the deleted schedule `scheduleId` to the default schedule, keeping its
|
|
26
|
+
* toggle on, as DTE falls back for a deleted schedule (and DTE refuses to save a binding to a
|
|
27
|
+
* schedule that no longer exists). Every other binding is returned unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export function clearDeletedSchedule(bindings, scheduleId) {
|
|
30
|
+
return bindings.map(b => (b.scheduleId === scheduleId ? { fieldKey: b.fieldKey } : b));
|
|
31
|
+
}
|