Fixes eventype form (#777)

* Type fixes

* Uses all integrations and session fixes on getting started page

* eventtype form fixes

* Update pages/event-types/[type].tsx

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Omar López 2021-09-26 15:49:16 -06:00 committed by GitHub
parent b23c032a4c
commit 7ab49acebe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 88 additions and 103 deletions

View file

@ -2,6 +2,18 @@ export function asStringOrNull(str: unknown) {
return typeof str === "string" ? str : null; return typeof str === "string" ? str : null;
} }
export function asStringOrUndefined(str: unknown) {
return typeof str === "string" ? str : undefined;
}
export function asNumberOrUndefined(str: unknown) {
return typeof str === "string" ? parseInt(str) : undefined;
}
export function asNumberOrThrow(str: unknown) {
return parseInt(asStringOrThrow(str));
}
export function asStringOrThrow(str: unknown): string { export function asStringOrThrow(str: unknown): string {
const type = typeof str; const type = typeof str;
if (type !== "string") { if (type !== "string") {

View file

@ -3,8 +3,12 @@ import { EventType } from "@prisma/client";
import * as fetch from "@lib/core/http/fetch-wrapper"; import * as fetch from "@lib/core/http/fetch-wrapper";
import { EventTypeInput } from "@lib/types/event-type"; import { EventTypeInput } from "@lib/types/event-type";
type EventTypeResponse = {
eventType: EventType;
};
const updateEventType = async (data: EventTypeInput) => { const updateEventType = async (data: EventTypeInput) => {
const response = await fetch.patch<EventTypeInput, EventType>("/api/availability/eventtype", data); const response = await fetch.patch<EventTypeInput, EventTypeResponse>("/api/availability/eventtype", data);
return response; return response;
}; };

View file

@ -20,6 +20,16 @@ export type AdvancedOptions = {
periodEndDate?: Date | string; periodEndDate?: Date | string;
periodCountCalendarDays?: boolean; periodCountCalendarDays?: boolean;
requiresConfirmation?: boolean; requiresConfirmation?: boolean;
disableGuests?: boolean;
minimumBookingNotice?: number;
price?: number;
currency?: string;
schedulingType?: SchedulingType;
users?: {
value: number;
label: string;
avatar: string;
}[];
}; };
export type EventTypeCustomInput = { export type EventTypeCustomInput = {

View file

@ -89,7 +89,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
periodStartDate: req.body.periodStartDate, periodStartDate: req.body.periodStartDate,
periodEndDate: req.body.periodEndDate, periodEndDate: req.body.periodEndDate,
periodCountCalendarDays: req.body.periodCountCalendarDays, periodCountCalendarDays: req.body.periodCountCalendarDays,
minimumBookingNotice: req.body.minimumBookingNotice, minimumBookingNotice: req.body.minimumBookingNotice
? parseInt(req.body.minimumBookingNotice)
: undefined,
price: req.body.price, price: req.body.price,
currency: req.body.currency, currency: req.body.currency,
}; };

View file

@ -2,18 +2,18 @@
import { Disclosure, RadioGroup } from "@headlessui/react"; import { Disclosure, RadioGroup } from "@headlessui/react";
import { PhoneIcon, XIcon } from "@heroicons/react/outline"; import { PhoneIcon, XIcon } from "@heroicons/react/outline";
import { import {
LocationMarkerIcon,
LinkIcon,
PlusIcon,
DocumentIcon,
ChevronRightIcon, ChevronRightIcon,
ClockIcon, ClockIcon,
TrashIcon, DocumentIcon,
ExternalLinkIcon, ExternalLinkIcon,
UsersIcon, LinkIcon,
LocationMarkerIcon,
PlusIcon,
TrashIcon,
UserAddIcon, UserAddIcon,
UsersIcon,
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import { EventTypeCustomInput, EventTypeCustomInputType, SchedulingType } from "@prisma/client"; import { EventTypeCustomInput, EventTypeCustomInputType, Prisma, SchedulingType } from "@prisma/client";
import dayjs from "dayjs"; import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone"; import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
@ -28,9 +28,10 @@ import "react-dates/lib/css/_datepicker.css";
import { FormattedNumber, IntlProvider } from "react-intl"; import { FormattedNumber, IntlProvider } from "react-intl";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
import Select, { OptionTypeBase } from "react-select"; import Select, { OptionTypeBase } from "react-select";
import Stripe from "stripe";
import { asStringOrThrow } from "@lib/asStringOrNull"; import { StripeData } from "@ee/lib/stripe/server";
import { asNumberOrThrow, asNumberOrUndefined, asStringOrThrow } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
@ -144,7 +145,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
} }
}, [contentSize]); }, [contentSize]);
const [users, setUsers] = useState([]); const [users, setUsers] = useState<AdvancedOptions["users"]>([]);
const [enteredAvailability, setEnteredAvailability] = useState(); const [enteredAvailability, setEnteredAvailability] = useState();
const [showLocationModal, setShowLocationModal] = useState(false); const [showLocationModal, setShowLocationModal] = useState(false);
const [showAddCustomModal, setShowAddCustomModal] = useState(false); const [showAddCustomModal, setShowAddCustomModal] = useState(false);
@ -184,12 +185,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const [hidden, setHidden] = useState<boolean>(eventType.hidden); const [hidden, setHidden] = useState<boolean>(eventType.hidden);
const titleRef = useRef<HTMLInputElement>(null); const titleRef = useRef<HTMLInputElement>(null);
const slugRef = useRef<HTMLInputElement>(null);
const requiresConfirmationRef = useRef<HTMLInputElement>(null);
const eventNameRef = useRef<HTMLInputElement>(null); const eventNameRef = useRef<HTMLInputElement>(null);
const periodDaysRef = useRef<HTMLInputElement>(null); const isAdvancedSettingsVisible = !!eventNameRef.current;
const periodDaysTypeRef = useRef<HTMLSelectElement>(null);
const priceRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
setSelectedTimeZone(eventType.timeZone); setSelectedTimeZone(eventType.timeZone);
@ -200,45 +197,47 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const formData = Object.fromEntries(new FormData(event.target).entries()); const formData = Object.fromEntries(new FormData(event.target).entries());
const enteredTitle: string = titleRef.current.value; const enteredTitle: string = titleRef.current!.value;
const enteredSlug: string = slugRef.current.value;
const enteredPrice = requirePayment ? Math.round(parseFloat(priceRef.current.value) * 100) : 0;
const advancedOptionsPayload: AdvancedOptions = {}; const advancedPayload: AdvancedOptions = {};
if (requiresConfirmationRef.current) { if (isAdvancedSettingsVisible) {
advancedOptionsPayload.eventName = eventNameRef.current.value; advancedPayload.eventName = eventNameRef.current.value;
advancedOptionsPayload.periodType = periodType.type; advancedPayload.periodType = periodType?.type;
advancedOptionsPayload.periodDays = parseInt(periodDaysRef?.current?.value); advancedPayload.periodDays = asNumberOrUndefined(formData.periodDays);
advancedOptionsPayload.periodCountCalendarDays = Boolean(parseInt(periodDaysTypeRef?.current.value)); advancedPayload.periodCountCalendarDays = Boolean(
advancedOptionsPayload.periodStartDate = periodStartDate ? periodStartDate.toDate() : null; asNumberOrUndefined(formData.periodCountCalendarDays)
advancedOptionsPayload.periodEndDate = periodEndDate ? periodEndDate.toDate() : null; );
advancedPayload.periodStartDate = periodStartDate ? periodStartDate.toDate() : undefined;
advancedPayload.periodEndDate = periodEndDate ? periodEndDate.toDate() : undefined;
advancedPayload.minimumBookingNotice = asNumberOrUndefined(formData.minimumBookingNotice);
// prettier-ignore
advancedPayload.price =
!requirePayment ? undefined :
formData.price ? Math.round(parseFloat(asStringOrThrow(formData.price)) * 100) :
/* otherwise */ 0;
advancedPayload.currency = currency;
} }
const payload: EventTypeInput = { const payload: EventTypeInput = {
id: eventType.id, id: eventType.id,
title: enteredTitle, title: enteredTitle,
slug: enteredSlug, slug: asStringOrThrow(formData.slug),
description: formData.description as string, description: asStringOrThrow(formData.description),
// note(zomars) Why does this field doesnt need to be parsed... length: asNumberOrThrow(formData.length),
length: formData.length as unknown as number,
// note(zomars) ...But this does? (Is being sent as string, despite it's a number field)
minimumBookingNotice: parseInt(formData.minimumBookingNotice as unknown as string),
requiresConfirmation: formData.requiresConfirmation === "on", requiresConfirmation: formData.requiresConfirmation === "on",
disableGuests: formData.disableGuests === "on", disableGuests: formData.disableGuests === "on",
hidden, hidden,
locations, locations,
customInputs, customInputs,
timeZone: selectedTimeZone, timeZone: selectedTimeZone,
availability: enteredAvailability || null, availability: enteredAvailability || undefined,
...advancedOptionsPayload, ...advancedPayload,
...(team ...(team
? { ? {
schedulingType: formData.schedulingType as string, schedulingType: formData.schedulingType as SchedulingType,
users, users,
} }
: {}), : {}),
price: enteredPrice,
currency: currency,
}; };
updateMutation.mutate(payload); updateMutation.mutate(payload);
@ -411,7 +410,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
{team ? "team/" + team.slug : eventType.users[0].username}/ {team ? "team/" + team.slug : eventType.users[0].username}/
</span> </span>
<input <input
ref={slugRef}
type="text" type="text"
name="slug" name="slug"
id="slug" id="slug"
@ -727,7 +725,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</div> </div>
<CheckboxField <CheckboxField
ref={requiresConfirmationRef}
id="requiresConfirmation" id="requiresConfirmation"
name="requiresConfirmation" name="requiresConfirmation"
label="Opt-in booking" label="Opt-in booking"
@ -800,7 +797,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
{period.type === "rolling" && ( {period.type === "rolling" && (
<div className="inline-flex"> <div className="inline-flex">
<input <input
ref={periodDaysRef}
type="text" type="text"
name="periodDays" name="periodDays"
id="" id=""
@ -809,7 +805,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
defaultValue={eventType.periodDays || 30} defaultValue={eventType.periodDays || 30}
/> />
<select <select
ref={periodDaysTypeRef}
id="" id=""
name="periodDaysType" name="periodDaysType"
className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
@ -924,7 +919,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="w-full"> <div className="w-full">
<div className="mt-1 relative rounded-sm shadow-sm"> <div className="mt-1 relative rounded-sm shadow-sm">
<input <input
ref={priceRef}
type="number" type="number"
name="price" name="price"
id="price" id="price"
@ -1200,6 +1194,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}; };
} }
const userSelect = Prisma.validator<Prisma.UserSelect>()({
name: true,
id: true,
avatar: true,
email: true,
});
const eventType = await prisma.eventType.findFirst({ const eventType = await prisma.eventType.findFirst({
where: { where: {
AND: [ AND: [
@ -1251,24 +1252,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}, },
select: { select: {
user: { user: {
select: { select: userSelect,
name: true,
id: true,
avatar: true,
email: true,
},
}, },
}, },
}, },
}, },
}, },
users: { users: {
select: { select: userSelect,
name: true,
id: true,
avatar: true,
username: true,
},
}, },
schedulingType: true, schedulingType: true,
userId: true, userId: true,
@ -1284,17 +1275,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
} }
// backwards compat // backwards compat
if (eventType.users.length === 0) { if (eventType.users.length === 0 && !eventType.team) {
eventType.users.push( const fallbackUser = await prisma.user.findUnique({
await prisma.user.findUnique({ where: {
where: { id: session.user.id,
id: session.user.id, },
}, select: userSelect,
select: { });
username: true, if (!fallbackUser) throw Error("The event type doesn't have user and no fallback user was found");
}, eventType.users.push(fallbackUser);
})
);
} }
const credentials = await prisma.credential.findMany({ const credentials = await prisma.credential.findMany({
@ -1321,7 +1310,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" }); locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
} }
const currency = const currency =
(credentials.find((integration) => integration.type === "stripe_payment")?.key as Stripe.OAuthToken) (credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData)
?.default_currency || "usd"; ?.default_currency || "usd";
if (hasIntegration(integrations, "office365_calendar")) { if (hasIntegration(integrations, "office365_calendar")) {

View file

@ -24,7 +24,7 @@ import { getSession } from "@lib/auth";
import AddCalDavIntegration, { import AddCalDavIntegration, {
ADD_CALDAV_INTEGRATION_FORM_TITLE, ADD_CALDAV_INTEGRATION_FORM_TITLE,
} from "@lib/integrations/CalDav/components/AddCalDavIntegration"; } from "@lib/integrations/CalDav/components/AddCalDavIntegration";
import { validJson } from "@lib/jsonUtils"; import getIntegrations from "@lib/integrations/getIntegrations";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@components/Dialog"; import { Dialog, DialogClose, DialogContent, DialogHeader } from "@components/Dialog";
@ -688,40 +688,7 @@ export async function getServerSideProps(context: NextPageContext) {
}, },
}); });
integrations = [ integrations = getIntegrations(credentials);
{
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
credential: credentials.find((integration) => integration.type === "google_calendar") || null,
type: "google_calendar",
title: "Google Calendar",
imageSrc: "integrations/google-calendar.svg",
description: "Gmail, G Suite",
},
{
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
credential: credentials.find((integration) => integration.type === "office365_calendar") || null,
type: "office365_calendar",
title: "Office 365 Calendar",
imageSrc: "integrations/outlook.svg",
description: "Office 365, Outlook.com, live.com, or hotmail calendar",
},
{
installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
credential: credentials.find((integration) => integration.type === "zoom_video") || null,
type: "zoom_video",
title: "Zoom",
imageSrc: "integrations/zoom.svg",
description: "Video Conferencing",
},
{
installed: true,
credential: credentials.find((integration) => integration.type === "caldav_calendar") || null,
type: "caldav_calendar",
title: "Caldav",
imageSrc: "integrations/caldav.svg",
description: "CalDav Server",
},
];
eventTypes = await prisma.eventType.findMany({ eventTypes = await prisma.eventType.findMany({
where: { where: {
@ -748,6 +715,7 @@ export async function getServerSideProps(context: NextPageContext) {
return { return {
props: { props: {
session,
user, user,
integrations, integrations,
eventTypes, eventTypes,