cronofy-elements 1.40.1 → 1.40.2
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.40.2.js +2 -0
- package/build/{CronofyElements.v1.40.1.js.LICENSE.txt → CronofyElements.v1.40.2.js.LICENSE.txt} +0 -0
- package/build/npm/CronofyElements.js +2 -2
- package/package.json +1 -1
- package/src/js/components/DateTimePicker/Calendar.js +8 -6
- package/src/js/components/DateTimePicker/CalendarHeader.js +7 -1
- package/src/js/components/DateTimePicker/Confirm.js +1 -1
- package/src/js/components/DateTimePicker/DateTimePicker.js +11 -8
- package/src/js/components/DateTimePicker/Wrapper.js +47 -36
- package/src/js/components/DateTimePicker/contexts/status-context.js +3 -3
- package/src/js/components/DateTimePicker/contexts/status-reducer.js +73 -59
- package/src/js/helpers/utils.js +7 -1
- package/tests/DateTimePicker/contexts/status-reducer.test.js +263 -314
- package/build/CronofyElements.v1.40.1.js +0 -2
package/package.json
CHANGED
|
@@ -14,10 +14,12 @@ import DayHeadings from "./DayHeadings";
|
|
|
14
14
|
|
|
15
15
|
import { useStatus } from "./contexts/status-context";
|
|
16
16
|
import { useTheme } from "./contexts/theme-context";
|
|
17
|
+
import { useTz } from "./contexts/tz-context";
|
|
17
18
|
|
|
18
19
|
const Calendar = () => {
|
|
19
20
|
const [status, dispatchStatus] = useStatus();
|
|
20
21
|
const theme = useTheme();
|
|
22
|
+
const [tz] = useTz();
|
|
21
23
|
|
|
22
24
|
const weekdays = getWeekDays(status.startDay);
|
|
23
25
|
|
|
@@ -27,8 +29,7 @@ const Calendar = () => {
|
|
|
27
29
|
|
|
28
30
|
// Handles the keyboard navigating for the calendar table
|
|
29
31
|
const handleGridNavigation = (date, e) => {
|
|
30
|
-
const
|
|
31
|
-
const monthlySlots = status.monthlySlots.find(month => month.month === currentMonth);
|
|
32
|
+
const monthlySlots = status.monthlyView;
|
|
32
33
|
const availableDaysInGrid = getAvailableDaysInMonthDisplay(monthlySlots.days);
|
|
33
34
|
if (!date) {
|
|
34
35
|
dispatchStatus({
|
|
@@ -70,9 +71,10 @@ const Calendar = () => {
|
|
|
70
71
|
availableDays: status.availableDays,
|
|
71
72
|
});
|
|
72
73
|
dispatchStatus({
|
|
73
|
-
type: "
|
|
74
|
-
focusedDay: result.date,
|
|
74
|
+
type: "SELECT_MONTH",
|
|
75
75
|
month: result.month,
|
|
76
|
+
focusedDay: result.date,
|
|
77
|
+
tzid: tz.selectedTzid.tzid,
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -80,8 +82,8 @@ const Calendar = () => {
|
|
|
80
82
|
}
|
|
81
83
|
};
|
|
82
84
|
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
+
const monthlySlots = status.monthlyView;
|
|
86
|
+
const currentMonth = monthlySlots.month;
|
|
85
87
|
|
|
86
88
|
const grid = monthlySlots.days.map((row, i) => (
|
|
87
89
|
<tr className={theme.classBuilder("calendar-grid--row")} key={`${currentMonth}_row_${i}`}>
|
|
@@ -5,16 +5,22 @@ import { calculateMonthNav } from "./utils/calendar";
|
|
|
5
5
|
import { useI18n } from "../../contexts/i18n-context";
|
|
6
6
|
import { useStatus } from "./contexts/status-context";
|
|
7
7
|
import { useTheme } from "./contexts/theme-context";
|
|
8
|
+
import { useTz } from "./contexts/tz-context";
|
|
8
9
|
|
|
9
10
|
const CalendarHeader = () => {
|
|
10
11
|
const i18n = useI18n();
|
|
11
12
|
const [status, dispatchStatus] = useStatus();
|
|
12
13
|
const theme = useTheme();
|
|
14
|
+
const [tz] = useTz();
|
|
13
15
|
|
|
14
16
|
const [monthState, setMonthState] = useState(() => calculateMonthNav(status.months));
|
|
15
17
|
|
|
16
18
|
const handleMonthNav = target => {
|
|
17
|
-
dispatchStatus({
|
|
19
|
+
dispatchStatus({
|
|
20
|
+
type: "SELECT_MONTH",
|
|
21
|
+
month: target,
|
|
22
|
+
tzid: tz.selectedTzid.tzid,
|
|
23
|
+
});
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
useEffect(() => {
|
|
@@ -33,7 +33,7 @@ const Confirm = ({ confirmButtonRef }) => {
|
|
|
33
33
|
<div id="confirm-slot-details">
|
|
34
34
|
<p className={theme.classBuilder("confirm-details--info")}>
|
|
35
35
|
{i18n.customFormatedTimeZone(
|
|
36
|
-
moment(status.selected.start, "YYYY-MM-
|
|
36
|
+
moment(status.selected.start, "YYYY-MM-DDTHH:mm:00Z"),
|
|
37
37
|
tz.selectedTzid.tzid,
|
|
38
38
|
"dddd, MMMM Do YYYY"
|
|
39
39
|
)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
+
import moment from "moment-timezone";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
getMonthObjectsFromQuery,
|
|
@@ -23,15 +24,18 @@ const DateTimePicker = ({ options }) => {
|
|
|
23
24
|
const [statusOptions] = useState(() => {
|
|
24
25
|
const query = !options.demo ? parseQuery(options.query) : queryForDateTimePicker;
|
|
25
26
|
const months = getMonthObjectsFromQuery(query, options.tzid);
|
|
26
|
-
const
|
|
27
|
-
|
|
27
|
+
const currentMonth = months.length ? months[0].month : moment().format("YYYY-MM");
|
|
28
|
+
|
|
29
|
+
const monthlyView = {
|
|
30
|
+
month: currentMonth,
|
|
28
31
|
days: parseTimeSlots({
|
|
29
32
|
slots: {},
|
|
30
|
-
month:
|
|
33
|
+
month: currentMonth,
|
|
31
34
|
tzid: options.tzid,
|
|
32
35
|
startDay: options.config.startDay,
|
|
33
36
|
}),
|
|
34
|
-
}
|
|
37
|
+
};
|
|
38
|
+
|
|
35
39
|
return {
|
|
36
40
|
auth: {
|
|
37
41
|
token: options.token,
|
|
@@ -47,15 +51,14 @@ const DateTimePicker = ({ options }) => {
|
|
|
47
51
|
mode: options.config.mode, // confirm (default) | no_confirm
|
|
48
52
|
query,
|
|
49
53
|
months,
|
|
50
|
-
|
|
54
|
+
monthlyView,
|
|
51
55
|
selected: false,
|
|
52
|
-
selectedDay: false,
|
|
53
56
|
startDay: options.config.startDay,
|
|
54
|
-
focusedDay: false,
|
|
55
57
|
focusedSlot: false,
|
|
56
|
-
availableDays:
|
|
58
|
+
availableDays: [],
|
|
57
59
|
slots: {},
|
|
58
60
|
slotFetchCount: 0,
|
|
61
|
+
slotInjectionPoint: undefined,
|
|
59
62
|
tzid: options.tzid,
|
|
60
63
|
populated: false,
|
|
61
64
|
};
|
|
@@ -19,42 +19,36 @@ const Wrapper = () => {
|
|
|
19
19
|
const [status, dispatchStatus] = useStatus();
|
|
20
20
|
const [tz] = useTz();
|
|
21
21
|
const theme = useTheme();
|
|
22
|
+
|
|
22
23
|
const confirmButtonRef = useRef();
|
|
23
24
|
|
|
24
25
|
useEffect(() => {
|
|
25
26
|
if (!status.query.query_periods) {
|
|
26
|
-
dispatchStatus({ type: "SET_DEFAULT_SLOTS", slots: {} });
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
37
|
-
.then(res => {
|
|
30
|
+
const fetchMonthSlots = query => {
|
|
31
|
+
return getSlots({
|
|
32
|
+
query,
|
|
33
|
+
auth: status.auth,
|
|
34
|
+
tzid: status.tzid,
|
|
35
|
+
}).then(res => {
|
|
38
36
|
dispatchStatus({
|
|
39
|
-
type: "
|
|
37
|
+
type: "SET_SLOTS",
|
|
40
38
|
slots: res,
|
|
41
39
|
tzid: tz.selectedTzid.tzid,
|
|
42
40
|
});
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const currentMonth = status.months.find(m => m.current);
|
|
45
|
+
|
|
46
|
+
// Get slots for current month
|
|
47
|
+
fetchMonthSlots(currentMonth.query)
|
|
48
|
+
.then(() => {
|
|
49
|
+
// Get slots for remianing months
|
|
43
50
|
const remainingMonths = status.months.filter(month => !month.current);
|
|
44
|
-
remainingMonths.forEach(month =>
|
|
45
|
-
getSlots({
|
|
46
|
-
query: month.query,
|
|
47
|
-
auth: status.auth,
|
|
48
|
-
tzid: status.tzid,
|
|
49
|
-
}).then(res => {
|
|
50
|
-
dispatchStatus({
|
|
51
|
-
type: "SET_ADDITIONAL_SLOTS",
|
|
52
|
-
month: month.month,
|
|
53
|
-
slots: res,
|
|
54
|
-
tzid: tz.selectedTzid.tzid,
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
});
|
|
51
|
+
remainingMonths.forEach(month => fetchMonthSlots(month.query));
|
|
58
52
|
})
|
|
59
53
|
.catch(error => {
|
|
60
54
|
dispatchStatus({ type: "ERROR_GETTING_SLOTS", error });
|
|
@@ -71,26 +65,43 @@ const Wrapper = () => {
|
|
|
71
65
|
return;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
// Set grid display if there are available slots
|
|
75
|
-
if (slotsCount
|
|
76
|
-
|
|
68
|
+
// Set grid display if there are available slots within the current month
|
|
69
|
+
if (slotsCount < 1) {
|
|
70
|
+
return;
|
|
77
71
|
}
|
|
78
|
-
|
|
72
|
+
|
|
73
|
+
const weeksInMonth = status.monthlyView.days;
|
|
74
|
+
const firstDay = moment(weeksInMonth[0][0].date, "YYYY-MM-DD");
|
|
75
|
+
const lastWeekInMonth = weeksInMonth[weeksInMonth.length - 1];
|
|
76
|
+
const lastDay = moment(lastWeekInMonth[lastWeekInMonth.length - 1].date, "YYYY-MM-DD");
|
|
77
|
+
|
|
78
|
+
const injectionPoint = status.slotInjectionPoint
|
|
79
|
+
? moment(status.slotInjectionPoint, "YYYY-MM-DD")
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
if (injectionPoint && !injectionPoint.isBetween(firstDay, lastDay)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
dispatchStatus({
|
|
87
|
+
type: "RECALCULATE_MONTH_VIEW",
|
|
88
|
+
tzid: tz.selectedTzid.tzid,
|
|
89
|
+
});
|
|
90
|
+
}, [status.slotFetchCount, status.slotInjectionPoint]);
|
|
79
91
|
|
|
80
92
|
useEffect(() => {
|
|
81
93
|
if (status.selectedDay) {
|
|
82
|
-
if (!status.populated) {
|
|
83
|
-
dispatchStatus({
|
|
84
|
-
type: "SELECT_DAY_INITIAL",
|
|
85
|
-
day: status.selectedDay,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
const currentMonthParts = status.months.find(month => month.current).month.split("-");
|
|
89
94
|
const selectedDayParts = status.selectedDay.split("-");
|
|
95
|
+
const currentMonthParts = status.monthlyView.month.split("-");
|
|
90
96
|
const inCurrentMonth = currentMonthParts[1] === selectedDayParts[1];
|
|
97
|
+
|
|
91
98
|
if (!inCurrentMonth) {
|
|
92
99
|
const newMonth = moment(status.selectedDay, "YYYY-MM-DD").format("YYYY-MM");
|
|
93
|
-
dispatchStatus({
|
|
100
|
+
dispatchStatus({
|
|
101
|
+
type: "SELECT_MONTH",
|
|
102
|
+
month: newMonth,
|
|
103
|
+
tzid: tz.selectedTzid.tzid,
|
|
104
|
+
});
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
}, [status.selectedDay]);
|
|
@@ -2,16 +2,16 @@ import React, { createContext, useContext, useReducer } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { statusReducer } from "./status-reducer";
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const StatusContext = createContext();
|
|
6
6
|
|
|
7
7
|
export const StatusProvider = ({ children, options }) => {
|
|
8
8
|
const [status, dispatchStatus] = useReducer(statusReducer, options);
|
|
9
9
|
return (
|
|
10
|
-
<
|
|
10
|
+
<StatusContext.Provider value={[status, dispatchStatus]}>{children}</StatusContext.Provider>
|
|
11
11
|
);
|
|
12
12
|
};
|
|
13
13
|
export const useStatus = () => {
|
|
14
|
-
const context = useContext(
|
|
14
|
+
const context = useContext(StatusContext);
|
|
15
15
|
if (context === undefined) {
|
|
16
16
|
throw new Error("useStatus must be used within a StatusProvider");
|
|
17
17
|
}
|
|
@@ -30,10 +30,12 @@ export const statusReducer = (state, action) => {
|
|
|
30
30
|
selected: false,
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
|
|
33
34
|
case "CONFIRM_SELECTION": {
|
|
34
35
|
sendSlotNotification(action.tzid);
|
|
35
36
|
return state;
|
|
36
37
|
}
|
|
38
|
+
|
|
37
39
|
case "ERROR_GETTING_SLOTS": {
|
|
38
40
|
const notification = {
|
|
39
41
|
notification: {
|
|
@@ -44,80 +46,73 @@ export const statusReducer = (state, action) => {
|
|
|
44
46
|
state.callback(notification);
|
|
45
47
|
return { ...state, error: action.error, columnView: "error" };
|
|
46
48
|
}
|
|
49
|
+
|
|
47
50
|
case "NO_SLOTS_FOUND": {
|
|
48
51
|
return { ...state, columnView: "no-slots" };
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
|
|
54
|
+
case "RECALCULATE_MONTH_VIEW": {
|
|
55
|
+
const monthlyView = {
|
|
56
|
+
month: state.monthlyView.month,
|
|
53
57
|
days: parseTimeSlots({
|
|
54
58
|
slots: state.slots,
|
|
55
|
-
month:
|
|
59
|
+
month: state.monthlyView.month,
|
|
56
60
|
tzid: action.tzid,
|
|
57
61
|
startDay: state.startDay,
|
|
58
62
|
}),
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
case "SET_INITIAL_SLOTS": {
|
|
63
|
-
const hasNewSlots = Object.keys(action.slots).length >= 1;
|
|
64
|
-
const slotsObject = hasNewSlots
|
|
65
|
-
? addSlotsToObject(state.slots, action.slots)
|
|
66
|
-
: state.slots;
|
|
67
|
-
const hasAnySlots = Object.keys(slotsObject).length >= 1;
|
|
68
|
-
const selectedDay = hasAnySlots
|
|
69
|
-
? getFirstAvailableDay(slotsObject, action.tzid)
|
|
70
|
-
: false;
|
|
71
|
-
const availableDays = hasAnySlots ? getAvailableDays(slotsObject, state.tzid) : false;
|
|
63
|
+
};
|
|
64
|
+
|
|
72
65
|
return {
|
|
73
66
|
...state,
|
|
74
|
-
|
|
75
|
-
focusedDay: selectedDay,
|
|
76
|
-
slots: slotsObject,
|
|
77
|
-
availableDays: availableDays,
|
|
78
|
-
slotFetchCount: state.slotFetchCount + 1,
|
|
67
|
+
monthlyView,
|
|
79
68
|
};
|
|
80
69
|
}
|
|
81
|
-
|
|
82
|
-
case "
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
70
|
+
|
|
71
|
+
case "SET_SLOTS": {
|
|
72
|
+
if (!action.slots.length > 0) {
|
|
73
|
+
return state;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const slotsObject = addSlotsToObject(state.slots, action.slots);
|
|
77
|
+
|
|
78
|
+
let daySlots = state.daySlots;
|
|
79
|
+
let columnView = state.columnView;
|
|
80
|
+
let focusedDay = state.focusedDay;
|
|
81
|
+
let selectedDay = state.selectedDay;
|
|
82
|
+
let availableDays = getAvailableDays(addSlotsToObject({}, action.slots), action.tzid);
|
|
83
|
+
|
|
84
|
+
const availableDaysSet = new Set([...state.availableDays, ...availableDays]);
|
|
85
|
+
availableDays = [...availableDaysSet].sort();
|
|
86
|
+
|
|
87
|
+
if (!state.selectedDay) {
|
|
88
|
+
columnView = "slots";
|
|
89
|
+
focusedDay = availableDays[0];
|
|
90
|
+
selectedDay = availableDays[0];
|
|
91
|
+
daySlots = getSlotsByDay(slotsObject, selectedDay, action.tzid);
|
|
92
|
+
}
|
|
93
|
+
|
|
92
94
|
return {
|
|
93
95
|
...state,
|
|
94
|
-
|
|
96
|
+
daySlots,
|
|
97
|
+
columnView,
|
|
98
|
+
focusedDay,
|
|
95
99
|
selectedDay,
|
|
96
|
-
|
|
100
|
+
availableDays,
|
|
101
|
+
|
|
102
|
+
slots: slotsObject,
|
|
97
103
|
slotFetchCount: state.slotFetchCount + 1,
|
|
104
|
+
slotInjectionPoint: getLocalDayFromUtc(action.slots[0].start, action.tzid),
|
|
98
105
|
};
|
|
99
106
|
}
|
|
107
|
+
|
|
100
108
|
case "SET_FOCUS": {
|
|
101
|
-
return { ...state, focusedDay: action.focusedDay };
|
|
102
|
-
}
|
|
103
|
-
case "SET_FOCUS_FOR_NEW_MONTH": {
|
|
104
|
-
const months = state.months.map(month => ({
|
|
105
|
-
...month,
|
|
106
|
-
current: month.month === action.month,
|
|
107
|
-
}));
|
|
108
|
-
return { ...state, months, focusedDay: action.focusedDay };
|
|
109
|
-
}
|
|
110
|
-
case "SELECT_DAY_INITIAL":
|
|
111
|
-
const initialDaySlots = getSlotsByDay(state.slots, action.day, action.tzid);
|
|
112
109
|
return {
|
|
113
110
|
...state,
|
|
114
|
-
|
|
115
|
-
focusedDay: action.day,
|
|
116
|
-
columnView: "slots",
|
|
117
|
-
daySlots: initialDaySlots,
|
|
118
|
-
populated: true,
|
|
111
|
+
focusedDay: action.focusedDay,
|
|
119
112
|
};
|
|
120
|
-
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case "SELECT_DAY": {
|
|
121
116
|
const daySlots = getSlotsByDay(state.slots, action.day, action.tzid);
|
|
122
117
|
return {
|
|
123
118
|
...state,
|
|
@@ -128,16 +123,33 @@ export const statusReducer = (state, action) => {
|
|
|
128
123
|
daySlots,
|
|
129
124
|
populated: true,
|
|
130
125
|
};
|
|
131
|
-
|
|
126
|
+
}
|
|
127
|
+
case "SELECT_MONTH": {
|
|
132
128
|
const months = state.months.map(month => ({
|
|
133
129
|
...month,
|
|
134
130
|
current: month.month === action.month,
|
|
135
131
|
}));
|
|
132
|
+
|
|
133
|
+
let focusedDay = action.focusedDay ?? false;
|
|
134
|
+
|
|
135
|
+
const monthlyView = {
|
|
136
|
+
month: action.month,
|
|
137
|
+
days: parseTimeSlots({
|
|
138
|
+
tzid: action.tzid,
|
|
139
|
+
slots: state.slots,
|
|
140
|
+
month: action.month,
|
|
141
|
+
startDay: state.startDay,
|
|
142
|
+
}),
|
|
143
|
+
};
|
|
144
|
+
|
|
136
145
|
return {
|
|
137
146
|
...state,
|
|
138
147
|
months,
|
|
139
|
-
focusedDay
|
|
148
|
+
focusedDay,
|
|
149
|
+
monthlyView,
|
|
140
150
|
};
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
case "SELECT_SLOT": {
|
|
142
154
|
state = {
|
|
143
155
|
...state,
|
|
@@ -151,24 +163,26 @@ export const statusReducer = (state, action) => {
|
|
|
151
163
|
}
|
|
152
164
|
return state;
|
|
153
165
|
}
|
|
166
|
+
|
|
154
167
|
case "SELECT_TZID": {
|
|
155
168
|
const availableDays = getAvailableDays(state.slots, action.tzid);
|
|
156
169
|
const daySlots = getSlotsByDay(state.slots, state.selectedDay, action.tzid);
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
|
|
171
|
+
const monthlyView = {
|
|
172
|
+
month: state.monthlyView.month,
|
|
159
173
|
days: parseTimeSlots({
|
|
160
174
|
slots: state.slots,
|
|
161
|
-
month:
|
|
175
|
+
month: state.monthlyView.month,
|
|
162
176
|
tzid: action.tzid,
|
|
163
177
|
startDay: state.startDay,
|
|
164
178
|
}),
|
|
165
|
-
}
|
|
179
|
+
};
|
|
166
180
|
|
|
167
181
|
state = {
|
|
168
182
|
...state,
|
|
169
183
|
availableDays,
|
|
170
184
|
daySlots,
|
|
171
|
-
|
|
185
|
+
monthlyView,
|
|
172
186
|
};
|
|
173
187
|
|
|
174
188
|
if (state.selected.start) {
|
package/src/js/helpers/utils.js
CHANGED
|
@@ -16,7 +16,13 @@ export const objectToArray = (object, keepKey = true) => {
|
|
|
16
16
|
export const truncateString = (str, length = 240) =>
|
|
17
17
|
str.length <= length - 2 ? str : `${str.slice(0, length)}...`;
|
|
18
18
|
|
|
19
|
-
export const uniqueItems = array =>
|
|
19
|
+
export const uniqueItems = array => {
|
|
20
|
+
if (!Array.isArray(array)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [...new Set(array)];
|
|
25
|
+
};
|
|
20
26
|
|
|
21
27
|
export const objectIsEmpty = obj => {
|
|
22
28
|
for (let key in obj) {
|