diff --git a/lib/eventTypeInput.ts b/lib/eventTypeInput.ts
deleted file mode 100644
index e8c76e428c..0000000000
--- a/lib/eventTypeInput.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export enum EventTypeCustomInputType {
- Text = 'text',
- TextLong = 'textLong',
- Number = 'number',
- Bool = 'bool',
-}
-
-export interface EventTypeCustomInput {
- id?: number;
- type: EventTypeCustomInputType;
- label: string;
- required: boolean;
-}
diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx
index df9b19c53d..feb622870f 100644
--- a/pages/[user]/book.tsx
+++ b/pages/[user]/book.tsx
@@ -3,6 +3,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid";
import prisma, { whereAndSelect } from "../../lib/prisma";
+import { EventTypeCustomInputType } from "@prisma/client";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "../../lib/telemetry";
import { useEffect, useState } from "react";
import dayjs from "dayjs";
@@ -13,7 +14,6 @@ import PhoneInput from "react-phone-number-input";
import { LocationType } from "../../lib/location";
import Avatar from "../../components/Avatar";
import Button from "../../components/ui/Button";
-import { EventTypeCustomInputType } from "../../lib/eventTypeInput";
import Theme from "@components/Theme";
import { ReactMultiEmail } from "react-multi-email";
@@ -70,7 +70,7 @@ export default function Book(props: any): JSX.Element {
.map((input) => {
const data = event.target["custom_" + input.id];
if (data) {
- if (input.type === EventTypeCustomInputType.Bool) {
+ if (input.type === EventTypeCustomInputType.BOOL) {
return input.label + "\n" + (data.checked ? "Yes" : "No");
} else {
return input.label + "\n" + data.value;
@@ -272,14 +272,14 @@ export default function Book(props: any): JSX.Element {
.sort((a, b) => a.id - b.id)
.map((input) => (
- {input.type !== EventTypeCustomInputType.Bool && (
+ {input.type !== EventTypeCustomInputType.BOOL && (
)}
- {input.type === EventTypeCustomInputType.TextLong && (
+ {input.type === EventTypeCustomInputType.TEXTLONG && (
)}
- {input.type === EventTypeCustomInputType.Text && (
+ {input.type === EventTypeCustomInputType.TEXT && (
)}
- {input.type === EventTypeCustomInputType.Number && (
+ {input.type === EventTypeCustomInputType.NUMBER && (
)}
- {input.type === EventTypeCustomInputType.Bool && (
+ {input.type === EventTypeCustomInputType.BOOL && (
("horizontal");
- const [contentSize, setContentSize] = useState({ width: 0, height: 0 });
-
- const handleResizeEvent = () => {
- const elementWidth = parseFloat(getComputedStyle(document.body).width);
- const elementHeight = parseFloat(getComputedStyle(document.body).height);
-
- setContentSize({
- width: elementWidth,
- height: elementHeight,
- });
- };
-
- const throttledHandleResizeEvent = throttle(handleResizeEvent, 100);
-
- useEffect(() => {
- handleResizeEvent();
-
- window.addEventListener("resize", throttledHandleResizeEvent);
-
- return () => {
- window.removeEventListener("resize", throttledHandleResizeEvent);
- };
- }, []);
-
- useEffect(() => {
- if (contentSize.width < 500) {
- setDatePickerOrientation("vertical");
- } else {
- setDatePickerOrientation("horizontal");
- }
- }, [contentSize]);
-
- const [enteredAvailability, setEnteredAvailability] = useState();
- const [showLocationModal, setShowLocationModal] = useState(false);
- const [showAddCustomModal, setShowAddCustomModal] = useState(false);
- const [selectedTimeZone, setSelectedTimeZone] = useState("");
- const [selectedLocation, setSelectedLocation] = useState
(undefined);
- const [selectedInputOption, setSelectedInputOption] = useState(inputOptions[0]);
- const [locations, setLocations] = useState(eventType.locations || []);
- const [selectedCustomInput, setSelectedCustomInput] = useState(undefined);
- const [customInputs, setCustomInputs] = useState(
- eventType.customInputs.sort((a, b) => a.id - b.id) || []
- );
-
- const [periodStartDate, setPeriodStartDate] = useState(() => {
- if (eventType.periodType === "range" && eventType?.periodStartDate) {
- return toMomentObject(new Date(eventType.periodStartDate));
- }
-
- return null;
- });
-
- const [periodEndDate, setPeriodEndDate] = useState(() => {
- if (eventType.periodType === "range" && eventType.periodEndDate) {
- return toMomentObject(new Date(eventType?.periodEndDate));
- }
-
- return null;
- });
- const [focusedInput, setFocusedInput] = useState(null);
- const [periodType, setPeriodType] = useState(() => {
- return (
- PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
- PERIOD_TYPES.find((s) => s.type === "unlimited")
- );
- });
-
- const titleRef = useRef();
- const slugRef = useRef();
- const descriptionRef = useRef();
- const lengthRef = useRef();
- const isHiddenRef = useRef();
- const requiresConfirmationRef = useRef();
- const minimumBookingNoticeRef = useRef();
- const eventNameRef = useRef();
- const periodDaysRef = useRef();
- const periodDaysTypeRef = useRef();
-
- useEffect(() => {
- setSelectedTimeZone(eventType.timeZone || user.timeZone);
- }, []);
-
- async function updateEventTypeHandler(event) {
- event.preventDefault();
-
- const enteredTitle: string = titleRef.current.value;
- const enteredSlug: string = slugRef.current.value;
- const enteredDescription: string = descriptionRef.current.value;
- const enteredLength: number = parseInt(lengthRef.current.value);
- const enteredIsHidden: boolean = isHiddenRef.current.checked;
- const enteredMinimumBookingNotice: number = parseInt(minimumBookingNoticeRef.current.value);
- const enteredRequiresConfirmation: boolean = requiresConfirmationRef.current.checked;
- const enteredEventName: string = eventNameRef.current.value;
-
- const type = periodType.type;
- const enteredPeriodDays = parseInt(periodDaysRef?.current?.value);
- const enteredPeriodDaysType = Boolean(parseInt(periodDaysTypeRef?.current.value));
-
- const enteredPeriodStartDate = periodStartDate ? periodStartDate.toDate() : null;
- const enteredPeriodEndDate = periodEndDate ? periodEndDate.toDate() : null;
-
- // TODO: Add validation
-
- const payload: EventTypeInput = {
- id: eventType.id,
- title: enteredTitle,
- slug: enteredSlug,
- description: enteredDescription,
- length: enteredLength,
- hidden: enteredIsHidden,
- locations,
- eventName: enteredEventName,
- customInputs,
- timeZone: selectedTimeZone,
- periodType: type,
- periodDays: enteredPeriodDays,
- periodStartDate: enteredPeriodStartDate,
- periodEndDate: enteredPeriodEndDate,
- periodCountCalendarDays: enteredPeriodDaysType,
- minimumBookingNotice: enteredMinimumBookingNotice,
- requiresConfirmation: enteredRequiresConfirmation,
- };
-
- if (enteredAvailability) {
- payload.availability = enteredAvailability;
- }
-
- await fetch("/api/availability/eventtype", {
- method: "PATCH",
- body: JSON.stringify(payload),
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- router.push("/availability");
- }
-
- async function deleteEventTypeHandler(event) {
- event.preventDefault();
-
- await fetch("/api/availability/eventtype", {
- method: "DELETE",
- body: JSON.stringify({ id: eventType.id }),
- headers: {
- "Content-Type": "application/json",
- },
- });
-
- router.push("/availability");
- }
-
- const openLocationModal = (type: LocationType) => {
- setSelectedLocation(locationOptions.find((option) => option.value === type));
- setShowLocationModal(true);
- };
-
- const closeLocationModal = () => {
- setSelectedLocation(undefined);
- setShowLocationModal(false);
- };
-
- const closeAddCustomModal = () => {
- setSelectedInputOption(inputOptions[0]);
- setShowAddCustomModal(false);
- setSelectedCustomInput(undefined);
- };
-
- const updateLocations = (e) => {
- e.preventDefault();
-
- let details = {};
- if (e.target.location.value === LocationType.InPerson) {
- details = { address: e.target.address.value };
- }
-
- const existingIdx = locations.findIndex((loc) => e.target.location.value === loc.type);
- if (existingIdx !== -1) {
- const copy = locations;
- copy[existingIdx] = { ...locations[existingIdx], ...details };
- setLocations(copy);
- } else {
- setLocations(locations.concat({ type: e.target.location.value, ...details }));
- }
-
- setShowLocationModal(false);
- };
-
- const removeLocation = (selectedLocation) => {
- setLocations(locations.filter((location) => location.type !== selectedLocation.type));
- };
-
- const openEditCustomModel = (customInput: EventTypeCustomInput) => {
- setSelectedCustomInput(customInput);
- setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type));
- setShowAddCustomModal(true);
- };
-
- const LocationOptions = () => {
- if (!selectedLocation) {
- return null;
- }
- switch (selectedLocation.value) {
- case LocationType.InPerson:
- return (
-
-
-
- location.type === LocationType.InPerson)?.address}
- />
-
-
- );
- case LocationType.Phone:
- return (
- Calendso will ask your invitee to enter a phone number before scheduling.
- );
- case LocationType.GoogleMeet:
- return Calendso will provide a Google Meet location.
;
- case LocationType.Zoom:
- return Calendso will provide a Zoom meeting URL.
;
- }
- return null;
- };
-
- const updateCustom = (e) => {
- e.preventDefault();
-
- const customInput: EventTypeCustomInput = {
- label: e.target.label.value,
- required: e.target.required.checked,
- type: e.target.type.value,
- };
-
- if (e.target.id?.value) {
- const index = customInputs.findIndex((inp) => inp.id === +e.target.id?.value);
- if (index >= 0) {
- const input = customInputs[index];
- input.label = customInput.label;
- input.required = customInput.required;
- input.type = customInput.type;
- setCustomInputs(customInputs);
- }
- } else {
- setCustomInputs(customInputs.concat(customInput));
- }
- closeAddCustomModal();
- };
-
- const removeCustom = (customInput, e) => {
- e.preventDefault();
- const index = customInputs.findIndex((inp) => inp.id === customInput.id);
- if (index >= 0) {
- customInputs.splice(index, 1);
- setCustomInputs([...customInputs]);
- }
- };
-
- return (
-
-
-
{eventType.title} | Event Type | Calendso
-
-
-
-
-
-
-
-
-
Delete this event type
-
-
Once you delete this event type, it will be permanently removed.
-
-
-
-
-
-
-
-
- {showLocationModal && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Edit location
-
-
-
-
-
-
-
- )}
- {showAddCustomModal && (
-
-
-
-
-
-
-
-
-
-
-
-
-
- Add new custom input field
-
-
-
- This input will be shown when booking this event
-
-
-
-
-
-
-
-
- )}
-
-
- );
-}
-
-export const getServerSideProps: GetServerSideProps = async ({ req, query }) => {
- const session = await getSession({ req });
- if (!session) {
- return {
- redirect: {
- permanent: false,
- destination: "/auth/login",
- },
- };
- }
-
- const user: User = await prisma.user.findFirst({
- where: {
- email: session.user.email,
- },
- select: {
- username: true,
- timeZone: true,
- startTime: true,
- endTime: true,
- availability: true,
- },
- });
-
- const eventType: EventType | null = await prisma.eventType.findUnique({
- where: {
- id: parseInt(query.type as string),
- },
- select: {
- id: true,
- title: true,
- slug: true,
- description: true,
- length: true,
- hidden: true,
- locations: true,
- eventName: true,
- availability: true,
- customInputs: true,
- timeZone: true,
- periodType: true,
- periodDays: true,
- periodStartDate: true,
- periodEndDate: true,
- periodCountCalendarDays: true,
- requiresConfirmation: true,
- minimumBookingNotice: true,
- },
- });
-
- if (!eventType) {
- return {
- notFound: true,
- };
- }
-
- const credentials = await prisma.credential.findMany({
- where: {
- userId: user.id,
- },
- select: {
- id: true,
- type: true,
- key: true,
- },
- });
-
- const integrations = [
- {
- installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
- enabled: credentials.find((integration) => integration.type === "google_calendar") != null,
- type: "google_calendar",
- title: "Google Calendar",
- imageSrc: "integrations/google-calendar.svg",
- description: "For personal and business accounts",
- },
- {
- installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
- type: "office365_calendar",
- enabled: credentials.find((integration) => integration.type === "office365_calendar") != null,
- title: "Office 365 / Outlook.com Calendar",
- imageSrc: "integrations/outlook.svg",
- description: "For personal and business accounts",
- },
- ];
-
- const locationOptions: OptionBase[] = [
- { value: LocationType.InPerson, label: "In-person meeting" },
- { value: LocationType.Phone, label: "Phone call" },
- { value: LocationType.Zoom, label: "Zoom Video" },
- ];
-
- const hasGoogleCalendarIntegration = integrations.find(
- (i) => i.type === "google_calendar" && i.installed === true && i.enabled
- );
- if (hasGoogleCalendarIntegration) {
- locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
- }
-
- const hasOfficeIntegration = integrations.find(
- (i) => i.type === "office365_calendar" && i.installed === true && i.enabled
- );
- if (hasOfficeIntegration) {
- // TODO: Add default meeting option of the office integration.
- // Assuming it's Microsoft Teams.
- }
-
- const getAvailability = (providesAvailability) =>
- providesAvailability.availability && providesAvailability.availability.length
- ? providesAvailability.availability
- : null;
-
- const availability: Availability[] = getAvailability(eventType) ||
- getAvailability(user) || [
- {
- days: [0, 1, 2, 3, 4, 5, 6],
- startTime: user.startTime,
- endTime: user.endTime,
- },
- ];
-
- availability.sort((a, b) => a.startTime - b.startTime);
-
- const eventTypeObject = Object.assign({}, eventType, {
- periodStartDate: eventType.periodStartDate?.toString() ?? null,
- periodEndDate: eventType.periodEndDate?.toString() ?? null,
- });
-
- return {
- props: {
- user,
- eventType: eventTypeObject,
- locationOptions,
- availability,
- },
- };
-};
diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx
index de360693cd..81071a84f1 100644
--- a/pages/event-types/[type].tsx
+++ b/pages/event-types/[type].tsx
@@ -5,13 +5,13 @@ import { useRouter } from "next/router";
import React, { useEffect, useRef, useState } from "react";
import Select, { OptionBase } from "react-select";
import prisma from "@lib/prisma";
+import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
import { LocationType } from "@lib/location";
import Shell from "@components/Shell";
import { getSession } from "next-auth/client";
import { Scheduler } from "@components/ui/Scheduler";
import { Disclosure, RadioGroup } from "@headlessui/react";
import { PhoneIcon, XIcon } from "@heroicons/react/outline";
-import { EventTypeCustomInput, EventTypeCustomInputType } from "@lib/eventTypeInput";
import {
LocationMarkerIcon,
LinkIcon,
@@ -106,10 +106,10 @@ export default function EventTypePage({
const router = useRouter();
const inputOptions: OptionBase[] = [
- { value: EventTypeCustomInputType.Text, label: "Text" },
- { value: EventTypeCustomInputType.TextLong, label: "Multiline Text" },
- { value: EventTypeCustomInputType.Number, label: "Number" },
- { value: EventTypeCustomInputType.Bool, label: "Checkbox" },
+ { value: EventTypeCustomInputType.TEXT, label: "Text" },
+ { value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" },
+ { value: EventTypeCustomInputType.NUMBER, label: "Number" },
+ { value: EventTypeCustomInputType.BOOL, label: "Checkbox" },
];
const [DATE_PICKER_ORIENTATION, setDatePickerOrientation] = useState("horizontal");
@@ -342,28 +342,19 @@ export default function EventTypePage({
type: e.target.type.value,
};
- if (e.target.id?.value) {
- const index = customInputs.findIndex((inp) => inp.id === +e.target.id?.value);
- if (index >= 0) {
- const input = customInputs[index];
- input.label = customInput.label;
- input.required = customInput.required;
- input.type = customInput.type;
- setCustomInputs(customInputs);
- }
+ if (selectedCustomInput) {
+ selectedCustomInput.label = customInput.label;
+ selectedCustomInput.required = customInput.required;
+ selectedCustomInput.type = customInput.type;
} else {
setCustomInputs(customInputs.concat(customInput));
}
closeAddCustomModal();
};
- const removeCustom = (customInput, e) => {
- e.preventDefault();
- const index = customInputs.findIndex((inp) => inp.id === customInput.id);
- if (index >= 0) {
- customInputs.splice(index, 1);
- setCustomInputs([...customInputs]);
- }
+ const removeCustom = (index: number) => {
+ customInputs.splice(index, 1);
+ setCustomInputs([...customInputs]);
};
return (
@@ -638,8 +629,8 @@ export default function EventTypePage({
- {customInputs.map((customInput) => (
- -
+ {customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
+
-
@@ -661,7 +652,7 @@ export default function EventTypePage({
className="mr-2 text-sm text-primary-600">
Edit
-
diff --git a/prisma/migrations/20210814175645_custom_inputs_type_enum/migration.sql b/prisma/migrations/20210814175645_custom_inputs_type_enum/migration.sql
new file mode 100644
index 0000000000..614c2c30d5
--- /dev/null
+++ b/prisma/migrations/20210814175645_custom_inputs_type_enum/migration.sql
@@ -0,0 +1,14 @@
+
+-- CreateEnum
+CREATE TYPE "EventTypeCustomInputType" AS ENUM ('text', 'textLong', 'number', 'bool');
+
+--- AlterTable
+ALTER TABLE "EventTypeCustomInput" RENAME COLUMN "type" TO "type_old";
+ALTER TABLE "EventTypeCustomInput" ADD COLUMN "type" "EventTypeCustomInputType";
+
+-- UpdateTable
+UPDATE "EventTypeCustomInput" SET "type" = CAST( "type_old" AS "EventTypeCustomInputType" );
+
+-- AlterTable
+ALTER TABLE "EventTypeCustomInput" ALTER COLUMN "type" SET NOT NULL;
+ALTER TABLE "EventTypeCustomInput" DROP COLUMN "type_old";
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 02bc0bc6e6..ca51aa8158 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -163,12 +163,19 @@ model SelectedCalendar {
@@id([userId,integration,externalId])
}
+enum EventTypeCustomInputType {
+ TEXT @map("text")
+ TEXTLONG @map("textLong")
+ NUMBER @map("number")
+ BOOL @map("bool")
+}
+
model EventTypeCustomInput {
id Int @id @default(autoincrement())
eventTypeId Int
eventType EventType @relation(fields: [eventTypeId], references: [id])
label String
- type String
+ type EventTypeCustomInputType
required Boolean
}