cronofy-elements 1.45.0 → 1.48.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.
@@ -49,6 +49,7 @@
49
49
  sequence: [
50
50
  {
51
51
  sequence_id: "test",
52
+ sequence_title: "Test Event One",
52
53
  ordinal: 1,
53
54
  participants: [
54
55
  {
@@ -66,6 +67,7 @@
66
67
  },
67
68
  {
68
69
  sequence_id: "test-1",
70
+ sequence_title: "Test Event Two",
69
71
  ordinal: 2,
70
72
  participants: [
71
73
  {
@@ -86,31 +88,7 @@
86
88
  minimum: { minutes: 15 }
87
89
  }
88
90
  }
89
- },
90
- {
91
- sequence_id: "test-2",
92
- ordinal: 3,
93
- participants: [
94
- {
95
- required: "all",
96
- members: [
97
- {
98
- sub: "<%= sub %>",
99
- // managed_availability: true,
100
- // availability_rule_ids: ["weekly_work_hours"]
101
- }
102
- ]
103
- }
104
- ],
105
- required_duration: { minutes: 30 },
106
- start_interval: { minutes: 10 },
107
- buffer: {
108
- before: {
109
- minimum: { minutes: 15 }
110
- }
111
- }
112
- },
113
-
91
+ },
114
92
  ],
115
93
  query_periods: [
116
94
  { start: slotTimes(01,"08:00"), end: slotTimes(31,"17:00") }
@@ -136,15 +114,10 @@
136
114
  ]
137
115
  }
138
116
  ],
139
- required_duration: { minutes: 60 },
117
+ required_duration: { minutes: 15 },
140
118
  //start_interval: { minutes: 15 },
141
119
  query_periods: [
142
- { start: slotTimes(31,"03:00"), end: slotTimes(31,"12:00") },
143
- { start: slotTimes(32,"09:00"), end: slotTimes(32,"13:00") },
144
- { start: slotTimes(32,"14:00"), end: slotTimes(32,"17:00") },
145
- { start: slotTimes(35,"09:00"), end: slotTimes(35,"13:00") },
146
- { start: slotTimes(36,"14:00"), end: slotTimes(36,"17:00") },
147
- { start: slotTimes(38,"09:00"), end: slotTimes(65,"22:00") }
120
+ { start: slotTimes(31,"03:00"), end: slotTimes(150,"12:00") },
148
121
  ]
149
122
  },
150
123
  // tzid: "America/Mexico_City",
@@ -156,6 +129,7 @@
156
129
  },
157
130
  config: {
158
131
  logs: 'info',
132
+ //slot_button_mode: 'detailed' //summary | detailed
159
133
  //selected_date: "2022-05-15"
160
134
  //mode: 'no_confirm'
161
135
  // week_start_day: "tuesday"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronofy-elements",
3
- "version": "1.45.0",
3
+ "version": "1.48.0",
4
4
  "description": "Fast track scheduling with Cronofy's embeddable UI Elements",
5
5
  "main": "build/npm/CronofyElements.js",
6
6
  "scripts": {
@@ -115,17 +115,7 @@ const Wrapper = ({ options }) => {
115
115
  const allSlots = { ...slots, ...newSlots };
116
116
  setSlots(allSlots);
117
117
  const newRules = buildNewRules(allSlots);
118
- const callbackContent = {
119
- notification: {
120
- type: "availability_rule_edited",
121
- },
122
- availability_rule: {
123
- ...account,
124
- calendar_ids: calendars.active,
125
- weekly_periods: newRules,
126
- },
127
- };
128
- status.callback(callbackContent);
118
+ setRules(newRules);
129
119
  };
130
120
 
131
121
  useEffect(() => {
@@ -148,7 +138,6 @@ const Wrapper = ({ options }) => {
148
138
 
149
139
  useEffect(() => {
150
140
  if (!status.loading && !status.error) {
151
- const newRules = buildNewRules(slots);
152
141
  const callbackContent = {
153
142
  notification: {
154
143
  type: "availability_rule_edited",
@@ -156,12 +145,12 @@ const Wrapper = ({ options }) => {
156
145
  availability_rule: {
157
146
  ...account,
158
147
  calendar_ids: calendars.active,
159
- weekly_periods: newRules,
148
+ weekly_periods: rules,
160
149
  },
161
150
  };
162
151
  status.callback(callbackContent);
163
152
  }
164
- }, [calendars]);
153
+ }, [calendars, account, rules]);
165
154
 
166
155
  useEffect(() => {
167
156
  // Query the API for rules and calendars, but don't do anything until
@@ -295,10 +284,7 @@ const Wrapper = ({ options }) => {
295
284
  }, [tz.selectedTzid.tzid]);
296
285
 
297
286
  const generateRules = () => {
298
- const newRules = buildNewRules(slots);
299
-
300
- setRules(newRules);
301
- const rulesRequest = { ...account, weekly_periods: newRules };
287
+ const rulesRequest = { ...account, weekly_periods: rules };
302
288
 
303
289
  let tempStatus = status;
304
290
 
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useState } from "react";
2
2
  import moment from "moment-timezone";
3
3
 
4
4
  import { useI18n } from "../../contexts/i18n-context";
@@ -12,6 +12,12 @@ const Confirm = ({ confirmButtonRef }) => {
12
12
  const theme = useTheme();
13
13
  const [tz] = useTz();
14
14
 
15
+ const [sequenceTitlePresent, setSequenceTitlePresent] = useState(
16
+ () =>
17
+ status.selected.sequence &&
18
+ status.selected.sequence.every(obj => Object.keys(obj).includes("sequence_title"))
19
+ );
20
+
15
21
  const handleCancel = () => {
16
22
  dispatchStatus({ type: "CANCEL_SLOT_SELECTION" });
17
23
  };
@@ -52,6 +58,36 @@ const Confirm = ({ confirmButtonRef }) => {
52
58
  )}{" "}
53
59
  {`(${moment.tz(tz.selectedTzid.tzid).format("z")})`}
54
60
  </p>
61
+ {status.selected.sequence && sequenceTitlePresent && (
62
+ <dl className={theme.classBuilder("confirm-details--sequence")}>
63
+ {status.selected.sequence.map((slot, k) => (
64
+ <div key={k}>
65
+ <dt
66
+ className={theme.classBuilder(
67
+ "confirm-details--sequence-title"
68
+ )}
69
+ >
70
+ {slot.sequence_title}
71
+ </dt>
72
+ <dd
73
+ className={theme.classBuilder("confirm-details--sequence-time")}
74
+ >
75
+ {i18n.customFormatedTimeZone(
76
+ moment(slot.start, "YYYY-MM-DDTHH:mm:00Z"),
77
+ tz.selectedTzid.tzid,
78
+ "LT"
79
+ )}
80
+ {" - "}
81
+ {i18n.customFormatedTimeZone(
82
+ moment(slot.end, "YYYY-MM-DDTHH:mm:00Z"),
83
+ tz.selectedTzid.tzid,
84
+ "LT"
85
+ )}
86
+ </dd>
87
+ </div>
88
+ ))}
89
+ </dl>
90
+ )}
55
91
  </div>
56
92
  <button
57
93
  className={theme.classBuilder("confirm-button")}
@@ -5,7 +5,6 @@ import {
5
5
  getMonthObjectsFromQuery,
6
6
  parseTzList,
7
7
  getInitialSelectedTzid,
8
- getMonthsLoadingFromQuery,
9
8
  getDurationFromQuery,
10
9
  } from "./utils/slots";
11
10
  import { getMonthsInDisplay, parseTimeSlots } from "./utils/calendar";
@@ -37,7 +36,6 @@ const DateTimePicker = ({ options }) => {
37
36
  const startDateObject = moment(options.config.startDate, "YYYY-MM-DD");
38
37
  const currentMonthObject = selectedDateObject ?? startDateObject;
39
38
  const currentMonth = currentMonthObject.format("YYYY-MM");
40
- const monthsLoading = getMonthsLoadingFromQuery(options.query, options.tzid);
41
39
  const monthsInView = getMonthsInDisplay(currentMonth, options.config.startDay);
42
40
 
43
41
  const monthlyView = {
@@ -70,7 +68,6 @@ const DateTimePicker = ({ options }) => {
70
68
  query: options.query,
71
69
  months,
72
70
  monthlyView,
73
- monthsLoading,
74
71
  selected: false,
75
72
  startDay: options.config.startDay,
76
73
  focusedDay: options.config.selectedDay,
@@ -81,6 +78,7 @@ const DateTimePicker = ({ options }) => {
81
78
  availableDays: [],
82
79
  sequenced_availability: options.query.sequence ? true : false,
83
80
  slots: {},
81
+ slotButtonMode: options.config.slotButtonMode,
84
82
  slotFetchCount: 0,
85
83
  slotInjectionPoint: undefined,
86
84
  tzid: options.tzid,
@@ -41,29 +41,55 @@ const SequencedSlotButton = ({ slot }) => {
41
41
  }, [status.focusedSlot]);
42
42
 
43
43
  let classStub = "time-slot";
44
+ if (status.slotButtonMode === "detailed") classStub = classStub + " time-slot--detailed";
44
45
  if (status.selected.start === start) classStub = classStub + " time-slot--selected";
45
46
 
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>
47
+ const detailedSlot = slot.map((s, i) => (
48
+ <li key={i} className={theme.classBuilder("detailed-slot-list--item")}>
55
49
  {i18n.customFormatedTimeZone(
56
- moment(start, "YYYY-MM-DDTHH:mm:00Z"),
50
+ moment(s.start, "YYYY-MM-DDTHH:mm:00Z"),
57
51
  tz.selectedTzid.tzid,
58
52
  "LT"
59
53
  )}
60
54
  {" - "}
61
55
  {i18n.customFormatedTimeZone(
62
- moment(end, "YYYY-MM-DDTHH:mm:00Z"),
56
+ moment(s.end, "YYYY-MM-DDTHH:mm:00Z"),
63
57
  tz.selectedTzid.tzid,
64
58
  "LT"
65
59
  )}{" "}
66
60
  {`(${moment(start, "YYYY-MM-DDTHH:mm:00Z").tz(tz.selectedTzid.tzid).format("z")})`}
61
+ </li>
62
+ ));
63
+
64
+ return (
65
+ <button
66
+ className={theme.classBuilder(classStub)}
67
+ onClick={() => handleSlotSelection(slot)}
68
+ ref={slotButtonRef}
69
+ >
70
+ <span className={theme.classBuilder("visually-hidden")}>
71
+ {i18n.t("select_time_slot")}
72
+ </span>
73
+ {status.slotButtonMode === "detailed" ? (
74
+ <ul className={theme.classBuilder("detailed-slot-list")}>{detailedSlot}</ul>
75
+ ) : (
76
+ <>
77
+ {i18n.customFormatedTimeZone(
78
+ moment(start, "YYYY-MM-DDTHH:mm:00Z"),
79
+ tz.selectedTzid.tzid,
80
+ "LT"
81
+ )}
82
+ {" - "}
83
+ {i18n.customFormatedTimeZone(
84
+ moment(end, "YYYY-MM-DDTHH:mm:00Z"),
85
+ tz.selectedTzid.tzid,
86
+ "LT"
87
+ )}{" "}
88
+ {`(${moment(start, "YYYY-MM-DDTHH:mm:00Z")
89
+ .tz(tz.selectedTzid.tzid)
90
+ .format("z")})`}
91
+ </>
92
+ )}
67
93
  </button>
68
94
  );
69
95
  };
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
2
  import moment from "moment";
3
3
 
4
- import { getSequencedSlots, getSlots } from "./utils/slots";
4
+ import { cropPeriodsArbitrarily, getSlots } from "./utils/slots";
5
5
 
6
6
  import Calendar from "./Calendar";
7
7
  import Confirm from "./Confirm";
@@ -24,51 +24,109 @@ const Wrapper = () => {
24
24
  const [loadingCalendar, setLoadingCalendar] = useState(true);
25
25
 
26
26
  const confirmButtonRef = useRef();
27
+ const firstRender = useRef(true);
27
28
 
28
- useEffect(() => {
29
- if (!status.query.query_periods) {
30
- return;
31
- }
32
-
33
- const fetchMonthSlots = (query, month) => {
34
- const fetch = status.sequenced_availability ? getSequencedSlots : getSlots;
29
+ const fetchMonthSlots = (query, month) => {
30
+ return getSlots({
31
+ query,
32
+ auth: status.auth,
33
+ tzid: status.tzid,
34
+ sequence: status.sequenced_availability,
35
+ })
36
+ .then(res => {
37
+ if (res.length < 512) {
38
+ dispatchStatus({
39
+ type: "SET_SLOTS",
40
+ slots: res,
41
+ tzid: tz.selectedTzid.tzid,
42
+ month,
43
+ });
44
+ return;
45
+ }
35
46
 
36
- return fetch({
37
- query,
38
- auth: status.auth,
39
- tzid: status.tzid,
40
- }).then(res => {
41
47
  dispatchStatus({
42
48
  type: "SET_SLOTS",
43
49
  slots: res,
44
50
  tzid: tz.selectedTzid.tzid,
45
- month: month,
46
51
  });
52
+
53
+ // If we get here, the API has returned the maximum number
54
+ // of slots allowed, so we need to crop the query and try
55
+ // again to ensure we haven't missed any slots.
56
+ const startOfLastSlot = res[res.length - 1].start;
57
+ const endOfLastPeriod = query.query_periods[query.query_periods.length - 1].end;
58
+
59
+ const boundsForCropping = {
60
+ start: startOfLastSlot,
61
+ end: endOfLastPeriod,
62
+ };
63
+
64
+ const croppedPeriods = cropPeriodsArbitrarily(
65
+ query.query_periods,
66
+ boundsForCropping
67
+ );
68
+ const croppedQuery = { ...query, query_periods: croppedPeriods };
69
+
70
+ // Rerun the query
71
+ return fetchMonthSlots(croppedQuery, month);
72
+ })
73
+ .catch(error => {
74
+ if (error.type === 422) {
75
+ dispatchStatus({
76
+ type: "ERROR_LOADING_SLOTS",
77
+ error,
78
+ month,
79
+ });
80
+ return;
81
+ }
82
+ dispatchStatus({ type: "ERROR_GETTING_SLOTS", error });
47
83
  });
48
- };
84
+ };
85
+
86
+ useEffect(() => {
87
+ if (!status.query.query_periods) {
88
+ return;
89
+ }
49
90
 
50
91
  const currentMonth = status.months.find(m => m.current);
51
92
 
93
+ const croppedMonths = status.months.filter(el => {
94
+ return status.monthlyView.monthsInView.some(f => {
95
+ return f === el.month && el.loading && !el.current;
96
+ });
97
+ });
98
+
52
99
  // Get slots for current month
53
100
  fetchMonthSlots(currentMonth.query, currentMonth.month)
54
101
  .then(() => {
55
- // Get slots for remianing months
56
- const remainingMonths = status.months.filter(month => !month.current);
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
- );
102
+ // Get slots for remaining months
103
+ croppedMonths.forEach(month => fetchMonthSlots(month.query, month.month));
66
104
  })
67
105
  .catch(error => {
68
106
  dispatchStatus({ type: "ERROR_GETTING_SLOTS", error });
69
107
  });
70
108
  }, []);
71
109
 
110
+ useEffect(() => {
111
+ //stops this from firing on first render which would double up the initial API calls
112
+ if (firstRender.current) {
113
+ firstRender.current = false;
114
+ return;
115
+ }
116
+ if (status.monthlyView.monthsInView) {
117
+ //check if any of the months in view haven't been fetched yet
118
+ const croppedMonths = status.months.filter(el => {
119
+ return status.monthlyView.monthsInView.some(f => {
120
+ return f === el.month && el.loading === true;
121
+ });
122
+ });
123
+ //if they haven't, fetch them.
124
+ if (croppedMonths.length > 0) {
125
+ croppedMonths.forEach(month => fetchMonthSlots(month.query, month.month));
126
+ }
127
+ }
128
+ }, [status.monthlyView.monthsInView]);
129
+
72
130
  useEffect(() => {
73
131
  if (!status.slotInjectionPoint) {
74
132
  return;
@@ -97,6 +155,15 @@ const Wrapper = () => {
97
155
  const finishedCallingAllSlots = fetchCount === monthsCount;
98
156
  const hasAvailableDays = status.availableDays.length;
99
157
 
158
+ // No slots found but not all months called so try the next month
159
+ if (!finishedCallingAllSlots && !hasAvailableDays && fetchCount >= 2) {
160
+ const croppedMonths = status.months.filter(el => {
161
+ return el.loading === true;
162
+ });
163
+ fetchMonthSlots(croppedMonths[0].query, croppedMonths[0].month);
164
+ return;
165
+ }
166
+
100
167
  // No slots available after all queries have been made
101
168
  if (finishedCallingAllSlots && !hasAvailableDays) {
102
169
  dispatchStatus({ type: "NO_SLOTS_FOUND" });
@@ -117,15 +184,25 @@ const Wrapper = () => {
117
184
  return;
118
185
  }
119
186
 
120
- // For allowing only this to be called once
187
+ // For allowing only this to be called once slots have been fetched
121
188
  // 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;
189
+ if (status.selectedDay && fetchCount >= 1 && !status.selected) {
190
+ const currentMonth = moment(status.selectedDay, "YYYY-MM-DD").format("YYYY-MM");
191
+ const month = status.months.filter(el => {
192
+ return el.month === currentMonth;
193
+ })[0];
194
+ const monthStillLoading = month ? month.loading : false;
195
+ const dayIsAvailable = status.availableDays.includes(status.selectedDay);
196
+
197
+ //Only set the selectedDay if the month is finished loading or if the day has slots available
198
+ if (!monthStillLoading || dayIsAvailable) {
199
+ dispatchStatus({
200
+ type: "SELECT_DAY",
201
+ day: status.selectedDay,
202
+ tzid: tz.selectedTzid.tzid,
203
+ });
204
+ return;
205
+ }
129
206
  }
130
207
  }, [status.slotFetchCount, status.availableDays]);
131
208
 
@@ -159,9 +236,9 @@ const Wrapper = () => {
159
236
  }, [tz.selectedTzid.tzid]);
160
237
 
161
238
  useEffect(() => {
162
- if (status.monthsLoading) {
239
+ if (status.months) {
163
240
  for (let month of status.monthlyView.monthsInView) {
164
- const monthLoading = status.monthsLoading.find(m => m.month === month);
241
+ const monthLoading = status.months.find(m => m.month === month);
165
242
  if (monthLoading && monthLoading.loading) {
166
243
  setLoadingCalendar(true);
167
244
  return;
@@ -170,7 +247,7 @@ const Wrapper = () => {
170
247
  }
171
248
  }
172
249
  }
173
- }, [status.monthlyView, status.monthsLoading]);
250
+ }, [status.monthlyView, status.months]);
174
251
 
175
252
  return (
176
253
  <section className={theme.classBuilder()} style={theme.customProperties}>
@@ -9,6 +9,7 @@ import {
9
9
  addSequencedSlotsToObject,
10
10
  } from "../utils/slots";
11
11
  import { getMonthsInDisplay, parseTimeSlots } from "../utils/calendar";
12
+ import { uniqueItems } from "../../../helpers/utils";
12
13
 
13
14
  export const statusReducer = (state, action) => {
14
15
  const { type, ...actionBody } = action;
@@ -62,9 +63,9 @@ export const statusReducer = (state, action) => {
62
63
  body: action.error.body,
63
64
  },
64
65
  };
65
- const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
66
+ const months = removeMonthFromLoading(state.months, action.month);
66
67
  state.callback(notification);
67
- return { ...state, monthsLoading };
68
+ return { ...state, months };
68
69
  }
69
70
 
70
71
  case "NO_SLOTS_FOUND": {
@@ -99,19 +100,39 @@ export const statusReducer = (state, action) => {
99
100
 
100
101
  case "SET_SLOTS": {
101
102
  if (!action.slots.length > 0) {
102
- const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
103
+ const months = removeMonthFromLoading(state.months, action.month);
103
104
  return {
104
105
  ...state,
105
- monthsLoading,
106
+ months,
106
107
  slotFetchCount: state.slotFetchCount + 1,
107
108
  };
108
109
  }
109
110
 
111
+ // Add action slots to state slots
110
112
  const slotsObject = state.sequenced_availability
111
- ? addSequencedSlotsToObject(state.slots, action.slots)
113
+ ? addSequencedSlotsToObject(state.slots, action.slots, state.query.sequence)
112
114
  : addSlotsToObject(state.slots, action.slots);
113
- const availableDays = getAvailableDays(slotsObject, action.tzid);
114
- const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
115
+
116
+ // since we already know the available days for state slots
117
+ // find available days for action.slots and add them to state availableDays
118
+ // this way we don't have to loop through the entirety of the state slotsObject
119
+ // every time new slots are rendered
120
+ const actionSlotsObject = state.sequenced_availability
121
+ ? addSequencedSlotsToObject({}, action.slots, [])
122
+ : addSlotsToObject({}, action.slots);
123
+
124
+ const availableDays = uniqueItems([
125
+ ...state.availableDays,
126
+ ...getAvailableDays(actionSlotsObject, action.tzid),
127
+ ]).sort();
128
+
129
+ // if month is passed in, we render that month as done
130
+ // this change is for continiously loading slots as opposed to waiting for entire month
131
+ // to be loaded
132
+ let months = state.months;
133
+ if (action.month) {
134
+ months = removeMonthFromLoading(state.months, action.month);
135
+ }
115
136
 
116
137
  const injectionPoint = state.sequenced_availability
117
138
  ? getLocalDayFromUtc(action.slots[0].sequence[0].start, action.tzid)
@@ -120,7 +141,7 @@ export const statusReducer = (state, action) => {
120
141
  return {
121
142
  ...state,
122
143
  availableDays,
123
- monthsLoading,
144
+ months,
124
145
  slots: slotsObject,
125
146
  slotFetchCount: state.slotFetchCount + 1,
126
147
  slotInjectionPoint: injectionPoint,
@@ -10,6 +10,16 @@
10
10
  font-size: 1.5em;
11
11
  }
12
12
 
13
+ .DTP__confirm-details--sequence-title {
14
+ display: inline-block;
15
+ font-weight: bold;
16
+ }
17
+
18
+ .DTP__confirm-details--sequence-time {
19
+ display: inline-block;
20
+ margin-left: 0.5rem;
21
+ }
22
+
13
23
  .DTP__confirm-button {
14
24
  @extend .DTP__button;
15
25
  background-color: var(--buttonConfirm);
@@ -25,6 +25,16 @@
25
25
  border-radius: 0.4em;
26
26
  }
27
27
 
28
+ .DTP__time-slot--detailed {
29
+ height: unset;
30
+ }
31
+
32
+ .DTP__detailed-slot-list,
33
+ .DTP__detailed-slot-list--item {
34
+ @extend %unset;
35
+ list-style: none;
36
+ }
37
+
28
38
  .DTP__time-slot--selected {
29
39
  @extend .DTP__button;
30
40
  background-color: var(--buttonActive);