cronofy-elements 1.40.2 → 1.41.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.
@@ -102,6 +102,7 @@
102
102
  },
103
103
  config: {
104
104
  logs: 'info',
105
+ //selected_date: "2022-05-15"
105
106
  //mode: 'no_confirm'
106
107
  // week_start_day: "tuesday"
107
108
  // 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.41.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,8 @@
1
- import React, { useState } from "react";
1
+ import React, { useMemo } from "react";
2
2
  import moment from "moment-timezone";
3
3
 
4
- import {
5
- getMonthObjectsFromQuery,
6
- parseQuery,
7
- parseTzList,
8
- getInitialSelectedTzid,
9
- } from "./utils/slots";
4
+ import { getMonthObjectsFromQuery, parseTzList, getInitialSelectedTzid } from "./utils/slots";
10
5
  import { parseTimeSlots } from "./utils/calendar";
11
- import { queryForDateTimePicker } from "../../helpers/mocks";
12
6
 
13
7
  import Wrapper from "./Wrapper";
14
8
 
@@ -21,13 +15,25 @@ import { StatusProvider } from "./contexts/status-context";
21
15
  import { TzProvider } from "./contexts/tz-context";
22
16
 
23
17
  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");
18
+ const statusOptions = useMemo(() => {
19
+ const selectedDateObject =
20
+ options.config.selectedDate && moment(options.config.selectedDate, "YYYY-MM-DD");
21
+
22
+ const months = getMonthObjectsFromQuery(
23
+ options.query,
24
+ options.tzid,
25
+ selectedDateObject?.format("YYYY-MM")
26
+ );
27
+
28
+ const endDateObject = moment(options.config.endDate, "YYYY-MM-DD");
29
+ const startDateObject = moment(options.config.startDate, "YYYY-MM-DD");
30
+ const currentMonthObject = selectedDateObject ?? startDateObject;
31
+ const currentMonth = currentMonthObject.format("YYYY-MM");
28
32
 
29
33
  const monthlyView = {
30
34
  month: currentMonth,
35
+ hasNext: currentMonthObject.isBefore(endDateObject, "month"),
36
+ hasPrev: currentMonthObject.isAfter(startDateObject, "month"),
31
37
  days: parseTimeSlots({
32
38
  slots: {},
33
39
  month: currentMonth,
@@ -49,11 +55,15 @@ const DateTimePicker = ({ options }) => {
49
55
  daySlots: [],
50
56
  locale: options.locale,
51
57
  mode: options.config.mode, // confirm (default) | no_confirm
52
- query,
58
+ query: options.query,
53
59
  months,
54
60
  monthlyView,
55
61
  selected: false,
56
62
  startDay: options.config.startDay,
63
+ focusedDay: options.config.selectedDay,
64
+ selectedDay: options.config.selectedDate,
65
+ startDateObject,
66
+ endDateObject,
57
67
  focusedSlot: false,
58
68
  availableDays: [],
59
69
  slots: {},
@@ -62,7 +72,7 @@ const DateTimePicker = ({ options }) => {
62
72
  tzid: options.tzid,
63
73
  populated: false,
64
74
  };
65
- });
75
+ }, []);
66
76
 
67
77
  const themeOptions = {
68
78
  styles: { height: "auto", padding: "10px", ...options.styles },
@@ -56,17 +56,7 @@ const Wrapper = () => {
56
56
  }, []);
57
57
 
58
58
  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) {
59
+ if (!status.slotInjectionPoint) {
70
60
  return;
71
61
  }
72
62
 
@@ -75,11 +65,9 @@ const Wrapper = () => {
75
65
  const lastWeekInMonth = weeksInMonth[weeksInMonth.length - 1];
76
66
  const lastDay = moment(lastWeekInMonth[lastWeekInMonth.length - 1].date, "YYYY-MM-DD");
77
67
 
78
- const injectionPoint = status.slotInjectionPoint
79
- ? moment(status.slotInjectionPoint, "YYYY-MM-DD")
80
- : undefined;
68
+ const injectionPoint = moment(status.slotInjectionPoint, "YYYY-MM-DD");
81
69
 
82
- if (injectionPoint && !injectionPoint.isBetween(firstDay, lastDay)) {
70
+ if (!injectionPoint.isBetween(firstDay, lastDay)) {
83
71
  return;
84
72
  }
85
73
 
@@ -87,7 +75,45 @@ const Wrapper = () => {
87
75
  type: "RECALCULATE_MONTH_VIEW",
88
76
  tzid: tz.selectedTzid.tzid,
89
77
  });
90
- }, [status.slotFetchCount, status.slotInjectionPoint]);
78
+ }, [status.slotInjectionPoint]);
79
+
80
+ useEffect(() => {
81
+ const fetchCount = status.slotFetchCount;
82
+ const monthsCount = status.months.length;
83
+ const finishedCallingAllSlots = fetchCount === monthsCount;
84
+ const hasAvailableDays = status.availableDays.length;
85
+
86
+ // No slots available after all queries have been made
87
+ if (finishedCallingAllSlots && !hasAvailableDays) {
88
+ dispatchStatus({ type: "NO_SLOTS_FOUND" });
89
+ return;
90
+ }
91
+
92
+ // Set selectedDay to first available day
93
+ if (!status.selectedDay && hasAvailableDays) {
94
+ dispatchStatus({
95
+ type: "SELECT_DAY",
96
+ day: status.availableDays[0],
97
+ tzid: tz.selectedTzid.tzid,
98
+ });
99
+ return;
100
+ }
101
+
102
+ if (!status.selectedDay && !hasAvailableDays) {
103
+ return;
104
+ }
105
+
106
+ // For allowing only this to be called once
107
+ // Since we know the first month that is being fetched will be for the initial selected day
108
+ if (status.selectedDay && fetchCount === 1) {
109
+ dispatchStatus({
110
+ type: "SELECT_DAY",
111
+ day: status.selectedDay,
112
+ tzid: tz.selectedTzid.tzid,
113
+ });
114
+ return;
115
+ }
116
+ }, [status.slotFetchCount, status.availableDays]);
91
117
 
92
118
  useEffect(() => {
93
119
  if (status.selectedDay) {
@@ -1,6 +1,7 @@
1
+ import moment from "moment-timezone";
2
+
1
3
  import {
2
4
  addSlotsToObject,
3
- getFirstAvailableDay,
4
5
  getSlotsByDay,
5
6
  getAvailableDays,
6
7
  getLocalDayFromUtc,
@@ -54,6 +55,8 @@ export const statusReducer = (state, action) => {
54
55
  case "RECALCULATE_MONTH_VIEW": {
55
56
  const monthlyView = {
56
57
  month: state.monthlyView.month,
58
+ hasNext: state.monthlyView.hasNext,
59
+ hasPrev: state.monthlyView.hasPrev,
57
60
  days: parseTimeSlots({
58
61
  slots: state.slots,
59
62
  month: state.monthlyView.month,
@@ -74,31 +77,12 @@ export const statusReducer = (state, action) => {
74
77
  }
75
78
 
76
79
  const slotsObject = addSlotsToObject(state.slots, action.slots);
77
-
78
- let daySlots = state.daySlots;
79
- let columnView = state.columnView;
80
- let focusedDay = state.focusedDay;
81
- let selectedDay = state.selectedDay;
82
- let availableDays = getAvailableDays(addSlotsToObject({}, action.slots), action.tzid);
83
-
80
+ const availableDays = getAvailableDays(addSlotsToObject({}, action.slots), action.tzid);
84
81
  const availableDaysSet = new Set([...state.availableDays, ...availableDays]);
85
- availableDays = [...availableDaysSet].sort();
86
-
87
- if (!state.selectedDay) {
88
- columnView = "slots";
89
- focusedDay = availableDays[0];
90
- selectedDay = availableDays[0];
91
- daySlots = getSlotsByDay(slotsObject, selectedDay, action.tzid);
92
- }
93
82
 
94
83
  return {
95
84
  ...state,
96
- daySlots,
97
- columnView,
98
- focusedDay,
99
- selectedDay,
100
- availableDays,
101
-
85
+ availableDays: [...availableDaysSet].sort(),
102
86
  slots: slotsObject,
103
87
  slotFetchCount: state.slotFetchCount + 1,
104
88
  slotInjectionPoint: getLocalDayFromUtc(action.slots[0].start, action.tzid),
@@ -114,26 +98,41 @@ export const statusReducer = (state, action) => {
114
98
 
115
99
  case "SELECT_DAY": {
116
100
  const daySlots = getSlotsByDay(state.slots, action.day, action.tzid);
101
+ const focusedSlot = daySlots[0];
102
+
117
103
  return {
118
104
  ...state,
119
105
  selectedDay: action.day,
120
106
  focusedDay: action.day,
121
- focusedSlot: daySlots[0].start,
107
+ focusedSlot: focusedSlot?.start,
122
108
  columnView: "slots",
123
109
  daySlots,
124
110
  populated: true,
125
111
  };
126
112
  }
127
113
  case "SELECT_MONTH": {
114
+ const actionMonthObject = moment(action.month, "YYYY-MM");
115
+
116
+ if (
117
+ !actionMonthObject.isBetween(
118
+ state.startDateObject,
119
+ state.endDateObject,
120
+ "month",
121
+ "[]"
122
+ )
123
+ ) {
124
+ return state;
125
+ }
126
+
128
127
  const months = state.months.map(month => ({
129
128
  ...month,
130
129
  current: month.month === action.month,
131
130
  }));
132
131
 
133
- let focusedDay = action.focusedDay ?? false;
134
-
135
132
  const monthlyView = {
136
133
  month: action.month,
134
+ hasNext: actionMonthObject.isBefore(state.endDateObject, "month"),
135
+ hasPrev: actionMonthObject.isAfter(state.startDateObject, "month"),
137
136
  days: parseTimeSlots({
138
137
  tzid: action.tzid,
139
138
  slots: state.slots,
@@ -142,6 +141,8 @@ export const statusReducer = (state, action) => {
142
141
  }),
143
142
  };
144
143
 
144
+ let focusedDay = action.focusedDay ?? false;
145
+
145
146
  return {
146
147
  ...state,
147
148
  months,
@@ -170,6 +171,8 @@ export const statusReducer = (state, action) => {
170
171
 
171
172
  const monthlyView = {
172
173
  month: state.monthlyView.month,
174
+ hasNext: state.monthlyView.hasNext,
175
+ hasPrev: state.monthlyView.hasPrev,
173
176
  days: parseTimeSlots({
174
177
  slots: state.slots,
175
178
  month: state.monthlyView.month,
@@ -21,7 +21,7 @@ export const getMonthsCoveredByPeriod = (period, tzid) => {
21
21
  return months;
22
22
  };
23
23
 
24
- export const getMonthsFromQuery = (periods, tzid) => {
24
+ export const getMonthsFromQuery = (periods = [], tzid) => {
25
25
  const months = periods
26
26
  .map(period => getMonthsCoveredByPeriod(period, tzid))
27
27
  // getMonthsCoveredByPeriod returns an array, so flatten them...
@@ -42,9 +42,17 @@ export const getMonthObjectsFromQuery = (query, tzid, current = false) => {
42
42
  if (!query.query_periods || !query.query_periods.length) {
43
43
  return [getCurrentMonth()];
44
44
  }
45
+
45
46
  const monthStrings = getMonthsFromQuery(query.query_periods, tzid);
46
- const currentMonth = current ? current : monthStrings[0];
47
- const monthObjects = monthStrings.map(month => {
47
+ const startMonth = moment(monthStrings[0], "YYYY-MM");
48
+ const endMonth = moment(monthStrings[monthStrings.length - 1], "YYYY-MM");
49
+
50
+ const currentMonth =
51
+ current && moment(current, "YYYY-MM").isBetween(startMonth, endMonth)
52
+ ? current
53
+ : monthStrings[0];
54
+
55
+ return monthStrings.map(month => {
48
56
  const croppedQueryPeriods = cropPeriodsByMonth(query.query_periods, month, tzid);
49
57
  return {
50
58
  month,
@@ -56,7 +64,6 @@ export const getMonthObjectsFromQuery = (query, tzid, current = false) => {
56
64
  },
57
65
  };
58
66
  });
59
- return monthObjects;
60
67
  };
61
68
 
62
69
  export const getSlots = ({ query, auth, tzid, slots = [] }) =>
@@ -1,3 +1,5 @@
1
+ import moment from "moment-timezone";
2
+
1
3
  import {
2
4
  parseConnectionDomains,
3
5
  parseQuery,
@@ -8,6 +10,12 @@ import {
8
10
  validateLocaleModifiers,
9
11
  } from "./init";
10
12
  import { logConstructor } from "./logging";
13
+ import { queryForDateTimePicker } from "./mocks";
14
+
15
+ import {
16
+ getMonthsFromQuery,
17
+ parseQuery as parseWithOverlappingSlots,
18
+ } from "../components/DateTimePicker/utils/slots";
11
19
 
12
20
  export const parseDateTimePickerOptions = (options = {}) => {
13
21
  const config = typeof options.config === "undefined" ? {} : options.config;
@@ -66,10 +74,14 @@ export const parseDateTimePickerOptions = (options = {}) => {
66
74
  const isBookableEventsQuery = options.availability_query.bookable_events ? true : false;
67
75
 
68
76
  let query;
69
- if (isBookableEventsQuery) {
77
+ if (options.demo) {
78
+ query = queryForDateTimePicker;
79
+ } else if (isBookableEventsQuery) {
70
80
  query = options.availability_query;
81
+ query = parseWithOverlappingSlots(query);
71
82
  } else {
72
83
  query = parseQuery({ options, elementSlug: "date-time-picker", log });
84
+ query = parseWithOverlappingSlots(query);
73
85
  }
74
86
 
75
87
  const tzid = parseTimezone(options.tzid, "date-time-picker", log);
@@ -93,6 +105,44 @@ export const parseDateTimePickerOptions = (options = {}) => {
93
105
  });
94
106
  }
95
107
 
108
+ let selectedDate = config.selected_date;
109
+ if (typeof selectedDate !== "undefined") {
110
+ const validDate = moment(selectedDate, "YYYY-MM-DD", true).isValid();
111
+
112
+ if (!validDate) {
113
+ log.warn(
114
+ `The provided date ${selectedDate} is not valid. Please ensure it's formatted like "YYYY-MM-DD". Picking the first available date as starting date.`,
115
+ {
116
+ docsSlug: "#config.start_date",
117
+ }
118
+ );
119
+
120
+ selectedDate = undefined;
121
+ }
122
+ }
123
+
124
+ const months = getMonthsFromQuery(query?.query_periods, tzid.tzid);
125
+ const selectedDateMoment = selectedDate && moment(selectedDate, "YYYY-MM-DD");
126
+
127
+ let startDateMoment = months.length
128
+ ? moment(months[0], "YYYY-MM").startOf("month")
129
+ : moment().startOf("month");
130
+
131
+ if (selectedDateMoment?.isBefore(startDate)) {
132
+ startDateMoment = selectedDateMoment;
133
+ }
134
+
135
+ let endDateMoment = months.length
136
+ ? moment(months[months.length - 1], "YYYY-MM").endOf("month")
137
+ : moment().endOf("month");
138
+
139
+ if (selectedDateMoment?.isAfter(endDateMoment)) {
140
+ endDateMoment = selectedDateMoment;
141
+ }
142
+
143
+ const startDate = startDateMoment.format("YYYY-MM-DD");
144
+ const endDate = endDateMoment.format("YYYY-MM-DD");
145
+
96
146
  delete options.availability_query;
97
147
  delete options.element_token;
98
148
  delete options.target_id;
@@ -107,7 +157,16 @@ export const parseDateTimePickerOptions = (options = {}) => {
107
157
  token,
108
158
  domains,
109
159
  query,
110
- config: { ...config, mode, logs, startDay, tzList },
160
+ config: {
161
+ ...config,
162
+ mode,
163
+ logs,
164
+ startDay,
165
+ selectedDate,
166
+ startDate,
167
+ endDate,
168
+ tzList,
169
+ },
111
170
  translations,
112
171
  log,
113
172
  };