cronofy-elements 1.40.2 → 1.43.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.
@@ -64,10 +64,83 @@
64
64
  return output.utc().format();
65
65
  };
66
66
 
67
+ const sequencedAvailabilityQuery = {
68
+ sequence: [
69
+ {
70
+ sequence_id: "test",
71
+ ordinal: 1,
72
+ participants: [
73
+ {
74
+ required: "all",
75
+ members: [
76
+ {
77
+ sub: "<%= sub %>",
78
+ // managed_availability: true,
79
+ // availability_rule_ids: ["weekly_work_hours"]
80
+ }
81
+ ]
82
+ }
83
+ ],
84
+ required_duration: { minutes: 5 },
85
+ },
86
+ {
87
+ sequence_id: "test-1",
88
+ ordinal: 2,
89
+ participants: [
90
+ {
91
+ required: "all",
92
+ members: [
93
+ {
94
+ sub: "<%= sub %>",
95
+ // managed_availability: true,
96
+ // availability_rule_ids: ["weekly_work_hours"]
97
+ }
98
+ ]
99
+ }
100
+ ],
101
+ required_duration: { minutes: 45 },
102
+ start_interval: { minutes: 5 },
103
+ buffer: {
104
+ before: {
105
+ minimum: { minutes: 15 }
106
+ }
107
+ }
108
+ },
109
+ {
110
+ sequence_id: "test-2",
111
+ ordinal: 3,
112
+ participants: [
113
+ {
114
+ required: "all",
115
+ members: [
116
+ {
117
+ sub: "<%= sub %>",
118
+ // managed_availability: true,
119
+ // availability_rule_ids: ["weekly_work_hours"]
120
+ }
121
+ ]
122
+ }
123
+ ],
124
+ required_duration: { minutes: 30 },
125
+ start_interval: { minutes: 10 },
126
+ buffer: {
127
+ before: {
128
+ minimum: { minutes: 15 }
129
+ }
130
+ }
131
+ },
132
+
133
+ ],
134
+ query_periods: [
135
+ { start: slotTimes(01,"08:00"), end: slotTimes(31,"17:00") }
136
+ ]
137
+ }
138
+
67
139
  CronofyElements.DateTimePicker({
68
140
  element_token: "<%= date_time_picker_token %>",
69
141
  target_id: 'cronofy-date-time-picker',
70
142
  api_domain:"<%= api_domain %>",
143
+ //availability_query: sequencedAvailabilityQuery,
71
144
  availability_query: {
72
145
  // response_format: "slots",
73
146
  participants: [
@@ -102,6 +175,7 @@
102
175
  },
103
176
  config: {
104
177
  logs: 'info',
178
+ //selected_date: "2022-05-15"
105
179
  //mode: 'no_confirm'
106
180
  // week_start_day: "tuesday"
107
181
  // tz_list: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronofy-elements",
3
- "version": "1.40.2",
3
+ "version": "1.43.0",
4
4
  "description": "Fast track scheduling with Cronofy's embeddable UI Elements",
5
5
  "main": "build/npm/CronofyElements.js",
6
6
  "scripts": {
@@ -1,6 +1,5 @@
1
- import React, { useEffect, useState } from "react";
2
-
3
- import { calculateMonthNav } from "./utils/calendar";
1
+ import React from "react";
2
+ import moment from "moment-timezone";
4
3
 
5
4
  import { useI18n } from "../../contexts/i18n-context";
6
5
  import { useStatus } from "./contexts/status-context";
@@ -13,19 +12,27 @@ const CalendarHeader = () => {
13
12
  const theme = useTheme();
14
13
  const [tz] = useTz();
15
14
 
16
- const [monthState, setMonthState] = useState(() => calculateMonthNav(status.months));
15
+ const handlePrevMonth = () => {
16
+ const month = moment(status.monthlyView.month, "YYYY-MM")
17
+ .subtract(1, "month")
18
+ .format("YYYY-MM");
17
19
 
18
- const handleMonthNav = target => {
19
20
  dispatchStatus({
20
21
  type: "SELECT_MONTH",
21
- month: target,
22
+ month,
22
23
  tzid: tz.selectedTzid.tzid,
23
24
  });
24
25
  };
25
26
 
26
- useEffect(() => {
27
- setMonthState(() => calculateMonthNav(status.months));
28
- }, [status.months]);
27
+ const handleNextMonth = () => {
28
+ const month = moment(status.monthlyView.month, "YYYY-MM").add(1, "month").format("YYYY-MM");
29
+
30
+ dispatchStatus({
31
+ type: "SELECT_MONTH",
32
+ month,
33
+ tzid: tz.selectedTzid.tzid,
34
+ });
35
+ };
29
36
 
30
37
  return (
31
38
  <div className={theme.classBuilder("calendar-header")}>
@@ -33,8 +40,8 @@ const CalendarHeader = () => {
33
40
  type="button"
34
41
  className={theme.classBuilder("calendar-header-button")}
35
42
  aria-label={i18n.t("nav_previous_month")}
36
- disabled={!monthState.prev ? true : false}
37
- onClick={() => handleMonthNav(monthState.prev)}
43
+ disabled={!status.monthlyView.hasPrev}
44
+ onClick={handlePrevMonth}
38
45
  >
39
46
  <svg
40
47
  className={theme.classBuilder(
@@ -47,14 +54,14 @@ const CalendarHeader = () => {
47
54
  </svg>
48
55
  </button>
49
56
  <h2 className={theme.classBuilder("calendar-header--title")} id="calendar-title">
50
- {i18n.f(monthState.current, "MMMM YYYY")}
57
+ {i18n.f(moment(status.monthlyView.month, "YYYY-MM"), "MMMM YYYY")}
51
58
  </h2>
52
59
  <button
53
60
  type="button"
54
61
  className={theme.classBuilder("calendar-header-button")}
55
62
  aria-label={i18n.t("nav_next_month")}
56
- disabled={!monthState.next ? true : false}
57
- onClick={() => handleMonthNav(monthState.next)}
63
+ disabled={!status.monthlyView.hasNext}
64
+ onClick={handleNextMonth}
58
65
  >
59
66
  <svg
60
67
  className={theme.classBuilder(
@@ -1,14 +1,14 @@
1
- import React, { useState } from "react";
1
+ import React, { useMemo } from "react";
2
2
  import moment from "moment-timezone";
3
3
 
4
4
  import {
5
5
  getMonthObjectsFromQuery,
6
- parseQuery,
7
6
  parseTzList,
8
7
  getInitialSelectedTzid,
8
+ getMonthsLoadingFromQuery,
9
+ getDurationFromQuery,
9
10
  } from "./utils/slots";
10
- import { parseTimeSlots } from "./utils/calendar";
11
- import { queryForDateTimePicker } from "../../helpers/mocks";
11
+ import { getMonthsInDisplay, parseTimeSlots } from "./utils/calendar";
12
12
 
13
13
  import Wrapper from "./Wrapper";
14
14
 
@@ -21,13 +21,30 @@ import { StatusProvider } from "./contexts/status-context";
21
21
  import { TzProvider } from "./contexts/tz-context";
22
22
 
23
23
  const DateTimePicker = ({ options }) => {
24
- const [statusOptions] = useState(() => {
25
- const query = !options.demo ? parseQuery(options.query) : queryForDateTimePicker;
26
- const months = getMonthObjectsFromQuery(query, options.tzid);
27
- const currentMonth = months.length ? months[0].month : moment().format("YYYY-MM");
24
+ const statusOptions = useMemo(() => {
25
+ const selectedDateObject =
26
+ options.config.selectedDate && moment(options.config.selectedDate, "YYYY-MM-DD");
27
+
28
+ const months = getMonthObjectsFromQuery(
29
+ options.query,
30
+ options.tzid,
31
+ selectedDateObject?.format("YYYY-MM")
32
+ );
33
+
34
+ const duration = getDurationFromQuery(options.query);
35
+
36
+ const endDateObject = moment(options.config.endDate, "YYYY-MM-DD");
37
+ const startDateObject = moment(options.config.startDate, "YYYY-MM-DD");
38
+ const currentMonthObject = selectedDateObject ?? startDateObject;
39
+ const currentMonth = currentMonthObject.format("YYYY-MM");
40
+ const monthsLoading = getMonthsLoadingFromQuery(options.query, options.tzid);
41
+ const monthsInView = getMonthsInDisplay(currentMonth, options.config.startDay);
28
42
 
29
43
  const monthlyView = {
30
44
  month: currentMonth,
45
+ hasNext: currentMonthObject.isBefore(endDateObject, "month"),
46
+ hasPrev: currentMonthObject.isAfter(startDateObject, "month"),
47
+ monthsInView,
31
48
  days: parseTimeSlots({
32
49
  slots: {},
33
50
  month: currentMonth,
@@ -47,22 +64,29 @@ const DateTimePicker = ({ options }) => {
47
64
  : () => options.log.info("No `callback` option has been provided"),
48
65
  columnView: "loading", // loading | error | slots | confirm | no-slots
49
66
  daySlots: [],
67
+ duration,
50
68
  locale: options.locale,
51
69
  mode: options.config.mode, // confirm (default) | no_confirm
52
- query,
70
+ query: options.query,
53
71
  months,
54
72
  monthlyView,
73
+ monthsLoading,
55
74
  selected: false,
56
75
  startDay: options.config.startDay,
76
+ focusedDay: options.config.selectedDay,
77
+ selectedDay: options.config.selectedDate,
78
+ startDateObject,
79
+ endDateObject,
57
80
  focusedSlot: false,
58
81
  availableDays: [],
82
+ sequenced_availability: options.query.sequence ? true : false,
59
83
  slots: {},
60
84
  slotFetchCount: 0,
61
85
  slotInjectionPoint: undefined,
62
86
  tzid: options.tzid,
63
87
  populated: false,
64
88
  };
65
- });
89
+ }, []);
66
90
 
67
91
  const themeOptions = {
68
92
  styles: { height: "auto", padding: "10px", ...options.styles },
@@ -17,9 +17,11 @@ const Details = ({ duration, locale }) => {
17
17
  <TimeZoneSelector locale={locale} />
18
18
  </div>
19
19
  <div className={theme.classBuilder("details--duration")}>
20
- <p>
21
- <strong>{i18n.t("duration_label")}:</strong> {duration} {i18n.t("minutes")}
22
- </p>
20
+ {duration && (
21
+ <p>
22
+ <strong>{i18n.t("duration_label")}:</strong> {duration} {i18n.t("minutes")}
23
+ </p>
24
+ )}
23
25
  </div>
24
26
  </div>
25
27
  );
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+
3
+ import { useI18n } from "../../contexts/i18n-context";
4
+ import { useTheme } from "./contexts/theme-context";
5
+
6
+ const LoadingCalendar = () => {
7
+ const i18n = useI18n();
8
+ const theme = useTheme();
9
+
10
+ return (
11
+ <div className={theme.classBuilder("calendar-loading")}>
12
+ <svg aria-hidden="true" viewBox="0 0 489.711 489.711">
13
+ <path d="M112.156,97.111c72.3-65.4,180.5-66.4,253.8-6.7l-58.1,2.2c-7.5,0.3-13.3,6.5-13,14c0.3,7.3,6.3,13,13.5,13 c0.2,0,0.3,0,0.5,0l89.2-3.3c7.3-0.3,13-6.2,13-13.5v-1c0-0.2,0-0.3,0-0.5v-0.1l0,0l-3.3-88.2c-0.3-7.5-6.6-13.3-14-13 c-7.5,0.3-13.3,6.5-13,14l2.1,55.3c-36.3-29.7-81-46.9-128.8-49.3c-59.2-3-116.1,17.3-160,57.1c-60.4,54.7-86,137.9-66.8,217.1 c1.5,6.2,7,10.3,13.1,10.3c1.1,0,2.1-0.1,3.2-0.4c7.2-1.8,11.7-9.1,9.9-16.3C36.656,218.211,59.056,145.111,112.156,97.111z" />
14
+ <path d="M462.456,195.511c-1.8-7.2-9.1-11.7-16.3-9.9c-7.2,1.8-11.7,9.1-9.9,16.3c16.9,69.6-5.6,142.7-58.7,190.7 c-37.3,33.7-84.1,50.3-130.7,50.3c-44.5,0-88.9-15.1-124.7-44.9l58.8-5.3c7.4-0.7,12.9-7.2,12.2-14.7s-7.2-12.9-14.7-12.2l-88.9,8 c-7.4,0.7-12.9,7.2-12.2,14.7l8,88.9c0.6,7,6.5,12.3,13.4,12.3c0.4,0,0.8,0,1.2-0.1c7.4-0.7,12.9-7.2,12.2-14.7l-4.8-54.1 c36.3,29.4,80.8,46.5,128.3,48.9c3.8,0.2,7.6,0.3,11.3,0.3c55.1,0,107.5-20.2,148.7-57.4 C456.056,357.911,481.656,274.811,462.456,195.511z" />
15
+ </svg>
16
+ <span className={"calendar-loading__text"}>{i18n.t("loading_calendar")}</span>
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default LoadingCalendar;
@@ -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;
@@ -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,13 +1,14 @@
1
- import React, { useEffect, useRef } from "react";
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";
8
8
  import Error from "./Error";
9
9
  import Details from "./Details";
10
10
  import Loading from "./Loading";
11
+ import LoadingCalendar from "./LoadingCalendar";
11
12
  import NoSlotsFound from "./NoSlotsFound";
12
13
  import SlotsList from "./SlotsList";
13
14
 
@@ -20,6 +21,8 @@ const Wrapper = () => {
20
21
  const [tz] = useTz();
21
22
  const theme = useTheme();
22
23
 
24
+ const [loadingCalendar, setLoadingCalendar] = useState(true);
25
+
23
26
  const confirmButtonRef = useRef();
24
27
 
25
28
  useEffect(() => {
@@ -27,8 +30,10 @@ const Wrapper = () => {
27
30
  return;
28
31
  }
29
32
 
30
- const fetchMonthSlots = query => {
31
- return getSlots({
33
+ const fetchMonthSlots = (query, month) => {
34
+ const fetch = status.sequenced_availability ? getSequencedSlots : getSlots;
35
+
36
+ return fetch({
32
37
  query,
33
38
  auth: status.auth,
34
39
  tzid: status.tzid,
@@ -37,6 +42,7 @@ const Wrapper = () => {
37
42
  type: "SET_SLOTS",
38
43
  slots: res,
39
44
  tzid: tz.selectedTzid.tzid,
45
+ month: month,
40
46
  });
41
47
  });
42
48
  };
@@ -44,11 +50,19 @@ const Wrapper = () => {
44
50
  const currentMonth = status.months.find(m => m.current);
45
51
 
46
52
  // Get slots for current month
47
- fetchMonthSlots(currentMonth.query)
53
+ fetchMonthSlots(currentMonth.query, currentMonth.month)
48
54
  .then(() => {
49
55
  // Get slots for remianing months
50
56
  const remainingMonths = status.months.filter(month => !month.current);
51
- remainingMonths.forEach(month => fetchMonthSlots(month.query));
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
+ );
52
66
  })
53
67
  .catch(error => {
54
68
  dispatchStatus({ type: "ERROR_GETTING_SLOTS", error });
@@ -56,17 +70,7 @@ const Wrapper = () => {
56
70
  }, []);
57
71
 
58
72
  useEffect(() => {
59
- // Check for no-slots when all months have been fetched
60
- const fetchCount = status.slotFetchCount;
61
- const monthsCount = status.months.length;
62
- const slotsCount = Object.keys(status.slots).length;
63
- if (fetchCount === monthsCount && slotsCount < 1) {
64
- dispatchStatus({ type: "NO_SLOTS_FOUND" });
65
- return;
66
- }
67
-
68
- // Set grid display if there are available slots within the current month
69
- if (slotsCount < 1) {
73
+ if (!status.slotInjectionPoint) {
70
74
  return;
71
75
  }
72
76
 
@@ -75,11 +79,9 @@ const Wrapper = () => {
75
79
  const lastWeekInMonth = weeksInMonth[weeksInMonth.length - 1];
76
80
  const lastDay = moment(lastWeekInMonth[lastWeekInMonth.length - 1].date, "YYYY-MM-DD");
77
81
 
78
- const injectionPoint = status.slotInjectionPoint
79
- ? moment(status.slotInjectionPoint, "YYYY-MM-DD")
80
- : undefined;
82
+ const injectionPoint = moment(status.slotInjectionPoint, "YYYY-MM-DD");
81
83
 
82
- if (injectionPoint && !injectionPoint.isBetween(firstDay, lastDay)) {
84
+ if (!injectionPoint.isBetween(firstDay, lastDay, "day", "[]")) {
83
85
  return;
84
86
  }
85
87
 
@@ -87,7 +89,45 @@ const Wrapper = () => {
87
89
  type: "RECALCULATE_MONTH_VIEW",
88
90
  tzid: tz.selectedTzid.tzid,
89
91
  });
90
- }, [status.slotFetchCount, status.slotInjectionPoint]);
92
+ }, [status.slotInjectionPoint]);
93
+
94
+ useEffect(() => {
95
+ const fetchCount = status.slotFetchCount;
96
+ const monthsCount = status.months.length;
97
+ const finishedCallingAllSlots = fetchCount === monthsCount;
98
+ const hasAvailableDays = status.availableDays.length;
99
+
100
+ // No slots available after all queries have been made
101
+ if (finishedCallingAllSlots && !hasAvailableDays) {
102
+ dispatchStatus({ type: "NO_SLOTS_FOUND" });
103
+ return;
104
+ }
105
+
106
+ // Set selectedDay to first available day
107
+ if (!status.selectedDay && hasAvailableDays) {
108
+ dispatchStatus({
109
+ type: "SELECT_DAY",
110
+ day: status.availableDays[0],
111
+ tzid: tz.selectedTzid.tzid,
112
+ });
113
+ return;
114
+ }
115
+
116
+ if (!status.selectedDay && !hasAvailableDays) {
117
+ return;
118
+ }
119
+
120
+ // For allowing only this to be called once
121
+ // Since we know the first month that is being fetched will be for the initial selected day
122
+ if (status.selectedDay && fetchCount === 1) {
123
+ dispatchStatus({
124
+ type: "SELECT_DAY",
125
+ day: status.selectedDay,
126
+ tzid: tz.selectedTzid.tzid,
127
+ });
128
+ return;
129
+ }
130
+ }, [status.slotFetchCount, status.availableDays]);
91
131
 
92
132
  useEffect(() => {
93
133
  if (status.selectedDay) {
@@ -118,9 +158,23 @@ const Wrapper = () => {
118
158
  }
119
159
  }, [tz.selectedTzid.tzid]);
120
160
 
161
+ useEffect(() => {
162
+ if (status.monthsLoading) {
163
+ for (let month of status.monthlyView.monthsInView) {
164
+ const monthLoading = status.monthsLoading.find(m => m.month === month);
165
+ if (monthLoading && monthLoading.loading) {
166
+ setLoadingCalendar(true);
167
+ return;
168
+ } else {
169
+ setLoadingCalendar(false);
170
+ }
171
+ }
172
+ }
173
+ }, [status.monthlyView, status.monthsLoading]);
174
+
121
175
  return (
122
176
  <section className={theme.classBuilder()} style={theme.customProperties}>
123
- <Details duration={status.query.required_duration.minutes} locale={status.locale} />
177
+ <Details duration={status.duration} locale={status.locale} />
124
178
  <div className={theme.classBuilder("wrapper")}>
125
179
  <div
126
180
  className={
@@ -131,6 +185,7 @@ const Wrapper = () => {
131
185
  }
132
186
  >
133
187
  <Calendar />
188
+ {loadingCalendar && <LoadingCalendar />}
134
189
  </div>
135
190
  <div className={theme.classBuilder("column--right")}>
136
191
  {status.columnView === "error" && <Error />}