Merge remote-tracking branch 'origin/main' into chore/add-otel-new

perf/get-slots
Keith Williams 2023-08-08 12:52:33 +01:00
commit 022cfe6ab2
16 changed files with 1974 additions and 923 deletions

View File

@ -2,7 +2,6 @@ import type { NextApiRequest } from "next";
import { z } from "zod";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { availabilityUserSelect } from "@calcom/prisma";
@ -120,10 +119,8 @@ const availabilitySchema = z
async function handler(req: NextApiRequest) {
const { prisma, isAdmin, userId: reqUserId } = req;
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
if (!teamId)
return getUserAvailability({
orgSlug: isValidOrgDomain ? currentOrgDomain || undefined : undefined,
username,
dateFrom,
dateTo,

View File

@ -1,4 +1,5 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import type { Session } from "next-auth";
import { SessionProvider } from "next-auth/react";
import { useSession } from "next-auth/react";
import { EventCollectionProvider } from "next-collect/client";
@ -33,6 +34,7 @@ export type AppProps = Omit<
NextAppProps<
WithNonceProps & {
themeBasis?: string;
session: Session;
} & Record<string, unknown>
>,
"Component"
@ -81,6 +83,7 @@ const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
},
router: locale ? { locale } : props.router,
} as unknown as ComponentProps<typeof I18nextAdapter>;
return <I18nextAdapter {...passedProps} />;
};
@ -243,7 +246,6 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) {
}
const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.viewer.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const { pageProps, ...rest } = props;
@ -257,7 +259,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider session={session || undefined}>
<SessionProvider session={pageProps.session ?? undefined}>
<CustomI18nextProvider {...propsWithoutNonce}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}

View File

@ -11,58 +11,62 @@ test.afterEach(({ users }) => users.deleteAll());
test.describe("Workflow tests", () => {
test.describe("User Workflows", () => {
test("Create default reminder workflow & trigger when event type is booked", async ({ page, users }) => {
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await page.goto(`/workflows`);
// Fixme: This test is failing because the listing isn't immediately updated after the workflow is created
test.fixme(
"Create default reminder workflow & trigger when event type is booked",
async ({ page, users }) => {
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await page.goto(`/workflows`);
await page.click('[data-testid="create-button"]');
await page.click('[data-testid="create-button"]');
// select first event type
await page.getByText("Select...").click();
await page.getByText(eventType.title, { exact: true }).click();
// select first event type
await page.getByText("Select...").click();
await page.getByText(eventType.title, { exact: true }).click();
// name workflow
await page.fill('[data-testid="workflow-name"]', "Test workflow");
// name workflow
await page.fill('[data-testid="workflow-name"]', "Test workflow");
// save workflow
await page.click('[data-testid="save-workflow"]');
// save workflow
await page.click('[data-testid="save-workflow"]');
// check if workflow is saved
await expect(page.locator('[data-testid="workflow-list"] > li')).toHaveCount(1);
// check if workflow is saved
await expect(page.locator('[data-testid="workflow-list"] > li')).toHaveCount(1);
// book event type
await page.goto(`/${user.username}/${eventType.slug}`);
await selectSecondAvailableTimeSlotNextMonth(page);
// book event type
await page.goto(`/${user.username}/${eventType.slug}`);
await selectSecondAvailableTimeSlotNextMonth(page);
await page.fill('[name="name"]', "Test");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
await page.fill('[name="name"]', "Test");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
// Make sure booking is completed
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
// Make sure booking is completed
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const booking = await prisma.booking.findFirst({
where: {
eventTypeId: eventType.id,
},
});
const booking = await prisma.booking.findFirst({
where: {
eventTypeId: eventType.id,
},
});
// check if workflow triggered
const workflowReminders = await prisma.workflowReminder.findMany({
where: {
bookingUid: booking?.uid ?? "",
},
});
// check if workflow triggered
const workflowReminders = await prisma.workflowReminder.findMany({
where: {
bookingUid: booking?.uid ?? "",
},
});
expect(workflowReminders).toHaveLength(1);
expect(workflowReminders).toHaveLength(1);
const scheduledDate = dayjs(booking?.startTime).subtract(1, "day").toDate();
const scheduledDate = dayjs(booking?.startTime).subtract(1, "day").toDate();
expect(workflowReminders[0].method).toBe(WorkflowMethods.EMAIL);
expect(workflowReminders[0].scheduledDate.toISOString()).toBe(scheduledDate.toISOString());
});
expect(workflowReminders[0].method).toBe(WorkflowMethods.EMAIL);
expect(workflowReminders[0].scheduledDate.toISOString()).toBe(scheduledDate.toISOString());
}
);
// add all other actions to this workflow and test if they triggered
// cancel booking and test if workflow reminders are deleted

View File

@ -25,6 +25,7 @@
"email_verification_code_placeholder": "Geben Sie den Bestätigungscode ein, den Sie per E-Mail erhalten haben",
"incorrect_email_verification_code": "Der Bestätigungscode ist falsch.",
"email_sent": "E-Mail erfolgreich gesendet",
"email_not_sent": "Fehler beim Senden der E-Mail aufgetreten",
"event_declined_subject": "Abgelehnt: {{title}} am {{date}}",
"event_cancelled_subject": "Abgesagt: {{title}} am {{date}}",
"event_request_declined": "Ihre Event-Anfrage wurde abgelehnt",
@ -83,6 +84,7 @@
"event_awaiting_approval_recurring": "Ein wiederkehrender Termin wartet auf Ihre Bestätigung",
"someone_requested_an_event": "Jemand hat eine Terminanfrage gestellt.",
"someone_requested_password_reset": "Jemand hat einen Link angefordert, um Ihr Passwort zu ändern.",
"password_reset_email_sent": "Wenn diese E-Mail-Adresse in unserem System existiert, sollten Sie eine Zurücksetzungs-E-Mail erhalten.",
"password_reset_instructions": "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail sicher ignorieren und Ihr Passwort wird nicht geändert.",
"event_awaiting_approval_subject": "Warte auf Bestätigung: {{title}} um {{date}}",
"event_still_awaiting_approval": "Ein Termin wartet noch auf Ihre Bestätigung",
@ -222,6 +224,9 @@
"already_have_an_account": "Haben Sie bereits ein Konto?",
"create_account": "Konto erstellen",
"confirm_password": "Passwort bestätigen",
"confirm_auth_change": "Dies ändert die Art und Weise, wie Sie sich anmelden",
"reset_your_password": "Legen Sie Ihr neues Passwort mit den Anweisungen fest, die an Ihre E-Mail-Adresse gesendet wurden.",
"email_change": "Melden Sie sich mit Ihrer neuen E-Mail-Adresse und Ihrem Passwort wieder an.",
"create_your_account": "Erstellen Sie Ihr Konto",
"sign_up": "Registrieren",
"youve_been_logged_out": "Sie wurden abgemeldet",
@ -851,6 +856,7 @@
"team_view_user_availability_disabled": "Der Benutzer muss die Einladung annehmen, um freie Termine zu sehen",
"set_as_away": "Stelle deinen Status auf Abwesend",
"set_as_free": "Abwesenheitsstatus deaktivieren",
"toggle_away_error": "Fehler beim Aktualisieren des Abwesenheitsstatus",
"user_away": "Dieser Benutzer ist derzeit abwesend.",
"user_away_description": "Die Person, mit der Sie einen Termin buchen wollen, ist zur Zeit abwesend und akzeptiert daher keine neuen Buchungen.",
"meet_people_with_the_same_tokens": "Triff Leute mit den selben Token",
@ -1965,5 +1971,10 @@
"org_team_names_example_5": "z.B. Data Analytics Team",
"org_max_team_warnings": "Weitere Teams können Sie zu einem späteren Zeitpunkt hinzufügen.",
"what_is_this_meeting_about": "Worum geht es in diesem Termin?",
"org_admin_other_teams": "Weitere Teams",
"no_other_teams_found": "Keine weiteren Teams gefunden",
"no_other_teams_found_description": "Es gibt keine weiteren Teams in dieser Organisation.",
"attendee_first_name_variable": "Vorname des Teilnehmers",
"attendee_last_name_variable": "Nachname des Teilnehmers",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Zeichenketten über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -3,10 +3,7 @@ import { intersect } from "@calcom/lib/date-ranges";
import { SchedulingType } from "@calcom/prisma/enums";
export const getAggregatedAvailability = (
userAvailability: (Omit<
Awaited<ReturnType<Awaited<typeof import("./getUserAvailability")>["getUserAvailability"]>>,
"currentSeats"
> & { user?: { isFixed?: boolean } })[],
userAvailability: { dateRanges: DateRange[]; user?: { isFixed?: boolean } }[],
schedulingType: SchedulingType | null
): DateRange[] => {
const fixedHosts = userAvailability.filter(

View File

@ -90,8 +90,11 @@ export default function CalComAdapter(prismaClient: PrismaClient) {
return { user, session };
},
createSession: (data: Prisma.SessionCreateInput) => prismaClient.session.create({ data }),
updateSession: (data: Prisma.SessionWhereUniqueInput) =>
prismaClient.session.update({ where: { sessionToken: data.sessionToken }, data }),
updateSession: (data: Prisma.SessionUpdateInput) =>
prismaClient.session.update({
where: { sessionToken: typeof data.sessionToken === "string" ? data.sessionToken : undefined },
data,
}),
deleteSession: (sessionToken: string) => prismaClient.session.delete({ where: { sessionToken } }),
};
}

View File

@ -11,9 +11,11 @@ import z from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
import type { LocationObject } from "@calcom/app-store/locations";
import { OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations";
import { getLocationValueForDB } from "@calcom/app-store/locations";
import { MeetLocationType } from "@calcom/app-store/locations";
import {
getLocationValueForDB,
MeetLocationType,
OrganizerDefaultConferencingAppType,
} from "@calcom/app-store/locations";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import { getAppFromSlug } from "@calcom/app-store/utils";
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
@ -26,8 +28,8 @@ import {
sendAttendeeRequestEmail,
sendOrganizerRequestEmail,
sendRescheduledEmails,
sendScheduledEmails,
sendRescheduledSeatEmail,
sendScheduledEmails,
sendScheduledSeatsEmails,
} from "@calcom/emails";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
@ -38,8 +40,8 @@ import {
allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import {
scheduleWorkflowReminders,
cancelWorkflowReminders,
scheduleWorkflowReminders,
} from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { getFullName } from "@calcom/features/form-builder/utils";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
@ -47,7 +49,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import getIP from "@calcom/lib/getIP";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
@ -65,9 +67,9 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { userSelect } from "@calcom/prisma";
import type { BookingReference } from "@calcom/prisma/client";
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
import {
bookingCreateBodySchemaForApi,
bookingCreateSchemaLegacyPropsForApi,
customInputSchema,
EventTypeMetaDataSchema,
extendedBookingCreateBody,
@ -653,9 +655,7 @@ async function handler(
const tAttendees = await getTranslation(language ?? "en", "common");
const tGuests = await getTranslation("en", "common");
log.debug(`Booking eventType ${eventTypeId} started`);
const dynamicUserList = Array.isArray(reqBody.user)
? getGroupName(reqBody.user)
: getUsernameList(reqBody.user);
const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user);
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
const isTeamEventType =

View File

@ -40,11 +40,7 @@ interface handleChildrenEventTypesProps {
};
}[]
| undefined;
prisma: PrismaClient<
Prisma.PrismaClientOptions,
never,
Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined
>;
prisma: PrismaClient;
}
const sendAllSlugReplacementEmails = async (

View File

@ -93,7 +93,7 @@ export function buildDateRanges({
...groupedDateOverrides,
}).map(
// remove 0-length overrides that were kept to cancel out working dates until now.
(ranges) => ranges.filter((range) => !range.start.isSame(range.end))
(ranges) => ranges.filter((range) => range.start.valueOf() !== range.end.valueOf())
);
return dateRanges.flat();

View File

@ -18,11 +18,7 @@ import { TRPCError } from "@trpc/server";
interface getEventTypeByIdProps {
eventTypeId: number;
userId: number;
prisma: PrismaClient<
Prisma.PrismaClientOptions,
never,
Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined
>;
prisma: PrismaClient;
isTrpcCall?: boolean;
}

View File

@ -24,9 +24,9 @@
},
"dependencies": {
"@calcom/lib": "*",
"@prisma/client": "^4.16.0",
"@prisma/generator-helper": "^4.13.0",
"prisma": "^4.16.0",
"@prisma/client": "^5.0.0",
"@prisma/generator-helper": "^5.0.0",
"prisma": "^5.0.0",
"ts-node": "^10.9.1",
"zod": "^3.20.2",
"zod-prisma": "^0.5.4"

View File

@ -8,7 +8,7 @@ datasource db {
generator client {
provider = "prisma-client-js"
previewFeatures = ["views", "jsonProtocol"]
previewFeatures = ["views"]
}
generator zod {

View File

@ -1,7 +1,6 @@
import { countBy } from "lodash";
import { v4 as uuid } from "uuid";
import { getAggregateWorkingHours } from "@calcom/core/getAggregateWorkingHours";
import { getAggregatedAvailability } from "@calcom/core/getAggregatedAvailability";
import type { CurrentSeats } from "@calcom/core/getUserAvailability";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
@ -150,7 +149,11 @@ export async function getDynamicEventType(input: TGetScheduleInputSchema) {
const users = await prisma.user.findMany({
where: {
username: {
in: input.usernameList,
in: Array.isArray(input.usernameList)
? input.usernameList
: input.usernameList
? [input.usernameList]
: [],
},
},
select: {
@ -226,8 +229,6 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
);
const {
busy,
workingHours,
dateOverrides,
dateRanges,
currentSeats: _currentSeats,
timeZone,
@ -253,8 +254,6 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
userAvailabilitySpan.end();
return {
timeZone,
workingHours,
dateOverrides,
dateRanges,
busy,
user: currentUser,
@ -298,8 +297,6 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
const timeSlots = getSlots({
inviteeDate: startTime,
eventLength: input.duration || eventType.length,
workingHours,
dateOverrides,
offsetStart: eventType.offsetStart,
dateRanges: getAggregatedAvailability(userAvailability, eventType.schedulingType),
minimumBookingNotice: eventType.minimumBookingNotice,

View File

@ -62,7 +62,7 @@ export const DefaultToast = ({ message, toastVisible, onClose, toastId }: IToast
<span>
<Check className="h-4 w-4" />
</span>
<p>{message}</p>
<p data-testid="toast-default">{message}</p>
</button>
);

View File

@ -0,0 +1,33 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { vi } from "vitest";
import { SuccessToast, ErrorToast, WarningToast, DefaultToast } from "./showToast";
describe("Tests for Toast Components", () => {
const testToastComponent = (Component: typeof DefaultToast, toastTestId: string) => {
const message = "This is a test message";
const toastId = "some-id";
const onCloseMock = vi.fn();
render(<Component message={message} toastVisible={true} onClose={onCloseMock} toastId={toastId} />);
const toast = screen.getByTestId(toastTestId);
expect(toast).toBeInTheDocument();
expect(toast.textContent).toContain(message);
const closeButton = screen.getByRole("button");
fireEvent.click(closeButton);
expect(onCloseMock).toHaveBeenCalledWith(toastId);
};
const toastComponents: [string, typeof DefaultToast, string][] = [
["SuccessToast", SuccessToast, "toast-success"],
["ErrorToast", ErrorToast, "toast-error"],
["WarningToast", WarningToast, "toast-warning"],
["DefaultToast", DefaultToast, "toast-default"],
];
test.each(toastComponents)("Should render and close %s component", (_, ToastComponent, expectedClass) => {
testToastComponent(ToastComponent, expectedClass);
});
});

2693
yarn.lock

File diff suppressed because it is too large Load Diff