From c66af610e5e8c2d0cacba8611aeea08fdf178a2d Mon Sep 17 00:00:00 2001 From: nicolas Date: Sun, 13 Jun 2021 15:37:13 +0200 Subject: [PATCH 01/21] Added migration for current db schema --- .../migration.sql | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 prisma/migrations/20210613133618_add_team_membership_verification/migration.sql diff --git a/prisma/migrations/20210613133618_add_team_membership_verification/migration.sql b/prisma/migrations/20210613133618_add_team_membership_verification/migration.sql new file mode 100644 index 0000000000..cf9aa22320 --- /dev/null +++ b/prisma/migrations/20210613133618_add_team_membership_verification/migration.sql @@ -0,0 +1,26 @@ +-- CreateEnum +CREATE TYPE "MembershipRole" AS ENUM ('MEMBER', 'OWNER'); + +-- CreateTable +CREATE TABLE "Team" ( + "id" SERIAL NOT NULL, + "name" TEXT, + + PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Membership" ( + "teamId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "accepted" BOOLEAN NOT NULL DEFAULT false, + "role" "MembershipRole" NOT NULL, + + PRIMARY KEY ("userId","teamId") +); + +-- AddForeignKey +ALTER TABLE "Membership" ADD FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Membership" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; From d3b843169958f431208db1d382cbbb89baa77ef7 Mon Sep 17 00:00:00 2001 From: Malte Delfs Date: Mon, 14 Jun 2021 19:45:24 +0200 Subject: [PATCH 02/21] Implemented calendar selection for availability checking. Also upgraded outlook integration to be able to check all calendars instead of only the default one. --- .gitignore | 3 + calendso.yaml | 13 ++- lib/calendarClient.ts | 135 ++++++++++++++++------- pages/api/availability/[user].ts | 8 +- pages/api/availability/calendar.ts | 69 ++++++++++++ pages/integrations/index.tsx | 167 +++++++++++++++++++++++++++-- prisma/schema.prisma | 11 +- 7 files changed, 359 insertions(+), 47 deletions(-) create mode 100644 pages/api/availability/calendar.ts diff --git a/.gitignore b/.gitignore index 1756860634..126df0694f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ yarn-error.log* # vercel .vercel + +# Webstorm +.idea diff --git a/calendso.yaml b/calendso.yaml index 8f052f877f..d45d0f7d2b 100644 --- a/calendso.yaml +++ b/calendso.yaml @@ -110,6 +110,17 @@ paths: summary: Deletes an event type tags: - Availability + /api/availability/calendars: + post: + description: Selects calendar for availability checking. + summary: Adds selected calendar + tags: + - Availability + delete: + description: Removes a calendar from availability checking. + summary: Deletes a selected calendar + tags: + - Availability /api/book/:user: post: description: Creates a booking in the user's calendar. @@ -144,4 +155,4 @@ paths: description: Updates a user's profile. summary: Updates a user's profile tags: - - User \ No newline at end of file + - User diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index c11348f483..526e117737 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -66,6 +66,13 @@ interface CalendarEvent { attendees: Person[]; }; +interface IntegrationCalendar { + integration: string; + primary: boolean; + externalId: string; + name: string; +} + interface CalendarApiAdapter { createEvent(event: CalendarEvent): Promise; @@ -73,7 +80,9 @@ interface CalendarApiAdapter { deleteEvent(uid: String); - getAvailability(dateFrom, dateTo): Promise; + getAvailability(dateFrom, dateTo, selectedCalendars: IntegrationCalendar[]): Promise; + + listCalendars(): Promise; } const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { @@ -112,37 +121,59 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { } }; - return { - getAvailability: (dateFrom, dateTo) => { - const payload = { - schedules: [credential.key.email], - startTime: { - dateTime: dateFrom, - timeZone: 'UTC', - }, - endTime: { - dateTime: dateTo, - timeZone: 'UTC', - }, - availabilityViewInterval: 60 - }; + const integrationType = "office365_calendar"; + function listCalendars(): Promise { + return auth.getToken().then(accessToken => fetch('https://graph.microsoft.com/v1.0/me/calendars', { + method: 'get', + headers: { + 'Authorization': 'Bearer ' + accessToken, + 'Content-Type': 'application/json' + }, + }).then(handleErrorsJson) + .then(responseBody => { + return responseBody.value.map(cal => { + const calendar: IntegrationCalendar = { + externalId: cal.id, integration: integrationType, name: cal.name, primary: cal.isDefaultCalendar + } + return calendar; + }); + }) + ) + } + + return { + getAvailability: (dateFrom, dateTo, selectedCalendars) => { + const filter = "?$filter=start/dateTime ge '" + dateFrom + "' and end/dateTime le '" + dateTo + "'" return auth.getToken().then( - (accessToken) => fetch('https://graph.microsoft.com/v1.0/me/calendar/getSchedule', { - method: 'post', - headers: { - 'Authorization': 'Bearer ' + accessToken, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }) - .then(handleErrorsJson) - .then(responseBody => { - return responseBody.value[0].scheduleItems.map((evt) => ({ - start: evt.start.dateTime + 'Z', - end: evt.end.dateTime + 'Z' - })) + (accessToken) => { + const selectedCalendarIds = selectedCalendars.filter(e => e.integration === integrationType).map(e => e.externalId); + if (selectedCalendarIds.length == 0 && selectedCalendars.length > 0){ + // Only calendars of other integrations selected + return Promise.resolve([]); + } + + console.log("selectedCalendarIds.length: " + selectedCalendarIds.length) + return (selectedCalendarIds.length == 0 + ? listCalendars().then(cals => cals.map(e => e.externalId)) + : Promise.resolve(selectedCalendarIds).then(x => x)).then((ids: string[]) => { + const urls = ids.map(calendarId => 'https://graph.microsoft.com/v1.0/me/calendars/' + calendarId + '/events' + filter) + console.log("urls", urls) + return Promise.all(urls.map(url => fetch(url, { + method: 'get', + headers: { + 'Authorization': 'Bearer ' + accessToken, + 'Prefer': 'outlook.timezone="Etc/GMT"' + } + }) + .then(handleErrorsJson) + .then(responseBody => responseBody.value.map((evt) => ({ + start: evt.start.dateTime + 'Z', + end: evt.end.dateTime + 'Z' + })) + ))).then(results => results.reduce((acc, events) => acc.concat(events), [])) }) + } ).catch((err) => { console.log(err); }); @@ -172,28 +203,37 @@ const MicrosoftOffice365Calendar = (credential): CalendarApiAdapter => { }, body: JSON.stringify(translateEvent(event)) }).then(handleErrorsRaw)), + listCalendars } }; const GoogleCalendar = (credential): CalendarApiAdapter => { const myGoogleAuth = googleAuth(); myGoogleAuth.setCredentials(credential.key); + const integrationType = "google_calendar"; + return { - getAvailability: (dateFrom, dateTo) => new Promise((resolve, reject) => { + getAvailability: (dateFrom, dateTo, selectedCalendars) => new Promise((resolve, reject) => { const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); calendar.calendarList .list() .then(cals => { + const filteredItems = cals.data.items.filter(i => selectedCalendars.findIndex(e => e.externalId === i.id) > -1) + if (filteredItems.length == 0 && selectedCalendars.length > 0){ + // Only calendars of other integrations selected + resolve([]); + } calendar.freebusy.query({ requestBody: { timeMin: dateFrom, timeMax: dateTo, - items: cals.data.items + items: filteredItems.length > 0 ? filteredItems : cals.data.items } }, (err, apires) => { if (err) { reject(err); } + resolve( Object.values(apires.data.calendars).flatMap( (item) => item["busy"] @@ -300,6 +340,22 @@ const GoogleCalendar = (credential): CalendarApiAdapter => { } return resolve(event.data); }); + }), + listCalendars: () => new Promise((resolve, reject) => { + const calendar = google.calendar({version: 'v3', auth: myGoogleAuth}); + calendar.calendarList + .list() + .then(cals => { + resolve(cals.data.items.map(cal => { + const calendar: IntegrationCalendar = { + externalId: cal.id, integration: integrationType, name: cal.summary, primary: cal.primary + } + return calendar; + })) + }) + .catch((err) => { + reject(err); + }); }) }; }; @@ -316,11 +372,18 @@ const calendars = (withCredentials): CalendarApiAdapter[] => withCredentials.map } }).filter(Boolean); - -const getBusyTimes = (withCredentials, dateFrom, dateTo) => Promise.all( - calendars(withCredentials).map(c => c.getAvailability(dateFrom, dateTo)) +const getBusyTimes = (withCredentials, dateFrom, dateTo, selectedCalendars) => Promise.all( + calendars(withCredentials).map(c => c.getAvailability(dateFrom, dateTo, selectedCalendars)) ).then( - (results) => results.reduce((acc, availability) => acc.concat(availability), []) + (results) => { + return results.reduce((acc, availability) => acc.concat(availability), []) + } +); + +const listCalendars = (withCredentials) => Promise.all( + calendars(withCredentials).map(c => c.listCalendars()) +).then( + (results) => results.reduce((acc, calendars) => acc.concat(calendars), []) ); const createEvent = (credential, calEvent: CalendarEvent): Promise => { @@ -352,4 +415,4 @@ const deleteEvent = (credential, uid: String): Promise => { return Promise.resolve({}); }; -export {getBusyTimes, createEvent, updateEvent, deleteEvent, CalendarEvent}; +export {getBusyTimes, createEvent, updateEvent, deleteEvent, CalendarEvent, listCalendars, IntegrationCalendar}; diff --git a/pages/api/availability/[user].ts b/pages/api/availability/[user].ts index d3dfd8565d..40b4e256f3 100644 --- a/pages/api/availability/[user].ts +++ b/pages/api/availability/[user].ts @@ -15,6 +15,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } }); - const availability = await getBusyTimes(currentUser.credentials, req.query.dateFrom, req.query.dateTo); + const selectedCalendars = (await prisma.selectedCalendar.findMany({ + where: { + userId: currentUser.id + } + })); + + const availability = await getBusyTimes(currentUser.credentials, req.query.dateFrom, req.query.dateTo, selectedCalendars); res.status(200).json(availability); } diff --git a/pages/api/availability/calendar.ts b/pages/api/availability/calendar.ts new file mode 100644 index 0000000000..43bc929d70 --- /dev/null +++ b/pages/api/availability/calendar.ts @@ -0,0 +1,69 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getSession } from 'next-auth/client'; +import prisma from '../../../lib/prisma'; +import {IntegrationCalendar, listCalendars} from "../../../lib/calendarClient"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await getSession({req: req}); + + if (!session) { + res.status(401).json({message: "Not authenticated"}); + return; + } + + const currentUser = await prisma.user.findFirst({ + where: { + id: session.user.id, + }, + select: { + credentials: true, + timeZone: true, + id: true + } + }); + + if (req.method == "POST") { + await prisma.selectedCalendar.create({ + data: { + user: { + connect: { + id: currentUser.id + } + }, + integration: req.body.integration, + externalId: req.body.externalId + } + }); + res.status(200).json({message: "Calendar Selection Saved"}); + + } + + if (req.method == "DELETE") { + await prisma.selectedCalendar.delete({ + where: { + userId_integration_externalId: { + userId: currentUser.id, + externalId: req.body.externalId, + integration: req.body.integration + } + } + }); + + res.status(200).json({message: "Calendar Selection Saved"}); + } + + if (req.method == "GET") { + const selectedCalendarIds = await prisma.selectedCalendar.findMany({ + where: { + userId: currentUser.id + }, + select: { + externalId: true + } + }); + + const calendars: IntegrationCalendar[] = await listCalendars(currentUser.credentials); + const selectableCalendars = calendars.map(cal => {return {selected: selectedCalendarIds.findIndex(s => s.externalId === cal.externalId) > -1, ...cal}}); + res.status(200).json(selectableCalendars); + } +} diff --git a/pages/integrations/index.tsx b/pages/integrations/index.tsx index d732eee1cb..4363f4a888 100644 --- a/pages/integrations/index.tsx +++ b/pages/integrations/index.tsx @@ -2,29 +2,82 @@ import Head from 'next/head'; import Link from 'next/link'; import prisma from '../../lib/prisma'; import Shell from '../../components/Shell'; -import {useState} from 'react'; +import {useEffect, useState} from 'react'; import {getSession, useSession} from 'next-auth/client'; -import {CheckCircleIcon, ChevronRightIcon, PlusIcon, XCircleIcon} from '@heroicons/react/solid'; +import {CalendarIcon, CheckCircleIcon, ChevronRightIcon, PlusIcon, XCircleIcon} from '@heroicons/react/solid'; import {InformationCircleIcon} from '@heroicons/react/outline'; +import { Switch } from '@headlessui/react' export default function Home({ integrations }) { const [session, loading] = useSession(); const [showAddModal, setShowAddModal] = useState(false); - - if (loading) { - return

Loading...

; - } + const [showSelectCalendarModal, setShowSelectCalendarModal] = useState(false); + const [selectableCalendars, setSelectableCalendars] = useState([]); function toggleAddModal() { setShowAddModal(!showAddModal); } + function toggleShowCalendarModal() { + setShowSelectCalendarModal(!showSelectCalendarModal); + } + + function loadCalendars() { + fetch('api/availability/calendar') + .then((response) => response.json()) + .then(data => { + setSelectableCalendars(data) + }); + } + function integrationHandler(type) { fetch('/api/integrations/' + type.replace('_', '') + '/add') .then((response) => response.json()) .then((data) => window.location.href = data.url); } + function calendarSelectionHandler(calendar) { + return (selected) => { + let cals = [...selectableCalendars]; + let i = cals.findIndex(c => c.externalId === calendar.externalId); + cals[i].selected = selected; + setSelectableCalendars(cals); + if (selected) { + fetch('api/availability/calendar', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(cals[i]) + }).then((response) => response.json()); + } else { + fetch('api/availability/calendar', { + method: 'DELETE', headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify(cals[i]) + }).then((response) => response.json()); + } + } + } + + function getCalendarIntegrationImage(integrationType: string){ + switch (integrationType) { + case "google_calendar": return "integrations/google-calendar.png"; + case "office365_calendar": return "integrations/office-365.png"; + default: return ""; + } + } + + function classNames(...classes) { + return classes.filter(Boolean).join(' ') + } + + useEffect(loadCalendars, [integrations]); + + if (loading) { + return

Loading...

; + } + return (
@@ -39,7 +92,7 @@ export default function Home({ integrations }) { Add new integration
-
+
{integrations.filter( (ig) => ig.credential ).length !== 0 ?
    {integrations.filter(ig => ig.credential).map( (ig) => (
  • @@ -165,6 +218,104 @@ export default function Home({ integrations }) {
} +
+
+

+ Select calendars +

+
+

+ Select which calendars are checked for availability to prevent double bookings. +

+
+
+ +
+
+
+ {showSelectCalendarModal && +
+
+ {/* */} + + + {/* */} +
+
+
+ +
+
+ +
+

+ If no entry is selected, all calendars will be checked +

+
+
+
+
+
    + {selectableCalendars.map( (calendar) => (
  • +
    + {calendar.integration} +
    +
    +

    { calendar.name }

    +
    +
    + + Select calendar + +
    +
  • ))} +
+
+
+ +
+
+
+
+ } ); @@ -225,4 +376,4 @@ export async function getServerSideProps(context) { return { props: {integrations}, } -} \ No newline at end of file +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 88a8187dda..60067ab11f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,6 +49,7 @@ model User { credentials Credential[] teams Membership[] bookings Booking[] + selectedCalendars SelectedCalendar[] @@map(name: "users") } @@ -119,4 +120,12 @@ model Booking { createdAt DateTime @default(now()) updatedAt DateTime? -} \ No newline at end of file +} + +model SelectedCalendar { + user User @relation(fields: [userId], references: [id]) + userId Int + integration String + externalId String + @@id([userId,integration,externalId]) +} From 4caac5a88cf930e83f905aec7fdcddf63484f7e7 Mon Sep 17 00:00:00 2001 From: Malte Delfs Date: Mon, 14 Jun 2021 19:55:55 +0200 Subject: [PATCH 03/21] Layout fix --- pages/integrations/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/integrations/index.tsx b/pages/integrations/index.tsx index 4363f4a888..7e132e3b9f 100644 --- a/pages/integrations/index.tsx +++ b/pages/integrations/index.tsx @@ -282,10 +282,10 @@ export default function Home({ integrations }) {
{calendar.integration}
-
+

{ calendar.name }

-
+
Date: Mon, 14 Jun 2021 19:00:37 +0000 Subject: [PATCH 04/21] Fixes #266: ICS event is off by one month --- lib/emails/new-event.ts | 2 +- pages/success.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/emails/new-event.ts b/lib/emails/new-event.ts index 0513ee6716..448ddec033 100644 --- a/lib/emails/new-event.ts +++ b/lib/emails/new-event.ts @@ -26,7 +26,7 @@ export default function createNewEventEmail(calEvent: CalendarEvent, options: an const icalEventAsString = (calEvent: CalendarEvent): string => { const icsEvent = createEvent({ - start: dayjs(calEvent.startTime).utc().toArray().slice(0, 6), + start: dayjs(calEvent.startTime).utc().toArray().slice(0, 6).map((v, i) => i === 1 ? v + 1 : v), startInputType: 'utc', productId: 'calendso/ics', title: `${calEvent.type} with ${calEvent.attendees[0].name}`, diff --git a/pages/success.tsx b/pages/success.tsx index c36ab1731e..dcb8f6ed2a 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -35,7 +35,7 @@ export default function Success(props) { } const event = createEvent({ - start: date.utc().toArray().slice(0, 6), + start: date.utc().toArray().slice(0, 6).map((v, i) => i === 1 ? v + 1 : v), startInputType: 'utc', title: props.eventType.title + ' with ' + props.user.name, description: props.eventType.description, From 0dce92d4aeab304b4f2a59b5a929e8af2e5ecae5 Mon Sep 17 00:00:00 2001 From: Malte Delfs Date: Tue, 15 Jun 2021 16:03:41 +0200 Subject: [PATCH 05/21] added migration --- .../migration.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 prisma/migrations/20210615140247_added_selected_calendar/migration.sql diff --git a/prisma/migrations/20210615140247_added_selected_calendar/migration.sql b/prisma/migrations/20210615140247_added_selected_calendar/migration.sql new file mode 100644 index 0000000000..cdb2777880 --- /dev/null +++ b/prisma/migrations/20210615140247_added_selected_calendar/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "SelectedCalendar" ( + "userId" INTEGER NOT NULL, + "integration" TEXT NOT NULL, + "externalId" TEXT NOT NULL, + + PRIMARY KEY ("userId","integration","externalId") +); + +-- AddForeignKey +ALTER TABLE "SelectedCalendar" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; From 4fb0fbfe07e7c303b810e5ca29de178a6546cc5d Mon Sep 17 00:00:00 2001 From: Malte Delfs Date: Tue, 15 Jun 2021 17:26:16 +0200 Subject: [PATCH 06/21] Made event name customizable --- lib/event.ts | 5 +++++ pages/[user]/book.tsx | 6 +++--- pages/api/availability/eventtype.ts | 3 ++- pages/api/book/[user].ts | 16 ++++++++++++++-- pages/availability/event/[type].tsx | 13 +++++++++++-- pages/success.tsx | 19 ++++++++++++------- .../migration.sql | 2 ++ prisma/schema.prisma | 3 ++- 8 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 lib/event.ts create mode 100644 prisma/migrations/20210615142134_added_custom_event_name/migration.sql diff --git a/lib/event.ts b/lib/event.ts new file mode 100644 index 0000000000..829ee7e1e1 --- /dev/null +++ b/lib/event.ts @@ -0,0 +1,5 @@ +export function getEventName(name: string, eventTitle: string, eventNameTemplate?: string) { + return eventNameTemplate + ? eventNameTemplate.replace("{USER}", name) + : eventTitle + ' with ' + name +} diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index f27eda37c8..fb9165e9ad 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -56,7 +56,7 @@ export default function Book(props) { email: event.target.email.value, notes: event.target.notes.value, timeZone: preferredTimeZone, - eventName: props.eventType.title, + eventTypeId: props.eventType.id, rescheduleUid: rescheduleUid }; @@ -76,7 +76,7 @@ export default function Book(props) { } ); - let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=1`; + let successUrl = `/success?date=${date}&type=${props.eventType.id}&user=${props.user.username}&reschedule=1&name=${payload.name}`; if (payload['location']) { successUrl += "&location=" + encodeURIComponent(payload['location']); } @@ -217,4 +217,4 @@ export async function getServerSideProps(context) { booking }, } -} \ No newline at end of file +} diff --git a/pages/api/availability/eventtype.ts b/pages/api/availability/eventtype.ts index 8f03e3e91b..35d8c114e8 100644 --- a/pages/api/availability/eventtype.ts +++ b/pages/api/availability/eventtype.ts @@ -18,6 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) length: parseInt(req.body.length), hidden: req.body.hidden, locations: req.body.locations, + eventName: req.body.eventName }; if (req.method == "POST") { @@ -50,4 +51,4 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) res.status(200).json({message: 'Event deleted successfully'}); } -} \ No newline at end of file +} diff --git a/pages/api/book/[user].ts b/pages/api/book/[user].ts index 53ae3db23b..9c1f4f3e74 100644 --- a/pages/api/book/[user].ts +++ b/pages/api/book/[user].ts @@ -5,6 +5,7 @@ import createConfirmBookedEmail from "../../../lib/emails/confirm-booked"; import async from 'async'; import {v5 as uuidv5} from 'uuid'; import short from 'short-uuid'; +import {getEventName} from "../../../lib/event"; const translator = short(); @@ -26,9 +27,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const rescheduleUid = req.body.rescheduleUid; + const selectedEventType = await prisma.eventType.findFirst({ + where: { + userId: currentUser.id, + id: req.body.eventTypeId + }, + select: { + eventName: true, + title: true + } + }); + const evt: CalendarEvent = { - type: req.body.eventName, - title: req.body.eventName + ' with ' + req.body.name, + type: selectedEventType.title, + title: getEventName(req.body.name, selectedEventType.title, selectedEventType.eventName), description: req.body.notes, startTime: req.body.start, endTime: req.body.end, diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx index ed575f25f1..916e70c2b6 100644 --- a/pages/availability/event/[type].tsx +++ b/pages/availability/event/[type].tsx @@ -27,6 +27,7 @@ export default function EventType(props) { const descriptionRef = useRef(); const lengthRef = useRef(); const isHiddenRef = useRef(); + const eventNameRef = useRef(); if (loading) { return

Loading...

; @@ -40,11 +41,12 @@ export default function EventType(props) { const enteredDescription = descriptionRef.current.value; const enteredLength = lengthRef.current.value; const enteredIsHidden = isHiddenRef.current.checked; + const enteredEventName = eventNameRef.current.value; // TODO: Add validation const response = await fetch('/api/availability/eventtype', { method: 'PATCH', - body: JSON.stringify({id: props.eventType.id, title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden: enteredIsHidden, locations }), + body: JSON.stringify({id: props.eventType.id, title: enteredTitle, slug: enteredSlug, description: enteredDescription, length: enteredLength, hidden: enteredIsHidden, locations, eventName: enteredEventName }), headers: { 'Content-Type': 'application/json' } @@ -232,6 +234,12 @@ export default function EventType(props) {
+
+ +
+ +
+
@@ -348,6 +356,7 @@ export async function getServerSideProps(context) { length: true, hidden: true, locations: true, + eventName: true, } }); @@ -357,4 +366,4 @@ export async function getServerSideProps(context) { eventType }, } -} \ No newline at end of file +} diff --git a/pages/success.tsx b/pages/success.tsx index c36ab1731e..44a3ea4604 100644 --- a/pages/success.tsx +++ b/pages/success.tsx @@ -10,6 +10,7 @@ import utc from 'dayjs/plugin/utc'; import toArray from 'dayjs/plugin/toArray'; import timezone from 'dayjs/plugin/timezone'; import { createEvent } from 'ics'; +import {getEventName} from "../lib/event"; dayjs.extend(utc); dayjs.extend(toArray); @@ -18,6 +19,7 @@ dayjs.extend(timezone); export default function Success(props) { const router = useRouter(); const { location } = router.query; + const { name } = router.query; const [ is24h, setIs24h ] = useState(false); const [ date, setDate ] = useState(dayjs.utc(router.query.date)); @@ -27,6 +29,8 @@ export default function Success(props) { setIs24h(!!localStorage.getItem('timeOption.is24hClock')); }, []); + const eventName = getEventName(name, props.eventType.title, props.eventType.eventName); + function eventLink(): string { let optional = {}; @@ -37,7 +41,7 @@ export default function Success(props) { const event = createEvent({ start: date.utc().toArray().slice(0, 6), startInputType: 'utc', - title: props.eventType.title + ' with ' + props.user.name, + title: eventName, description: props.eventType.description, duration: { minutes: props.eventType.length }, ...optional @@ -53,7 +57,7 @@ export default function Success(props) { return(
- Booking Confirmed | {props.eventType.title} with {props.user.name || props.user.username} | Calendso + Booking Confirmed | {eventName} | Calendso
@@ -76,7 +80,7 @@ export default function Success(props) {

-

{props.eventType.title} with {props.user.name}

+

{eventName}

{props.eventType.length} minutes @@ -95,17 +99,17 @@ export default function Success(props) {

Add to your calendar
- Google - Microsoft Outlook - Microsoft Office @@ -149,7 +153,8 @@ export async function getServerSideProps(context) { id: true, title: true, description: true, - length: true + length: true, + eventName: true } }); diff --git a/prisma/migrations/20210615142134_added_custom_event_name/migration.sql b/prisma/migrations/20210615142134_added_custom_event_name/migration.sql new file mode 100644 index 0000000000..856cb4c781 --- /dev/null +++ b/prisma/migrations/20210615142134_added_custom_event_name/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "eventName" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 88a8187dda..5f3ee65d31 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -21,6 +21,7 @@ model EventType { user User? @relation(fields: [userId], references: [id]) userId Int? bookings Booking[] + eventName String? } model Credential { @@ -119,4 +120,4 @@ model Booking { createdAt DateTime @default(now()) updatedAt DateTime? -} \ No newline at end of file +} From 738d91787bfe5bec5e8574c7662ce37b09aaadb4 Mon Sep 17 00:00:00 2001 From: Malte Delfs Date: Tue, 15 Jun 2021 17:27:56 +0200 Subject: [PATCH 07/21] changed naming --- pages/availability/event/[type].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/availability/event/[type].tsx b/pages/availability/event/[type].tsx index 916e70c2b6..8dfd709a7f 100644 --- a/pages/availability/event/[type].tsx +++ b/pages/availability/event/[type].tsx @@ -235,7 +235,7 @@ export default function EventType(props) {
- +
From 13d318f4e31a2cceb911ab24d110e6cd044a2ad7 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Tue, 15 Jun 2021 15:41:45 +0000 Subject: [PATCH 08/21] Added migrations for adding external users --- .../migration.sql | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 prisma/migrations/20210615153759_add_email_verification_column/migration.sql diff --git a/prisma/migrations/20210615153759_add_email_verification_column/migration.sql b/prisma/migrations/20210615153759_add_email_verification_column/migration.sql new file mode 100644 index 0000000000..83352168ac --- /dev/null +++ b/prisma/migrations/20210615153759_add_email_verification_column/migration.sql @@ -0,0 +1,20 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "emailVerified" TIMESTAMP(3); + +-- CreateTable +CREATE TABLE "VerificationRequest" ( + "id" SERIAL NOT NULL, + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationRequest.token_unique" ON "VerificationRequest"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationRequest.identifier_token_unique" ON "VerificationRequest"("identifier", "token"); From 68bca9e7034f0d65147f61e9f896f8597cb444f3 Mon Sep 17 00:00:00 2001 From: Malte Delfs Date: Tue, 15 Jun 2021 18:19:00 +0200 Subject: [PATCH 09/21] Implemented a configurable buffer between events --- pages/api/availability/[user].ts | 12 ++++++-- pages/api/availability/day.ts | 6 ++-- pages/availability/index.tsx | 28 +++++++++++++++---- .../migration.sql | 2 ++ prisma/schema.prisma | 3 +- 5 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 prisma/migrations/20210615153546_added_buffer_time/migration.sql diff --git a/pages/api/availability/[user].ts b/pages/api/availability/[user].ts index d3dfd8565d..38f5fe56ce 100644 --- a/pages/api/availability/[user].ts +++ b/pages/api/availability/[user].ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import prisma from '../../../lib/prisma'; import { getBusyTimes } from '../../../lib/calendarClient'; +import dayjs from "dayjs"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { user } = req.query @@ -11,10 +12,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, select: { credentials: true, - timeZone: true + timeZone: true, + bufferTime: true } }); - const availability = await getBusyTimes(currentUser.credentials, req.query.dateFrom, req.query.dateTo); + let availability = await getBusyTimes(currentUser.credentials, req.query.dateFrom, req.query.dateTo); + + availability = availability.map(a => ({ + start: dayjs(a.start).subtract(currentUser.bufferTime, 'minute').toString(), + end: dayjs(a.end).add(currentUser.bufferTime, 'minute').toString() + })); + res.status(200).json(availability); } diff --git a/pages/api/availability/day.ts b/pages/api/availability/day.ts index b11b0400f1..31e0894a90 100644 --- a/pages/api/availability/day.ts +++ b/pages/api/availability/day.ts @@ -13,6 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (req.method == "PATCH") { const startMins = req.body.start; const endMins = req.body.end; + const bufferMins = req.body.buffer; const updateDay = await prisma.user.update({ where: { @@ -20,10 +21,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, data: { startTime: startMins, - endTime: endMins + endTime: endMins, + bufferTime: bufferMins }, }); res.status(200).json({message: 'Start and end times updated successfully'}); } -} \ No newline at end of file +} diff --git a/pages/availability/index.tsx b/pages/availability/index.tsx index d79b5072fb..1b7bbc6766 100644 --- a/pages/availability/index.tsx +++ b/pages/availability/index.tsx @@ -25,6 +25,8 @@ export default function Availability(props) { const startMinsRef = useRef(); const endHoursRef = useRef(); const endMinsRef = useRef(); + const bufferHoursRef = useRef(); + const bufferMinsRef = useRef(); if (loading) { return

Loading...

; @@ -80,15 +82,18 @@ export default function Availability(props) { const enteredStartMins = parseInt(startMinsRef.current.value); const enteredEndHours = parseInt(endHoursRef.current.value); const enteredEndMins = parseInt(endMinsRef.current.value); + const enteredBufferHours = parseInt(bufferHoursRef.current.value); + const enteredBufferMins = parseInt(bufferMinsRef.current.value); const startMins = enteredStartHours * 60 + enteredStartMins; const endMins = enteredEndHours * 60 + enteredEndMins; + const bufferMins = enteredBufferHours * 60 + enteredBufferMins; // TODO: Add validation const response = await fetch('/api/availability/day', { method: 'PATCH', - body: JSON.stringify({start: startMins, end: endMins}), + body: JSON.stringify({start: startMins, end: endMins, buffer: bufferMins}), headers: { 'Content-Type': 'application/json' } @@ -298,7 +303,7 @@ export default function Availability(props) {

- Set the start and end time of your day. + Set the start and end time of your day and a minimum buffer between your meetings.

@@ -316,7 +321,7 @@ export default function Availability(props) {
-
+
@@ -328,6 +333,18 @@ export default function Availability(props) {
+
+ +
+ + +
+ : +
+ + +
+