UNPKG

@backstage/plugin-microsoft-calendar

Version:
685 lines (670 loc) 23.5 kB
import React, { useState, useCallback } from 'react'; import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { sortBy } from 'lodash'; import { DateTime } from 'luxon'; import { Link, InfoCard, Progress } from '@backstage/core-components'; import Box from '@material-ui/core/Box'; import IconButton from '@material-ui/core/IconButton'; import Typography from '@material-ui/core/Typography'; import PrevIcon from '@material-ui/icons/NavigateBefore'; import NextIcon from '@material-ui/icons/NavigateNext'; import { useApi, errorApiRef, microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { microsoftCalendarApiRef } from '../index.esm.js'; import calendarIcon from '../icons/calendar.svg'; import classnames from 'classnames'; import { usePopupState, bindTrigger, bindPopover } from 'material-ui-popup-state/hooks'; import Paper from '@material-ui/core/Paper'; import Popover from '@material-ui/core/Popover'; import Tooltip from '@material-ui/core/Tooltip'; import { makeStyles, styled } from '@material-ui/core/styles'; import webcamIcon from '../icons/webcam.svg'; import DOMPurify from 'dompurify'; import Divider from '@material-ui/core/Divider'; import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; import Badge from '@material-ui/core/Badge'; import Chip from '@material-ui/core/Chip'; import CancelIcon from '@material-ui/icons/Cancel'; import CheckIcon from '@material-ui/icons/CheckCircle'; import Checkbox from '@material-ui/core/Checkbox'; import FormControl from '@material-ui/core/FormControl'; import Input from '@material-ui/core/Input'; import ListItemText from '@material-ui/core/ListItemText'; import MenuItem from '@material-ui/core/MenuItem'; import Select from '@material-ui/core/Select'; import Button from '@material-ui/core/Button'; import useAsync from 'react-use/esm/useAsync'; import '@backstage/errors'; const ResponseStatusMap = { needsAction: "none", accepted: "accepted", declined: "declined", maybe: "tentativelyAccepted", notResponded: "notResponded" }; const useCalendarsQuery = ({ enabled }) => { const calendarApi = useApi(microsoftCalendarApiRef); const errorApi = useApi(errorApiRef); return useQuery( ["calendars-list"], async () => { const calendars = []; const value = await calendarApi.getCalendars(); calendars.push(...value); return calendars; }, { enabled, // old data will be returned if last request was made within 3 mins. staleTime: 18e4, refetchInterval: 36e5, onError: () => { errorApi.post({ name: "API error", message: "Failed to fetch calendars." }); } } ); }; const useEventsQuery = ({ calendarId, enabled, timeMin, timeMax, timeZone }) => { const calendarApi = useApi(microsoftCalendarApiRef); return useQuery( ["calendarEvents", calendarId, timeMin, timeMax, timeZone], async () => { const data = await calendarApi.getEvents( calendarId, { startDateTime: timeMin, endDateTime: timeMax }, { Prefer: `outlook.timezone="${timeZone}"` } ); return data; }, { cacheTime: 3e5, enabled, refetchInterval: 6e4, refetchIntervalInBackground: true } ); }; const useSignIn = () => { const [isSignedIn, setSignedIn] = useState(false); const [isInitialized, setInitialized] = useState(false); const authApi = useApi(microsoftAuthApiRef); const signIn = useCallback( async (optional = false) => { const token = await authApi.getAccessToken("Calendars.Read", { optional, instantPopup: !optional }); setSignedIn(!!token); setInitialized(true); }, [authApi, setSignedIn] ); return { isSignedIn, isInitialized, signIn }; }; const useStyles$3 = makeStyles((theme) => { const getIconColor = (responseStatus) => { if (!responseStatus) return theme.palette.primary.light; return { [ResponseStatusMap.accepted]: theme.palette.status.ok, [ResponseStatusMap.declined]: theme.palette.status.error }[responseStatus]; }; return { responseStatus: { color: ({ responseStatus }) => getIconColor(responseStatus) }, badge: { right: 10, top: 5, "& svg": { height: 16, width: 16, background: theme.palette.common.white } } }; }); const ResponseIcon = ({ responseStatus }) => { if (responseStatus === ResponseStatusMap.accepted) { return /* @__PURE__ */ React.createElement(CheckIcon, { "data-testid": "accepted-icon" }); } if (responseStatus === ResponseStatusMap.declined) { return /* @__PURE__ */ React.createElement(CancelIcon, { "data-testid": "declined-icon" }); } return null; }; const AttendeeChip = ({ user }) => { var _a, _b, _c; const classes = useStyles$3({ responseStatus: ((_a = user.status) == null ? void 0 : _a.response) || "" }); return /* @__PURE__ */ React.createElement( Badge, { classes: { root: classes.responseStatus, badge: classes.badge }, badgeContent: /* @__PURE__ */ React.createElement(ResponseIcon, { responseStatus: ((_b = user.status) == null ? void 0 : _b.response) || "" }) }, /* @__PURE__ */ React.createElement( Chip, { size: "small", variant: "outlined", label: (_c = user.emailAddress) == null ? void 0 : _c.address, color: "primary" } ) ); }; function getOnlineMeetingLink(event) { var _a; const onlineEntrypoint = ((_a = event.onlineMeeting) == null ? void 0 : _a.joinUrl) || event.onlineMeetingUrl; if (onlineEntrypoint) { return onlineEntrypoint; } return ""; } function getTimePeriod(event) { var _a, _b; if (isAllDay(event)) { return getAllDayTimePeriod(event); } const format = { hour: "2-digit", minute: "2-digit" }; const startTime = DateTime.fromISO(((_a = event.start) == null ? void 0 : _a.dateTime) || ""); const endTime = DateTime.fromISO(((_b = event.end) == null ? void 0 : _b.dateTime) || ""); return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString( format )}`; } function getAllDayTimePeriod(event) { var _a, _b; const format = { month: "long", day: "numeric" }; const startTime = DateTime.fromISO(((_a = event.start) == null ? void 0 : _a.dateTime) || ""); const endTime = DateTime.fromISO(((_b = event.end) == null ? void 0 : _b.dateTime) || "").minus({ day: 1 }); if (startTime.toISO() === endTime.toISO()) { return startTime.toLocaleString(format); } return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString( format )}`; } function isPassed(event) { var _a, _b; if (!((_a = event.end) == null ? void 0 : _a.dateTime)) return false; const eventDate = DateTime.fromISO((_b = event.end) == null ? void 0 : _b.dateTime); return DateTime.now() >= eventDate; } function isAllDay(event) { var _a, _b; const startTime = DateTime.fromISO(((_a = event.start) == null ? void 0 : _a.dateTime) || ""); const endTime = DateTime.fromISO(((_b = event.end) == null ? void 0 : _b.dateTime) || ""); return endTime.diff(startTime, "day").days >= 1; } function getStartDate(event) { var _a; return DateTime.fromISO(((_a = event.start) == null ? void 0 : _a.dateTime) || ""); } const useStyles$2 = makeStyles( (theme) => ({ description: { wordBreak: "break-word", "& a": { color: theme.palette.primary.main, fontWeight: 500 } }, divider: { marginTop: theme.spacing(2), marginBottom: theme.spacing(2) } }), { name: "MicrosoftCalendarEventPopoverContent" } ); const CalendarEventPopoverContent = ({ event }) => { const classes = useStyles$2(); const onlineMeetingLink = getOnlineMeetingLink(event); return /* @__PURE__ */ React.createElement(Box, { display: "flex", flexDirection: "column", width: 400, p: 2 }, /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center" }, /* @__PURE__ */ React.createElement(Box, { flex: 1 }, /* @__PURE__ */ React.createElement(Typography, { variant: "h6" }, event.subject), /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle2" }, getTimePeriod(event))), event.webLink && /* @__PURE__ */ React.createElement(Tooltip, { title: "Open in Calendar" }, /* @__PURE__ */ React.createElement( Link, { "data-testid": "open-calendar-link", to: event.webLink, onClick: (_e) => { }, noTrack: true }, /* @__PURE__ */ React.createElement(IconButton, null, /* @__PURE__ */ React.createElement(ArrowForwardIcon, null)) ))), onlineMeetingLink && /* @__PURE__ */ React.createElement(Link, { to: onlineMeetingLink, onClick: (_e) => { }, noTrack: true }, "Join Online Meeting"), event.bodyPreview && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Divider, { className: classes.divider, variant: "fullWidth" }), /* @__PURE__ */ React.createElement( Box, { className: classes.description, dangerouslySetInnerHTML: { __html: DOMPurify.sanitize( event.body && event.body.content || "", { USE_PROFILES: { html: true } } ) } } )), event.attendees && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Divider, { className: classes.divider, variant: "fullWidth" }), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle2" }, "Attendees"), /* @__PURE__ */ React.createElement(Box, { mb: 1 }), sortBy(event.attendees || [], "emailAddress").map((user) => { var _a; return /* @__PURE__ */ React.createElement( AttendeeChip, { key: ((_a = user.emailAddress) == null ? void 0 : _a.address) || "", user } ); })))); }; const useStyles$1 = makeStyles( (theme) => ({ event: { display: "flex", alignItems: "center", marginBottom: theme.spacing(1), cursor: "pointer", paddingRight: 12 }, declined: { textDecoration: "line-through" }, passed: { opacity: 0.6, transition: "opacity 0.15s ease-in-out", "&:hover": { opacity: 1 } }, link: { width: 48, height: 48, display: "inline-block", padding: 8, borderRadius: "50%", "&:hover": { backgroundColor: theme.palette.grey[100] } }, calendarColor: { width: 8, borderTopLeftRadius: 4, borderBottomLeftRadius: 4 } }), { name: "MicrosoftCalendarEvent" } ); const CalendarEvent = ({ event }) => { const classes = useStyles$1(); const popoverState = usePopupState({ variant: "popover", popupId: event.id, disableAutoFocus: true }); const [hovered, setHovered] = useState(false); const onlineMeetingLink = getOnlineMeetingLink(event); const { onClick, ...restBindProps } = bindTrigger(popoverState); return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement( Paper, { onClick: (e) => { onClick(e); }, ...restBindProps, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), elevation: hovered ? 4 : 1, className: classnames(classes.event, { [classes.passed]: isPassed(event) }), "data-testid": "microsoft-calendar-event" }, /* @__PURE__ */ React.createElement(Box, { className: classes.calendarColor, mr: 1, alignSelf: "stretch" }), /* @__PURE__ */ React.createElement(Box, { flex: 1, pt: 1, pb: 1 }, /* @__PURE__ */ React.createElement( Typography, { variant: "subtitle2", className: classnames({ [classes.declined]: event.isCancelled }) }, event.subject ), !isAllDay(event) && /* @__PURE__ */ React.createElement(Typography, { variant: "body2", "data-testid": "calendar-event-time" }, getTimePeriod(event))), event.isOnlineMeeting && /* @__PURE__ */ React.createElement(Tooltip, { title: "Join Online Meeting" }, /* @__PURE__ */ React.createElement( Link, { "data-testid": "calendar-event-online-meeting-link", className: classes.link, to: onlineMeetingLink, onClick: (e) => { e.stopPropagation(); }, noTrack: true }, /* @__PURE__ */ React.createElement( "img", { height: 32, width: 32, src: webcamIcon, alt: "Online Meeting link" } ) )) ), /* @__PURE__ */ React.createElement( Popover, { ...bindPopover(popoverState), anchorOrigin: { vertical: "top", horizontal: "left" }, transformOrigin: { vertical: "top", horizontal: "center" }, "data-testid": "calendar-event-popover" }, /* @__PURE__ */ React.createElement(CalendarEventPopoverContent, { event }) )); }; const useStyles = makeStyles( { formControl: { width: 120 }, selectedCalendars: { textOverflow: "ellipsis", overflow: "hidden" } }, { name: "MicrosoftCalendarSelect" } ); const CalendarSelect = ({ disabled, selectedCalendarId, setSelectedCalendarId, calendars }) => { const classes = useStyles(); return /* @__PURE__ */ React.createElement(FormControl, { className: classes.formControl }, /* @__PURE__ */ React.createElement( Select, { labelId: "calendars-label", disabled: disabled || calendars.length === 0, value: selectedCalendarId || "", onChange: async (e) => setSelectedCalendarId(e.target.value), input: /* @__PURE__ */ React.createElement(Input, null), renderValue: (selected) => { var _a; return /* @__PURE__ */ React.createElement(Typography, { className: classes.selectedCalendars, variant: "body2" }, (_a = calendars.find((c) => c.id === selected)) == null ? void 0 : _a.name); }, MenuProps: { PaperProps: { style: { width: 350 } } } }, sortBy(calendars, "name").map((c) => /* @__PURE__ */ React.createElement(MenuItem, { key: c.id, value: c.id }, /* @__PURE__ */ React.createElement(Checkbox, { checked: c.id === selectedCalendarId }), /* @__PURE__ */ React.createElement(ListItemText, { primary: c.name }))) )); }; var mockEvents = [ { id: "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", createdDateTime: "2022-12-22T08:28:36.6131501Z", lastModifiedDateTime: "2023-01-20T05:24:31.9666518Z", changeKey: "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", categories: [ ], transactionId: null, originalStartTimeZone: "India Standard Time", originalEndTimeZone: "India Standard Time", iCalUId: "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", reminderMinutesBeforeStart: 15, isReminderOn: true, hasAttachments: false, subject: "Abhay Soni", bodyPreview: "", importance: "normal", sensitivity: "normal", isAllDay: false, isCancelled: false, isOrganizer: false, responseRequested: true, seriesMasterId: "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", showAs: "tentative", type: "occurrence", webLink: "", onlineMeetingUrl: "", isOnlineMeeting: true, onlineMeetingProvider: "teamsForBusiness", allowNewTimeProposals: true, occurrenceId: "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", isDraft: false, hideAttendees: false, responseStatus: { response: "notResponded", time: "0001-01-01T00:00:00Z" }, body: { contentType: "html", content: "" }, start: { dateTime: "2023-01-16T11:00:00.0000000", timeZone: "Asia/Calcutta" }, end: { dateTime: "2023-01-16T11:30:00.0000000", timeZone: "Asia/Calcutta" }, location: { displayName: "", locationType: "default", uniqueIdType: "unknown", address: { }, coordinates: { } }, locations: [ ], recurrence: null, attendees: [ ], organizer: { emailAddress: { name: "Abhay Soni", address: "abhaysoni.developer@gmail.com" } }, onlineMeeting: { joinUrl: "https://abhay-soni-developer.github.io/MyReusme/" } }, { id: "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", createdDateTime: "2022-12-22T08:28:36.6131501Z", lastModifiedDateTime: "2023-01-20T05:24:31.9666518Z", changeKey: "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", categories: [ ], transactionId: null, originalStartTimeZone: "India Standard Time", originalEndTimeZone: "India Standard Time", iCalUId: "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", reminderMinutesBeforeStart: 15, isReminderOn: true, hasAttachments: false, subject: "https://abhay-soni-developer.github.io/MyReusme/", bodyPreview: "", importance: "normal", sensitivity: "normal", isAllDay: false, isCancelled: false, isOrganizer: false, responseRequested: true, seriesMasterId: "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", showAs: "tentative", type: "occurrence", webLink: "", onlineMeetingUrl: "", isOnlineMeeting: true, onlineMeetingProvider: "teamsForBusiness", allowNewTimeProposals: true, occurrenceId: "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", isDraft: false, hideAttendees: false, responseStatus: { response: "notResponded", time: "0001-01-01T00:00:00Z" }, body: { contentType: "html", content: "" }, start: { dateTime: "2023-01-16T11:00:00.0000000", timeZone: "Asia/Calcutta" }, end: { dateTime: "2023-01-16T11:30:00.0000000", timeZone: "Asia/Calcutta" }, location: { displayName: "", locationType: "default", uniqueIdType: "unknown", address: { }, coordinates: { } }, locations: [ ], recurrence: null, attendees: [ ], organizer: { emailAddress: { name: "Abhay Soni", address: "abhaysoni.developer@gmail.com" } }, onlineMeeting: { joinUrl: "https://abhay-soni-developer.github.io/MyReusme/" } } ]; const TransparentBox = styled(Box)({ opacity: 0.3, filter: "blur(1.5px)" }); const SignInContent = ({ handleAuthClick }) => { return /* @__PURE__ */ React.createElement(Box, { position: "relative", height: "100%", width: "100%" }, /* @__PURE__ */ React.createElement(TransparentBox, { p: 1 }, mockEvents.map((event) => /* @__PURE__ */ React.createElement(CalendarEvent, { key: event.id, event }))), /* @__PURE__ */ React.createElement( Box, { height: "100%", width: "100%", display: "flex", justifyContent: "center", alignItems: "center", position: "absolute", left: 0, top: 0 }, /* @__PURE__ */ React.createElement( Button, { variant: "contained", color: "primary", onClick: handleAuthClick, size: "large" }, "Sign in" ) )); }; const CalendarCard = () => { var _a, _b; const [date, setDate] = useState(DateTime.now()); const [selectedCalendarId, setSelectedCalendarId] = useState(""); const changeDay = (offset = 1) => { setDate((prev) => prev.plus({ day: offset })); }; const { isSignedIn, isInitialized, signIn } = useSignIn(); useAsync(async () => signIn(true), [signIn]); const { isLoading: isCalendarLoading, isFetching: isCalendarFetching, data: calendars = [] } = useCalendarsQuery({ enabled: isSignedIn }); const defaultCalendarId = (_a = calendars.find((c) => c.isDefaultCalendar)) == null ? void 0 : _a.id; const { data: events, isLoading: isEventLoading } = useEventsQuery({ calendarId: selectedCalendarId || defaultCalendarId || "", enabled: isSignedIn && calendars.length > 0, timeMin: date.startOf("day").toISO(), timeMax: date.endOf("day").toISO(), timeZone: (_b = date.zoneName) != null ? _b : void 0 }); const showLoader = isCalendarLoading && isCalendarFetching || isEventLoading || !isInitialized; return /* @__PURE__ */ React.createElement( InfoCard, { noPadding: true, title: /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center" }, /* @__PURE__ */ React.createElement(Box, { height: 32, width: 32, mr: 1 }, /* @__PURE__ */ React.createElement("img", { src: calendarIcon, alt: "Microsoft Calendar" })), isSignedIn ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(IconButton, { onClick: () => changeDay(-1), size: "small" }, /* @__PURE__ */ React.createElement(PrevIcon, null)), /* @__PURE__ */ React.createElement(IconButton, { onClick: () => changeDay(1), size: "small" }, /* @__PURE__ */ React.createElement(NextIcon, null)), /* @__PURE__ */ React.createElement(Box, { mr: 0.5 }), /* @__PURE__ */ React.createElement(Typography, { variant: "h6" }, date.toLocaleString({ weekday: "short", month: "short", day: "numeric" })), /* @__PURE__ */ React.createElement(Box, { flex: 1 }), /* @__PURE__ */ React.createElement( CalendarSelect, { calendars, selectedCalendarId: selectedCalendarId || defaultCalendarId, setSelectedCalendarId, disabled: isCalendarFetching && isCalendarLoading || !isSignedIn } )) : /* @__PURE__ */ React.createElement(Typography, { variant: "h6" }, "Agenda")), deepLink: { link: "https://outlook.office.com/calendar/", title: "Go to Calendar" } }, /* @__PURE__ */ React.createElement(Box, null, showLoader && /* @__PURE__ */ React.createElement(Box, { py: 2 }, /* @__PURE__ */ React.createElement(Progress, { variant: "query" })), !isSignedIn && isInitialized && /* @__PURE__ */ React.createElement(SignInContent, { handleAuthClick: () => signIn(false) }), !isEventLoading && !isCalendarLoading && isSignedIn && /* @__PURE__ */ React.createElement(Box, { p: 1, pb: 0, maxHeight: 602, overflow: "auto" }, (events == null ? void 0 : events.length) === 0 && /* @__PURE__ */ React.createElement(Box, { pt: 2, pb: 2 }, /* @__PURE__ */ React.createElement(Typography, { align: "center", variant: "h6" }, "No events")), sortBy(events, [getStartDate]).map((event) => /* @__PURE__ */ React.createElement(CalendarEvent, { key: `${event.id}`, event })))) ); }; const queryClient = new QueryClient(); const MicrosoftCalendar = () => { return /* @__PURE__ */ React.createElement(QueryClientProvider, { client: queryClient }, /* @__PURE__ */ React.createElement(CalendarCard, null)); }; export { MicrosoftCalendar }; //# sourceMappingURL=index-DGcR6krL.esm.js.map