Merge remote-tracking branch 'origin/main' into chore/update-storybook
This commit is contained in:
commit
a241bcb119
|
@ -33,12 +33,14 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
|||
position: true,
|
||||
eventName: true,
|
||||
timeZone: true,
|
||||
schedulingType: true,
|
||||
// START Limit future bookings
|
||||
periodType: true,
|
||||
periodStartDate: true,
|
||||
schedulingType: true,
|
||||
periodEndDate: true,
|
||||
periodDays: true,
|
||||
periodCountCalendarDays: true,
|
||||
// END Limit future bookings
|
||||
requiresConfirmation: true,
|
||||
disableGuests: true,
|
||||
hideCalendarNotes: true,
|
||||
|
@ -51,6 +53,8 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
|||
slotInterval: true,
|
||||
successRedirectUrl: true,
|
||||
locations: true,
|
||||
bookingLimits: true,
|
||||
durationLimits: true,
|
||||
})
|
||||
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
|
||||
.partial()
|
||||
|
@ -126,6 +130,8 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
|||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
locations: z
|
||||
|
|
|
@ -13,16 +13,18 @@ const schemaMembershipRequiredParams = z.object({
|
|||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export const membershipCreateBodySchema = Membership.partial({
|
||||
accepted: true,
|
||||
role: true,
|
||||
disableImpersonation: true,
|
||||
}).transform((v) => ({
|
||||
accepted: false,
|
||||
role: MembershipRole.MEMBER,
|
||||
disableImpersonation: false,
|
||||
...v,
|
||||
}));
|
||||
export const membershipCreateBodySchema = Membership.omit({ id: true })
|
||||
.partial({
|
||||
accepted: true,
|
||||
role: true,
|
||||
disableImpersonation: true,
|
||||
})
|
||||
.transform((v) => ({
|
||||
accepted: false,
|
||||
role: MembershipRole.MEMBER,
|
||||
disableImpersonation: false,
|
||||
...v,
|
||||
}));
|
||||
|
||||
export const membershipEditBodySchema = Membership.omit({
|
||||
/** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { withValidation } from "next-validations";
|
||||
import { z } from "zod";
|
||||
|
||||
import { baseApiParams } from "./baseApiParams";
|
||||
|
||||
// Extracted out as utility function so can be reused
|
||||
// at different endpoints that require this validation.
|
||||
export const schemaQueryUserEmail = baseApiParams.extend({
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export const schemaQuerySingleOrMultipleUserEmails = z.object({
|
||||
email: z.union([z.string().email(), z.array(z.string().email())]),
|
||||
});
|
||||
|
||||
export const withValidQueryUserEmail = withValidation({
|
||||
schema: schemaQueryUserEmail,
|
||||
type: "Zod",
|
||||
mode: "query",
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { z } from "zod";
|
||||
|
||||
|
@ -202,10 +202,17 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
|
|||
export async function patchHandler(req: NextApiRequest) {
|
||||
const { prisma, query, body } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const { hosts = [], ...parsedBody } = schemaEventTypeEditBodyParams.parse(body);
|
||||
const {
|
||||
hosts = [],
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
...parsedBody
|
||||
} = schemaEventTypeEditBodyParams.parse(body);
|
||||
|
||||
const data: Prisma.EventTypeUpdateArgs["data"] = {
|
||||
...parsedBody,
|
||||
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
|
||||
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
|
||||
};
|
||||
|
||||
if (hosts) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
@ -259,12 +259,19 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
|
|||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isAdmin, prisma, body } = req;
|
||||
|
||||
const { hosts = [], ...parsedBody } = schemaEventTypeCreateBodyParams.parse(body || {});
|
||||
const {
|
||||
hosts = [],
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
...parsedBody
|
||||
} = schemaEventTypeCreateBodyParams.parse(body || {});
|
||||
|
||||
let data: Prisma.EventTypeCreateArgs["data"] = {
|
||||
...parsedBody,
|
||||
userId,
|
||||
users: { connect: { id: userId } },
|
||||
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
|
||||
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
|
||||
};
|
||||
|
||||
await checkPermissions(req);
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { NextApiRequest } from "next";
|
|||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
import { schemaQuerySingleOrMultipleUserEmails } from "~/lib/validations/shared/queryUserEmail";
|
||||
import { schemaUsersReadPublic } from "~/lib/validations/user";
|
||||
|
||||
/**
|
||||
|
@ -19,6 +20,17 @@ import { schemaUsersReadPublic } from "~/lib/validations/user";
|
|||
* schema:
|
||||
* type: string
|
||||
* description: Your API key
|
||||
* - in: query
|
||||
* name: email
|
||||
* required: false
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* format: email
|
||||
* style: form
|
||||
* explode: true
|
||||
* description: The email address or an array of email addresses to filter by
|
||||
* tags:
|
||||
* - users
|
||||
* responses:
|
||||
|
@ -39,6 +51,14 @@ export async function getHandler(req: NextApiRequest) {
|
|||
const where: Prisma.UserWhereInput = {};
|
||||
// If user is not ADMIN, return only his data.
|
||||
if (!isAdmin) where.id = userId;
|
||||
|
||||
if (req.query.email) {
|
||||
const validationResult = schemaQuerySingleOrMultipleUserEmails.parse(req.query);
|
||||
where.email = {
|
||||
in: Array.isArray(validationResult.email) ? validationResult.email : [validationResult.email],
|
||||
};
|
||||
}
|
||||
|
||||
const [total, data] = await prisma.$transaction([
|
||||
prisma.user.count({ where }),
|
||||
prisma.user.findMany({ where, take, skip }),
|
||||
|
|
|
@ -1,9 +1,29 @@
|
|||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
const ns = ["common"];
|
||||
const supportedLngs = ["en", "fr"];
|
||||
const resources = ns.reduce((acc, n) => {
|
||||
supportedLngs.forEach((lng) => {
|
||||
if (!acc[lng]) acc[lng] = {};
|
||||
acc[lng] = {
|
||||
...acc[lng],
|
||||
[n]: require(`../../web/public/static/locales/${lng}/${n}.json`),
|
||||
};
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: [],
|
||||
debug: true,
|
||||
fallbackLng: "en",
|
||||
defaultNS: "common",
|
||||
ns,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: { useSuspense: true },
|
||||
resources,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
|
|
@ -21,6 +21,14 @@ export const parameters = {
|
|||
push() {},
|
||||
Provider: AppRouterContext.Provider,
|
||||
},
|
||||
globals: {
|
||||
locale: "en",
|
||||
locales: {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
},
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ export function VariantsTable({
|
|||
const columns = React.Children.toArray(children) as ReactElement<RowProps>[];
|
||||
return (
|
||||
<div
|
||||
id="light-variant"
|
||||
className={classNames(
|
||||
isDark &&
|
||||
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#1C1C1C]"
|
||||
|
@ -43,7 +44,7 @@ export function VariantsTable({
|
|||
</table>
|
||||
</div>
|
||||
{!isDark && (
|
||||
<div data-mode="dark" className="dark">
|
||||
<div id="dark-variant" data-mode="dark" className="dark">
|
||||
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
|
||||
{children}
|
||||
</VariantsTable>
|
||||
|
|
|
@ -336,7 +336,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
|
||||
<tr data-testid="booking-item" className="hover:bg-muted group flex flex-col sm:flex-row">
|
||||
<td
|
||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||
onClick={onClickTableData}>
|
||||
|
|
|
@ -22,7 +22,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
|||
|
||||
const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId;
|
||||
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId },
|
||||
{ scheduleId: defaultScheduleId ?? undefined },
|
||||
{
|
||||
enabled: !!scheduleId,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.2.3",
|
||||
"version": "3.2.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
|
|
@ -75,6 +75,7 @@ export default function Custom404() {
|
|||
)}`
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const isSuccessPage = pathname?.startsWith("/booking");
|
||||
|
|
|
@ -53,7 +53,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
|||
orgSlug: _orgSlug,
|
||||
...query
|
||||
} = useRouterQuery();
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
|
||||
/*
|
||||
const telemetry = useTelemetry();
|
||||
|
|
|
@ -7,54 +7,6 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
|||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end();
|
||||
const t = await getTranslation("en", "common");
|
||||
const language = { translate: t, locale: "en" };
|
||||
|
||||
const evt = {
|
||||
type: "30min",
|
||||
title: "30min between Pro Example and pro@example.com",
|
||||
description: null,
|
||||
additionalNotes: "asdasdas",
|
||||
startTime: "2022-06-03T09:00:00-06:00",
|
||||
endTime: "2022-06-03T09:30:00-06:00",
|
||||
organizer: {
|
||||
name: "Pro Example",
|
||||
email: "pro@example.com",
|
||||
timeZone: "Europe/London",
|
||||
language,
|
||||
},
|
||||
attendees: [
|
||||
{
|
||||
email: "pro@example.com",
|
||||
name: "pro@example.com",
|
||||
timeZone: "America/Chihuahua",
|
||||
language,
|
||||
},
|
||||
],
|
||||
location: "Zoom video",
|
||||
destinationCalendar: null,
|
||||
hideCalendarNotes: false,
|
||||
uid: "xxyPr4cg2xx4XoS2KeMEQy",
|
||||
metadata: {},
|
||||
recurringEvent: null,
|
||||
appsStatus: [
|
||||
{
|
||||
appName: "Outlook Calendar",
|
||||
type: "office365_calendar",
|
||||
success: 1,
|
||||
failures: 0,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
{
|
||||
appName: "Google Meet",
|
||||
type: "conferencing",
|
||||
success: 0,
|
||||
failures: 1,
|
||||
errors: [],
|
||||
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
res.statusCode = 200;
|
||||
|
||||
|
|
|
@ -34,13 +34,17 @@ const triggerWebhook = async ({
|
|||
booking: {
|
||||
userId: number | undefined;
|
||||
eventTypeId: number | null;
|
||||
eventTypeParentId: number | null | undefined;
|
||||
teamId?: number | null;
|
||||
};
|
||||
}) => {
|
||||
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
|
||||
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
||||
|
||||
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
|
||||
|
||||
const subscriberOptions = {
|
||||
userId: booking.userId,
|
||||
userId: triggerForUser ? booking.userId : null,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
triggerEvent: eventTrigger,
|
||||
teamId: booking.teamId,
|
||||
|
@ -183,6 +187,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
booking: {
|
||||
userId: booking?.user?.id,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
eventTypeParentId: booking.eventType?.parentId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -179,7 +179,7 @@ export default function Bookings() {
|
|||
)}
|
||||
<div className="pt-2 xl:pt-0">
|
||||
<div className="border-subtle overflow-hidden rounded-md border">
|
||||
<table className="w-full max-w-full table-fixed">
|
||||
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
|
||||
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
|
||||
{query.data.pages.map((page, index) => (
|
||||
<Fragment key={index}>
|
||||
|
|
|
@ -411,6 +411,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
throw new Error(t("seats_and_no_show_fee_error"));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
...rest,
|
||||
|
@ -495,6 +496,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
...rest,
|
||||
|
|
|
@ -77,7 +77,7 @@ type FormValues = {
|
|||
const ProfileView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { data: _session, update } = useSession();
|
||||
const { update } = useSession();
|
||||
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
|
||||
|
|
|
@ -199,19 +199,27 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
|||
<SubTeams />
|
||||
) : (
|
||||
<>
|
||||
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
|
||||
{(showMembers.isOn || !team.eventTypes.length) &&
|
||||
(team.isPrivate ? (
|
||||
<div className="w-full text-center">
|
||||
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_team_members")}</h2>
|
||||
</div>
|
||||
) : (
|
||||
<Team team={team} />
|
||||
))}
|
||||
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
||||
<div className="mx-auto max-w-3xl ">
|
||||
<EventTypes />
|
||||
|
||||
{!team.hideBookATeamMember && (
|
||||
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
|
||||
{(!team.hideBookATeamMember || team.isPrivate) && (
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="border-subtle w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
|
||||
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -158,9 +158,10 @@ test.describe("pro user", () => {
|
|||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
additionalGuests.forEach(async (email) => {
|
||||
const promises = additionalGuests.map(async (email) => {
|
||||
await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
test("Time slots should be reserved when selected", async ({ context, page }) => {
|
||||
|
|
|
@ -8,57 +8,59 @@ import { test } from "./lib/fixtures";
|
|||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Bookings", () => {
|
||||
test.only("Upcoming bookings", async ({ page, users, bookings }) => {
|
||||
const firstUser = await users.create();
|
||||
const secondUser = await users.create();
|
||||
const firstEventType = firstUser.eventTypes[0];
|
||||
test.describe("Upcoming bookings", () => {
|
||||
test("show attendee bookings and organizer bookings in asc order by startDate", async ({
|
||||
page,
|
||||
users,
|
||||
bookings,
|
||||
}) => {
|
||||
const firstUser = await users.create();
|
||||
const secondUser = await users.create();
|
||||
|
||||
const bookingWhereFirstUserIsOrganizerFixture = await createBooking({
|
||||
title: "Booking as organizer",
|
||||
bookingsFixture: bookings,
|
||||
relativeDate: 3,
|
||||
organizer: firstUser,
|
||||
organizerEventType: firstUser.eventTypes[0],
|
||||
attendees: [
|
||||
{ name: "First", email: "first@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
|
||||
],
|
||||
const bookingWhereFirstUserIsOrganizerFixture = await createBooking({
|
||||
title: "Booking as organizer",
|
||||
bookingsFixture: bookings,
|
||||
// Create a booking 3 days from today
|
||||
relativeDate: 3,
|
||||
organizer: firstUser,
|
||||
organizerEventType: firstUser.eventTypes[0],
|
||||
attendees: [
|
||||
{ name: "First", email: "first@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
|
||||
],
|
||||
});
|
||||
const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self();
|
||||
|
||||
const bookingWhereFirstUserIsAttendeeFixture = await createBooking({
|
||||
title: "Booking as attendee",
|
||||
bookingsFixture: bookings,
|
||||
organizer: secondUser,
|
||||
// Booking created 2 days from today
|
||||
relativeDate: 2,
|
||||
organizerEventType: secondUser.eventTypes[0],
|
||||
attendees: [
|
||||
{ name: "OrganizerAsBooker", email: firstUser.email, timeZone: "Europe/Berlin" },
|
||||
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
|
||||
],
|
||||
});
|
||||
const bookingWhereFirstUserIsAttendee = await bookingWhereFirstUserIsAttendeeFixture.self();
|
||||
|
||||
await firstUser.apiLogin();
|
||||
await page.goto(`/bookings/upcoming`);
|
||||
const upcomingBookings = page.locator('[data-testid="upcoming-bookings"]');
|
||||
const firstUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(0);
|
||||
const secondUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(1);
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
firstUpcomingBooking.locator(`text=${bookingWhereFirstUserIsAttendee!.title}`)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`)
|
||||
).toBeVisible();
|
||||
});
|
||||
const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self();
|
||||
|
||||
const bookingWhereFirstUserIsAttendeeFixture = await createBooking({
|
||||
title: "Booking as attendee",
|
||||
bookingsFixture: bookings,
|
||||
organizer: secondUser,
|
||||
relativeDate: 2,
|
||||
organizerEventType: secondUser.eventTypes[0],
|
||||
attendees: [
|
||||
{ name: "OrganizerAsBooker", email: firstUser.email, timeZone: "Europe/Berlin" },
|
||||
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
|
||||
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
|
||||
],
|
||||
});
|
||||
const bookingWhereFirstUserIsAttendee = await bookingWhereFirstUserIsAttendeeFixture.self();
|
||||
|
||||
await firstUser.apiLogin();
|
||||
await page.goto(`/bookings/upcoming`);
|
||||
const firstUpcomingBooking = page
|
||||
.locator('[data-testid="upcoming-bookings"] [data-testid="booking-item"]')
|
||||
.nth(0);
|
||||
const secondUpcomingBooking = page
|
||||
.locator('[data-testid="upcoming-bookings"] [data-testid="booking-item"]')
|
||||
.nth(1);
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
firstUpcomingBooking.locator(`text=${bookingWhereFirstUserIsAttendee!.title}`)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`)
|
||||
).toBeVisible();
|
||||
|
||||
await page.pause();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -67,6 +69,11 @@ async function createBooking({
|
|||
organizer,
|
||||
organizerEventType,
|
||||
attendees,
|
||||
/**
|
||||
* Relative date from today
|
||||
* 0 means today
|
||||
* 1 means tomorrow
|
||||
*/
|
||||
relativeDate = 0,
|
||||
durationMins = 30,
|
||||
title,
|
||||
|
|
|
@ -19,9 +19,12 @@ export const createBookingsFixture = (page: Page) => {
|
|||
username: string | null,
|
||||
eventTypeId = -1,
|
||||
{
|
||||
title = "",
|
||||
rescheduled = false,
|
||||
paid = false,
|
||||
status = "ACCEPTED",
|
||||
startTime,
|
||||
endTime,
|
||||
attendees = {
|
||||
create: {
|
||||
email: "attendee@example.com",
|
||||
|
@ -39,9 +42,9 @@ export const createBookingsFixture = (page: Page) => {
|
|||
const booking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: uid,
|
||||
title: "30min",
|
||||
startTime: startDate,
|
||||
endTime: endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
|
||||
title: title || "30min",
|
||||
startTime: startTime || startDate,
|
||||
endTime: endTime || endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
|
|
|
@ -542,7 +542,7 @@ export async function apiLogin(
|
|||
.then((json) => json.csrfToken);
|
||||
const data = {
|
||||
email: user.email ?? `${user.username}@example.com`,
|
||||
password: user.password ?? user.username!,
|
||||
password: user.password ?? user.username,
|
||||
callbackURL: "http://localhost:3000/",
|
||||
redirect: "false",
|
||||
json: "true",
|
||||
|
|
|
@ -303,6 +303,7 @@ describe("handleChildrenEventTypes", () => {
|
|||
timeZone: _timeZone,
|
||||
parentId: _parentId,
|
||||
userId: _userId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
requiresBookerEmailVerification,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { appDataSchema } from "../zod";
|
|||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
|
||||
const { t } = useLocale();
|
||||
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const [_getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const [additionalParameters, setAdditionalParameters] = useState("");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
|
|
|
@ -66,7 +66,6 @@ function Field({
|
|||
hookForm,
|
||||
hookFieldNamespace,
|
||||
deleteField,
|
||||
fieldIndex,
|
||||
moveUp,
|
||||
moveDown,
|
||||
appUrl,
|
||||
|
|
|
@ -3,7 +3,6 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { stringify } from "querystring";
|
||||
|
||||
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import type { StripeData } from "../lib/server";
|
||||
import stripe from "../lib/server";
|
||||
|
@ -31,8 +30,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
|
||||
const response = await stripe.oauth.token({
|
||||
grant_type: "authorization_code",
|
||||
code: code?.toString(),
|
||||
|
|
|
@ -9,7 +9,6 @@ import stripe from "../lib/server";
|
|||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST" && req.method !== "GET")
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
const { referer } = req.headers;
|
||||
|
||||
// if (!referer) return res.status(400).json({ message: "Missing referrer" });
|
||||
|
||||
|
|
|
@ -121,18 +121,21 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
|
|||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
|
||||
(ref) => !!credentialsMap.get(ref.type)
|
||||
);
|
||||
|
||||
const promises = bookingRefsFiltered.map(async (bookingRef) => {
|
||||
if (!bookingRef.uid) return;
|
||||
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
});
|
||||
try {
|
||||
bookingRefsFiltered.forEach(async (bookingRef) => {
|
||||
if (bookingRef.uid) {
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
// FIXME: error logging - non-Error type errors are currently discarded
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
|
|
|
@ -121,17 +121,19 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
|
|||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
|
||||
(ref) => !!credentialsMap.get(ref.type)
|
||||
);
|
||||
|
||||
const promises = bookingRefsFiltered.map(async (bookingRef) => {
|
||||
if (!bookingRef.uid) return;
|
||||
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
});
|
||||
try {
|
||||
bookingRefsFiltered.forEach(async (bookingRef) => {
|
||||
if (bookingRef.uid) {
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
|
|
|
@ -22,8 +22,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
|
||||
try {
|
||||
const where: Prisma.BookingWhereInput = {};
|
||||
if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
|
||||
else where.userId = validKey.userId;
|
||||
if (validKey.teamId) {
|
||||
where.eventType = {
|
||||
OR: [{ teamId: validKey.teamId }, { parent: { teamId: validKey.teamId } }],
|
||||
};
|
||||
} else {
|
||||
where.userId = validKey.userId;
|
||||
}
|
||||
|
||||
const bookings = await prisma.booking.findMany({
|
||||
take: 3,
|
||||
where,
|
||||
|
|
|
@ -10,6 +10,7 @@ export async function scheduleTrigger(
|
|||
) {
|
||||
try {
|
||||
//schedule job to call subscriber url at the end of meeting
|
||||
// FIXME: in-process scheduling - job will vanish on server crash / restart
|
||||
const job = schedule.scheduleJob(
|
||||
`${subscriber.appId}_${subscriber.id}`,
|
||||
booking.endTime,
|
||||
|
@ -57,38 +58,39 @@ export async function cancelScheduledJobs(
|
|||
appId?: string | null,
|
||||
isReschedule?: boolean
|
||||
) {
|
||||
try {
|
||||
let scheduledJobs = booking.scheduledJobs || [];
|
||||
if (!booking.scheduledJobs) return;
|
||||
|
||||
if (booking.scheduledJobs) {
|
||||
booking.scheduledJobs.forEach(async (scheduledJob) => {
|
||||
if (appId) {
|
||||
if (scheduledJob.startsWith(appId)) {
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
|
||||
}
|
||||
} else {
|
||||
//if no specific appId given, delete all scheduled jobs of booking
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = [];
|
||||
let scheduledJobs = booking.scheduledJobs || [];
|
||||
const promises = booking.scheduledJobs.map(async (scheduledJob) => {
|
||||
if (appId) {
|
||||
if (scheduledJob.startsWith(appId)) {
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
|
||||
}
|
||||
} else {
|
||||
//if no specific appId given, delete all scheduled jobs of booking
|
||||
if (schedule.scheduledJobs[scheduledJob]) {
|
||||
schedule.scheduledJobs[scheduledJob].cancel();
|
||||
}
|
||||
scheduledJobs = [];
|
||||
}
|
||||
|
||||
if (!isReschedule) {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: booking.uid,
|
||||
},
|
||||
data: {
|
||||
scheduledJobs: scheduledJobs,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!isReschedule) {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: booking.uid,
|
||||
},
|
||||
data: {
|
||||
scheduledJobs: scheduledJobs,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.error("Error cancelling scheduled jobs", error);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ const libraries = [
|
|||
},
|
||||
];
|
||||
|
||||
libraries.forEach(async (lib) => {
|
||||
const promises = libraries.map(async (lib) => {
|
||||
await build({
|
||||
build: {
|
||||
outDir: `./dist/${lib.fileName}`,
|
||||
|
@ -29,3 +29,4 @@ libraries.forEach(async (lib) => {
|
|||
},
|
||||
});
|
||||
});
|
||||
await Promise.all(promises);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { Page, Frame } from "@playwright/test";
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export function todo(title: string) {
|
||||
|
|
|
@ -562,7 +562,6 @@ class CalApi {
|
|||
|
||||
modal({
|
||||
calLink,
|
||||
calOrigin,
|
||||
config = {},
|
||||
uid,
|
||||
}: {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { useMemo, useRef, useEffect } from "react";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
import { useSlotsForAvailableDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useBookerStore } from "../store";
|
||||
import { useEvent, useScheduleForEvent } from "../utils/event";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
type AvailableTimeSlotsProps = {
|
||||
extraDays?: number;
|
||||
|
@ -28,7 +28,13 @@ type AvailableTimeSlotsProps = {
|
|||
* will also fetch the next `extraDays` days and show multiple days
|
||||
* in columns next to each other.
|
||||
*/
|
||||
export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot, prefetchNextMonth, monthCount}: AvailableTimeSlotsProps) => {
|
||||
export const AvailableTimeSlots = ({
|
||||
extraDays,
|
||||
limitHeight,
|
||||
seatsPerTimeSlot,
|
||||
prefetchNextMonth,
|
||||
monthCount,
|
||||
}: AvailableTimeSlotsProps) => {
|
||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
|
@ -68,15 +74,19 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot, p
|
|||
prefetchNextMonth,
|
||||
monthCount,
|
||||
});
|
||||
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots)
|
||||
const nonEmptyScheduleDaysFromSelectedDate = nonEmptyScheduleDays.filter((slot)=>dayjs(selectedDate).diff(slot,'day')<=0);
|
||||
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
|
||||
const nonEmptyScheduleDaysFromSelectedDate = nonEmptyScheduleDays.filter(
|
||||
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
||||
);
|
||||
|
||||
// Creates an array of dates to fetch slots for.
|
||||
// If `extraDays` is passed in, we will extend the array with the next `extraDays` days.
|
||||
const dates = !extraDays
|
||||
? [date]: nonEmptyScheduleDaysFromSelectedDate.length > 0
|
||||
? nonEmptyScheduleDaysFromSelectedDate.slice(0, extraDays):[];
|
||||
|
||||
? [date]
|
||||
: nonEmptyScheduleDaysFromSelectedDate.length > 0
|
||||
? nonEmptyScheduleDaysFromSelectedDate.slice(0, extraDays)
|
||||
: [];
|
||||
|
||||
const slotsPerDay = useSlotsForAvailableDates(dates, schedule?.data?.slots);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -106,10 +116,13 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot, p
|
|||
date={dayjs(slots.date)}
|
||||
slots={slots.slots}
|
||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||
availableMonth={dayjs(selectedDate).format("MM")!==dayjs(slots.date).format("MM")?dayjs(slots.date).format("MMM"):undefined}
|
||||
availableMonth={
|
||||
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
|
||||
? dayjs(slots.date).format("MMM")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ type AvailableTimesProps = {
|
|||
seatsPerTimeSlot?: number | null;
|
||||
showTimeFormatToggle?: boolean;
|
||||
className?: string;
|
||||
availableMonth?: String | undefined;
|
||||
availableMonth?: string | undefined;
|
||||
selectedSlots?: string[];
|
||||
};
|
||||
|
||||
|
@ -66,7 +66,8 @@ export const AvailableTimes = ({
|
|||
"inline-flex items-center justify-center rounded-3xl px-1 pt-0.5 font-medium",
|
||||
isMonthView ? "text-default text-sm" : "text-xs"
|
||||
)}>
|
||||
{date.format("DD")}{availableMonth && `, ${availableMonth}`}
|
||||
{date.format("DD")}
|
||||
{availableMonth && `, ${availableMonth}`}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
@ -136,4 +137,3 @@ export const AvailableTimesSkeleton = () => (
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -157,8 +157,10 @@ async function handler(req: CustomRequest) {
|
|||
},
|
||||
});
|
||||
|
||||
const triggerForUser = !teamId || (teamId && bookingToDelete.eventType?.parentId);
|
||||
|
||||
const subscriberOptions = {
|
||||
userId: bookingToDelete.userId,
|
||||
userId: triggerForUser ? bookingToDelete.userId : null,
|
||||
eventTypeId: bookingToDelete.eventTypeId as number,
|
||||
triggerEvent: eventTrigger,
|
||||
teamId,
|
||||
|
@ -428,9 +430,9 @@ async function handler(req: CustomRequest) {
|
|||
bookingToDelete.recurringEventId &&
|
||||
allRemainingBookings
|
||||
) {
|
||||
bookingToDelete.user.credentials
|
||||
const promises = bookingToDelete.user.credentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.forEach(async (credential) => {
|
||||
.map(async (credential) => {
|
||||
const calendar = await getCalendar(credential);
|
||||
for (const updBooking of updatedBookings) {
|
||||
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
|
||||
|
@ -441,6 +443,13 @@ async function handler(req: CustomRequest) {
|
|||
}
|
||||
}
|
||||
});
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error(error.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
|
||||
}
|
||||
|
@ -601,11 +610,13 @@ async function handler(req: CustomRequest) {
|
|||
});
|
||||
|
||||
// delete scheduled jobs of cancelled bookings
|
||||
// FIXME: async calls into ether
|
||||
updatedBookings.forEach((booking) => {
|
||||
cancelScheduledJobs(booking);
|
||||
});
|
||||
|
||||
//Workflows - cancel all reminders for cancelled bookings
|
||||
// FIXME: async calls into ether
|
||||
updatedBookings.forEach((booking) => {
|
||||
booking.workflowReminders.forEach((reminder) => {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
|
@ -620,11 +631,14 @@ async function handler(req: CustomRequest) {
|
|||
|
||||
const prismaPromises: Promise<unknown>[] = [bookingReferenceDeletes];
|
||||
|
||||
// @TODO: find a way in the future if a promise fails don't stop the rest of the promises
|
||||
// Also if emails fails try to requeue them
|
||||
try {
|
||||
await Promise.all(prismaPromises.concat(apiDeletes));
|
||||
const settled = await Promise.allSettled(prismaPromises.concat(apiDeletes));
|
||||
const rejected = settled.filter(({ status }) => status === "rejected") as PromiseRejectedResult[];
|
||||
if (rejected.length) {
|
||||
throw new Error(`Reasons: ${rejected.map(({ reason }) => reason)}`);
|
||||
}
|
||||
|
||||
// TODO: if emails fail try to requeue them
|
||||
await sendCancelledEmails(evt, { eventName: bookingToDelete?.eventType?.eventName });
|
||||
} catch (error) {
|
||||
console.error("Error deleting event", error);
|
||||
|
|
|
@ -293,14 +293,16 @@ export async function handleConfirmation(args: {
|
|||
},
|
||||
});
|
||||
|
||||
const triggerForUser = !teamId || (teamId && booking.eventType?.parentId);
|
||||
|
||||
const subscribersBookingCreated = await getWebhooks({
|
||||
userId: booking.userId,
|
||||
userId: triggerForUser ? booking.userId : null,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
||||
teamId,
|
||||
});
|
||||
const subscribersMeetingEnded = await getWebhooks({
|
||||
userId: booking.userId,
|
||||
userId: triggerForUser ? booking.userId : null,
|
||||
eventTypeId: booking.eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
teamId: booking.eventType?.teamId,
|
||||
|
|
|
@ -1126,8 +1126,10 @@ async function handler(
|
|||
|
||||
const teamId = await getTeamIdFromEventType({ eventType });
|
||||
|
||||
const triggerForUser = !teamId || (teamId && eventType.parentId);
|
||||
|
||||
const subscriberOptions: GetSubscriberOptions = {
|
||||
userId: organizerUser.id,
|
||||
userId: triggerForUser ? organizerUser.id : null,
|
||||
eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
||||
teamId,
|
||||
|
@ -1140,7 +1142,7 @@ async function handler(
|
|||
subscriberOptions.triggerEvent = eventTrigger;
|
||||
|
||||
const subscriberOptionsMeetingEnded = {
|
||||
userId: organizerUser.id,
|
||||
userId: triggerForUser ? organizerUser.id : null,
|
||||
eventTypeId,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
teamId,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useMemo } from "react";
|
||||
|
||||
import type { Slots } from "./types";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
/**
|
||||
* Get's slots for a specific date from the schedul cache.
|
||||
|
@ -18,8 +17,7 @@ export const useSlotsForDate = (date: string | null, slots?: Slots) => {
|
|||
return slotsForDate;
|
||||
};
|
||||
|
||||
export const useSlotsForAvailableDates = ( dates: (string | null)[], slots?: Slots) => {
|
||||
|
||||
export const useSlotsForAvailableDates = (dates: (string | null)[], slots?: Slots) => {
|
||||
const slotsForDates = useMemo(() => {
|
||||
if (slots === undefined) return [];
|
||||
return dates
|
||||
|
|
|
@ -10,16 +10,15 @@ export type GetSubscriberOptions = {
|
|||
};
|
||||
|
||||
const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient = defaultPrisma) => {
|
||||
const userId = options.teamId ? 0 : options.userId ?? 0;
|
||||
const userId = options.userId ?? 0;
|
||||
const eventTypeId = options.eventTypeId ?? 0;
|
||||
const teamId = options.teamId ?? 0;
|
||||
|
||||
// if we have userId and teamId it is a managed event type and should trigger for team and user
|
||||
const allWebhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
eventTypeId,
|
||||
|
|
|
@ -30,7 +30,9 @@ export type WebhookDataType = CalendarEvent &
|
|||
downloadLink?: string;
|
||||
};
|
||||
|
||||
function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: string }): string {
|
||||
function getZapierPayload(
|
||||
data: CalendarEvent & EventTypeInfo & { status?: string; createdAt: string }
|
||||
): string {
|
||||
const attendees = data.attendees.map((attendee) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
|
@ -69,6 +71,7 @@ function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: strin
|
|||
length: data.length,
|
||||
},
|
||||
attendees: attendees,
|
||||
createdAt: data.createdAt,
|
||||
};
|
||||
return JSON.stringify(body);
|
||||
}
|
||||
|
@ -112,7 +115,7 @@ const sendPayload = async (
|
|||
|
||||
/* Zapier id is hardcoded in the DB, we send the raw data for this case */
|
||||
if (appId === "zapier") {
|
||||
body = getZapierPayload(data);
|
||||
body = getZapierPayload({ ...data, createdAt });
|
||||
} else if (template) {
|
||||
body = applyTemplate(template, { ...data, triggerEvent, createdAt }, contentType);
|
||||
} else {
|
||||
|
|
|
@ -57,6 +57,7 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
|||
search.set(String(key), String(value));
|
||||
router.replace(`${pathname}?${search.toString()}`);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[parsedQuery, router]
|
||||
);
|
||||
|
||||
|
|
|
@ -79,7 +79,9 @@ model EventType {
|
|||
bookingFields Json?
|
||||
timeZone String?
|
||||
periodType PeriodType @default(UNLIMITED)
|
||||
/// @zod.custom(imports.coerceToDate)
|
||||
periodStartDate DateTime?
|
||||
/// @zod.custom(imports.coerceToDate)
|
||||
periodEndDate DateTime?
|
||||
periodDays Int?
|
||||
periodCountCalendarDays Boolean?
|
||||
|
|
|
@ -608,3 +608,5 @@ export const ZVerifyCodeInputSchema = z.object({
|
|||
});
|
||||
|
||||
export type ZVerifyCodeInputSchema = z.infer<typeof ZVerifyCodeInputSchema>;
|
||||
|
||||
export const coerceToDate = z.coerce.date();
|
||||
|
|
|
@ -11,7 +11,7 @@ type GetOptions = {
|
|||
input: TListMembersSchema;
|
||||
};
|
||||
|
||||
export const listPaginatedHandler = async ({ ctx, input }: GetOptions) => {
|
||||
export const listPaginatedHandler = async ({ input }: GetOptions) => {
|
||||
const { cursor, limit, searchTerm } = input;
|
||||
|
||||
const getTotalUsers = await prisma.user.count();
|
||||
|
@ -56,7 +56,7 @@ export const listPaginatedHandler = async ({ ctx, input }: GetOptions) => {
|
|||
let nextCursor: typeof cursor | undefined = undefined;
|
||||
if (users && users.length > limit) {
|
||||
const nextItem = users.pop();
|
||||
nextCursor = nextItem!.id;
|
||||
nextCursor = nextItem?.id;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -14,7 +14,7 @@ type GetOptions = {
|
|||
input: TAdminPasswordResetSchema;
|
||||
};
|
||||
|
||||
export const sendPasswordResetHandler = async ({ ctx, input }: GetOptions) => {
|
||||
export const sendPasswordResetHandler = async ({ input }: GetOptions) => {
|
||||
const { userId } = input;
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
|
|
|
@ -10,7 +10,7 @@ type checkForGlobalKeys = {
|
|||
input: CheckGlobalKeysSchemaType;
|
||||
};
|
||||
|
||||
export const checkForGlobalKeysHandler = async ({ ctx, input }: checkForGlobalKeys) => {
|
||||
export const checkForGlobalKeysHandler = async ({ input }: checkForGlobalKeys) => {
|
||||
const appIsGloballyInstalled = await prisma.app.findUnique({
|
||||
where: {
|
||||
slug: input.slug,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { sendEmailVerificationByCode } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import logger from "@calcom/lib/logger";
|
||||
|
||||
import type { TSendVerifyEmailCodeSchema } from "./sendVerifyEmailCode.schema";
|
||||
|
||||
|
@ -7,8 +6,6 @@ type SendVerifyEmailCode = {
|
|||
input: TSendVerifyEmailCodeSchema;
|
||||
};
|
||||
|
||||
const log = logger.getChildLogger({ prefix: [`[[Auth] `] });
|
||||
|
||||
export const sendVerifyEmailCodeHandler = async ({ input }: SendVerifyEmailCode) => {
|
||||
const email = await sendEmailVerificationByCode({
|
||||
email: input.email,
|
||||
|
|
|
@ -32,7 +32,6 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
|||
timeZone: true,
|
||||
eventType: {
|
||||
select: {
|
||||
_count: true,
|
||||
id: true,
|
||||
eventName: true,
|
||||
team: {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||
|
@ -29,22 +27,11 @@ async function getTeamMembers({
|
|||
cursor: number | null | undefined;
|
||||
limit: number;
|
||||
}) {
|
||||
let whereQuery: Prisma.MembershipWhereInput = {
|
||||
teamId,
|
||||
};
|
||||
|
||||
if (teamIds) {
|
||||
whereQuery = {
|
||||
teamId: {
|
||||
in: teamIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
...whereQuery,
|
||||
accepted: true,
|
||||
teamId: {
|
||||
in: teamId ? [teamId] : teamIds,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -116,23 +103,15 @@ async function getInfoForAllTeams({ ctx, input }: GetOptions) {
|
|||
},
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
},
|
||||
})
|
||||
.then((memberships) => memberships.map((membership) => membership.id));
|
||||
.then((memberships) => memberships.map((membership) => membership.teamId));
|
||||
|
||||
if (!teamIds.length) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization or team." });
|
||||
}
|
||||
|
||||
const getTotalMembers = await prisma.$queryRaw<{
|
||||
count: number;
|
||||
}>(Prisma.sql`
|
||||
SELECT
|
||||
COUNT(DISTINCT "userId") as "count"
|
||||
FROM "Membership"
|
||||
WHERE "teamId" IN (${Prisma.join(teamIds)})
|
||||
`);
|
||||
|
||||
const teamMembers = await getTeamMembers({
|
||||
teamIds,
|
||||
cursor,
|
||||
|
@ -141,7 +120,7 @@ async function getInfoForAllTeams({ ctx, input }: GetOptions) {
|
|||
|
||||
return {
|
||||
teamMembers,
|
||||
totalTeamMembers: getTotalMembers.count,
|
||||
totalTeamMembers: teamMembers.length,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -102,168 +102,172 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
|||
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" });
|
||||
}
|
||||
|
||||
if (bookingToReschedule) {
|
||||
let event: Partial<EventType> = {};
|
||||
if (bookingToReschedule.eventTypeId) {
|
||||
event = await prisma.eventType.findFirstOrThrow({
|
||||
select: {
|
||||
title: true,
|
||||
users: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingToReschedule.eventTypeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.booking.update({
|
||||
if (!bookingToReschedule) return;
|
||||
|
||||
let event: Partial<EventType> = {};
|
||||
if (bookingToReschedule.eventTypeId) {
|
||||
event = await prisma.eventType.findFirstOrThrow({
|
||||
select: {
|
||||
title: true,
|
||||
users: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingToReschedule.id,
|
||||
},
|
||||
data: {
|
||||
rescheduled: true,
|
||||
cancellationReason,
|
||||
status: BookingStatus.CANCELLED,
|
||||
updatedAt: dayjs().toISOString(),
|
||||
id: bookingToReschedule.eventTypeId,
|
||||
},
|
||||
});
|
||||
|
||||
// delete scheduled jobs of previous booking
|
||||
cancelScheduledJobs(bookingToReschedule);
|
||||
|
||||
//cancel workflow reminders of previous booking
|
||||
bookingToReschedule.workflowReminders.forEach((reminder) => {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
||||
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
|
||||
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
|
||||
}
|
||||
});
|
||||
|
||||
const [mainAttendee] = bookingToReschedule.attendees;
|
||||
// @NOTE: Should we assume attendees language?
|
||||
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
|
||||
const usersToPeopleType = (
|
||||
users: PersonAttendeeCommonFields[],
|
||||
selectedLanguage: TFunction
|
||||
): Person[] => {
|
||||
return users?.map((user) => {
|
||||
return {
|
||||
email: user.email || "",
|
||||
name: user.name || "",
|
||||
username: user?.username || "",
|
||||
language: { translate: selectedLanguage, locale: user.locale || "en" },
|
||||
timeZone: user?.timeZone,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const userTranslation = await getTranslation(user.locale ?? "en", "common");
|
||||
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
||||
|
||||
const builder = new CalendarEventBuilder();
|
||||
builder.init({
|
||||
title: bookingToReschedule.title,
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
startTime: bookingToReschedule.startTime.toISOString(),
|
||||
endTime: bookingToReschedule.endTime.toISOString(),
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
organizer: userAsPeopleType,
|
||||
});
|
||||
|
||||
const director = new CalendarEventDirector();
|
||||
director.setBuilder(builder);
|
||||
director.setExistingBooking(bookingToReschedule);
|
||||
cancellationReason && director.setCancellationReason(cancellationReason);
|
||||
if (event) {
|
||||
await director.buildForRescheduleEmail();
|
||||
} else {
|
||||
await director.buildWithoutEventTypeForRescheduleEmail();
|
||||
}
|
||||
|
||||
// Handling calendar and videos cancellation
|
||||
// This can set previous time as available, until virtual calendar is done
|
||||
const credentials = await getUsersCredentials(user.id);
|
||||
const credentialsMap = new Map();
|
||||
credentials.forEach((credential) => {
|
||||
credentialsMap.set(credential.type, credential);
|
||||
});
|
||||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter((ref) =>
|
||||
credentialsMap.has(ref.type)
|
||||
);
|
||||
bookingRefsFiltered.forEach(async (bookingRef) => {
|
||||
if (bookingRef.uid) {
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent, bookingRef.externalCalendarId);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Send emails
|
||||
await sendRequestRescheduleEmail(builder.calendarEvent, {
|
||||
rescheduleLink: builder.rescheduleLink,
|
||||
});
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
title: bookingToReschedule?.title,
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
description: bookingToReschedule?.description || "",
|
||||
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
|
||||
...getCalEventResponses({
|
||||
booking: bookingToReschedule,
|
||||
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
|
||||
}),
|
||||
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
||||
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
||||
organizer: userAsPeopleType,
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
uid: bookingToReschedule?.uid,
|
||||
location: bookingToReschedule?.location,
|
||||
destinationCalendar:
|
||||
bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
||||
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
||||
};
|
||||
|
||||
// Send webhook
|
||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
||||
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: bookingToReschedule.eventType?.teamId ?? null },
|
||||
parentId: bookingToReschedule?.eventType?.parentId ?? null,
|
||||
},
|
||||
});
|
||||
// Send Webhook call if hooked to BOOKING.CANCELLED
|
||||
const subscriberOptions = {
|
||||
userId: bookingToReschedule.userId,
|
||||
eventTypeId: bookingToReschedule.eventTypeId as number,
|
||||
triggerEvent: eventTrigger,
|
||||
teamId,
|
||||
};
|
||||
const webhooks = await getWebhooks(subscriberOptions);
|
||||
const promises = webhooks.map((webhook) =>
|
||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
||||
...evt,
|
||||
smsReminderNumber: bookingToReschedule.smsReminderNumber || undefined,
|
||||
}).catch((e) => {
|
||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: bookingToReschedule.id,
|
||||
},
|
||||
data: {
|
||||
rescheduled: true,
|
||||
cancellationReason,
|
||||
status: BookingStatus.CANCELLED,
|
||||
updatedAt: dayjs().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
// delete scheduled jobs of previous booking
|
||||
// FIXME: async fn off into the ether
|
||||
cancelScheduledJobs(bookingToReschedule);
|
||||
|
||||
//cancel workflow reminders of previous booking
|
||||
// FIXME: more async fns off into the ether
|
||||
bookingToReschedule.workflowReminders.forEach((reminder) => {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
||||
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
|
||||
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
|
||||
}
|
||||
});
|
||||
|
||||
const [mainAttendee] = bookingToReschedule.attendees;
|
||||
// @NOTE: Should we assume attendees language?
|
||||
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
|
||||
const usersToPeopleType = (users: PersonAttendeeCommonFields[], selectedLanguage: TFunction): Person[] => {
|
||||
return users?.map((user) => {
|
||||
return {
|
||||
email: user.email || "",
|
||||
name: user.name || "",
|
||||
username: user?.username || "",
|
||||
language: { translate: selectedLanguage, locale: user.locale || "en" },
|
||||
timeZone: user?.timeZone,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const userTranslation = await getTranslation(user.locale ?? "en", "common");
|
||||
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
||||
|
||||
const builder = new CalendarEventBuilder();
|
||||
builder.init({
|
||||
title: bookingToReschedule.title,
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
startTime: bookingToReschedule.startTime.toISOString(),
|
||||
endTime: bookingToReschedule.endTime.toISOString(),
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
organizer: userAsPeopleType,
|
||||
});
|
||||
|
||||
const director = new CalendarEventDirector();
|
||||
director.setBuilder(builder);
|
||||
director.setExistingBooking(bookingToReschedule);
|
||||
cancellationReason && director.setCancellationReason(cancellationReason);
|
||||
if (event) {
|
||||
await director.buildForRescheduleEmail();
|
||||
} else {
|
||||
await director.buildWithoutEventTypeForRescheduleEmail();
|
||||
}
|
||||
|
||||
// Handling calendar and videos cancellation
|
||||
// This can set previous time as available, until virtual calendar is done
|
||||
const credentials = await getUsersCredentials(user.id);
|
||||
const credentialsMap = new Map();
|
||||
credentials.forEach((credential) => {
|
||||
credentialsMap.set(credential.type, credential);
|
||||
});
|
||||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter((ref) =>
|
||||
credentialsMap.has(ref.type)
|
||||
);
|
||||
|
||||
// FIXME: error-handling
|
||||
await Promise.allSettled(
|
||||
bookingRefsFiltered.map(async (bookingRef) => {
|
||||
if (!bookingRef.uid) return;
|
||||
|
||||
if (bookingRef.type.endsWith("_calendar")) {
|
||||
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
|
||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent, bookingRef.externalCalendarId);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Send emails
|
||||
await sendRequestRescheduleEmail(builder.calendarEvent, {
|
||||
rescheduleLink: builder.rescheduleLink,
|
||||
});
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
title: bookingToReschedule?.title,
|
||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||
description: bookingToReschedule?.description || "",
|
||||
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
|
||||
...getCalEventResponses({
|
||||
booking: bookingToReschedule,
|
||||
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
|
||||
}),
|
||||
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
||||
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
||||
organizer: userAsPeopleType,
|
||||
attendees: usersToPeopleType(
|
||||
// username field doesn't exists on attendee but could be in the future
|
||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||
tAttendees
|
||||
),
|
||||
uid: bookingToReschedule?.uid,
|
||||
location: bookingToReschedule?.location,
|
||||
destinationCalendar: bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
||||
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
||||
};
|
||||
|
||||
// Send webhook
|
||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
||||
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: bookingToReschedule.eventType?.teamId ?? null },
|
||||
parentId: bookingToReschedule?.eventType?.parentId ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const triggerForUser = !teamId || (teamId && bookingToReschedule.eventType?.parentId);
|
||||
|
||||
// Send Webhook call if hooked to BOOKING.CANCELLED
|
||||
const subscriberOptions = {
|
||||
userId: triggerForUser ? bookingToReschedule.userId : null,
|
||||
eventTypeId: bookingToReschedule.eventTypeId as number,
|
||||
triggerEvent: eventTrigger,
|
||||
teamId,
|
||||
};
|
||||
const webhooks = await getWebhooks(subscriberOptions);
|
||||
const promises = webhooks.map((webhook) =>
|
||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
||||
...evt,
|
||||
smsReminderNumber: bookingToReschedule.smsReminderNumber || undefined,
|
||||
}).catch((e) => {
|
||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
|
||||
})
|
||||
);
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
|
|
@ -32,7 +32,7 @@ export const checkForGWorkspace = async ({ ctx }: CheckForGCalOptions) => {
|
|||
return { id: gWorkspacePresent?.id };
|
||||
};
|
||||
|
||||
export const getUsersFromGWorkspace = async ({ ctx }: CheckForGCalOptions) => {
|
||||
export const getUsersFromGWorkspace = async ({}: CheckForGCalOptions) => {
|
||||
const { client_id, client_secret } = await getAppKeysFromSlug("google-calendar");
|
||||
if (!client_id || typeof client_id !== "string") throw new Error("Google client_id missing.");
|
||||
if (!client_secret || typeof client_secret !== "string") throw new Error("Google client_secret missing.");
|
||||
|
|
|
@ -9,7 +9,7 @@ type AdminGetAllOptions = {
|
|||
};
|
||||
};
|
||||
|
||||
export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetAllOptions) => {
|
||||
export const adminGetUnverifiedHandler = async ({}: AdminGetAllOptions) => {
|
||||
const allOrgs = await prisma.team.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
|
|
|
@ -161,12 +161,14 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
|||
},
|
||||
});
|
||||
|
||||
if (!createOwnerOrg.organizationId) throw Error("User not created");
|
||||
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
userId: createOwnerOrg.id,
|
||||
role: MembershipRole.OWNER,
|
||||
accepted: true,
|
||||
teamId: createOwnerOrg.organizationId!,
|
||||
teamId: createOwnerOrg.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
|
|||
let nextCursor: typeof cursor | undefined = undefined;
|
||||
if (teamMembers && teamMembers.length > limit) {
|
||||
const nextItem = teamMembers.pop();
|
||||
nextCursor = nextItem!.id;
|
||||
nextCursor = nextItem?.id;
|
||||
}
|
||||
|
||||
const members = teamMembers?.map((member) => {
|
||||
|
|
|
@ -21,7 +21,7 @@ type ListOptions = {
|
|||
input: TListOtherTeamMembersSchema;
|
||||
};
|
||||
|
||||
export const listOtherTeamMembers = async ({ ctx, input }: ListOptions) => {
|
||||
export const listOtherTeamMembers = async ({ input }: ListOptions) => {
|
||||
const whereConditional: Prisma.MembershipWhereInput = {
|
||||
teamId: input.teamId,
|
||||
};
|
||||
|
|
|
@ -264,13 +264,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
},
|
||||
});
|
||||
|
||||
steps.forEach(async (step) => {
|
||||
const promiseSteps = steps.map(async (step) => {
|
||||
if (
|
||||
step.action !== WorkflowActions.SMS_ATTENDEE &&
|
||||
step.action !== WorkflowActions.WHATSAPP_ATTENDEE
|
||||
) {
|
||||
//as we do not have attendees phone number (user is notified about that when setting this action)
|
||||
bookingsForReminders.forEach(async (booking) => {
|
||||
const promiseScheduleReminders = bookingsForReminders.map(async (booking) => {
|
||||
const defaultLocale = "en";
|
||||
const bookingInfo = {
|
||||
uid: booking.uid,
|
||||
|
@ -367,29 +367,28 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
);
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseScheduleReminders);
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseSteps);
|
||||
}
|
||||
//create all workflow - eventtypes relationships
|
||||
activeOnEventTypes.forEach(async (eventType) => {
|
||||
await ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: {
|
||||
workflowId: id,
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (eventType.children.length) {
|
||||
eventType.children.forEach(async (chEventType) => {
|
||||
await ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: {
|
||||
workflowId: id,
|
||||
eventTypeId: chEventType.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
await ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: activeOnEventTypes.map((eventType) => ({
|
||||
workflowId: id,
|
||||
eventTypeId: eventType.id,
|
||||
})),
|
||||
});
|
||||
await Promise.all(
|
||||
activeOnEventTypes.map((eventType) =>
|
||||
ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: eventType.children.map((chEventType) => ({
|
||||
workflowId: id,
|
||||
eventTypeId: chEventType.id,
|
||||
})),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
userWorkflow.steps.map(async (oldStep) => {
|
||||
|
@ -465,11 +464,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
});
|
||||
|
||||
//cancel all workflow reminders from steps that were edited
|
||||
remindersToUpdate.forEach(async (reminder) => {
|
||||
// FIXME: async calls into ether
|
||||
remindersToUpdate.forEach((reminder) => {
|
||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
||||
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
||||
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
|
||||
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
|
||||
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
|
||||
}
|
||||
});
|
||||
const eventTypesToUpdateReminders = activeOn.filter((eventTypeId) => {
|
||||
|
@ -497,7 +499,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
user: true,
|
||||
},
|
||||
});
|
||||
bookingsOfEventTypes.forEach(async (booking) => {
|
||||
const promiseScheduleReminders = bookingsOfEventTypes.map(async (booking) => {
|
||||
const defaultLocale = "en";
|
||||
const bookingInfo = {
|
||||
uid: booking.uid,
|
||||
|
@ -594,6 +596,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
);
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseScheduleReminders);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -617,7 +620,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
return activeEventType;
|
||||
}
|
||||
});
|
||||
addedSteps.forEach(async (step) => {
|
||||
const promiseAddedSteps = addedSteps.map(async (step) => {
|
||||
if (step) {
|
||||
const { senderName, ...newStep } = step;
|
||||
newStep.sender = getSender({
|
||||
|
@ -749,6 +752,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseAddedSteps);
|
||||
}
|
||||
|
||||
//update trigger, name, time, timeUnit
|
||||
|
|
|
@ -141,7 +141,7 @@ export function SelectWithValidation<
|
|||
}: SelectProps<Option, IsMulti, Group> & { required?: boolean }) {
|
||||
const [hiddenInputValue, _setHiddenInputValue] = React.useState(() => {
|
||||
if (value instanceof Array || !value) {
|
||||
return;
|
||||
return "";
|
||||
}
|
||||
return value.value || "";
|
||||
});
|
||||
|
|
|
@ -0,0 +1,308 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* eslint-disable playwright/missing-playwright-await */
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { Select, SelectField, SelectWithValidation } from "./Select";
|
||||
|
||||
const options = [
|
||||
{ label: "Option 1", value: "option1" },
|
||||
{ label: "Option 2", value: "option2" },
|
||||
{ label: "Option 3", value: "option3" },
|
||||
];
|
||||
|
||||
const onChangeMock = vi.fn();
|
||||
|
||||
const props: any = {
|
||||
name: "test",
|
||||
options: options,
|
||||
defaultValue: { label: "Option 3", value: "option3" },
|
||||
onChange: onChangeMock,
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
singleValue: () => "w-1",
|
||||
valueContainer: () => "w-2",
|
||||
control: () => "w-3",
|
||||
input: () => "w-4",
|
||||
option: () => "w-5",
|
||||
menuList: () => "w-6",
|
||||
menu: () => "w-7",
|
||||
multiValue: () => "w-8",
|
||||
};
|
||||
|
||||
const renderSelectWithForm = (newProps?: any) => {
|
||||
render(
|
||||
<form aria-label="test-form">
|
||||
<label htmlFor="test">Test</label>
|
||||
<Select {...props} {...newProps} inputId="test" />
|
||||
<p>Click Outside</p>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const selectOption = async (optionText: string) => {
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
screen.getByText(optionText);
|
||||
const option = screen.getByText(optionText);
|
||||
fireEvent.click(option);
|
||||
};
|
||||
|
||||
describe("Tests for Select File", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Test Select Component", () => {
|
||||
describe("Tests the default Behavior of Select component", () => {
|
||||
beforeEach(() => {
|
||||
renderSelectWithForm();
|
||||
});
|
||||
|
||||
test("Should render with the correct default value", async () => {
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option3" });
|
||||
});
|
||||
|
||||
test("Should select a correct option", async () => {
|
||||
await waitFor(async () => {
|
||||
await selectOption("Option 2");
|
||||
});
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
label: "Option 2",
|
||||
value: "option2",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
action: "select-option",
|
||||
name: "test",
|
||||
option: undefined,
|
||||
})
|
||||
);
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option2" });
|
||||
});
|
||||
|
||||
test("Should keep the default value after selections are shown and the user clicks outside the selection element", async () => {
|
||||
await waitFor(async () => {
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
|
||||
screen.getByText("Option 2");
|
||||
const outsideButton = screen.getByText("Click Outside");
|
||||
fireEvent.click(outsideButton);
|
||||
});
|
||||
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option3" });
|
||||
});
|
||||
|
||||
test("Should keep the selected value after the user has selected an option and clicked out of the selection element", async () => {
|
||||
await waitFor(async () => {
|
||||
await selectOption("Option 2");
|
||||
const outsideButton = screen.getByText("Click Outside");
|
||||
fireEvent.click(outsideButton);
|
||||
});
|
||||
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option2" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests the Select Component with isMulti", () => {
|
||||
test("Should have the right behavior when it has the prop isMulti", async () => {
|
||||
renderSelectWithForm({ isMulti: true });
|
||||
|
||||
await waitFor(async () => {
|
||||
const optionText = options[1].label;
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
const option = screen.getByText(optionText);
|
||||
fireEvent.click(option);
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
});
|
||||
|
||||
const option2Selected = screen.getByLabelText("Remove Option 2");
|
||||
const option3Selected = screen.getAllByLabelText("Remove Option 3");
|
||||
|
||||
expect(option2Selected).toBeInTheDocument();
|
||||
expect(option3Selected.length).toBeGreaterThan(0);
|
||||
|
||||
fireEvent.click(option2Selected);
|
||||
|
||||
expect(option2Selected).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests the classes and CSS of the Select component", () => {
|
||||
test("Should render classes correctly when isDisabled is true", async () => {
|
||||
renderSelectWithForm({ isDisabled: true });
|
||||
const singleValueEl = screen.getByText("Option 3");
|
||||
const valueContainerEl = singleValueEl.parentElement;
|
||||
const cotrolEl = valueContainerEl?.parentElement;
|
||||
|
||||
expect(cotrolEl).toHaveClass("bg-subtle");
|
||||
});
|
||||
|
||||
test("Should render classes correctly when classNames props is passed", async () => {
|
||||
renderSelectWithForm({ classNames });
|
||||
|
||||
const singleValueEl = screen.getByText("Option 3");
|
||||
const valueContainerEl = singleValueEl.parentElement;
|
||||
const cotrolEl = valueContainerEl?.parentElement;
|
||||
const inputEl = screen.getByRole("combobox", { hidden: true }).parentElement;
|
||||
|
||||
expect(singleValueEl).toHaveClass("w-1");
|
||||
expect(valueContainerEl).toHaveClass("w-2");
|
||||
expect(cotrolEl).toHaveClass("w-3");
|
||||
expect(inputEl).toHaveClass("w-4");
|
||||
|
||||
await waitFor(async () => {
|
||||
const optionText = options[1].label;
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
screen.getByText(optionText);
|
||||
});
|
||||
const optionEl = screen.getByText("Option 2").parentElement?.parentElement;
|
||||
const menuListEl = optionEl?.parentElement;
|
||||
const menuEl = menuListEl?.parentElement;
|
||||
|
||||
expect(optionEl).toHaveClass("w-5");
|
||||
expect(menuListEl).toHaveClass("w-6");
|
||||
expect(menuEl).toHaveClass("w-7");
|
||||
});
|
||||
|
||||
test("Should render classes correctly for multiValue when classNames and isMulti props are passed and menu is open", async () => {
|
||||
renderSelectWithForm({ classNames, isMulti: true });
|
||||
|
||||
const singleValueEl = screen.getByText("Option 3");
|
||||
const multiValueEl = singleValueEl.parentElement;
|
||||
|
||||
expect(singleValueEl).not.toHaveClass("w-1");
|
||||
expect(multiValueEl).toHaveClass("w-8");
|
||||
|
||||
await waitFor(async () => {
|
||||
const optionText = options[1].label;
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
const option = screen.getByText(optionText);
|
||||
fireEvent.click(option);
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
});
|
||||
|
||||
const option2 = screen.getByText(options[1].label);
|
||||
const menuIsOpenEl = option2.parentElement?.parentElement?.nextSibling;
|
||||
expect(menuIsOpenEl).toHaveClass(
|
||||
"[&>*:last-child]:rotate-180 [&>*:last-child]:transition-transform "
|
||||
);
|
||||
});
|
||||
|
||||
test("Should render classes correctly when focused, selected and menu is open", async () => {
|
||||
renderSelectWithForm();
|
||||
await waitFor(async () => {
|
||||
const optionText = options[1].label;
|
||||
const element = screen.getByLabelText("Test");
|
||||
element.focus();
|
||||
fireEvent.keyDown(element, { key: "ArrowDown", code: "ArrowDown" });
|
||||
const option = screen.getByText(optionText);
|
||||
option.focus();
|
||||
});
|
||||
const option1 = screen.getByText("Option 1");
|
||||
const option1Parent = option1.parentElement?.parentElement;
|
||||
const option3 = screen.getAllByText("Option 3");
|
||||
const option3Parent = option3[1].parentElement?.parentElement;
|
||||
const menuIsOpenEl = option3[0].parentElement?.nextSibling;
|
||||
|
||||
expect(option1).toBeInTheDocument();
|
||||
expect(option1Parent).toHaveClass("bg-subtle");
|
||||
expect(option3[1]).toBeInTheDocument();
|
||||
expect(option3Parent).toHaveClass("bg-emphasis text-default");
|
||||
expect(menuIsOpenEl).toHaveClass("rotate-180 transition-transform");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for SelectField component", () => {
|
||||
const renderSelectField = (newProps?: any) => {
|
||||
render(
|
||||
<form aria-label="test-form">
|
||||
<SelectField {...{ ...props, ...newProps }} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
test("Should render name as fallback label", () => {
|
||||
renderSelectField();
|
||||
expect(screen.getByText(props.name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should not render the label element when label not passed and name is undefined", () => {
|
||||
renderSelectField({ name: undefined });
|
||||
expect(screen.queryByRole("label")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render with the default value and label", () => {
|
||||
renderSelectField({ label: "Test SelectField", name: "test" });
|
||||
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option3" });
|
||||
|
||||
const labelElement = screen.getByText("Test SelectField");
|
||||
expect(labelElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for SelectWithValidation component", () => {
|
||||
const handleSubmit = vi.fn((event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
const renderSelectWithValidation = (required: boolean) => {
|
||||
render(
|
||||
<form onSubmit={handleSubmit} aria-label="test-form">
|
||||
<label htmlFor="test">Test</label>
|
||||
<SelectWithValidation {...props} required={required} inputId="test" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
test("Should render with the default value", () => {
|
||||
renderSelectWithValidation(true);
|
||||
|
||||
expect(screen.getByText("Option 3")).toBeInTheDocument();
|
||||
expect(screen.getByRole("form")).toHaveFormValues({ test: "option3" });
|
||||
});
|
||||
|
||||
test("Should render an input element with the value passed as prop", () => {
|
||||
renderSelectWithValidation(false);
|
||||
expect(screen.getAllByDisplayValue("option3")).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("Should submit the form with the selected value after validation", async () => {
|
||||
renderSelectWithValidation(true);
|
||||
|
||||
await waitFor(async () => {
|
||||
await selectOption("Option 2");
|
||||
const submitButton = screen.getByRole("button", { name: "Submit" });
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("Should fail to submit if nothing is selected", async () => {
|
||||
renderSelectWithValidation(true);
|
||||
|
||||
const submitButton = screen.getByRole("button", { name: "Submit" });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(handleSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import React, { forwardRef } from "react";
|
||||
|
||||
export const Discord = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(function Discord(props, ref) {
|
||||
export const Discord = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(function Discord(props) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import { Title, VariantRow, VariantsTable, CustomArgsTable } from "@calcom/storybook/components";
|
||||
|
||||
import ImageUploader from "./ImageUploader";
|
||||
|
||||
<Meta title="UI/ImageUploader" component={ImageUploader} />
|
||||
|
||||
<Title title="ImageUploader" suffix="Brief" subtitle="Version 1.0 — Last Update: 21 Aug 2023" />
|
||||
|
||||
## Definitions
|
||||
|
||||
`ImageUploader` is used to upload images of all image types
|
||||
|
||||
## Structure
|
||||
|
||||
Below are the props for `Image uploader`
|
||||
|
||||
<CustomArgsTable of={ImageUploader} />
|
||||
|
||||
## ImageUploader Story
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="ImageUploader"
|
||||
play={({ canvasElement }) => {
|
||||
const darkVariantContainer = canvasElement.querySelector("#dark-variant");
|
||||
const imageUploaderElement = darkVariantContainer.querySelector("button");
|
||||
imageUploaderElement?.addEventListener("click", () => {
|
||||
setTimeout(() => {
|
||||
document.querySelector('[role="dialog"]')?.classList.add("dark");
|
||||
}, 1);
|
||||
});
|
||||
}}
|
||||
args={{
|
||||
id: "image-1",
|
||||
buttonMsg: "upload",
|
||||
target: "target",
|
||||
handleAvatarChange: (src) => {
|
||||
console.debug(src);
|
||||
},
|
||||
}}>
|
||||
{({ id, buttonMsg, handleAvatarChange, imageSrc, target }) => (
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||
<VariantRow>
|
||||
<ImageUploader
|
||||
id={id}
|
||||
buttonMsg={buttonMsg}
|
||||
handleAvatarChange={handleAvatarChange}
|
||||
imageSrc={imageSrc}
|
||||
target={target}
|
||||
/>
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
|
@ -1,10 +1,8 @@
|
|||
import { render } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { ScrollableArea } from "./ScrollableArea";
|
||||
|
||||
describe("Tests for ScrollableArea Component", () => {
|
||||
const toJSONMock = vi.fn();
|
||||
test("Should render children inside the scrollable area", () => {
|
||||
const { getByText } = render(
|
||||
<ScrollableArea>
|
||||
|
|
|
@ -35,6 +35,7 @@ SheetPortal.displayName = SheetPrimitive.Portal.displayName;
|
|||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={classNames(
|
||||
|
|
|
@ -44,6 +44,14 @@ vi.mock("@calcom/lib/OgImages", async () => {
|
|||
return {};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useLocale", () => ({
|
||||
useLocale: () => {
|
||||
return {
|
||||
t: (str: string) => str,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
expect.extend({
|
||||
tabToBeDisabled(received) {
|
||||
const isDisabled = received.classList.contains("pointer-events-none");
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
import classNames from "classnames";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import type { ComponentType, ReactNode } from "react";
|
||||
|
||||
import type { LucideIcon, LucideProps } from "@calcom/ui/components/icon";
|
||||
import { X, AlertTriangle, Info } from "@calcom/ui/components/icon";
|
||||
import { AlertTriangle, Info } from "@calcom/ui/components/icon";
|
||||
|
||||
export type TopBannerProps = {
|
||||
Icon?: ComponentType<LucideProps> & LucideIcon;
|
||||
text: string;
|
||||
variant?: keyof typeof variantClassName;
|
||||
actions?: ReactNode;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const variantClassName = {
|
||||
|
@ -26,14 +23,14 @@ const defaultIconProps = {
|
|||
} as LucideProps;
|
||||
|
||||
export function TopBanner(props: TopBannerProps) {
|
||||
const { Icon, variant = "default", text, actions, onClose } = props;
|
||||
const { Icon, variant = "default", text, actions } = props;
|
||||
|
||||
const renderDefaultIconByVariant = () => {
|
||||
switch (variant) {
|
||||
case "error":
|
||||
return <AlertTriangle {...defaultIconProps} />;
|
||||
return <AlertTriangle {...defaultIconProps} data-testid="variant-error" />;
|
||||
case "warning":
|
||||
return <Info {...defaultIconProps} />;
|
||||
return <Info {...defaultIconProps} data-testid="variant-warning" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -49,19 +46,11 @@ export function TopBanner(props: TopBannerProps) {
|
|||
)}>
|
||||
<div className="flex flex-1 flex-col items-start justify-center gap-2 p-1 lg:flex-row lg:items-center">
|
||||
<p className="text-emphasis flex flex-col items-start justify-center gap-2 text-left font-sans text-sm font-medium leading-4 lg:flex-row lg:items-center">
|
||||
{Icon ? <Icon {...defaultIconProps} /> : defaultIcon}
|
||||
{Icon ? <Icon data-testid="variant-default" {...defaultIconProps} /> : defaultIcon}
|
||||
{text}
|
||||
</p>
|
||||
{actions && <div className="text-sm font-medium">{actions}</div>}
|
||||
</div>
|
||||
{typeof onClose === "function" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={noop}
|
||||
className="hover:bg-gray-20 text-muted flex items-center rounded-lg p-1.5 text-sm">
|
||||
<X className="text-emphasis h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
|
||||
import { ArrowDown } from "@calcom/ui/components/icon";
|
||||
|
||||
import { TopBanner } from "./TopBanner";
|
||||
|
||||
describe("Tests for TopBanner component", () => {
|
||||
test("Should render the component properly", () => {
|
||||
render(<TopBanner text="the banner test" />);
|
||||
|
||||
const bannerElt = screen.getByTestId("banner");
|
||||
expect(bannerElt).toBeInTheDocument();
|
||||
|
||||
const btnElt = screen.queryByRole("button");
|
||||
expect(btnElt).toBeNull();
|
||||
});
|
||||
|
||||
test("Should render actions", () => {
|
||||
render(<TopBanner text="the banner test" actions={<div>cta</div>} />);
|
||||
|
||||
const ctaElt = screen.getByText("cta");
|
||||
expect(ctaElt).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render the default variant", () => {
|
||||
render(<TopBanner text="the banner test" Icon={ArrowDown} />);
|
||||
|
||||
const variant = screen.getByTestId("variant-default");
|
||||
expect(variant).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render the alert variant", () => {
|
||||
render(<TopBanner text="the banner test" variant="error" />);
|
||||
|
||||
const variant = screen.getByTestId("variant-error");
|
||||
expect(variant).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("Should render the warning variant", () => {
|
||||
render(<TopBanner text="the banner test" variant="warning" />);
|
||||
|
||||
const variant = screen.getByTestId("variant-warning");
|
||||
expect(variant).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import { Title, CustomArgsTable, VariantsTable, VariantRow } from "@calcom/storybook/components";
|
||||
|
||||
import { UnpublishedEntity } from "./UnpublishedEntity";
|
||||
|
||||
<Meta title="UI/UnpublishedEntity" component={UnpublishedEntity} />
|
||||
|
||||
<Title title="UnpublishedEntity" suffix="Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
|
||||
|
||||
## Definition
|
||||
|
||||
The `UnpublishedEntity` component is used to display an "empty screen" with relevant information for unpublished teams or organizations.
|
||||
|
||||
## Structure
|
||||
|
||||
The `UnpublishedEntity` component consists of the following parts:
|
||||
|
||||
- An `EmptyScreen` component from `@calcom/ui` that displays a visual representation of an "empty screen."
|
||||
- An `Avatar` component from `@calcom/ui` that shows an avatar image associated with the organization or team.
|
||||
- Translated text obtained using the `useLocale` hook for displaying messages based on the entity's status.
|
||||
|
||||
<CustomArgsTable of={UnpublishedEntity} />
|
||||
|
||||
## UnpublishedEntity Story
|
||||
|
||||
<Canvas>
|
||||
<Story name="UnpublishedEntity" args={{ name: "TeamExample", teamSlug: "team-example" }}>
|
||||
{(args) => (
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||
<VariantRow>
|
||||
<UnpublishedEntity {...args} />
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
Loading…
Reference in New Issue
Block a user