cronofy-elements 1.42.1 → 1.45.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/Makefile +6 -12
- package/build/CronofyElements.v1.45.0.js +2 -0
- package/build/{CronofyElements.v1.42.1.js.LICENSE.txt → CronofyElements.v1.45.0.js.LICENSE.txt} +1 -1
- package/build/npm/CronofyElements.js +2 -2
- package/demo/date-time-picker.ejs +83 -23
- package/demo/rules.ejs +7 -1
- package/package.json +57 -57
- package/src/js/components/AvailabilityRules/AvailabilityRules.js +15 -1
- package/src/js/components/AvailabilityRules/CalendarSelector.js +1 -1
- package/src/js/components/AvailabilityRules/Calendars.js +1 -1
- package/src/js/components/AvailabilityRules/TimeZoneDisplay.js +13 -2
- package/src/js/components/AvailabilityRules/Wrapper.js +41 -16
- package/src/js/components/AvailabilityRules/scss/_base.buttons.scss +58 -0
- package/src/js/components/AvailabilityRules/scss/_base.theme.scss +4 -0
- package/src/js/components/AvailabilityRules/scss/_components.timezoneselector.scss +74 -0
- package/src/js/components/AvailabilityRules/scss/_generic.reset.scss +13 -0
- package/src/js/components/AvailabilityRules/scss/_settings.colours.scss +12 -0
- package/src/js/components/AvailabilityRules/scss/availabilityrules.scss +5 -0
- package/src/js/components/AvailabilityRules/utils/tz-utils.js +44 -0
- package/src/js/components/DateTimePicker/Calendar.js +1 -1
- package/src/js/components/DateTimePicker/CalendarHeader.js +1 -1
- package/src/js/components/DateTimePicker/Confirm.js +1 -1
- package/src/js/components/DateTimePicker/DateTimePicker.js +6 -1
- package/src/js/components/DateTimePicker/DayButton.js +1 -1
- package/src/js/components/DateTimePicker/Details.js +10 -5
- package/src/js/components/DateTimePicker/SequencedSlotButton.js +71 -0
- package/src/js/components/DateTimePicker/SlotButton.js +1 -1
- package/src/js/components/DateTimePicker/SlotsList.js +6 -1
- package/src/js/components/DateTimePicker/Wrapper.js +22 -20
- package/src/js/components/DateTimePicker/contexts/status-reducer.js +24 -7
- package/src/js/components/DateTimePicker/utils/slots.js +69 -3
- package/src/js/components/{DateTimePicker → generic}/TimeZoneSelector.js +9 -16
- package/src/js/{components/DateTimePicker/contexts → contexts}/tz-context.js +0 -0
- package/src/js/helpers/connections.js +38 -0
- package/src/js/helpers/init.DateTimePicker.js +4 -0
- package/src/js/{components/DateTimePicker/utils → helpers}/tz-list.js +0 -0
- package/tests/AvailabilityRules/__snapshots__/AvailabilityRules.test.js.snap +36 -6
- package/tests/DateTimePicker/SequencedSlotButton.test.js +130 -0
- package/tests/DateTimePicker/SlotButton.test.js +1 -1
- package/tests/DateTimePicker/contexts/status-reducer.test.js +226 -67
- package/tests/DateTimePicker/dummy-data.js +277 -0
- package/tests/DateTimePicker/utils.test.js +7 -1
- package/tests/components/TimezoneSelector.test.js +124 -0
- package/build/CronofyElements.v1.42.1.js +0 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import moment from "moment-timezone";
|
|
2
|
+
import { humanizeTzName } from "../../../helpers/utils";
|
|
3
|
+
import { defaultTimeZones } from "../../../helpers/tz-list";
|
|
4
|
+
|
|
5
|
+
export const createTzObject = tzid => {
|
|
6
|
+
const name = humanizeTzName(tzid);
|
|
7
|
+
|
|
8
|
+
const zone = moment.tz(tzid);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
tzid: tzid,
|
|
12
|
+
offset: zone.format("Z"),
|
|
13
|
+
offsetMins: zone.utcOffset(),
|
|
14
|
+
name: name,
|
|
15
|
+
abbr: zone.zoneAbbr(),
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const parseTzList = (timezones, tzid) => {
|
|
20
|
+
const tzList = timezones ? timezones : moment.tz.names();
|
|
21
|
+
const filtered = timezones ? timezones : tzList.filter(item => defaultTimeZones.includes(item));
|
|
22
|
+
|
|
23
|
+
const result = [];
|
|
24
|
+
|
|
25
|
+
filtered.map(tz => {
|
|
26
|
+
const item = createTzObject(tz);
|
|
27
|
+
result.push(item);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const isInList = result.findIndex(item => tzid === item.tzid);
|
|
31
|
+
|
|
32
|
+
if (isInList <= -1) {
|
|
33
|
+
const item = createTzObject(tzid);
|
|
34
|
+
result.push(item);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
result.sort((tzA, tzB) => tzA.offsetMins - tzB.offsetMins);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const getInitialSelectedTzid = (tzList, tzid) => {
|
|
42
|
+
const result = tzList.find(tz => tzid === tz.tzid);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
@@ -14,7 +14,7 @@ import DayHeadings from "./DayHeadings";
|
|
|
14
14
|
|
|
15
15
|
import { useStatus } from "./contexts/status-context";
|
|
16
16
|
import { useTheme } from "./contexts/theme-context";
|
|
17
|
-
import { useTz } from "
|
|
17
|
+
import { useTz } from "../../contexts/tz-context";
|
|
18
18
|
|
|
19
19
|
const Calendar = () => {
|
|
20
20
|
const [status, dispatchStatus] = useStatus();
|
|
@@ -4,7 +4,7 @@ import moment from "moment-timezone";
|
|
|
4
4
|
import { useI18n } from "../../contexts/i18n-context";
|
|
5
5
|
import { useStatus } from "./contexts/status-context";
|
|
6
6
|
import { useTheme } from "./contexts/theme-context";
|
|
7
|
-
import { useTz } from "
|
|
7
|
+
import { useTz } from "../../contexts/tz-context";
|
|
8
8
|
|
|
9
9
|
const CalendarHeader = () => {
|
|
10
10
|
const i18n = useI18n();
|
|
@@ -4,7 +4,7 @@ import moment from "moment-timezone";
|
|
|
4
4
|
import { useI18n } from "../../contexts/i18n-context";
|
|
5
5
|
import { useStatus } from "./contexts/status-context";
|
|
6
6
|
import { useTheme } from "./contexts/theme-context";
|
|
7
|
-
import { useTz } from "
|
|
7
|
+
import { useTz } from "../../contexts/tz-context";
|
|
8
8
|
|
|
9
9
|
const Confirm = ({ confirmButtonRef }) => {
|
|
10
10
|
const i18n = useI18n();
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
parseTzList,
|
|
7
7
|
getInitialSelectedTzid,
|
|
8
8
|
getMonthsLoadingFromQuery,
|
|
9
|
+
getDurationFromQuery,
|
|
9
10
|
} from "./utils/slots";
|
|
10
11
|
import { getMonthsInDisplay, parseTimeSlots } from "./utils/calendar";
|
|
11
12
|
|
|
@@ -17,7 +18,7 @@ import { I18nProvider } from "../../contexts/i18n-context";
|
|
|
17
18
|
import { LogProvider } from "../../contexts/log-context";
|
|
18
19
|
import { ThemeProvider } from "./contexts/theme-context";
|
|
19
20
|
import { StatusProvider } from "./contexts/status-context";
|
|
20
|
-
import { TzProvider } from "
|
|
21
|
+
import { TzProvider } from "../../contexts/tz-context";
|
|
21
22
|
|
|
22
23
|
const DateTimePicker = ({ options }) => {
|
|
23
24
|
const statusOptions = useMemo(() => {
|
|
@@ -30,6 +31,8 @@ const DateTimePicker = ({ options }) => {
|
|
|
30
31
|
selectedDateObject?.format("YYYY-MM")
|
|
31
32
|
);
|
|
32
33
|
|
|
34
|
+
const duration = getDurationFromQuery(options.query);
|
|
35
|
+
|
|
33
36
|
const endDateObject = moment(options.config.endDate, "YYYY-MM-DD");
|
|
34
37
|
const startDateObject = moment(options.config.startDate, "YYYY-MM-DD");
|
|
35
38
|
const currentMonthObject = selectedDateObject ?? startDateObject;
|
|
@@ -61,6 +64,7 @@ const DateTimePicker = ({ options }) => {
|
|
|
61
64
|
: () => options.log.info("No `callback` option has been provided"),
|
|
62
65
|
columnView: "loading", // loading | error | slots | confirm | no-slots
|
|
63
66
|
daySlots: [],
|
|
67
|
+
duration,
|
|
64
68
|
locale: options.locale,
|
|
65
69
|
mode: options.config.mode, // confirm (default) | no_confirm
|
|
66
70
|
query: options.query,
|
|
@@ -75,6 +79,7 @@ const DateTimePicker = ({ options }) => {
|
|
|
75
79
|
endDateObject,
|
|
76
80
|
focusedSlot: false,
|
|
77
81
|
availableDays: [],
|
|
82
|
+
sequenced_availability: options.query.sequence ? true : false,
|
|
78
83
|
slots: {},
|
|
79
84
|
slotFetchCount: 0,
|
|
80
85
|
slotInjectionPoint: undefined,
|
|
@@ -4,7 +4,7 @@ import moment from "moment-timezone";
|
|
|
4
4
|
import { useI18n } from "../../contexts/i18n-context";
|
|
5
5
|
import { useStatus } from "./contexts/status-context";
|
|
6
6
|
import { useTheme } from "./contexts/theme-context";
|
|
7
|
-
import { useTz } from "
|
|
7
|
+
import { useTz } from "../../contexts/tz-context";
|
|
8
8
|
|
|
9
9
|
const DayButton = ({ day, selected = false, focused = false }) => {
|
|
10
10
|
const i18n = useI18n();
|
|
@@ -2,11 +2,14 @@ import React, { memo } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { useI18n } from "../../contexts/i18n-context";
|
|
4
4
|
import { useTheme } from "./contexts/theme-context";
|
|
5
|
-
import TimeZoneSelector from "
|
|
5
|
+
import TimeZoneSelector from "../generic/TimeZoneSelector";
|
|
6
|
+
|
|
7
|
+
import { useTz } from "../../contexts/tz-context";
|
|
6
8
|
|
|
7
9
|
const Details = ({ duration, locale }) => {
|
|
8
10
|
const i18n = useI18n();
|
|
9
11
|
const theme = useTheme();
|
|
12
|
+
const [tz, setTz] = useTz();
|
|
10
13
|
|
|
11
14
|
return (
|
|
12
15
|
<div className={theme.classBuilder("details")}>
|
|
@@ -14,12 +17,14 @@ const Details = ({ duration, locale }) => {
|
|
|
14
17
|
<p className={theme.classBuilder("details--tz-label")}>
|
|
15
18
|
<strong>{i18n.t("time_zone")}:</strong>
|
|
16
19
|
</p>
|
|
17
|
-
<TimeZoneSelector locale={locale} />
|
|
20
|
+
<TimeZoneSelector locale={locale} theme={theme} tz={tz} setTz={setTz} />
|
|
18
21
|
</div>
|
|
19
22
|
<div className={theme.classBuilder("details--duration")}>
|
|
20
|
-
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
+
{duration && (
|
|
24
|
+
<p>
|
|
25
|
+
<strong>{i18n.t("duration_label")}:</strong> {duration} {i18n.t("minutes")}
|
|
26
|
+
</p>
|
|
27
|
+
)}
|
|
23
28
|
</div>
|
|
24
29
|
</div>
|
|
25
30
|
);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import moment from "moment-timezone";
|
|
3
|
+
|
|
4
|
+
import { useI18n } from "../../contexts/i18n-context";
|
|
5
|
+
import { useStatus } from "./contexts/status-context";
|
|
6
|
+
import { useTheme } from "./contexts/theme-context";
|
|
7
|
+
import { useTz } from "../../contexts/tz-context";
|
|
8
|
+
|
|
9
|
+
const SequencedSlotButton = ({ slot }) => {
|
|
10
|
+
const i18n = useI18n();
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
const [status, dispatchStatus] = useStatus();
|
|
13
|
+
const [tz] = useTz();
|
|
14
|
+
|
|
15
|
+
const slotButtonRef = useRef();
|
|
16
|
+
|
|
17
|
+
const startArray = slot.map(a => a.start);
|
|
18
|
+
const endArray = slot.map(a => a.end);
|
|
19
|
+
|
|
20
|
+
const start = startArray.reduce((prev, current) => {
|
|
21
|
+
return prev < current ? prev : current;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const end = endArray.reduce((prev, current) => {
|
|
25
|
+
return prev > current ? prev : current;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const handleSlotSelection = slot => {
|
|
29
|
+
const selectedSlot = {
|
|
30
|
+
start,
|
|
31
|
+
end,
|
|
32
|
+
sequence: slot,
|
|
33
|
+
};
|
|
34
|
+
dispatchStatus({ type: "SELECT_SLOT", slot: selectedSlot, tzid: tz.selectedTzid.tzid });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (status.focusedSlot === start) {
|
|
39
|
+
slotButtonRef.current.focus();
|
|
40
|
+
}
|
|
41
|
+
}, [status.focusedSlot]);
|
|
42
|
+
|
|
43
|
+
let classStub = "time-slot";
|
|
44
|
+
if (status.selected.start === start) classStub = classStub + " time-slot--selected";
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
className={theme.classBuilder(classStub)}
|
|
49
|
+
onClick={() => handleSlotSelection(slot)}
|
|
50
|
+
ref={slotButtonRef}
|
|
51
|
+
>
|
|
52
|
+
<span className={theme.classBuilder("visually-hidden")}>
|
|
53
|
+
{i18n.t("select_time_slot")}
|
|
54
|
+
</span>
|
|
55
|
+
{i18n.customFormatedTimeZone(
|
|
56
|
+
moment(start, "YYYY-MM-DDTHH:mm:00Z"),
|
|
57
|
+
tz.selectedTzid.tzid,
|
|
58
|
+
"LT"
|
|
59
|
+
)}
|
|
60
|
+
{" - "}
|
|
61
|
+
{i18n.customFormatedTimeZone(
|
|
62
|
+
moment(end, "YYYY-MM-DDTHH:mm:00Z"),
|
|
63
|
+
tz.selectedTzid.tzid,
|
|
64
|
+
"LT"
|
|
65
|
+
)}{" "}
|
|
66
|
+
{`(${moment(start, "YYYY-MM-DDTHH:mm:00Z").tz(tz.selectedTzid.tzid).format("z")})`}
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default SequencedSlotButton;
|
|
@@ -4,7 +4,7 @@ import moment from "moment-timezone";
|
|
|
4
4
|
import { useI18n } from "../../contexts/i18n-context";
|
|
5
5
|
import { useStatus } from "./contexts/status-context";
|
|
6
6
|
import { useTheme } from "./contexts/theme-context";
|
|
7
|
-
import { useTz } from "
|
|
7
|
+
import { useTz } from "../../contexts/tz-context";
|
|
8
8
|
|
|
9
9
|
const SlotButton = ({ slot }) => {
|
|
10
10
|
const i18n = useI18n();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
import SlotButton from "./SlotButton";
|
|
4
|
+
import SequencedSlotButton from "./SequencedSlotButton";
|
|
4
5
|
|
|
5
6
|
import { useI18n } from "../../contexts/i18n-context";
|
|
6
7
|
import { useStatus } from "./contexts/status-context";
|
|
@@ -14,7 +15,11 @@ const SlotsList = () => {
|
|
|
14
15
|
const renderSlots = status.daySlots.map((slot, key) => {
|
|
15
16
|
return (
|
|
16
17
|
<li key={key} className={theme.classBuilder("slot-list--item")}>
|
|
17
|
-
|
|
18
|
+
{status.sequenced_availability ? (
|
|
19
|
+
<SequencedSlotButton slot={slot} />
|
|
20
|
+
) : (
|
|
21
|
+
<SlotButton slot={slot} />
|
|
22
|
+
)}
|
|
18
23
|
</li>
|
|
19
24
|
);
|
|
20
25
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import moment from "moment";
|
|
3
3
|
|
|
4
|
-
import { getSlots } from "./utils/slots";
|
|
4
|
+
import { getSequencedSlots, getSlots } from "./utils/slots";
|
|
5
5
|
|
|
6
6
|
import Calendar from "./Calendar";
|
|
7
7
|
import Confirm from "./Confirm";
|
|
@@ -14,7 +14,7 @@ import SlotsList from "./SlotsList";
|
|
|
14
14
|
|
|
15
15
|
import { useStatus } from "./contexts/status-context";
|
|
16
16
|
import { useTheme } from "./contexts/theme-context";
|
|
17
|
-
import { useTz } from "
|
|
17
|
+
import { useTz } from "../../contexts/tz-context";
|
|
18
18
|
|
|
19
19
|
const Wrapper = () => {
|
|
20
20
|
const [status, dispatchStatus] = useStatus();
|
|
@@ -31,26 +31,20 @@ const Wrapper = () => {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const fetchMonthSlots = (query, month) => {
|
|
34
|
-
|
|
34
|
+
const fetch = status.sequenced_availability ? getSequencedSlots : getSlots;
|
|
35
|
+
|
|
36
|
+
return fetch({
|
|
35
37
|
query,
|
|
36
38
|
auth: status.auth,
|
|
37
39
|
tzid: status.tzid,
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
month: month,
|
|
45
|
-
});
|
|
46
|
-
})
|
|
47
|
-
.catch(res => {
|
|
48
|
-
dispatchStatus({
|
|
49
|
-
type: "ERROR_LOADING_SLOTS",
|
|
50
|
-
error: res,
|
|
51
|
-
month: month,
|
|
52
|
-
});
|
|
40
|
+
}).then(res => {
|
|
41
|
+
dispatchStatus({
|
|
42
|
+
type: "SET_SLOTS",
|
|
43
|
+
slots: res,
|
|
44
|
+
tzid: tz.selectedTzid.tzid,
|
|
45
|
+
month: month,
|
|
53
46
|
});
|
|
47
|
+
});
|
|
54
48
|
};
|
|
55
49
|
|
|
56
50
|
const currentMonth = status.months.find(m => m.current);
|
|
@@ -60,7 +54,15 @@ const Wrapper = () => {
|
|
|
60
54
|
.then(() => {
|
|
61
55
|
// Get slots for remianing months
|
|
62
56
|
const remainingMonths = status.months.filter(month => !month.current);
|
|
63
|
-
remainingMonths.forEach(month =>
|
|
57
|
+
remainingMonths.forEach(month =>
|
|
58
|
+
fetchMonthSlots(month.query, month.month).catch(res => {
|
|
59
|
+
dispatchStatus({
|
|
60
|
+
type: "ERROR_LOADING_SLOTS",
|
|
61
|
+
error: res,
|
|
62
|
+
month: month.month,
|
|
63
|
+
});
|
|
64
|
+
})
|
|
65
|
+
);
|
|
64
66
|
})
|
|
65
67
|
.catch(error => {
|
|
66
68
|
dispatchStatus({ type: "ERROR_GETTING_SLOTS", error });
|
|
@@ -172,7 +174,7 @@ const Wrapper = () => {
|
|
|
172
174
|
|
|
173
175
|
return (
|
|
174
176
|
<section className={theme.classBuilder()} style={theme.customProperties}>
|
|
175
|
-
<Details duration={status.
|
|
177
|
+
<Details duration={status.duration} locale={status.locale} />
|
|
176
178
|
<div className={theme.classBuilder("wrapper")}>
|
|
177
179
|
<div
|
|
178
180
|
className={
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getAvailableDays,
|
|
7
7
|
getLocalDayFromUtc,
|
|
8
8
|
removeMonthFromLoading,
|
|
9
|
+
addSequencedSlotsToObject,
|
|
9
10
|
} from "../utils/slots";
|
|
10
11
|
import { getMonthsInDisplay, parseTimeSlots } from "../utils/calendar";
|
|
11
12
|
|
|
@@ -43,6 +44,7 @@ export const statusReducer = (state, action) => {
|
|
|
43
44
|
notification: {
|
|
44
45
|
type: "error",
|
|
45
46
|
message: "There was an error getting the slots",
|
|
47
|
+
body: action.error.body,
|
|
46
48
|
},
|
|
47
49
|
};
|
|
48
50
|
state.callback(notification);
|
|
@@ -66,6 +68,12 @@ export const statusReducer = (state, action) => {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
case "NO_SLOTS_FOUND": {
|
|
71
|
+
const notification = {
|
|
72
|
+
notification: {
|
|
73
|
+
type: "no_slots_found",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
state.callback(notification);
|
|
69
77
|
return { ...state, columnView: "no-slots" };
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -99,18 +107,23 @@ export const statusReducer = (state, action) => {
|
|
|
99
107
|
};
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
const slotsObject =
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
const slotsObject = state.sequenced_availability
|
|
111
|
+
? addSequencedSlotsToObject(state.slots, action.slots)
|
|
112
|
+
: addSlotsToObject(state.slots, action.slots);
|
|
113
|
+
const availableDays = getAvailableDays(slotsObject, action.tzid);
|
|
105
114
|
const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
|
|
106
115
|
|
|
116
|
+
const injectionPoint = state.sequenced_availability
|
|
117
|
+
? getLocalDayFromUtc(action.slots[0].sequence[0].start, action.tzid)
|
|
118
|
+
: getLocalDayFromUtc(action.slots[0].start, action.tzid);
|
|
119
|
+
|
|
107
120
|
return {
|
|
108
121
|
...state,
|
|
109
|
-
availableDays
|
|
122
|
+
availableDays,
|
|
110
123
|
monthsLoading,
|
|
111
124
|
slots: slotsObject,
|
|
112
125
|
slotFetchCount: state.slotFetchCount + 1,
|
|
113
|
-
slotInjectionPoint:
|
|
126
|
+
slotInjectionPoint: injectionPoint,
|
|
114
127
|
};
|
|
115
128
|
}
|
|
116
129
|
|
|
@@ -123,13 +136,17 @@ export const statusReducer = (state, action) => {
|
|
|
123
136
|
|
|
124
137
|
case "SELECT_DAY": {
|
|
125
138
|
const daySlots = getSlotsByDay(state.slots, action.day, action.tzid);
|
|
126
|
-
const focusedSlot =
|
|
139
|
+
const focusedSlot = state.sequenced_availability
|
|
140
|
+
? daySlots[0]?.reduce((prev, current) => {
|
|
141
|
+
return prev.start < current.start ? prev.start : current.start;
|
|
142
|
+
})
|
|
143
|
+
: daySlots[0]?.start;
|
|
127
144
|
|
|
128
145
|
return {
|
|
129
146
|
...state,
|
|
130
147
|
selectedDay: action.day,
|
|
131
148
|
focusedDay: action.day,
|
|
132
|
-
focusedSlot: focusedSlot
|
|
149
|
+
focusedSlot: focusedSlot,
|
|
133
150
|
columnView: "slots",
|
|
134
151
|
daySlots,
|
|
135
152
|
populated: true,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import moment from "moment-timezone";
|
|
2
2
|
|
|
3
|
-
import { getAvailability } from "../../../helpers/connections";
|
|
3
|
+
import { getAvailability, getSequencedAvailability } from "../../../helpers/connections";
|
|
4
4
|
import { uniqueItems, humanizeTzName } from "../../../helpers/utils";
|
|
5
5
|
import { errorMessages } from "../../../helpers/logging";
|
|
6
6
|
|
|
7
|
-
import { defaultTimeZones } from "
|
|
7
|
+
import { defaultTimeZones } from "../../../helpers/tz-list";
|
|
8
8
|
|
|
9
9
|
export const getMonthsCoveredByPeriod = (period, tzid) => {
|
|
10
10
|
const start = moment
|
|
@@ -33,6 +33,12 @@ export const getMonthsFromQuery = (periods = [], tzid) => {
|
|
|
33
33
|
return uniqueMonths;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export const getDurationFromQuery = query => {
|
|
37
|
+
const duration = query.sequence ? false : query.required_duration.minutes;
|
|
38
|
+
|
|
39
|
+
return duration;
|
|
40
|
+
};
|
|
41
|
+
|
|
36
42
|
export const getMonthsLoadingFromQuery = (query, tzid) => {
|
|
37
43
|
if (!query.query_periods || !query.query_periods.length) {
|
|
38
44
|
return [
|
|
@@ -104,7 +110,7 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
|
104
110
|
tzid,
|
|
105
111
|
auth.demo
|
|
106
112
|
).then(res => {
|
|
107
|
-
if (res.
|
|
113
|
+
if (res.status === 422) {
|
|
108
114
|
throw {
|
|
109
115
|
type: 422,
|
|
110
116
|
message: errorMessages[422].message,
|
|
@@ -142,6 +148,53 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
|
142
148
|
});
|
|
143
149
|
});
|
|
144
150
|
|
|
151
|
+
export const getSequencedSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
152
|
+
getSequencedAvailability(
|
|
153
|
+
auth.token,
|
|
154
|
+
auth.domains.apiDomain,
|
|
155
|
+
query,
|
|
156
|
+
"DateTimePicker",
|
|
157
|
+
tzid,
|
|
158
|
+
auth.demo
|
|
159
|
+
).then(res => {
|
|
160
|
+
if (res.errors) {
|
|
161
|
+
throw {
|
|
162
|
+
type: 422,
|
|
163
|
+
message: errorMessages[422].message,
|
|
164
|
+
body: res.errors,
|
|
165
|
+
docsSlug: errorMessages[422].docsSlug,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// This will intentionally throw an error if the
|
|
169
|
+
// result is not in the correct format:
|
|
170
|
+
const returnedSlots = parseSlotsResult(res);
|
|
171
|
+
|
|
172
|
+
const allSlots = [...slots, ...returnedSlots];
|
|
173
|
+
|
|
174
|
+
if (returnedSlots.length < 512) return [...slots, ...returnedSlots];
|
|
175
|
+
|
|
176
|
+
// If we get here, the API has returned the maximum number
|
|
177
|
+
// of slots allowed, so we need to crop the query and try
|
|
178
|
+
// again to ensure we haven't missed any slots.
|
|
179
|
+
|
|
180
|
+
const startOfLastSlot = returnedSlots[returnedSlots.length - 1].start;
|
|
181
|
+
const endOfLastPeriod = query.query_periods[query.query_periods.length - 1].end;
|
|
182
|
+
const boundsForCropping = {
|
|
183
|
+
start: startOfLastSlot,
|
|
184
|
+
end: endOfLastPeriod,
|
|
185
|
+
};
|
|
186
|
+
const croppedPeriods = cropPeriodsArbitrarily(query.query_periods, boundsForCropping);
|
|
187
|
+
const croppedQuery = { ...query, query_periods: croppedPeriods };
|
|
188
|
+
|
|
189
|
+
// Rerun the query
|
|
190
|
+
return getSequencedSlots({
|
|
191
|
+
query: croppedQuery,
|
|
192
|
+
auth,
|
|
193
|
+
tzid,
|
|
194
|
+
slots: allSlots,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
145
198
|
export const parseQuery = query => {
|
|
146
199
|
if (!query.bookable_events) {
|
|
147
200
|
return {
|
|
@@ -156,6 +209,8 @@ export const parseSlotsResult = res => {
|
|
|
156
209
|
let returnedSlots;
|
|
157
210
|
if (typeof res.available_bookable_events !== "undefined") {
|
|
158
211
|
returnedSlots = res.available_bookable_events;
|
|
212
|
+
} else if (typeof res.sequences !== "undefined") {
|
|
213
|
+
returnedSlots = res.sequences;
|
|
159
214
|
} else {
|
|
160
215
|
returnedSlots = res.available_slots;
|
|
161
216
|
|
|
@@ -209,6 +264,17 @@ export const addSlotsToObject = (slotsObject, newSlotsArray) => {
|
|
|
209
264
|
return slotsObject;
|
|
210
265
|
};
|
|
211
266
|
|
|
267
|
+
export const addSequencedSlotsToObject = (slotsObject, newSlotsArray) => {
|
|
268
|
+
newSlotsArray.forEach(slot => {
|
|
269
|
+
const startArray = slot.sequence.map(a => a.start);
|
|
270
|
+
const start = startArray.reduce((prev, current) => {
|
|
271
|
+
return prev < current ? prev : current;
|
|
272
|
+
});
|
|
273
|
+
slotsObject[start] = slot.sequence;
|
|
274
|
+
});
|
|
275
|
+
return slotsObject;
|
|
276
|
+
};
|
|
277
|
+
|
|
212
278
|
export const getSlotsByDay = (slots, day, tzid) => {
|
|
213
279
|
const slotKeys = Object.keys(slots);
|
|
214
280
|
const dayObject = moment.tz(day, "YYYY-MM-DD", tzid);
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
|
-
import { useTheme } from "./contexts/theme-context";
|
|
4
|
-
|
|
5
3
|
import { tzi18n } from "../../helpers/i18n";
|
|
6
|
-
import { useTz } from "./contexts/tz-context";
|
|
7
|
-
|
|
8
|
-
const TimeZoneSelector = ({ locale }) => {
|
|
9
|
-
const theme = useTheme();
|
|
10
|
-
const [tz, setTz] = useTz();
|
|
11
4
|
|
|
5
|
+
const TimeZoneSelector = ({ locale, theme, tz, setTz }) => {
|
|
12
6
|
const [showList, setShowList] = useState(() => false);
|
|
13
7
|
const [focused, setFocused] = useState(() => false);
|
|
14
8
|
|
|
@@ -31,27 +25,26 @@ const TimeZoneSelector = ({ locale }) => {
|
|
|
31
25
|
};
|
|
32
26
|
|
|
33
27
|
const handleKeyDown = e => {
|
|
28
|
+
const list = tz.list;
|
|
34
29
|
switch (e.key) {
|
|
35
30
|
case "ArrowDown":
|
|
36
31
|
e.preventDefault();
|
|
37
|
-
const nextFocusedItemIndex =
|
|
38
|
-
const nextItem =
|
|
39
|
-
nextFocusedItemIndex > tz.list.length - 1 ? 0 : nextFocusedItemIndex;
|
|
32
|
+
const nextFocusedItemIndex = list.findIndex(item => focused === item.tzid) + 1;
|
|
33
|
+
const nextItem = nextFocusedItemIndex > list.length - 1 ? 0 : nextFocusedItemIndex;
|
|
40
34
|
|
|
41
|
-
setFocused(
|
|
35
|
+
setFocused(list[nextItem].tzid);
|
|
42
36
|
|
|
43
37
|
break;
|
|
44
38
|
case "ArrowUp":
|
|
45
39
|
e.preventDefault();
|
|
46
|
-
const prevFocusedItemIndex =
|
|
47
|
-
const prevItem =
|
|
48
|
-
prevFocusedItemIndex < 0 ? tz.list.length - 1 : prevFocusedItemIndex;
|
|
40
|
+
const prevFocusedItemIndex = list.findIndex(item => focused === item.tzid) - 1;
|
|
41
|
+
const prevItem = prevFocusedItemIndex < 0 ? list.length - 1 : prevFocusedItemIndex;
|
|
49
42
|
|
|
50
|
-
setFocused(
|
|
43
|
+
setFocused(list[prevItem].tzid);
|
|
51
44
|
|
|
52
45
|
break;
|
|
53
46
|
case "Enter":
|
|
54
|
-
const tz =
|
|
47
|
+
const tz = list.find(item => focused === item.tzid);
|
|
55
48
|
handleOptionSelect(tz);
|
|
56
49
|
break;
|
|
57
50
|
case "Escape":
|
|
File without changes
|
|
@@ -268,6 +268,44 @@ export const getAvailability = (
|
|
|
268
268
|
.catch(catchAndRethrowErrors);
|
|
269
269
|
};
|
|
270
270
|
|
|
271
|
+
export const getSequencedAvailability = (
|
|
272
|
+
token,
|
|
273
|
+
api_domain,
|
|
274
|
+
params = {},
|
|
275
|
+
element = "sequenced-availability",
|
|
276
|
+
tzid = "Etc/UTC",
|
|
277
|
+
mock = false
|
|
278
|
+
) => {
|
|
279
|
+
if (
|
|
280
|
+
(mock && params.response_format === "slots") ||
|
|
281
|
+
(mock && typeof params.start_interval === "undefined")
|
|
282
|
+
) {
|
|
283
|
+
return mocks.availabilitySlots;
|
|
284
|
+
}
|
|
285
|
+
if (mock && params.response_format === "overlapping_slots") {
|
|
286
|
+
return mocks.availabilityOverlappingSlots(
|
|
287
|
+
params.start_interval.minutes,
|
|
288
|
+
params.required_duration.minutes,
|
|
289
|
+
params.query_periods,
|
|
290
|
+
tzid
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if (mock) return mocks.availability;
|
|
294
|
+
|
|
295
|
+
return fetch(`${api_domain}/v1/sequenced_availability?et=${token}`, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: {
|
|
298
|
+
"Cronofy-Element": `v${packageDetails.version}, ${element}`,
|
|
299
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
300
|
+
},
|
|
301
|
+
body: JSON.stringify(params),
|
|
302
|
+
})
|
|
303
|
+
.then(handleInvalidResponses)
|
|
304
|
+
.then(res => res.json())
|
|
305
|
+
.then(handle422Responses)
|
|
306
|
+
.catch(catchAndRethrowErrors);
|
|
307
|
+
};
|
|
308
|
+
|
|
271
309
|
export const getAvailabilityRules = ({
|
|
272
310
|
token,
|
|
273
311
|
api_domain,
|
|
@@ -72,6 +72,7 @@ export const parseDateTimePickerOptions = (options = {}) => {
|
|
|
72
72
|
const target = parseTarget(options, "date-time-picker", log);
|
|
73
73
|
|
|
74
74
|
const isBookableEventsQuery = options.availability_query.bookable_events ? true : false;
|
|
75
|
+
const isSequencedAvailabilityQuery = options.availability_query.sequence ? true : false;
|
|
75
76
|
|
|
76
77
|
let query;
|
|
77
78
|
if (options.demo) {
|
|
@@ -79,6 +80,9 @@ export const parseDateTimePickerOptions = (options = {}) => {
|
|
|
79
80
|
} else if (isBookableEventsQuery) {
|
|
80
81
|
query = options.availability_query;
|
|
81
82
|
query = parseWithOverlappingSlots(query);
|
|
83
|
+
} else if (isSequencedAvailabilityQuery) {
|
|
84
|
+
query = options.availability_query;
|
|
85
|
+
query = parseWithOverlappingSlots(query);
|
|
82
86
|
} else {
|
|
83
87
|
query = parseQuery({ options, elementSlug: "date-time-picker", log });
|
|
84
88
|
query = parseWithOverlappingSlots(query);
|
|
File without changes
|