cronofy-elements 1.42.1 → 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/build/CronofyElements.v1.43.0.js +2 -0
- package/build/{CronofyElements.v1.42.1.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 +73 -0
- package/package.json +1 -1
- package/src/js/components/DateTimePicker/DateTimePicker.js +5 -0
- package/src/js/components/DateTimePicker/Details.js +5 -3
- 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 +21 -19
- package/src/js/components/DateTimePicker/contexts/status-reducer.js +18 -7
- package/src/js/components/DateTimePicker/utils/slots.js +68 -2
- package/src/js/helpers/connections.js +38 -0
- package/src/js/helpers/init.DateTimePicker.js +4 -0
- package/tests/DateTimePicker/SequencedSlotButton.test.js +130 -0
- package/tests/DateTimePicker/contexts/status-reducer.test.js +217 -67
- package/tests/DateTimePicker/dummy-data.js +277 -0
- package/tests/DateTimePicker/utils.test.js +7 -1
- package/build/CronofyElements.v1.42.1.js +0 -2
|
@@ -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: [
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
parseTzList,
|
|
7
7
|
getInitialSelectedTzid,
|
|
8
8
|
getMonthsLoadingFromQuery,
|
|
9
|
+
getDurationFromQuery,
|
|
9
10
|
} from "./utils/slots";
|
|
10
11
|
import { getMonthsInDisplay, parseTimeSlots } from "./utils/calendar";
|
|
11
12
|
|
|
@@ -30,6 +31,8 @@ const DateTimePicker = ({ options }) => {
|
|
|
30
31
|
selectedDateObject?.format("YYYY-MM")
|
|
31
32
|
);
|
|
32
33
|
|
|
34
|
+
const duration = getDurationFromQuery(options.query);
|
|
35
|
+
|
|
33
36
|
const endDateObject = moment(options.config.endDate, "YYYY-MM-DD");
|
|
34
37
|
const startDateObject = moment(options.config.startDate, "YYYY-MM-DD");
|
|
35
38
|
const currentMonthObject = selectedDateObject ?? startDateObject;
|
|
@@ -61,6 +64,7 @@ const DateTimePicker = ({ options }) => {
|
|
|
61
64
|
: () => options.log.info("No `callback` option has been provided"),
|
|
62
65
|
columnView: "loading", // loading | error | slots | confirm | no-slots
|
|
63
66
|
daySlots: [],
|
|
67
|
+
duration,
|
|
64
68
|
locale: options.locale,
|
|
65
69
|
mode: options.config.mode, // confirm (default) | no_confirm
|
|
66
70
|
query: options.query,
|
|
@@ -75,6 +79,7 @@ const DateTimePicker = ({ options }) => {
|
|
|
75
79
|
endDateObject,
|
|
76
80
|
focusedSlot: false,
|
|
77
81
|
availableDays: [],
|
|
82
|
+
sequenced_availability: options.query.sequence ? true : false,
|
|
78
83
|
slots: {},
|
|
79
84
|
slotFetchCount: 0,
|
|
80
85
|
slotInjectionPoint: undefined,
|
|
@@ -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
|
-
|
|
21
|
-
<
|
|
22
|
-
|
|
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,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
|
-
|
|
18
|
+
{status.sequenced_availability ? (
|
|
19
|
+
<SequencedSlotButton slot={slot} />
|
|
20
|
+
) : (
|
|
21
|
+
<SlotButton slot={slot} />
|
|
22
|
+
)}
|
|
18
23
|
</li>
|
|
19
24
|
);
|
|
20
25
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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";
|
|
@@ -31,26 +31,20 @@ const Wrapper = () => {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const fetchMonthSlots = (query, month) => {
|
|
34
|
-
|
|
34
|
+
const fetch = status.sequenced_availability ? getSequencedSlots : getSlots;
|
|
35
|
+
|
|
36
|
+
return fetch({
|
|
35
37
|
query,
|
|
36
38
|
auth: status.auth,
|
|
37
39
|
tzid: status.tzid,
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
month: month,
|
|
45
|
-
});
|
|
46
|
-
})
|
|
47
|
-
.catch(res => {
|
|
48
|
-
dispatchStatus({
|
|
49
|
-
type: "ERROR_LOADING_SLOTS",
|
|
50
|
-
error: res,
|
|
51
|
-
month: month,
|
|
52
|
-
});
|
|
40
|
+
}).then(res => {
|
|
41
|
+
dispatchStatus({
|
|
42
|
+
type: "SET_SLOTS",
|
|
43
|
+
slots: res,
|
|
44
|
+
tzid: tz.selectedTzid.tzid,
|
|
45
|
+
month: month,
|
|
53
46
|
});
|
|
47
|
+
});
|
|
54
48
|
};
|
|
55
49
|
|
|
56
50
|
const currentMonth = status.months.find(m => m.current);
|
|
@@ -60,7 +54,15 @@ const Wrapper = () => {
|
|
|
60
54
|
.then(() => {
|
|
61
55
|
// Get slots for remianing months
|
|
62
56
|
const remainingMonths = status.months.filter(month => !month.current);
|
|
63
|
-
remainingMonths.forEach(month =>
|
|
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
|
+
);
|
|
64
66
|
})
|
|
65
67
|
.catch(error => {
|
|
66
68
|
dispatchStatus({ type: "ERROR_GETTING_SLOTS", error });
|
|
@@ -172,7 +174,7 @@ const Wrapper = () => {
|
|
|
172
174
|
|
|
173
175
|
return (
|
|
174
176
|
<section className={theme.classBuilder()} style={theme.customProperties}>
|
|
175
|
-
<Details duration={status.
|
|
177
|
+
<Details duration={status.duration} locale={status.locale} />
|
|
176
178
|
<div className={theme.classBuilder("wrapper")}>
|
|
177
179
|
<div
|
|
178
180
|
className={
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getAvailableDays,
|
|
7
7
|
getLocalDayFromUtc,
|
|
8
8
|
removeMonthFromLoading,
|
|
9
|
+
addSequencedSlotsToObject,
|
|
9
10
|
} from "../utils/slots";
|
|
10
11
|
import { getMonthsInDisplay, parseTimeSlots } from "../utils/calendar";
|
|
11
12
|
|
|
@@ -43,6 +44,7 @@ export const statusReducer = (state, action) => {
|
|
|
43
44
|
notification: {
|
|
44
45
|
type: "error",
|
|
45
46
|
message: "There was an error getting the slots",
|
|
47
|
+
body: action.error.body,
|
|
46
48
|
},
|
|
47
49
|
};
|
|
48
50
|
state.callback(notification);
|
|
@@ -99,18 +101,23 @@ export const statusReducer = (state, action) => {
|
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
const slotsObject =
|
|
103
|
-
|
|
104
|
-
|
|
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);
|
|
105
108
|
const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
|
|
106
109
|
|
|
110
|
+
const injectionPoint = state.sequenced_availability
|
|
111
|
+
? getLocalDayFromUtc(action.slots[0].sequence[0].start, action.tzid)
|
|
112
|
+
: getLocalDayFromUtc(action.slots[0].start, action.tzid);
|
|
113
|
+
|
|
107
114
|
return {
|
|
108
115
|
...state,
|
|
109
|
-
availableDays
|
|
116
|
+
availableDays,
|
|
110
117
|
monthsLoading,
|
|
111
118
|
slots: slotsObject,
|
|
112
119
|
slotFetchCount: state.slotFetchCount + 1,
|
|
113
|
-
slotInjectionPoint:
|
|
120
|
+
slotInjectionPoint: injectionPoint,
|
|
114
121
|
};
|
|
115
122
|
}
|
|
116
123
|
|
|
@@ -123,13 +130,17 @@ export const statusReducer = (state, action) => {
|
|
|
123
130
|
|
|
124
131
|
case "SELECT_DAY": {
|
|
125
132
|
const daySlots = getSlotsByDay(state.slots, action.day, action.tzid);
|
|
126
|
-
const focusedSlot =
|
|
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;
|
|
127
138
|
|
|
128
139
|
return {
|
|
129
140
|
...state,
|
|
130
141
|
selectedDay: action.day,
|
|
131
142
|
focusedDay: action.day,
|
|
132
|
-
focusedSlot: focusedSlot
|
|
143
|
+
focusedSlot: focusedSlot,
|
|
133
144
|
columnView: "slots",
|
|
134
145
|
daySlots,
|
|
135
146
|
populated: true,
|
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
import { errorMessages } from "../../../helpers/logging";
|
|
6
6
|
|
|
@@ -33,6 +33,12 @@ export const getMonthsFromQuery = (periods = [], tzid) => {
|
|
|
33
33
|
return uniqueMonths;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export const getDurationFromQuery = query => {
|
|
37
|
+
const duration = query.sequence ? false : query.required_duration.minutes;
|
|
38
|
+
|
|
39
|
+
return duration;
|
|
40
|
+
};
|
|
41
|
+
|
|
36
42
|
export const getMonthsLoadingFromQuery = (query, tzid) => {
|
|
37
43
|
if (!query.query_periods || !query.query_periods.length) {
|
|
38
44
|
return [
|
|
@@ -104,7 +110,7 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
|
104
110
|
tzid,
|
|
105
111
|
auth.demo
|
|
106
112
|
).then(res => {
|
|
107
|
-
if (res.
|
|
113
|
+
if (res.status === 422) {
|
|
108
114
|
throw {
|
|
109
115
|
type: 422,
|
|
110
116
|
message: errorMessages[422].message,
|
|
@@ -142,6 +148,53 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
|
|
|
142
148
|
});
|
|
143
149
|
});
|
|
144
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
|
+
|
|
145
198
|
export const parseQuery = query => {
|
|
146
199
|
if (!query.bookable_events) {
|
|
147
200
|
return {
|
|
@@ -156,6 +209,8 @@ export const parseSlotsResult = res => {
|
|
|
156
209
|
let returnedSlots;
|
|
157
210
|
if (typeof res.available_bookable_events !== "undefined") {
|
|
158
211
|
returnedSlots = res.available_bookable_events;
|
|
212
|
+
} else if (typeof res.sequences !== "undefined") {
|
|
213
|
+
returnedSlots = res.sequences;
|
|
159
214
|
} else {
|
|
160
215
|
returnedSlots = res.available_slots;
|
|
161
216
|
|
|
@@ -209,6 +264,17 @@ export const addSlotsToObject = (slotsObject, newSlotsArray) => {
|
|
|
209
264
|
return slotsObject;
|
|
210
265
|
};
|
|
211
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
|
+
|
|
212
278
|
export const getSlotsByDay = (slots, day, tzid) => {
|
|
213
279
|
const slotKeys = Object.keys(slots);
|
|
214
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,
|
|
@@ -72,6 +72,7 @@ export const parseDateTimePickerOptions = (options = {}) => {
|
|
|
72
72
|
const target = parseTarget(options, "date-time-picker", log);
|
|
73
73
|
|
|
74
74
|
const isBookableEventsQuery = options.availability_query.bookable_events ? true : false;
|
|
75
|
+
const isSequencedAvailabilityQuery = options.availability_query.sequence ? true : false;
|
|
75
76
|
|
|
76
77
|
let query;
|
|
77
78
|
if (options.demo) {
|
|
@@ -79,6 +80,9 @@ export const parseDateTimePickerOptions = (options = {}) => {
|
|
|
79
80
|
} else if (isBookableEventsQuery) {
|
|
80
81
|
query = options.availability_query;
|
|
81
82
|
query = parseWithOverlappingSlots(query);
|
|
83
|
+
} else if (isSequencedAvailabilityQuery) {
|
|
84
|
+
query = options.availability_query;
|
|
85
|
+
query = parseWithOverlappingSlots(query);
|
|
82
86
|
} else {
|
|
83
87
|
query = parseQuery({ options, elementSlug: "date-time-picker", log });
|
|
84
88
|
query = parseWithOverlappingSlots(query);
|
|
@@ -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
|
+
});
|