cronofy-elements 1.45.0 → 1.46.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/build/CronofyElements.v1.46.0.js +2 -0
- package/build/{CronofyElements.v1.45.0.js.LICENSE.txt → CronofyElements.v1.46.0.js.LICENSE.txt} +0 -0
- package/build/npm/CronofyElements.js +2 -2
- package/demo/date-time-picker.ejs +2 -7
- package/package.json +1 -1
- package/src/js/components/DateTimePicker/DateTimePicker.js +0 -3
- package/src/js/components/DateTimePicker/Wrapper.js +114 -37
- package/src/js/components/DateTimePicker/contexts/status-reducer.js +28 -7
- package/src/js/components/DateTimePicker/utils/slots.js +7 -75
- package/tests/DateTimePicker/contexts/status-reducer.test.js +175 -21
- package/tests/DateTimePicker/utils.test.js +8 -0
- package/build/CronofyElements.v1.45.0.js +0 -2
|
@@ -136,15 +136,10 @@
|
|
|
136
136
|
]
|
|
137
137
|
}
|
|
138
138
|
],
|
|
139
|
-
required_duration: { minutes:
|
|
139
|
+
required_duration: { minutes: 15 },
|
|
140
140
|
//start_interval: { minutes: 15 },
|
|
141
141
|
query_periods: [
|
|
142
|
-
{ start: slotTimes(31,"03:00"), end: slotTimes(
|
|
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") }
|
|
142
|
+
{ start: slotTimes(31,"03:00"), end: slotTimes(150,"12:00") },
|
|
148
143
|
]
|
|
149
144
|
},
|
|
150
145
|
// tzid: "America/Mexico_City",
|
package/package.json
CHANGED
|
@@ -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,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import moment from "moment";
|
|
3
3
|
|
|
4
|
-
import {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
56
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
189
|
+
if (status.selectedDay && fetchCount >= 1) {
|
|
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.
|
|
239
|
+
if (status.months) {
|
|
163
240
|
for (let month of status.monthlyView.monthsInView) {
|
|
164
|
-
const monthLoading = status.
|
|
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.
|
|
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
|
|
66
|
+
const months = removeMonthFromLoading(state.months, action.month);
|
|
66
67
|
state.callback(notification);
|
|
67
|
-
return { ...state,
|
|
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
|
|
103
|
+
const months = removeMonthFromLoading(state.months, action.month);
|
|
103
104
|
return {
|
|
104
105
|
...state,
|
|
105
|
-
|
|
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
113
|
? addSequencedSlotsToObject(state.slots, action.slots)
|
|
112
114
|
: addSlotsToObject(state.slots, action.slots);
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
144
|
+
months,
|
|
124
145
|
slots: slotsObject,
|
|
125
146
|
slotFetchCount: state.slotFetchCount + 1,
|
|
126
147
|
slotInjectionPoint: injectionPoint,
|
|
@@ -92,6 +92,7 @@ export const getMonthObjectsFromQuery = (query, tzid, current = false) => {
|
|
|
92
92
|
return {
|
|
93
93
|
month,
|
|
94
94
|
current: currentMonth === month,
|
|
95
|
+
loading: true,
|
|
95
96
|
query: {
|
|
96
97
|
...query,
|
|
97
98
|
query_periods: croppedQueryPeriods,
|
|
@@ -101,55 +102,10 @@ export const getMonthObjectsFromQuery = (query, tzid, current = false) => {
|
|
|
101
102
|
});
|
|
102
103
|
};
|
|
103
104
|
|
|
104
|
-
export const getSlots = ({ query, auth, tzid,
|
|
105
|
-
getAvailability
|
|
106
|
-
auth.token,
|
|
107
|
-
auth.domains.apiDomain,
|
|
108
|
-
query,
|
|
109
|
-
"DateTimePicker",
|
|
110
|
-
tzid,
|
|
111
|
-
auth.demo
|
|
112
|
-
).then(res => {
|
|
113
|
-
if (res.status === 422) {
|
|
114
|
-
throw {
|
|
115
|
-
type: 422,
|
|
116
|
-
message: errorMessages[422].message,
|
|
117
|
-
body: res.errors,
|
|
118
|
-
docsSlug: errorMessages[422].docsSlug,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
// This will intentionally throw an error if the
|
|
122
|
-
// result is not in the correct format:
|
|
123
|
-
const returnedSlots = parseSlotsResult(res);
|
|
124
|
-
|
|
125
|
-
const allSlots = [...slots, ...returnedSlots];
|
|
126
|
-
|
|
127
|
-
if (returnedSlots.length < 512) return [...slots, ...returnedSlots];
|
|
128
|
-
|
|
129
|
-
// If we get here, the API has returned the maximum number
|
|
130
|
-
// of slots allowed, so we need to crop the query and try
|
|
131
|
-
// again to ensure we haven't missed any slots.
|
|
132
|
-
|
|
133
|
-
const startOfLastSlot = returnedSlots[returnedSlots.length - 1].start;
|
|
134
|
-
const endOfLastPeriod = query.query_periods[query.query_periods.length - 1].end;
|
|
135
|
-
const boundsForCropping = {
|
|
136
|
-
start: startOfLastSlot,
|
|
137
|
-
end: endOfLastPeriod,
|
|
138
|
-
};
|
|
139
|
-
const croppedPeriods = cropPeriodsArbitrarily(query.query_periods, boundsForCropping);
|
|
140
|
-
const croppedQuery = { ...query, query_periods: croppedPeriods };
|
|
141
|
-
|
|
142
|
-
// Rerun the query
|
|
143
|
-
return getSlots({
|
|
144
|
-
query: croppedQuery,
|
|
145
|
-
auth,
|
|
146
|
-
tzid,
|
|
147
|
-
slots: allSlots,
|
|
148
|
-
});
|
|
149
|
-
});
|
|
105
|
+
export const getSlots = ({ query, auth, tzid, sequence = false }) => {
|
|
106
|
+
const availabilityFetch = sequence ? getSequencedAvailability : getAvailability;
|
|
150
107
|
|
|
151
|
-
|
|
152
|
-
getSequencedAvailability(
|
|
108
|
+
return availabilityFetch(
|
|
153
109
|
auth.token,
|
|
154
110
|
auth.domains.apiDomain,
|
|
155
111
|
query,
|
|
@@ -157,7 +113,7 @@ export const getSequencedSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
|
157
113
|
tzid,
|
|
158
114
|
auth.demo
|
|
159
115
|
).then(res => {
|
|
160
|
-
if (res.
|
|
116
|
+
if (res.status === 422) {
|
|
161
117
|
throw {
|
|
162
118
|
type: 422,
|
|
163
119
|
message: errorMessages[422].message,
|
|
@@ -167,33 +123,9 @@ export const getSequencedSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
|
167
123
|
}
|
|
168
124
|
// This will intentionally throw an error if the
|
|
169
125
|
// result is not in the correct format:
|
|
170
|
-
|
|
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
|
-
});
|
|
126
|
+
return parseSlotsResult(res);
|
|
196
127
|
});
|
|
128
|
+
};
|
|
197
129
|
|
|
198
130
|
export const parseQuery = query => {
|
|
199
131
|
if (!query.bookable_events) {
|