cronofy-elements
Advanced tools
Comparing version 1.16.1 to 1.17.0
@@ -15,2 +15,5 @@ # Cronofy UI Elements | ||
You will need to ensure `/src/js/env.js` is updated with your local development settings. See `/src/js/env.example.js` for an example of this. | ||
To start development, run: | ||
@@ -20,2 +23,6 @@ | ||
If you're running SSL on your local dev server then you will need to tell Node to ignore TLS verifications. | ||
NODE_TLS_REJECT_UNAUTHORIZED=0 make dev | ||
This will watch the source files and rebuild whenever you save changes. It will also host the demo app (`demo/index.ejs`) on port `8080` | ||
@@ -33,3 +40,3 @@ | ||
To bump a patch version (e.g. from `0.0.1` to `0.0.2`) and make a production build, run: | ||
bump_patch | ||
@@ -36,0 +43,0 @@ |
{ | ||
"name": "cronofy-elements", | ||
"version": "1.16.1", | ||
"version": "1.17.0", | ||
"description": "Fast track scheduling with Cronofy's embeddable UI Elements", | ||
@@ -5,0 +5,0 @@ "main": "build/npm/CronofyElements.js", |
@@ -9,4 +9,3 @@ import React, { useState, useEffect } from "react"; | ||
getSlotsForWeek, | ||
calculateSlotHeight, | ||
parseInterval | ||
calculateSlotHeight | ||
} from "../../helpers/slots"; | ||
@@ -16,5 +15,7 @@ import { objectIsEmpty, uniqueItems } from "../../helpers/utils"; | ||
addKeysToSlots, | ||
checkSelectedState, | ||
cropQuery, | ||
generateWeeklySlots, | ||
generateStaticWeeks | ||
generateStaticWeeks, | ||
removeManagedAvailability | ||
} from "../../helpers/utils.AvailabilityViewer"; | ||
@@ -57,3 +58,3 @@ import { parseStyleOptions } from "../../helpers/theming"; | ||
: 60, | ||
interval: parseInterval(options.config.interval) | ||
interval: options.config.interval | ||
}), | ||
@@ -84,3 +85,2 @@ sizes: { | ||
: false, | ||
multiSelect: options.config.mode === "multi_select" || false, | ||
preloading: false, | ||
@@ -114,3 +114,3 @@ notificationCallback: options.callback | ||
end: options.config.end_time ? options.config.end_time : "17:30", | ||
interval: parseInterval(options.config.interval) | ||
interval: options.config.interval | ||
}); | ||
@@ -262,3 +262,3 @@ | ||
if (!error) { | ||
const query = { | ||
let query = { | ||
...options.query, | ||
@@ -269,2 +269,6 @@ response_format: "overlapping_slots", | ||
if (status.mode === "free_select") { | ||
query = removeManagedAvailability(query); | ||
} | ||
const croppedQuery = cropQuery(query, { | ||
@@ -336,3 +340,9 @@ currentWeek: weeks.current, | ||
}); | ||
setSlotData(slots.slots); | ||
let updatedSlots = slots.slots; | ||
if (status.mode === "free_select") { | ||
// If we're overriding the slots state (with `setSlotData`), then | ||
// We'll need to make sure we persit any "selected" slots. | ||
updatedSlots = checkSelectedState(slots.slots, slotData); | ||
} | ||
setSlotData(updatedSlots); | ||
} | ||
@@ -339,0 +349,0 @@ }, [limits, rawData]); |
@@ -9,3 +9,3 @@ import React, { useContext } from "react"; | ||
const DayColumn = ({ day, columnCount }) => { | ||
const DayColumn = ({ day, columnCount, toggleMultiple = false }) => { | ||
const [theme, setTheme] = useContext(ThemeContext); | ||
@@ -28,2 +28,3 @@ return ( | ||
stylePrefix={theme.prefix} | ||
toggleMultiple={toggleMultiple} | ||
/> | ||
@@ -30,0 +31,0 @@ ) : ( |
@@ -7,3 +7,3 @@ import React, { useContext } from "react"; | ||
const LegendItem = ({ available }) => { | ||
const LegendItem = ({ status }) => { | ||
const [theme, setTheme] = useContext(ThemeContext); | ||
@@ -27,12 +27,8 @@ const i18n = useContext(I18nContext); | ||
margin-right: 10px; | ||
background: ${available | ||
? theme.colors.available | ||
: theme.colors.unavailable}; | ||
background: ${theme.colors[status]}; | ||
`} | ||
className={`${theme.prefix}__example ${ | ||
theme.prefix | ||
}__example--${available ? "available" : "unavailable"}`} | ||
className={`${theme.prefix}__example ${theme.prefix}__example--${status}`} | ||
/> | ||
<span className={`${theme.prefix}__legend-label`}> | ||
{available ? i18n.t("available") : i18n.t("unavailable")} | ||
{i18n.t(status)} | ||
</span> | ||
@@ -57,4 +53,5 @@ </div> | ||
> | ||
<LegendItem available={true} /> | ||
<LegendItem available={false} /> | ||
<LegendItem status={"available"} /> | ||
<LegendItem status={"unavailable"} /> | ||
{status.mode === "free_select" && <LegendItem status={"booked"} />} | ||
{status.customtzid ? <TimeZoneDisplay /> : null} | ||
@@ -61,0 +58,0 @@ </div> |
@@ -5,6 +5,8 @@ import React, { useContext } from "react"; | ||
import Slot from "./Slot"; | ||
import SlotFreeSelect from "./SlotFreeSelect"; | ||
import { ThemeContext } from "./AvailabilityViewer"; | ||
import { StatusContext, ThemeContext } from "./AvailabilityViewer"; | ||
const Overlay = ({ day, columnCount }) => { | ||
const Overlay = ({ day, columnCount, toggleMultiple }) => { | ||
const [status, setStatus] = useContext(StatusContext); | ||
const [theme, setTheme] = useContext(ThemeContext); | ||
@@ -28,9 +30,21 @@ | ||
> | ||
<Slot | ||
slot={slot} | ||
day={day} | ||
columnCount={columnCount} | ||
topSlot={topSlot} | ||
blockBelow={blockBelow} | ||
/> | ||
{status.mode !== "free_select" && ( | ||
<Slot | ||
slot={slot} | ||
day={day} | ||
columnCount={columnCount} | ||
topSlot={topSlot} | ||
blockBelow={blockBelow} | ||
/> | ||
)} | ||
{status.mode === "free_select" && ( | ||
<SlotFreeSelect | ||
slot={slot} | ||
day={day} | ||
columnCount={columnCount} | ||
topSlot={topSlot} | ||
blockBelow={blockBelow} | ||
toggleMultiple={toggleMultiple} | ||
/> | ||
)} | ||
</div> | ||
@@ -37,0 +51,0 @@ ); |
@@ -1,5 +0,4 @@ | ||
import React, { useContext } from "react"; | ||
import moment from "moment-timezone"; | ||
import React, { useContext, useState } from "react"; | ||
import { css, jsx } from "@emotion/core"; | ||
import { darken, transparentize } from "polished"; | ||
import { transparentize } from "polished"; | ||
@@ -9,3 +8,6 @@ import Tooltip from "./Tooltip"; | ||
import { filterObject } from "../../helpers/utils"; | ||
import { createExportableSelection } from "../../helpers/utils.AvailabilityViewer"; | ||
import { | ||
createExportableSelection, | ||
calculateDisplayText | ||
} from "../../helpers/utils.AvailabilityViewer"; | ||
import { buttonReset } from "../../styles/utils"; | ||
@@ -23,13 +25,6 @@ | ||
const startObject = moment.tz( | ||
slot.start, | ||
"YYYY-MM-DDTTHH:mm:00Z", | ||
status.tzid | ||
const [displayText, setDisplayText] = useState(() => | ||
calculateDisplayText(i18n, slot, status) | ||
); | ||
const endObject = moment.tz(slot.end, "YYYY-MM-DDTTHH:mm:00Z", status.tzid); | ||
const startDisplay = i18n.f(startObject, "LT").replace(" ", ""); | ||
const endDisplay = i18n.f(endObject, "LT").replace(" ", ""); | ||
const timeDisplay = `${startDisplay} - ${endDisplay}`; | ||
const removeFromSelectionList = () => { | ||
@@ -39,3 +34,3 @@ const filteredSlots = filterObject(slot.start, selectedSlots); | ||
const exportableSelection = createExportableSelection(filteredSlots); | ||
const callbackContent = { | ||
let callbackContent = { | ||
notification: { | ||
@@ -58,4 +53,10 @@ type: "slot_removed", | ||
background: ${theme.colors.availableActive}; | ||
border: 1px solid ${darken(0.05, theme.colors.availableActive)}; | ||
box-shadow: ${transparentize(0.9, theme.colors.black)} 0 5px 5px -2px; | ||
${ | ||
status.mode !== "free_select" | ||
? `box-shadow: ${transparentize( | ||
0.9, | ||
theme.colors.black | ||
)} 0 5px 5px -2px;` | ||
: "" | ||
} | ||
&:hover .${theme.prefix}__slot-tooltip { | ||
@@ -68,5 +69,7 @@ opacity: 1; | ||
> | ||
{!status.multiSelect && status.mode !== "no_confirm" ? ( | ||
{status.mode !== "multi_select" && | ||
status.mode !== "no_confirm" && | ||
status.mode !== "free_select" ? ( | ||
<Tooltip | ||
time={timeDisplay} | ||
time={displayText.time} | ||
top={slotDetails.topSlot} | ||
@@ -81,3 +84,5 @@ hover={true} | ||
${buttonReset}; | ||
background: ${theme.colors.availableActive}; | ||
background: ${status.mode === "free_select" | ||
? theme.colors.available | ||
: theme.colors.availableActive}; | ||
width: 100%; | ||
@@ -87,6 +92,10 @@ height: 100%; | ||
color: ${theme.colors.white}; | ||
&:focus { | ||
outline: none; | ||
} | ||
`} | ||
className={`${theme.prefix}__selected-slot__button`} | ||
onClick={() => | ||
status.multiSelect | ||
status.mode === "multi_select" || | ||
status.mode === "free_select" | ||
? removeFromSelectionList() | ||
@@ -101,5 +110,3 @@ : status.notificationCallback({ | ||
> | ||
{status.mode === "multi_select" || status.mode === "no_confirm" | ||
? startDisplay | ||
: i18n.t("confirm")} | ||
{displayText.text} | ||
</button> | ||
@@ -106,0 +113,0 @@ </div> |
@@ -32,6 +32,6 @@ import React, { useState, useContext, useEffect } from "react"; | ||
const defaultTimeDisplayStart = i18n | ||
.f(moment(slot.start, "YYYY-MM-DDTTHH:mm:00Z").tz(status.tzid), "LT") | ||
.f(moment(slot.start, "YYYY-MM-DDTHH:mm:00Z").tz(status.tzid), "LT") | ||
.replace(" ", ""); | ||
const defaultTimeDisplayEnd = i18n | ||
.f(moment(slot.end, "YYYY-MM-DDTTHH:mm:00Z").tz(status.tzid), "LT") | ||
.f(moment(slot.end, "YYYY-MM-DDTHH:mm:00Z").tz(status.tzid), "LT") | ||
.replace(" ", ""); | ||
@@ -94,7 +94,7 @@ | ||
slotData.slot.start, | ||
"YYYY-MM-DDTTHH:mm:00Z" | ||
"YYYY-MM-DDTHH:mm:00Z" | ||
).tz(status.tzid); | ||
const timeDisplayStart = i18n.f(timeStart, "LT").replace(" ", ""); | ||
const timeEnd = moment(slotData.slot.end, "YYYY-MM-DDTTHH:mm:00Z").tz( | ||
const timeEnd = moment(slotData.slot.end, "YYYY-MM-DDTHH:mm:00Z").tz( | ||
status.tzid | ||
@@ -164,3 +164,3 @@ ); | ||
// If we're in multi_select mode, | ||
if (status.multiSelect) { | ||
if (status.mode === "multi_select") { | ||
const exportableSelection = createExportableSelection( | ||
@@ -167,0 +167,0 @@ newSlots |
import React, { useContext } from "react"; | ||
import { css, jsx } from "@emotion/core"; | ||
import { ThemeContext } from "./AvailabilityViewer"; | ||
import { ThemeContext, StatusContext } from "./AvailabilityViewer"; | ||
const Slots = ({ day }) => { | ||
const [theme, setTheme] = useContext(ThemeContext); | ||
const slotsMarkup = day.slots.map((slot, i) => ( | ||
<div | ||
css={css` | ||
display: block; | ||
height: ${theme.slotHeightCalc.height}px; | ||
position: relative; | ||
const [status, setStatus] = useContext(StatusContext); | ||
${slot.visiblyAvailable | ||
? `background: ${theme.colors.available};` | ||
: ""} | ||
`} | ||
className={`${theme.prefix}__slot-background ${ | ||
theme.prefix | ||
}__slot-background--${ | ||
slot.visiblyAvailable ? "available" : "unavailable" | ||
}`} | ||
key={i} | ||
/> | ||
)); | ||
const slotsMarkup = day.slots.map((slot, i) => { | ||
let diagonal = Math.sqrt(2 * Math.pow(theme.slotHeightCalc.height, 2)); | ||
let colour = | ||
status.mode !== "free_select" && slot.visiblyAvailable | ||
? theme.colors.available | ||
: ""; | ||
if (status.mode === "free_select" && slot.selected) { | ||
colour = theme.colors.available; | ||
} | ||
if (status.mode === "free_select" && !slot.visiblyAvailable) { | ||
colour = theme.colors.booked; | ||
} | ||
return ( | ||
<div | ||
css={css` | ||
display: block; | ||
height: ${theme.slotHeightCalc.height}px; | ||
position: relative; | ||
background: ${colour}; | ||
${status.mode === "free_select" && | ||
!slot.visiblyAvailable && | ||
slot.selected | ||
? ` | ||
background: none; | ||
background-size: ${theme.slotHeightCalc.height}px ${ | ||
theme.slotHeightCalc.height | ||
}px; | ||
background-image: repeating-linear-gradient( | ||
to bottom right, | ||
${theme.colors.available}, | ||
${theme.colors.available} ${diagonal / 4}px, | ||
${theme.colors.booked} ${diagonal / 4}px, | ||
${theme.colors.booked} ${diagonal / 2}px | ||
);` | ||
: ``} | ||
`} | ||
className={`${theme.prefix}__slot-background ${ | ||
theme.prefix | ||
}__slot-background--${ | ||
slot.visiblyAvailable ? "available" : "unavailable" | ||
}`} | ||
key={i} | ||
/> | ||
); | ||
}); | ||
@@ -28,0 +56,0 @@ return slotsMarkup; |
@@ -7,2 +7,3 @@ import React, { useContext, useState, useEffect } from "react"; | ||
import DayColumnDisplay from "./DayColumnDisplay"; | ||
import DayColumnWrapper from "./DayColumnWrapper"; | ||
import Error from "../generic/Error"; | ||
@@ -25,3 +26,7 @@ import HoverSlot from "./HoverSlot"; | ||
} from "./AvailabilityViewer"; | ||
import { checkBlockedSlots } from "../../helpers/utils.AvailabilityViewer"; | ||
import { | ||
buildPeriodsFromSlots, | ||
checkBlockedSlots, | ||
getSelectedSlots | ||
} from "../../helpers/utils.AvailabilityViewer"; | ||
import { objectIsEmpty } from "../../helpers/utils"; | ||
@@ -31,24 +36,4 @@ | ||
export const SelectionContext = React.createContext(); | ||
export const DragContext = React.createContext(); | ||
const DayColumnWrapper = ({ children, layer = 1 }) => { | ||
const [theme, setTheme] = useContext(ThemeContext); | ||
return ( | ||
<div | ||
css={css` | ||
display: flex; | ||
justify-content: stretch; | ||
position: absolute; | ||
z-index: ${layer}; | ||
top: 0; | ||
left: 0; | ||
width: 100%; | ||
height: 100%; | ||
`} | ||
className={`${theme.prefix}__grid-columns`} | ||
> | ||
{children} | ||
</div> | ||
); | ||
}; | ||
const Week = () => { | ||
@@ -62,2 +47,9 @@ const [limits, setLimits] = useContext(LimitsContext); | ||
const [drag, setDrag] = useState({ | ||
mouseDown: false, | ||
toggleState: "available", | ||
dragged: [], | ||
dragStart: false | ||
}); | ||
const [hover, setHover] = useState({ | ||
@@ -82,2 +74,37 @@ times: false, | ||
const toggleMultiple = (IDs, toggleStatus) => { | ||
const updatedSlotData = slotData.map(day => ({ | ||
...day, | ||
slots: day.slots.map(slot => | ||
IDs.includes(slot.start) | ||
? { ...slot, selected: toggleStatus } | ||
: slot | ||
) | ||
})); | ||
const selectedSlots = getSelectedSlots(updatedSlotData); | ||
const queryPeriods = buildPeriodsFromSlots(selectedSlots); | ||
const callbackContent = { | ||
notification: { | ||
type: "query_periods_edited", | ||
query_periods: queryPeriods | ||
} | ||
}; | ||
status.notificationCallback(callbackContent); | ||
setSlotData(updatedSlotData); | ||
}; | ||
const handleMouseOut = () => { | ||
if (drag.mouseDown) { | ||
toggleMultiple(drag.dragged, drag.toggleState); | ||
setDrag({ | ||
mouseDown: false, | ||
toggleState: true, | ||
dragged: [], | ||
dragStart: false | ||
}); | ||
} | ||
}; | ||
useEffect(() => { | ||
@@ -99,3 +126,3 @@ if ( | ||
if (objectIsEmpty(selectedSlots)) { | ||
if (objectIsEmpty(selectedSlots) && status.mode !== "free_select") { | ||
hoverElement.current.style.display = "none"; | ||
@@ -107,7 +134,11 @@ hoverElementTooltip.current.style.display = "none"; | ||
useEffect(() => { | ||
hoverElement.current.style.display = "flex"; | ||
hoverElement.current.style.opacity = hover.visible ? "1" : "0"; | ||
if (status.mode !== "free_select") { | ||
hoverElement.current.style.display = "flex"; | ||
hoverElement.current.style.opacity = hover.visible ? "1" : "0"; | ||
hoverElementTooltip.current.style.display = "flex"; | ||
hoverElementTooltip.current.style.opacity = hover.visible ? "1" : "0"; | ||
hoverElementTooltip.current.style.display = "flex"; | ||
hoverElementTooltip.current.style.opacity = hover.visible | ||
? "1" | ||
: "0"; | ||
} | ||
}, [hover]); | ||
@@ -118,2 +149,4 @@ | ||
? hover.times | ||
: status.mode === "free_select" | ||
? i18n.t("booked") | ||
: i18n.t("unavailable"); | ||
@@ -135,3 +168,3 @@ | ||
useEffect(() => { | ||
if (!status.multiSelect) { | ||
if (status.mode !== "multi_select") { | ||
setSelectedSlots({}); | ||
@@ -170,2 +203,3 @@ } | ||
className={`${theme.prefix}__grid`} | ||
onMouseLeave={handleMouseOut} | ||
> | ||
@@ -180,54 +214,18 @@ {!status.loading ? ( | ||
<TimeLines /> | ||
<div | ||
ref={hoverElement} | ||
css={css` | ||
position: absolute; | ||
z-index: 2; | ||
display: none; | ||
width: ${theme.sizes.columnWidth}px; | ||
height: ${hoverHeight}px; | ||
top: ${hover.position.y}px; | ||
left: ${hover.position.x * | ||
theme.sizes.columnWidth}px; | ||
`} | ||
className={`${theme.prefix}__hover-positioner ${theme.prefix}__hover-positioner--main`} | ||
> | ||
<HoverSlot | ||
available={hover.available} | ||
times={tooltipText} | ||
topSlot={hover.topSlot} | ||
/> | ||
</div> | ||
<DayColumnWrapper layer={4}> | ||
{slots.map((day, columnCount) => ( | ||
<DayColumn | ||
key={day.day} | ||
columnCount={columnCount} | ||
day={day} | ||
/> | ||
))} | ||
</DayColumnWrapper> | ||
{!objectIsEmpty(selectedSlots) && | ||
status.mode === "confirm" ? ( | ||
<SelectionMask /> | ||
) : null} | ||
<SelectedSlots bounds={[slots[0].day, slots[6].day]} /> | ||
<div | ||
ref={hoverElementTooltip} | ||
css={css` | ||
position: absolute; | ||
z-index: 5; | ||
display: none; | ||
pointer-events: none; | ||
user-select: none; | ||
top: ${hover.position.y}px; | ||
left: ${hover.position.x * | ||
theme.sizes.columnWidth}px; | ||
width: ${theme.sizes.columnWidth}px; | ||
height: ${hoverHeight}px; | ||
`} | ||
className={`${theme.prefix}__hover-positioner ${theme.prefix}__hover-positioner--tooltip`} | ||
> | ||
{!hover.hideTooltip ? ( | ||
<HoverTooltip | ||
{status.mode !== "free_select" && ( | ||
<div | ||
ref={hoverElement} | ||
css={css` | ||
position: absolute; | ||
z-index: 2; | ||
display: none; | ||
width: ${theme.sizes.columnWidth}px; | ||
height: ${hoverHeight}px; | ||
top: ${hover.position.y}px; | ||
left: ${hover.position.x * | ||
theme.sizes.columnWidth}px; | ||
`} | ||
className={`${theme.prefix}__hover-positioner ${theme.prefix}__hover-positioner--main`} | ||
> | ||
<HoverSlot | ||
available={hover.available} | ||
@@ -237,4 +235,54 @@ times={tooltipText} | ||
/> | ||
) : null} | ||
</div> | ||
</div> | ||
)} | ||
<DayColumnWrapper layer={4}> | ||
<DragContext.Provider value={[drag, setDrag]}> | ||
{slots.map((day, columnCount) => ( | ||
<DayColumn | ||
key={day.day} | ||
columnCount={columnCount} | ||
day={day} | ||
toggleMultiple={toggleMultiple} | ||
/> | ||
))} | ||
</DragContext.Provider> | ||
</DayColumnWrapper> | ||
{status.mode !== "free_select" && ( | ||
<React.Fragment> | ||
{!objectIsEmpty(selectedSlots) && | ||
status.mode === "confirm" ? ( | ||
<SelectionMask /> | ||
) : null} | ||
<SelectedSlots | ||
bounds={[slots[0].day, slots[6].day]} | ||
/> | ||
<div | ||
ref={hoverElementTooltip} | ||
css={css` | ||
position: absolute; | ||
z-index: 5; | ||
display: none; | ||
pointer-events: none; | ||
user-select: none; | ||
top: ${hover.position.y}px; | ||
left: ${hover.position.x * | ||
theme.sizes.columnWidth}px; | ||
width: ${theme.sizes.columnWidth}px; | ||
height: ${hoverHeight}px; | ||
`} | ||
className={`${theme.prefix}__hover-positioner ${theme.prefix}__hover-positioner--tooltip`} | ||
> | ||
{!hover.hideTooltip ? ( | ||
<HoverTooltip | ||
available={hover.available} | ||
times={tooltipText} | ||
topSlot={hover.topSlot} | ||
/> | ||
) : null} | ||
</div> | ||
</React.Fragment> | ||
)} | ||
{status.loading ? <Loading /> : null} | ||
@@ -241,0 +289,0 @@ {status.preloading ? <Preloading /> : null} |
@@ -80,3 +80,3 @@ import React, { useContext } from "react"; | ||
{i18n.f( | ||
moment(slots.day, "YYYY-MM-DDTThh:mm:00Z").tz( | ||
moment(slots.day, "YYYY-MM-DDThh:mm:00Z").tz( | ||
status.tzid | ||
@@ -83,0 +83,0 @@ ), |
// import React from "react"; | ||
import ReactDOM from "react-dom"; | ||
import merge from "deepmerge"; | ||
@@ -64,2 +65,3 @@ import Error from "../components/generic/Error"; | ||
const reload = newRawOptions => { | ||
console.log("newRawOptions", newRawOptions); | ||
// Increment the key - this will ensure the React | ||
@@ -75,4 +77,4 @@ // component re-renders when the `options` change. | ||
return { | ||
update: newOptions => reload({ ...originalOptions, ...newOptions }) | ||
update: newOptions => reload(merge(originalOptions, newOptions)) | ||
}; | ||
}; |
import moment from "moment-timezone"; | ||
import { | ||
parseInterval, | ||
parseToken, | ||
@@ -35,5 +36,5 @@ parseTarget, | ||
const target = parseTarget(options, "availability-viewer", log); | ||
const query = parseQuery(options, "availability-viewer", log); | ||
if (!query) return false; | ||
const interval = parseInterval(options.config.interval); | ||
if (typeof options.extras !== "undefined") { | ||
@@ -75,2 +76,27 @@ log.warn( | ||
const query = parseQuery(options, "availability-viewer", log); | ||
if (!query) return false; | ||
if (config.mode === "free_select") { | ||
query.required_duration.minutes = interval; | ||
const now = moment.utc().endOf("hour"); | ||
// Adding 34 days minus 1 hour to ensure that the resulting | ||
// query stays within the requiered bounds defined in | ||
// https://docs.cronofy.com/developers/api/scheduling/availability/#param-participants.members.available_periods.end | ||
query.query_periods = [ | ||
{ | ||
start: now.format("YYYY-MM-DDTHH:mm[:00Z]"), | ||
end: now | ||
.clone() | ||
.add(35, "days") | ||
.subtract(1, "hours") | ||
.format("YYYY-MM-DDTHH:mm[:00Z]") | ||
} | ||
]; | ||
config.week_start_day = now | ||
.tz(options.tzid) | ||
.format("dddd") | ||
.toLowerCase(); | ||
} | ||
const startDay = | ||
@@ -103,3 +129,11 @@ typeof config.week_start_day === "undefined" || | ||
config = { ...config, startDay, mode, boundsControl, slot_selection, logs }; | ||
config = { | ||
...config, | ||
interval, | ||
startDay, | ||
mode, | ||
boundsControl, | ||
slot_selection, | ||
logs | ||
}; | ||
@@ -106,0 +140,0 @@ const domains = parseConnectionDomains( |
@@ -125,1 +125,14 @@ import merge from "deepmerge"; | ||
}; | ||
export const parseInterval = option => { | ||
// Make sure we have an interval to start with... | ||
let interval = option ? option : 15; | ||
// Round it to the nearest 15 mins | ||
interval = Math.ceil(interval / 15) * 15; | ||
// If it's 45, bump it up to 60 [^1] | ||
interval = interval === 45 ? 60 : interval; | ||
// If it's over 60, round down [^1] | ||
interval = interval > 60 ? 60 : interval; | ||
// [:1] We currently only support the following intervals: 15 | 30 | 60 | ||
return interval; | ||
}; |
@@ -318,15 +318,2 @@ const moment = require("moment-timezone"); | ||
export const parseInterval = option => { | ||
// Make sure we have an interval to start with... | ||
let interval = option ? option : 15; | ||
// Round it to the nearest 15 mins | ||
interval = Math.ceil(interval / 15) * 15; | ||
// If it's 45, bump it up to 60 [^1] | ||
interval = interval === 45 ? 60 : interval; | ||
// If it's over 60, round down [^1] | ||
interval = interval > 60 ? 60 : interval; | ||
// [:1] We currently only support the following intervals: 15 | 30 | 60 | ||
return interval; | ||
}; | ||
export const filterOverflowingSlots = (slots, overflow) => | ||
@@ -333,0 +320,0 @@ slots.filter((slot, i) => { |
@@ -190,3 +190,2 @@ import moment from "moment-timezone"; | ||
export const connectSlots = (start_id, current_id) => { | ||
// console.log("start_id, current_id", start_id, current_id); | ||
const start_indexes = start_id.split("_"); | ||
@@ -193,0 +192,0 @@ const end___indexes = current_id.split("_"); |
@@ -28,7 +28,6 @@ import { darken, lighten } from "polished"; | ||
primaryLight: "#B8E8F2", | ||
// primaryDark: "#B8E8F2", | ||
secondary: "#CB359B", | ||
// secondaryLight: "#B8E8F2", | ||
// secondaryDark: "#B8E8F2", | ||
background: "#F2F2F2", | ||
booked: "#E3E3E3", | ||
bookedSelected: "#DBECC9", | ||
available: "#E2FAC8", | ||
@@ -35,0 +34,0 @@ availableHover: "#C0E992", |
@@ -189,2 +189,3 @@ import moment from "moment-timezone"; | ||
blocked: false, | ||
selected: false, | ||
target: false, | ||
@@ -488,1 +489,190 @@ targetOffset: 0, | ||
}; | ||
export const removeManagedAvailability = query => ({ | ||
...query, | ||
participants: query.participants.map(parts => ({ | ||
...parts, | ||
members: parts.members.map(member => { | ||
delete member.availability_rule_ids; | ||
delete member.managed_availability; | ||
return member; | ||
}) | ||
})) | ||
}); | ||
export const calculateDisplayText = (i18n, slot, status) => { | ||
const startObject = moment.tz( | ||
slot.start, | ||
"YYYY-MM-DDTHH:mm:00Z", | ||
status.tzid | ||
); | ||
const endObject = moment.tz(slot.end, "YYYY-MM-DDTHH:mm:00Z", status.tzid); | ||
const startTime = i18n.f(startObject, "LT").replace(" ", ""); | ||
const endTime = i18n.f(endObject, "LT").replace(" ", ""); | ||
let text; | ||
switch (status.mode) { | ||
case "multi_select": | ||
case "no_confirm": | ||
text = `${startTime} - ${endTime}`; | ||
break; | ||
case "free_select": | ||
text = ""; | ||
break; | ||
default: | ||
text = i18n.t("confirm"); | ||
} | ||
return { | ||
time: `${startTime} - ${endTime}`, | ||
text | ||
}; | ||
}; | ||
export const buildPeriodsFromSlots = slots => { | ||
const sortedSlots = slots.sort((a, b) => { | ||
if (a.start < b.start) { | ||
return -1; | ||
} | ||
if (a.start > b.start) { | ||
return 1; | ||
} | ||
return 0; | ||
}); | ||
const periods = sortedSlots.reduce((acc, curr, i) => { | ||
const prev = slots[i - 1]; | ||
if (!prev) { | ||
// This is the last slot, so just add it to the array | ||
return [...acc, curr]; | ||
} | ||
if (curr.start === prev.end) { | ||
// This slot abuts the one that came before, so | ||
// let's remove the previous one and combine it with | ||
// the current one. | ||
const lastItem = acc.pop(); // Note: we're relying on pop() mutating acc here | ||
const combo = { | ||
start: lastItem.start, | ||
end: curr.end | ||
}; | ||
return [...acc, combo]; | ||
} | ||
// The slots don't abut, so just add this one | ||
return [...acc, curr]; | ||
}, []); | ||
return periods; | ||
}; | ||
export const addSlotStarts = (start, interval, count) => { | ||
const times = []; | ||
for (let i = 0; i <= count; i++) { | ||
const time = start.clone().add(i * interval, "minutes"); | ||
times.push(time.format("YYYY-MM-DDTHH:mm[:00Z]")); | ||
} | ||
return times; | ||
}; | ||
// Given a start and end slot-time, caluculate the start-times | ||
// of all slots that fall between those times | ||
export const tweenSlots = ({ start, end, tzid, interval }) => { | ||
const first = start < end ? start : end; | ||
const last = start > end ? start : end; | ||
const startTime = moment.utc(first, "YYYY-MM-DDTHH:mm:00Z"); | ||
const endTime = moment.utc(last, "YYYY-MM-DDTHH:mm:00Z"); | ||
const diff = endTime.diff(startTime, "minutes"); | ||
const firstDate = startTime.clone().tz(tzid); | ||
const lastDate = endTime.clone().tz(tzid); | ||
if (firstDate.format("YYYY-MM-DD") !== lastDate.format("YYYY-MM-DD")) { | ||
// The selection spans mulitple days | ||
const localFirstTime = startTime.clone().tz(tzid).format("HH:mm"); | ||
const localLastTime = endTime.clone().tz(tzid).format("HH:mm"); | ||
const sortedLocalFirstTime = | ||
localFirstTime < localLastTime ? localFirstTime : localLastTime; | ||
const sortedLocalLastTime = | ||
localFirstTime > localLastTime ? localFirstTime : localLastTime; | ||
const timeDiff = moment(sortedLocalLastTime, "HH:mm").diff( | ||
moment(sortedLocalFirstTime, "HH:mm"), | ||
"minutes" | ||
); | ||
const dayTweenCount = timeDiff / interval; | ||
const dayDiff = lastDate | ||
.clone() | ||
.endOf("day") | ||
.diff(firstDate.clone().startOf("day"), "hours"); | ||
const dayCount = Math.floor(dayDiff / 24); | ||
const dates = [firstDate.format("YYYY-MM-DD")]; | ||
for (let i = 1; i < dayCount; i++) { | ||
const tweenDay = firstDate.clone().add(i, "days"); | ||
dates.push(tweenDay.format("YYYY-MM-DD")); | ||
} | ||
dates.push(lastDate.format("YYYY-MM-DD")); | ||
const daysTweens = dates.map(day => { | ||
const dayStart = moment | ||
.tz(`${day} ${sortedLocalFirstTime}`, "YYYY-MM-DD HH:mm", tzid) | ||
.utc(); | ||
const dayTweens = addSlotStarts(dayStart, interval, dayTweenCount); | ||
return dayTweens; | ||
}); | ||
const flattenedTweens = [].concat.apply([], daysTweens); | ||
return flattenedTweens; | ||
} | ||
const tweenCount = diff / interval; | ||
const tweens = addSlotStarts(startTime, interval, tweenCount); | ||
return tweens; | ||
}; | ||
export const calculateTimeString = (slot, i18n, tzid) => { | ||
const startTimeString = i18n | ||
.f(moment(slot.start, "YYYY-MM-DDTHH:mm:00Z").tz(tzid), "LT") | ||
.replace(" ", ""); | ||
const endTimeString = i18n | ||
.f(moment(slot.end, "YYYY-MM-DDTHH:mm:00Z").tz(tzid), "LT") | ||
.replace(" ", ""); | ||
const timeString = `${startTimeString} - ${endTimeString}`; | ||
return timeString; | ||
}; | ||
export const getSelectedSlots = slots => { | ||
const flattenedSlots = [].concat.apply( | ||
[], | ||
slots.map(day => day.slots) | ||
); | ||
const selectedSlots = flattenedSlots.filter(slot => slot.selected); | ||
return selectedSlots; | ||
}; | ||
export const checkSelectedState = (newSlots, oldSlots) => { | ||
// Slots are grouped by day. | ||
const mergedSlots = newSlots.map(day => { | ||
const match = oldSlots.find(d => d.day === day.day); | ||
if (match && day.slots.length === match.slots.length) { | ||
// If there is a match (and they have the same number of slots) | ||
// then it's safe to assume that the slots match 1:1 and that | ||
// `day.slots[n]` is the same slot as `match.slots[n]` | ||
const updated = day.slots.map((slot, i) => { | ||
const old = match.slots[i]; | ||
if (old.selected) { | ||
// This slot should be "selected" | ||
return { | ||
...slot, | ||
selected: true | ||
}; | ||
} | ||
// The slot is not "selected", and can be returned unchanged. | ||
return slot; | ||
}); | ||
return { ...day, slots: updated }; | ||
} | ||
return day; | ||
}); | ||
return mergedSlots; | ||
}; |
{ | ||
"availability_viewer": { | ||
"available": "Available", | ||
"booked": "Booked", | ||
"confirm": "Confirm", | ||
@@ -5,0 +6,0 @@ "end": "End", |
@@ -229,2 +229,3 @@ import moment from "moment-timezone"; | ||
end_time: "02:00", | ||
interval: 60, | ||
week_start_day: "monday" | ||
@@ -240,8 +241,9 @@ }, | ||
boundsControl: true, | ||
end_time: "02:00", | ||
interval: 60, | ||
logs: "warn", | ||
mode: "confirm", | ||
logs: "warn", | ||
slot_selection: "available", | ||
start_time: "01:00", | ||
end_time: "02:00", | ||
startDay: "monday" | ||
startDay: "monday", | ||
start_time: "01:00" | ||
}, | ||
@@ -248,0 +250,0 @@ customtzid: true, |
@@ -709,2 +709,3 @@ import * as utils from "../src/js/helpers/utils.AvailabilityViewer"; | ||
// checkWeekdaysSlotAvailability | ||
it("checks visibility status", () => { | ||
@@ -824,2 +825,202 @@ const emptySlots = [ | ||
}); | ||
// buildPeriodsFromSlots | ||
it("combines abutting slots into periods", () => { | ||
const slots = [ | ||
{ | ||
start: "2020-06-16T08:00:00Z", | ||
end: "2020-06-16T08:30:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T08:30:00Z", | ||
end: "2020-06-16T09:00:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T10:00:00Z", | ||
end: "2020-06-16T10:30:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T10:30:00Z", | ||
end: "2020-06-16T11:00:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T11:00:00Z", | ||
end: "2020-06-16T11:30:00Z" | ||
} | ||
]; | ||
const expectedPeriods = [ | ||
{ | ||
start: "2020-06-16T08:00:00Z", | ||
end: "2020-06-16T09:00:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T10:00:00Z", | ||
end: "2020-06-16T11:30:00Z" | ||
} | ||
]; | ||
const periods = utils.buildPeriodsFromSlots(slots); | ||
expect(periods.length).toEqual(2); | ||
expect(periods[0]).toEqual(expectedPeriods[0]); | ||
}); | ||
it("combines unsorted abutting slots into periods", () => { | ||
const slots = [ | ||
{ | ||
start: "2020-06-16T08:30:00Z", | ||
end: "2020-06-16T09:00:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T08:00:00Z", | ||
end: "2020-06-16T08:30:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T10:30:00Z", | ||
end: "2020-06-16T11:00:00Z" | ||
}, | ||
{ | ||
start: "2020-06-17T10:30:00Z", | ||
end: "2020-06-17T11:00:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T11:00:00Z", | ||
end: "2020-06-16T11:30:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T10:00:00Z", | ||
end: "2020-06-16T10:30:00Z" | ||
} | ||
]; | ||
const expectedPeriods = [ | ||
{ | ||
start: "2020-06-16T08:00:00Z", | ||
end: "2020-06-16T09:00:00Z" | ||
}, | ||
{ | ||
start: "2020-06-16T10:00:00Z", | ||
end: "2020-06-16T11:30:00Z" | ||
}, | ||
{ | ||
start: "2020-06-17T10:30:00Z", | ||
end: "2020-06-17T11:00:00Z" | ||
} | ||
]; | ||
const periods = utils.buildPeriodsFromSlots(slots); | ||
expect(periods.length).toEqual(3); | ||
expect(periods[0]).toEqual(expectedPeriods[0]); | ||
expect(periods[1]).toEqual(expectedPeriods[1]); | ||
expect(periods[2]).toEqual(expectedPeriods[2]); | ||
}); | ||
// tweenSlots | ||
it("calculates tweens for slot-starts", () => { | ||
const options = { | ||
start: "2020-06-16T08:00:00Z", | ||
end: "2020-06-16T11:30:00Z", | ||
tzid: "Europe/London", | ||
interval: 30 | ||
}; | ||
const expected = [ | ||
"2020-06-16T08:00:00Z", | ||
"2020-06-16T08:30:00Z", | ||
"2020-06-16T09:00:00Z", | ||
"2020-06-16T09:30:00Z", | ||
"2020-06-16T10:00:00Z", | ||
"2020-06-16T10:30:00Z", | ||
"2020-06-16T11:00:00Z", | ||
"2020-06-16T11:30:00Z" | ||
]; | ||
const result = utils.tweenSlots(options); | ||
expect(result).toEqual(expected); | ||
expect(Array.isArray(result)).toBe(true); | ||
expect(result.length).toBe(expected.length); | ||
}); | ||
it("calculates tweens for cross-day slot-starts", () => { | ||
const options = { | ||
start: "2020-06-16T08:00:00Z", | ||
end: "2020-06-17T10:00:00Z", | ||
tzid: "America/Toronto", | ||
interval: 60 | ||
}; | ||
const expected = [ | ||
"2020-06-16T08:00:00Z", | ||
"2020-06-16T09:00:00Z", | ||
"2020-06-16T10:00:00Z", | ||
"2020-06-17T08:00:00Z", | ||
"2020-06-17T09:00:00Z", | ||
"2020-06-17T10:00:00Z" | ||
]; | ||
const result = utils.tweenSlots(options); | ||
expect(result).toEqual(expected); | ||
expect(Array.isArray(result)).toBe(true); | ||
expect(result.length).toBe(expected.length); | ||
}); | ||
it("calculates tweens for cross-day slot-starts over timezone boundaries", () => { | ||
const options = { | ||
// In America/Toronto, these times cross midnight | ||
// so there should be blocks for FOUR days (not | ||
// just the three that 23rd - 25th would suggest) | ||
start: "2020-06-23T02:00:00Z", | ||
end: "2020-06-25T21:00:00Z", | ||
tzid: "America/Toronto", | ||
interval: 60 | ||
}; | ||
const expected = [ | ||
// Block 1 | ||
"2020-06-22T21:00:00Z", | ||
"2020-06-22T22:00:00Z", | ||
"2020-06-22T23:00:00Z", | ||
"2020-06-23T00:00:00Z", | ||
"2020-06-23T01:00:00Z", | ||
"2020-06-23T02:00:00Z", | ||
// Block 2 | ||
"2020-06-23T21:00:00Z", | ||
"2020-06-23T22:00:00Z", | ||
"2020-06-23T23:00:00Z", | ||
"2020-06-24T00:00:00Z", | ||
"2020-06-24T01:00:00Z", | ||
"2020-06-24T02:00:00Z", | ||
// Block 3 | ||
"2020-06-24T21:00:00Z", | ||
"2020-06-24T22:00:00Z", | ||
"2020-06-24T23:00:00Z", | ||
"2020-06-25T00:00:00Z", | ||
"2020-06-25T01:00:00Z", | ||
"2020-06-25T02:00:00Z", | ||
// Block 4 | ||
"2020-06-25T21:00:00Z", | ||
"2020-06-25T22:00:00Z", | ||
"2020-06-25T23:00:00Z", | ||
"2020-06-26T00:00:00Z", | ||
"2020-06-26T01:00:00Z", | ||
"2020-06-26T02:00:00Z" | ||
]; | ||
const result = utils.tweenSlots(options); | ||
console.log("expected", expected); | ||
console.log("result", result); | ||
expect(result).toEqual(expected); | ||
expect(Array.isArray(result)).toBe(true); | ||
expect(result.length).toBe(expected.length); | ||
}); | ||
// addSlotStarts | ||
it("adds N slot start times", () => { | ||
const options = [ | ||
moment("2020-06-16T08:00:00Z", "YYYY-MM-DDTHH:mm:00Z"), | ||
30, | ||
4 | ||
]; | ||
const expected = [ | ||
"2020-06-16T08:00:00Z", | ||
"2020-06-16T08:30:00Z", | ||
"2020-06-16T09:00:00Z", | ||
"2020-06-16T09:30:00Z", | ||
"2020-06-16T10:00:00Z" | ||
]; | ||
const result = utils.addSlotStarts(...options); | ||
expect(result).toEqual(expected); | ||
}); | ||
}); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2663579
257
24295