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.
Files changed (44) hide show
  1. package/Makefile +6 -12
  2. package/build/CronofyElements.v1.45.0.js +2 -0
  3. package/build/{CronofyElements.v1.42.1.js.LICENSE.txt → CronofyElements.v1.45.0.js.LICENSE.txt} +1 -1
  4. package/build/npm/CronofyElements.js +2 -2
  5. package/demo/date-time-picker.ejs +83 -23
  6. package/demo/rules.ejs +7 -1
  7. package/package.json +57 -57
  8. package/src/js/components/AvailabilityRules/AvailabilityRules.js +15 -1
  9. package/src/js/components/AvailabilityRules/CalendarSelector.js +1 -1
  10. package/src/js/components/AvailabilityRules/Calendars.js +1 -1
  11. package/src/js/components/AvailabilityRules/TimeZoneDisplay.js +13 -2
  12. package/src/js/components/AvailabilityRules/Wrapper.js +41 -16
  13. package/src/js/components/AvailabilityRules/scss/_base.buttons.scss +58 -0
  14. package/src/js/components/AvailabilityRules/scss/_base.theme.scss +4 -0
  15. package/src/js/components/AvailabilityRules/scss/_components.timezoneselector.scss +74 -0
  16. package/src/js/components/AvailabilityRules/scss/_generic.reset.scss +13 -0
  17. package/src/js/components/AvailabilityRules/scss/_settings.colours.scss +12 -0
  18. package/src/js/components/AvailabilityRules/scss/availabilityrules.scss +5 -0
  19. package/src/js/components/AvailabilityRules/utils/tz-utils.js +44 -0
  20. package/src/js/components/DateTimePicker/Calendar.js +1 -1
  21. package/src/js/components/DateTimePicker/CalendarHeader.js +1 -1
  22. package/src/js/components/DateTimePicker/Confirm.js +1 -1
  23. package/src/js/components/DateTimePicker/DateTimePicker.js +6 -1
  24. package/src/js/components/DateTimePicker/DayButton.js +1 -1
  25. package/src/js/components/DateTimePicker/Details.js +10 -5
  26. package/src/js/components/DateTimePicker/SequencedSlotButton.js +71 -0
  27. package/src/js/components/DateTimePicker/SlotButton.js +1 -1
  28. package/src/js/components/DateTimePicker/SlotsList.js +6 -1
  29. package/src/js/components/DateTimePicker/Wrapper.js +22 -20
  30. package/src/js/components/DateTimePicker/contexts/status-reducer.js +24 -7
  31. package/src/js/components/DateTimePicker/utils/slots.js +69 -3
  32. package/src/js/components/{DateTimePicker → generic}/TimeZoneSelector.js +9 -16
  33. package/src/js/{components/DateTimePicker/contexts → contexts}/tz-context.js +0 -0
  34. package/src/js/helpers/connections.js +38 -0
  35. package/src/js/helpers/init.DateTimePicker.js +4 -0
  36. package/src/js/{components/DateTimePicker/utils → helpers}/tz-list.js +0 -0
  37. package/tests/AvailabilityRules/__snapshots__/AvailabilityRules.test.js.snap +36 -6
  38. package/tests/DateTimePicker/SequencedSlotButton.test.js +130 -0
  39. package/tests/DateTimePicker/SlotButton.test.js +1 -1
  40. package/tests/DateTimePicker/contexts/status-reducer.test.js +226 -67
  41. package/tests/DateTimePicker/dummy-data.js +277 -0
  42. package/tests/DateTimePicker/utils.test.js +7 -1
  43. package/tests/components/TimezoneSelector.test.js +124 -0
  44. 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 "./contexts/tz-context";
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 "./contexts/tz-context";
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 "./contexts/tz-context";
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 "./contexts/tz-context";
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 "./contexts/tz-context";
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 "./TimeZoneSelector";
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
- <p>
21
- <strong>{i18n.t("duration_label")}:</strong> {duration} {i18n.t("minutes")}
22
- </p>
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 "./contexts/tz-context";
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
- <SlotButton slot={slot} />
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 "./contexts/tz-context";
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
- return getSlots({
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
- .then(res => {
40
- dispatchStatus({
41
- type: "SET_SLOTS",
42
- slots: res,
43
- tzid: tz.selectedTzid.tzid,
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 => fetchMonthSlots(month.query, month.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.query.required_duration.minutes} locale={status.locale} />
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 = addSlotsToObject(state.slots, action.slots);
103
- const availableDays = getAvailableDays(addSlotsToObject({}, action.slots), action.tzid);
104
- const availableDaysSet = new Set([...state.availableDays, ...availableDays]);
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: [...availableDaysSet].sort(),
122
+ availableDays,
110
123
  monthsLoading,
111
124
  slots: slotsObject,
112
125
  slotFetchCount: state.slotFetchCount + 1,
113
- slotInjectionPoint: getLocalDayFromUtc(action.slots[0].start, action.tzid),
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 = daySlots[0];
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?.start,
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 "./tz-list";
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.errors) {
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 = tz.list.findIndex(item => focused === item.tzid) + 1;
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(tz.list[nextItem].tzid);
35
+ setFocused(list[nextItem].tzid);
42
36
 
43
37
  break;
44
38
  case "ArrowUp":
45
39
  e.preventDefault();
46
- const prevFocusedItemIndex = tz.list.findIndex(item => focused === item.tzid) - 1;
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(tz.list[prevItem].tzid);
43
+ setFocused(list[prevItem].tzid);
51
44
 
52
45
  break;
53
46
  case "Enter":
54
- const tz = tz.list.find(item => focused === item.tzid);
47
+ const tz = list.find(item => focused === item.tzid);
55
48
  handleOptionSelect(tz);
56
49
  break;
57
50
  case "Escape":
@@ -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);