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.
- package/.eslintrc.yaml +31 -30
- package/build/CronofyElements.v1.43.0.js +2 -0
- package/build/{CronofyElements.v1.40.2.js.LICENSE.txt → CronofyElements.v1.43.0.js.LICENSE.txt} +0 -0
- package/build/npm/CronofyElements.js +2 -2
- package/demo/date-time-picker.ejs +74 -0
- package/package.json +1 -1
- package/src/js/components/DateTimePicker/CalendarHeader.js +21 -14
- package/src/js/components/DateTimePicker/DateTimePicker.js +34 -10
- package/src/js/components/DateTimePicker/Details.js +5 -3
- package/src/js/components/DateTimePicker/LoadingCalendar.js +21 -0
- package/src/js/components/DateTimePicker/SequencedSlotButton.js +71 -0
- package/src/js/components/DateTimePicker/SlotsList.js +6 -1
- package/src/js/components/DateTimePicker/Wrapper.js +78 -23
- package/src/js/components/DateTimePicker/contexts/status-reducer.js +69 -28
- package/src/js/components/DateTimePicker/scss/_components.loading.scss +10 -0
- package/src/js/components/DateTimePicker/utils/calendar.js +23 -0
- package/src/js/components/DateTimePicker/utils/slots.js +115 -5
- package/src/js/helpers/connections.js +38 -0
- package/src/js/helpers/init.DateTimePicker.js +65 -2
- package/src/js/translations/en/date_time_picker.json +1 -0
- package/tests/DateTimePicker/SequencedSlotButton.test.js +130 -0
- package/tests/DateTimePicker/contexts/status-reducer.test.js +497 -187
- package/tests/DateTimePicker/dummy-data.js +277 -0
- package/tests/DateTimePicker/utils.test.js +53 -1
- package/build/CronofyElements.v1.40.2.js +0 -2
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import moment from "moment-timezone";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
addSlotsToObject,
|
|
3
|
-
getFirstAvailableDay,
|
|
4
5
|
getSlotsByDay,
|
|
5
6
|
getAvailableDays,
|
|
6
7
|
getLocalDayFromUtc,
|
|
8
|
+
removeMonthFromLoading,
|
|
9
|
+
addSequencedSlotsToObject,
|
|
7
10
|
} from "../utils/slots";
|
|
8
|
-
import { parseTimeSlots } from "../utils/calendar";
|
|
11
|
+
import { getMonthsInDisplay, parseTimeSlots } from "../utils/calendar";
|
|
9
12
|
|
|
10
13
|
export const statusReducer = (state, action) => {
|
|
11
14
|
const { type, ...actionBody } = action;
|
|
@@ -41,12 +44,29 @@ export const statusReducer = (state, action) => {
|
|
|
41
44
|
notification: {
|
|
42
45
|
type: "error",
|
|
43
46
|
message: "There was an error getting the slots",
|
|
47
|
+
body: action.error.body,
|
|
44
48
|
},
|
|
45
49
|
};
|
|
46
50
|
state.callback(notification);
|
|
47
51
|
return { ...state, error: action.error, columnView: "error" };
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
case "ERROR_LOADING_SLOTS": {
|
|
55
|
+
const notification = {
|
|
56
|
+
notification: {
|
|
57
|
+
type: "error",
|
|
58
|
+
message:
|
|
59
|
+
"There was a problem with your availability query. Slots for the month of " +
|
|
60
|
+
action.month +
|
|
61
|
+
" could not be loaded.",
|
|
62
|
+
body: action.error.body,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
|
|
66
|
+
state.callback(notification);
|
|
67
|
+
return { ...state, monthsLoading };
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
case "NO_SLOTS_FOUND": {
|
|
51
71
|
return { ...state, columnView: "no-slots" };
|
|
52
72
|
}
|
|
@@ -54,6 +74,9 @@ export const statusReducer = (state, action) => {
|
|
|
54
74
|
case "RECALCULATE_MONTH_VIEW": {
|
|
55
75
|
const monthlyView = {
|
|
56
76
|
month: state.monthlyView.month,
|
|
77
|
+
hasNext: state.monthlyView.hasNext,
|
|
78
|
+
hasPrev: state.monthlyView.hasPrev,
|
|
79
|
+
monthsInView: state.monthlyView.monthsInView,
|
|
57
80
|
days: parseTimeSlots({
|
|
58
81
|
slots: state.slots,
|
|
59
82
|
month: state.monthlyView.month,
|
|
@@ -70,38 +93,31 @@ export const statusReducer = (state, action) => {
|
|
|
70
93
|
|
|
71
94
|
case "SET_SLOTS": {
|
|
72
95
|
if (!action.slots.length > 0) {
|
|
73
|
-
|
|
96
|
+
const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
|
|
97
|
+
return {
|
|
98
|
+
...state,
|
|
99
|
+
monthsLoading,
|
|
100
|
+
slotFetchCount: state.slotFetchCount + 1,
|
|
101
|
+
};
|
|
74
102
|
}
|
|
75
103
|
|
|
76
|
-
const slotsObject =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
let selectedDay = state.selectedDay;
|
|
82
|
-
let availableDays = getAvailableDays(addSlotsToObject({}, action.slots), action.tzid);
|
|
104
|
+
const slotsObject = state.sequenced_availability
|
|
105
|
+
? addSequencedSlotsToObject(state.slots, action.slots)
|
|
106
|
+
: addSlotsToObject(state.slots, action.slots);
|
|
107
|
+
const availableDays = getAvailableDays(slotsObject, action.tzid);
|
|
108
|
+
const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
|
|
83
109
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (!state.selectedDay) {
|
|
88
|
-
columnView = "slots";
|
|
89
|
-
focusedDay = availableDays[0];
|
|
90
|
-
selectedDay = availableDays[0];
|
|
91
|
-
daySlots = getSlotsByDay(slotsObject, selectedDay, action.tzid);
|
|
92
|
-
}
|
|
110
|
+
const injectionPoint = state.sequenced_availability
|
|
111
|
+
? getLocalDayFromUtc(action.slots[0].sequence[0].start, action.tzid)
|
|
112
|
+
: getLocalDayFromUtc(action.slots[0].start, action.tzid);
|
|
93
113
|
|
|
94
114
|
return {
|
|
95
115
|
...state,
|
|
96
|
-
daySlots,
|
|
97
|
-
columnView,
|
|
98
|
-
focusedDay,
|
|
99
|
-
selectedDay,
|
|
100
116
|
availableDays,
|
|
101
|
-
|
|
117
|
+
monthsLoading,
|
|
102
118
|
slots: slotsObject,
|
|
103
119
|
slotFetchCount: state.slotFetchCount + 1,
|
|
104
|
-
slotInjectionPoint:
|
|
120
|
+
slotInjectionPoint: injectionPoint,
|
|
105
121
|
};
|
|
106
122
|
}
|
|
107
123
|
|
|
@@ -114,26 +130,46 @@ export const statusReducer = (state, action) => {
|
|
|
114
130
|
|
|
115
131
|
case "SELECT_DAY": {
|
|
116
132
|
const daySlots = getSlotsByDay(state.slots, action.day, action.tzid);
|
|
133
|
+
const focusedSlot = state.sequenced_availability
|
|
134
|
+
? daySlots[0]?.reduce((prev, current) => {
|
|
135
|
+
return prev.start < current.start ? prev.start : current.start;
|
|
136
|
+
})
|
|
137
|
+
: daySlots[0]?.start;
|
|
138
|
+
|
|
117
139
|
return {
|
|
118
140
|
...state,
|
|
119
141
|
selectedDay: action.day,
|
|
120
142
|
focusedDay: action.day,
|
|
121
|
-
focusedSlot:
|
|
143
|
+
focusedSlot: focusedSlot,
|
|
122
144
|
columnView: "slots",
|
|
123
145
|
daySlots,
|
|
124
146
|
populated: true,
|
|
125
147
|
};
|
|
126
148
|
}
|
|
127
149
|
case "SELECT_MONTH": {
|
|
150
|
+
const actionMonthObject = moment(action.month, "YYYY-MM");
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
!actionMonthObject.isBetween(
|
|
154
|
+
state.startDateObject,
|
|
155
|
+
state.endDateObject,
|
|
156
|
+
"month",
|
|
157
|
+
"[]"
|
|
158
|
+
)
|
|
159
|
+
) {
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
|
|
128
163
|
const months = state.months.map(month => ({
|
|
129
164
|
...month,
|
|
130
165
|
current: month.month === action.month,
|
|
131
166
|
}));
|
|
132
167
|
|
|
133
|
-
let focusedDay = action.focusedDay ?? false;
|
|
134
|
-
|
|
135
168
|
const monthlyView = {
|
|
136
169
|
month: action.month,
|
|
170
|
+
hasNext: actionMonthObject.isBefore(state.endDateObject, "month"),
|
|
171
|
+
hasPrev: actionMonthObject.isAfter(state.startDateObject, "month"),
|
|
172
|
+
monthsInView: getMonthsInDisplay(action.month, state.startDay),
|
|
137
173
|
days: parseTimeSlots({
|
|
138
174
|
tzid: action.tzid,
|
|
139
175
|
slots: state.slots,
|
|
@@ -142,6 +178,8 @@ export const statusReducer = (state, action) => {
|
|
|
142
178
|
}),
|
|
143
179
|
};
|
|
144
180
|
|
|
181
|
+
let focusedDay = action.focusedDay ?? false;
|
|
182
|
+
|
|
145
183
|
return {
|
|
146
184
|
...state,
|
|
147
185
|
months,
|
|
@@ -170,6 +208,9 @@ export const statusReducer = (state, action) => {
|
|
|
170
208
|
|
|
171
209
|
const monthlyView = {
|
|
172
210
|
month: state.monthlyView.month,
|
|
211
|
+
hasNext: state.monthlyView.hasNext,
|
|
212
|
+
hasPrev: state.monthlyView.hasPrev,
|
|
213
|
+
monthsInView: state.monthlyView.monthsInView,
|
|
173
214
|
days: parseTimeSlots({
|
|
174
215
|
slots: state.slots,
|
|
175
216
|
month: state.monthlyView.month,
|
|
@@ -92,6 +92,29 @@ export const getDatesInMonthDisplay = (month, availableDays, startDay) => {
|
|
|
92
92
|
return rows;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
export const getMonthsInDisplay = (month, startDay) => {
|
|
96
|
+
const monthObject = moment(month, "YYYY-MM");
|
|
97
|
+
const daysInMonth = monthObject.daysInMonth();
|
|
98
|
+
const offset = weekDayOffset(monthObject, startDay);
|
|
99
|
+
|
|
100
|
+
const result = [month];
|
|
101
|
+
const daysLength = daysInMonth + offset;
|
|
102
|
+
|
|
103
|
+
//Add prev month if in display...
|
|
104
|
+
if (offset > 0) {
|
|
105
|
+
const prevMonth = monthObject.clone().add(-1, "months").format("YYYY-MM");
|
|
106
|
+
result.push(prevMonth);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add next month if in display
|
|
110
|
+
if (daysLength < 42) {
|
|
111
|
+
const nextMonth = monthObject.clone().add(1, "months").format("YYYY-MM");
|
|
112
|
+
result.push(nextMonth);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
};
|
|
117
|
+
|
|
95
118
|
export const parseTimeSlots = ({ slots, month, tzid, startDay }) => {
|
|
96
119
|
//Get array of available days
|
|
97
120
|
const availableDays = getAvailableDays(slots, tzid);
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
+
import { errorMessages } from "../../../helpers/logging";
|
|
5
6
|
|
|
6
7
|
import { defaultTimeZones } from "./tz-list";
|
|
7
8
|
|
|
@@ -21,7 +22,7 @@ export const getMonthsCoveredByPeriod = (period, tzid) => {
|
|
|
21
22
|
return months;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
export const getMonthsFromQuery = (periods, tzid) => {
|
|
25
|
+
export const getMonthsFromQuery = (periods = [], tzid) => {
|
|
25
26
|
const months = periods
|
|
26
27
|
.map(period => getMonthsCoveredByPeriod(period, tzid))
|
|
27
28
|
// getMonthsCoveredByPeriod returns an array, so flatten them...
|
|
@@ -32,6 +33,40 @@ export const getMonthsFromQuery = (periods, tzid) => {
|
|
|
32
33
|
return uniqueMonths;
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
export const getDurationFromQuery = query => {
|
|
37
|
+
const duration = query.sequence ? false : query.required_duration.minutes;
|
|
38
|
+
|
|
39
|
+
return duration;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const getMonthsLoadingFromQuery = (query, tzid) => {
|
|
43
|
+
if (!query.query_periods || !query.query_periods.length) {
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
month: moment.tz().format("YYYY-MM"),
|
|
47
|
+
loading: true,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
const monthStrings = getMonthsFromQuery(query.query_periods, tzid);
|
|
52
|
+
const monthObjects = monthStrings.map(month => {
|
|
53
|
+
return {
|
|
54
|
+
month,
|
|
55
|
+
loading: true,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
return monthObjects;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const removeMonthFromLoading = (monthsLoading, month) => {
|
|
62
|
+
const monthPos = monthsLoading.findIndex(m => m.month === month);
|
|
63
|
+
const newMonthsLoading = [...monthsLoading];
|
|
64
|
+
if (monthsLoading && monthPos >= 0) {
|
|
65
|
+
newMonthsLoading[monthPos]["loading"] = false;
|
|
66
|
+
}
|
|
67
|
+
return newMonthsLoading;
|
|
68
|
+
};
|
|
69
|
+
|
|
35
70
|
export const getCurrentMonth = query => ({
|
|
36
71
|
month: moment.tz().format("YYYY-MM"),
|
|
37
72
|
current: true,
|
|
@@ -42,9 +77,17 @@ export const getMonthObjectsFromQuery = (query, tzid, current = false) => {
|
|
|
42
77
|
if (!query.query_periods || !query.query_periods.length) {
|
|
43
78
|
return [getCurrentMonth()];
|
|
44
79
|
}
|
|
80
|
+
|
|
45
81
|
const monthStrings = getMonthsFromQuery(query.query_periods, tzid);
|
|
46
|
-
const
|
|
47
|
-
const
|
|
82
|
+
const startMonth = moment(monthStrings[0], "YYYY-MM");
|
|
83
|
+
const endMonth = moment(monthStrings[monthStrings.length - 1], "YYYY-MM");
|
|
84
|
+
|
|
85
|
+
const currentMonth =
|
|
86
|
+
current && moment(current, "YYYY-MM").isBetween(startMonth, endMonth, "month", "[]")
|
|
87
|
+
? current
|
|
88
|
+
: monthStrings[0];
|
|
89
|
+
|
|
90
|
+
return monthStrings.map(month => {
|
|
48
91
|
const croppedQueryPeriods = cropPeriodsByMonth(query.query_periods, month, tzid);
|
|
49
92
|
return {
|
|
50
93
|
month,
|
|
@@ -56,7 +99,6 @@ export const getMonthObjectsFromQuery = (query, tzid, current = false) => {
|
|
|
56
99
|
},
|
|
57
100
|
};
|
|
58
101
|
});
|
|
59
|
-
return monthObjects;
|
|
60
102
|
};
|
|
61
103
|
|
|
62
104
|
export const getSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
@@ -68,6 +110,14 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
|
68
110
|
tzid,
|
|
69
111
|
auth.demo
|
|
70
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
|
+
}
|
|
71
121
|
// This will intentionally throw an error if the
|
|
72
122
|
// result is not in the correct format:
|
|
73
123
|
const returnedSlots = parseSlotsResult(res);
|
|
@@ -98,6 +148,53 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
|
98
148
|
});
|
|
99
149
|
});
|
|
100
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
|
+
|
|
101
198
|
export const parseQuery = query => {
|
|
102
199
|
if (!query.bookable_events) {
|
|
103
200
|
return {
|
|
@@ -112,6 +209,8 @@ export const parseSlotsResult = res => {
|
|
|
112
209
|
let returnedSlots;
|
|
113
210
|
if (typeof res.available_bookable_events !== "undefined") {
|
|
114
211
|
returnedSlots = res.available_bookable_events;
|
|
212
|
+
} else if (typeof res.sequences !== "undefined") {
|
|
213
|
+
returnedSlots = res.sequences;
|
|
115
214
|
} else {
|
|
116
215
|
returnedSlots = res.available_slots;
|
|
117
216
|
|
|
@@ -165,6 +264,17 @@ export const addSlotsToObject = (slotsObject, newSlotsArray) => {
|
|
|
165
264
|
return slotsObject;
|
|
166
265
|
};
|
|
167
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
|
+
|
|
168
278
|
export const getSlotsByDay = (slots, day, tzid) => {
|
|
169
279
|
const slotKeys = Object.keys(slots);
|
|
170
280
|
const dayObject = moment.tz(day, "YYYY-MM-DD", tzid);
|
|
@@ -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,
|
|
@@ -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;
|
|
@@ -64,12 +72,20 @@ export const parseDateTimePickerOptions = (options = {}) => {
|
|
|
64
72
|
const target = parseTarget(options, "date-time-picker", log);
|
|
65
73
|
|
|
66
74
|
const isBookableEventsQuery = options.availability_query.bookable_events ? true : false;
|
|
75
|
+
const isSequencedAvailabilityQuery = options.availability_query.sequence ? true : false;
|
|
67
76
|
|
|
68
77
|
let query;
|
|
69
|
-
if (
|
|
78
|
+
if (options.demo) {
|
|
79
|
+
query = queryForDateTimePicker;
|
|
80
|
+
} else if (isBookableEventsQuery) {
|
|
81
|
+
query = options.availability_query;
|
|
82
|
+
query = parseWithOverlappingSlots(query);
|
|
83
|
+
} else if (isSequencedAvailabilityQuery) {
|
|
70
84
|
query = options.availability_query;
|
|
85
|
+
query = parseWithOverlappingSlots(query);
|
|
71
86
|
} else {
|
|
72
87
|
query = parseQuery({ options, elementSlug: "date-time-picker", log });
|
|
88
|
+
query = parseWithOverlappingSlots(query);
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
const tzid = parseTimezone(options.tzid, "date-time-picker", log);
|
|
@@ -93,6 +109,44 @@ export const parseDateTimePickerOptions = (options = {}) => {
|
|
|
93
109
|
});
|
|
94
110
|
}
|
|
95
111
|
|
|
112
|
+
let selectedDate = config.selected_date;
|
|
113
|
+
if (typeof selectedDate !== "undefined") {
|
|
114
|
+
const validDate = moment(selectedDate, "YYYY-MM-DD", true).isValid();
|
|
115
|
+
|
|
116
|
+
if (!validDate) {
|
|
117
|
+
log.warn(
|
|
118
|
+
`The provided date ${selectedDate} is not valid. Please ensure it's formatted like "YYYY-MM-DD". Picking the first available date as starting date.`,
|
|
119
|
+
{
|
|
120
|
+
docsSlug: "#config.start_date",
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
selectedDate = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const months = getMonthsFromQuery(query?.query_periods, tzid.tzid);
|
|
129
|
+
const selectedDateMoment = selectedDate && moment(selectedDate, "YYYY-MM-DD");
|
|
130
|
+
|
|
131
|
+
let startDateMoment = months.length
|
|
132
|
+
? moment(months[0], "YYYY-MM").startOf("month")
|
|
133
|
+
: moment().startOf("month");
|
|
134
|
+
|
|
135
|
+
if (selectedDateMoment?.isBefore(startDate)) {
|
|
136
|
+
startDateMoment = selectedDateMoment;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let endDateMoment = months.length
|
|
140
|
+
? moment(months[months.length - 1], "YYYY-MM").endOf("month")
|
|
141
|
+
: moment().endOf("month");
|
|
142
|
+
|
|
143
|
+
if (selectedDateMoment?.isAfter(endDateMoment)) {
|
|
144
|
+
endDateMoment = selectedDateMoment;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const startDate = startDateMoment.format("YYYY-MM-DD");
|
|
148
|
+
const endDate = endDateMoment.format("YYYY-MM-DD");
|
|
149
|
+
|
|
96
150
|
delete options.availability_query;
|
|
97
151
|
delete options.element_token;
|
|
98
152
|
delete options.target_id;
|
|
@@ -107,7 +161,16 @@ export const parseDateTimePickerOptions = (options = {}) => {
|
|
|
107
161
|
token,
|
|
108
162
|
domains,
|
|
109
163
|
query,
|
|
110
|
-
config: {
|
|
164
|
+
config: {
|
|
165
|
+
...config,
|
|
166
|
+
mode,
|
|
167
|
+
logs,
|
|
168
|
+
startDay,
|
|
169
|
+
selectedDate,
|
|
170
|
+
startDate,
|
|
171
|
+
endDate,
|
|
172
|
+
tzList,
|
|
173
|
+
},
|
|
111
174
|
translations,
|
|
112
175
|
log,
|
|
113
176
|
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, fireEvent } from "@testing-library/react";
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
|
|
5
|
+
import SequencedSlotButton from "../../src/js/components/DateTimePicker/SequencedSlotButton";
|
|
6
|
+
|
|
7
|
+
import { I18nProvider } from "../../src/js/contexts/i18n-context";
|
|
8
|
+
import { ThemeProvider } from "../../src/js/components/DateTimePicker/contexts/theme-context";
|
|
9
|
+
import { StatusProvider } from "../../src/js/components/DateTimePicker/contexts/status-context";
|
|
10
|
+
import { TzProvider } from "../../src/js/components/DateTimePicker/contexts/tz-context";
|
|
11
|
+
|
|
12
|
+
const wrapper = ({ children, status }) => (
|
|
13
|
+
<ThemeProvider options={{ name: "DTP" }}>
|
|
14
|
+
<I18nProvider
|
|
15
|
+
options={{
|
|
16
|
+
locale: "en",
|
|
17
|
+
slug: "date_time_picker",
|
|
18
|
+
tzid: "Europe/London",
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<TzProvider
|
|
22
|
+
options={{
|
|
23
|
+
selectedTzid: {
|
|
24
|
+
tzid: "Europe/London",
|
|
25
|
+
offset: "+01:00",
|
|
26
|
+
offsetMins: 60,
|
|
27
|
+
name: "London",
|
|
28
|
+
abbr: "BST",
|
|
29
|
+
},
|
|
30
|
+
list: [],
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<StatusProvider
|
|
34
|
+
options={{
|
|
35
|
+
selected: false,
|
|
36
|
+
...status,
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</StatusProvider>
|
|
41
|
+
</TzProvider>
|
|
42
|
+
</I18nProvider>
|
|
43
|
+
</ThemeProvider>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
describe("SequencedSlotButton", () => {
|
|
47
|
+
const slot = [
|
|
48
|
+
{ sequence_id: "test-1", start: "2022-10-05T09:30:00Z", end: "2022-10-05T10:00:00Z" },
|
|
49
|
+
{ sequence_id: "test-2", start: "2022-10-05T10:30:00Z", end: "2022-10-05T11:00:00Z" },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
it("displays slot button with correct start and end time", () => {
|
|
53
|
+
const { container } = render(<SequencedSlotButton slot={slot} />, { wrapper });
|
|
54
|
+
|
|
55
|
+
const button = container.querySelector(".DTP__time-slot");
|
|
56
|
+
|
|
57
|
+
expect(button).toBeInTheDocument();
|
|
58
|
+
expect(button.innerHTML).toEqual(expect.stringContaining("10:30 AM - 12:00 PM (BST)"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("displays slot button with time in BST", () => {
|
|
62
|
+
const { container } = render(<SequencedSlotButton slot={slot} />, { wrapper });
|
|
63
|
+
|
|
64
|
+
const button = container.querySelector(".DTP__time-slot");
|
|
65
|
+
|
|
66
|
+
expect(button).toBeInTheDocument();
|
|
67
|
+
expect(button.innerHTML).toEqual(expect.stringContaining("10:30 AM - 12:00 PM (BST)"));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("displays slot button with time in GMT", () => {
|
|
71
|
+
const slot = [
|
|
72
|
+
{ sequence_id: "test-1", start: "2022-11-05T09:30:00Z", end: "2022-11-05T10:00:00Z" },
|
|
73
|
+
{ sequence_id: "test-2", start: "2022-11-05T10:30:00Z", end: "2022-11-05T11:00:00Z" },
|
|
74
|
+
];
|
|
75
|
+
const { container } = render(<SequencedSlotButton slot={slot} />, { wrapper });
|
|
76
|
+
|
|
77
|
+
const button = container.querySelector(".DTP__time-slot");
|
|
78
|
+
|
|
79
|
+
expect(button).toBeInTheDocument();
|
|
80
|
+
expect(button.innerHTML).toEqual(expect.stringContaining("9:30 AM - 11:00 AM (GMT)"));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("focuses if slot start time matches status.focusedSlot", () => {
|
|
84
|
+
const status = {
|
|
85
|
+
focusedSlot: "2022-10-05T09:30:00Z",
|
|
86
|
+
selected: false,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const { container } = render(<SequencedSlotButton slot={slot} />, {
|
|
90
|
+
wrapper: e => wrapper({ ...e, status }),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const button = container.querySelector("button");
|
|
94
|
+
|
|
95
|
+
expect(button).toHaveFocus();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("sets the slot to selected when button is clicked", () => {
|
|
99
|
+
const { container } = render(<SequencedSlotButton slot={slot} />, { wrapper });
|
|
100
|
+
|
|
101
|
+
const button = container.querySelector("button");
|
|
102
|
+
|
|
103
|
+
expect(button).not.toHaveClass("DTP_time-slot--selected");
|
|
104
|
+
|
|
105
|
+
fireEvent.click(button);
|
|
106
|
+
|
|
107
|
+
expect(button).toHaveFocus();
|
|
108
|
+
expect(button.classList).toContain("DTP__time-slot--selected");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("shows selected className if preselected slot exists", () => {
|
|
112
|
+
const status = {
|
|
113
|
+
focusedSlot: "2022-10-05T09:30:00Z",
|
|
114
|
+
selected: {
|
|
115
|
+
start: "2022-10-05T09:30:00Z",
|
|
116
|
+
end: "2022-10-05T11:00:00Z",
|
|
117
|
+
sequence: slot,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const { container } = render(<SequencedSlotButton slot={slot} />, {
|
|
122
|
+
wrapper: e => wrapper({ ...e, status }),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const button = container.querySelector("button");
|
|
126
|
+
|
|
127
|
+
expect(button).toHaveFocus();
|
|
128
|
+
expect(button).toHaveClass("DTP__time-slot--selected");
|
|
129
|
+
});
|
|
130
|
+
});
|