Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4540c79580 | ||
|
|
00d01882d4 | ||
|
|
6ce4122528 | ||
|
|
7ddbcfcdcf | ||
|
|
b1b01ed383 | ||
|
|
6f5d9dc41f | ||
|
|
ac190b89dd | ||
|
|
990441295c | ||
|
|
1e4c20073c | ||
|
|
80353d67a0 | ||
|
|
d55b4b051a | ||
|
|
25d90e3e12 | ||
|
|
1727eb5625 | ||
|
|
58f4224c66 | ||
|
|
5d491e0a12 | ||
|
|
1eef5a56c3 | ||
|
|
d6740503be | ||
|
|
9f647ead46 | ||
|
|
f480afcace | ||
|
|
8d73d15b8a | ||
|
|
e8370ef950 | ||
|
|
e859c8dc58 |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
addDecorator((storyFn) => (
|
||||
|
||||
@@ -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.4",
|
||||
"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();
|
||||
|
||||
@@ -39,19 +39,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const username = slugify(data.username);
|
||||
const userEmail = email.toLowerCase();
|
||||
|
||||
const validationResponse = (
|
||||
incomingEmail: string,
|
||||
validation: { isValid: boolean; email: string | undefined }
|
||||
) => {
|
||||
const { isValid, email } = validation;
|
||||
if (!isValid) {
|
||||
const message: string =
|
||||
email !== incomingEmail ? "Username already taken" : "Email address is already registered";
|
||||
|
||||
return res.status(409).json({ message });
|
||||
}
|
||||
};
|
||||
|
||||
if (!username) {
|
||||
res.status(422).json({ message: "Invalid username" });
|
||||
return;
|
||||
@@ -79,11 +66,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
if (foundToken?.teamId) {
|
||||
const teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId);
|
||||
return validationResponse(userEmail, teamUserValidation);
|
||||
if (!teamUserValidation.isValid) {
|
||||
return res.status(409).json({ message: "Username or email is already taken" });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const userValidation = await validateUsername(username, userEmail);
|
||||
return validationResponse(userEmail, userValidation);
|
||||
if (!userValidation.isValid) {
|
||||
return res.status(409).json({ message: "Username or email is already taken" });
|
||||
}
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
@@ -94,7 +85,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
id: foundToken.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (team) {
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,26 +77,25 @@ 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({
|
||||
onSuccess: async (res) => {
|
||||
await update(res);
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
if (res.signOutUser && tempFormValues) {
|
||||
if (res.passwordReset) {
|
||||
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
|
||||
// sign out the user to avoid unauthorized access error
|
||||
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
|
||||
} else {
|
||||
// sign out the user to avoid unauthorized access error
|
||||
await signOut({ callbackUrl: "/auth/logout?emailChange=true" });
|
||||
}
|
||||
|
||||
// signout user only in case of password reset
|
||||
if (res.signOutUser && tempFormValues && res.passwordReset) {
|
||||
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
|
||||
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
|
||||
} else {
|
||||
utils.viewer.me.invalidate();
|
||||
utils.viewer.avatar.invalidate();
|
||||
utils.viewer.shouldVerifyEmail.invalidate();
|
||||
}
|
||||
utils.viewer.me.invalidate();
|
||||
utils.viewer.avatar.invalidate();
|
||||
|
||||
setConfirmAuthEmailChangeWarningDialogOpen(false);
|
||||
update(res);
|
||||
setTempFormValues(null);
|
||||
},
|
||||
onError: () => {
|
||||
@@ -325,7 +324,10 @@ const ProfileView = () => {
|
||||
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
|
||||
</div>
|
||||
<DialogFooter showDivider>
|
||||
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
|
||||
<Button
|
||||
color="primary"
|
||||
loading={confirmPasswordMutation.isLoading}
|
||||
onClick={(e) => onConfirmPassword(e)}>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
@@ -345,7 +347,7 @@ const ProfileView = () => {
|
||||
<DialogFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={updateProfileMutation.isLoading}
|
||||
loading={updateProfileMutation.isLoading}
|
||||
onClick={(e) => onConfirmAuthEmailChange(e)}>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { BookingStatus } from "@calcom/prisma/client";
|
||||
|
||||
import type { Fixtures } from "./lib/fixtures";
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Bookings", () => {
|
||||
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,
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createBooking({
|
||||
bookingsFixture,
|
||||
organizer,
|
||||
organizerEventType,
|
||||
attendees,
|
||||
/**
|
||||
* Relative date from today
|
||||
* 0 means today
|
||||
* 1 means tomorrow
|
||||
*/
|
||||
relativeDate = 0,
|
||||
durationMins = 30,
|
||||
title,
|
||||
}: {
|
||||
bookingsFixture: Fixtures["bookings"];
|
||||
organizer: {
|
||||
id: number;
|
||||
username: string | null;
|
||||
};
|
||||
organizerEventType: {
|
||||
id: number;
|
||||
};
|
||||
attendees: {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
}[];
|
||||
relativeDate?: number;
|
||||
durationMins?: number;
|
||||
title: string;
|
||||
}) {
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const bookingDurationMs = durationMins * 60 * 1000;
|
||||
const startTime = new Date(Date.now() + relativeDate * DAY_MS);
|
||||
const endTime = new Date(Date.now() + relativeDate * DAY_MS + bookingDurationMs);
|
||||
return await bookingsFixture.create(organizer.id, organizer.username, organizerEventType.id, {
|
||||
title,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime,
|
||||
endTime,
|
||||
attendees: {
|
||||
createMany: {
|
||||
data: [...attendees],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -91,6 +92,7 @@ test.describe("Event Types tests", () => {
|
||||
expect(formTitle).toBe(firstTitle);
|
||||
expect(formSlug).toContain(firstSlug);
|
||||
});
|
||||
|
||||
test("edit first event", async ({ page }) => {
|
||||
const $eventTypes = page.locator("[data-testid=event-types] > li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
@@ -152,5 +154,46 @@ test.describe("Event Types tests", () => {
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Different Locations Tests", () => {
|
||||
test("can add Attendee Phone Number location and book with it", async ({ page }) => {
|
||||
await gotoFirstEventType(page);
|
||||
await selectAttendeePhoneNumber(page);
|
||||
await saveEventType(page);
|
||||
await gotoBookingPage(page);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.locator(`[data-fob-field-name="location"] input`).fill("9199999999");
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
await expect(page.locator("text=+19199999999")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const selectAttendeePhoneNumber = async (page: Page) => {
|
||||
const locationOptionText = "Attendee Phone Number";
|
||||
await page.locator("#location-select").click();
|
||||
await page.locator(`text=${locationOptionText}`).click();
|
||||
};
|
||||
|
||||
async function gotoFirstEventType(page: Page) {
|
||||
const $eventTypes = page.locator("[data-testid=event-types] > li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
await firstEventTypeElement.click();
|
||||
await page.waitForURL((url) => {
|
||||
return !!url.pathname.match(/\/event-types\/.+/);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveEventType(page: Page) {
|
||||
await page.locator("[data-testid=update-eventtype]").click();
|
||||
}
|
||||
|
||||
async function gotoBookingPage(page: Page) {
|
||||
const previewLink = await page.locator("[data-testid=preview-button]").getAttribute("href");
|
||||
|
||||
await page.goto(previewLink ?? "");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -269,12 +269,14 @@ describe("getSchedule", () => {
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
},
|
||||
});
|
||||
|
||||
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
|
||||
@@ -353,13 +355,15 @@ describe("getSchedule", () => {
|
||||
|
||||
// Day Plus 2 is completely free - It only has non accepted bookings
|
||||
const scheduleOnCompletelyFreeDay = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
// EventTypeSlug doesn't matter for non-dynamic events
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
// EventTypeSlug doesn't matter for non-dynamic events
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
// getSchedule returns timeslots in GMT
|
||||
@@ -384,12 +388,14 @@ describe("getSchedule", () => {
|
||||
|
||||
// Day plus 3
|
||||
const scheduleForDayWithOneBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
|
||||
@@ -448,12 +454,14 @@ describe("getSchedule", () => {
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
const scheduleForEventWith30Length = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
||||
@@ -482,12 +490,14 @@ describe("getSchedule", () => {
|
||||
);
|
||||
|
||||
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
// `slotInterval` takes precedence over `length`
|
||||
// 4:30 is utc so it is 10:00 in IST
|
||||
@@ -545,12 +555,14 @@ describe("getSchedule", () => {
|
||||
const { dateString: todayDateString } = getDate();
|
||||
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
|
||||
const scheduleForEventWithBookingNotice13Hrs = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
||||
[
|
||||
@@ -564,12 +576,14 @@ describe("getSchedule", () => {
|
||||
);
|
||||
|
||||
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
|
||||
[
|
||||
@@ -627,12 +641,14 @@ describe("getSchedule", () => {
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForEventOnADayWithNonCalBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
|
||||
@@ -700,12 +716,14 @@ describe("getSchedule", () => {
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForEventOnADayWithCalBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
|
||||
@@ -757,12 +775,14 @@ describe("getSchedule", () => {
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const schedule = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(schedule).toHaveTimeSlots(
|
||||
@@ -820,12 +840,14 @@ describe("getSchedule", () => {
|
||||
createBookingScenario(scenarioData);
|
||||
|
||||
const scheduleForEventOnADayWithDateOverride = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
|
||||
@@ -905,12 +927,14 @@ describe("getSchedule", () => {
|
||||
// Requesting this user's availability for their
|
||||
// individual Event Type
|
||||
const thisUserAvailability = await getSchedule({
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
input: {
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
}
|
||||
});
|
||||
|
||||
expect(thisUserAvailability).toHaveTimeSlots(
|
||||
@@ -1002,12 +1026,14 @@ describe("getSchedule", () => {
|
||||
});
|
||||
|
||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
});
|
||||
|
||||
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
||||
@@ -1030,12 +1056,14 @@ describe("getSchedule", () => {
|
||||
);
|
||||
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
});
|
||||
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
@@ -1137,12 +1165,14 @@ describe("getSchedule", () => {
|
||||
hosts: [],
|
||||
});
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
|
||||
@@ -1163,12 +1193,14 @@ describe("getSchedule", () => {
|
||||
);
|
||||
|
||||
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: true,
|
||||
}
|
||||
});
|
||||
// A user with blocked time in another event, still affects Team Event availability
|
||||
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
|
||||
|
||||
@@ -303,6 +303,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
timeZone: _timeZone,
|
||||
parentId: _parentId,
|
||||
userId: _userId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
requiresBookerEmailVerification,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
|
||||
@@ -40,6 +40,7 @@ type App = Partial<AppMeta> & {
|
||||
function generateFiles() {
|
||||
const browserOutput = [`import dynamic from "next/dynamic"`];
|
||||
const metadataOutput = [];
|
||||
const bookerMetadataOutput = [];
|
||||
const schemasOutput = [];
|
||||
const appKeysSchemasOutput = [];
|
||||
const serverOutput = [];
|
||||
@@ -70,22 +71,30 @@ function generateFiles() {
|
||||
}
|
||||
});
|
||||
|
||||
function forEachAppDir(callback: (arg: App) => void) {
|
||||
function forEachAppDir(callback: (arg: App) => void, filter: (arg: App) => boolean = () => true) {
|
||||
for (let i = 0; i < appDirs.length; i++) {
|
||||
const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json");
|
||||
const metadataPath = path.join(APP_STORE_PATH, appDirs[i].path, "_metadata.ts");
|
||||
let app;
|
||||
|
||||
if (fs.existsSync(configPath)) {
|
||||
app = JSON.parse(fs.readFileSync(configPath).toString());
|
||||
} else if (fs.existsSync(metadataPath)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
app = require(metadataPath).metadata;
|
||||
} else {
|
||||
app = {};
|
||||
}
|
||||
|
||||
callback({
|
||||
const finalApp = {
|
||||
...app,
|
||||
name: appDirs[i].name,
|
||||
path: appDirs[i].path,
|
||||
});
|
||||
};
|
||||
|
||||
if (filter(finalApp)) {
|
||||
callback(finalApp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +139,8 @@ function generateFiles() {
|
||||
lazyImport?: boolean;
|
||||
importConfig: ImportConfig;
|
||||
entryObjectKeyGetter?: (arg: App, importName?: string) => string;
|
||||
}
|
||||
},
|
||||
filter?: (arg: App) => boolean
|
||||
) {
|
||||
const output: string[] = [];
|
||||
|
||||
@@ -174,7 +184,7 @@ function generateFiles() {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, filter);
|
||||
}
|
||||
|
||||
function createExportObject() {
|
||||
@@ -201,7 +211,7 @@ function generateFiles() {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, filter);
|
||||
|
||||
output.push(`};`);
|
||||
}
|
||||
@@ -247,6 +257,25 @@ function generateFiles() {
|
||||
})
|
||||
);
|
||||
|
||||
bookerMetadataOutput.push(
|
||||
...getExportedObject(
|
||||
"appStoreMetadata",
|
||||
{
|
||||
// Try looking for config.json and if it's not found use _metadata.ts to generate appStoreMetadata
|
||||
importConfig: [
|
||||
{
|
||||
fileToBeImported: "config.json",
|
||||
importName: "default",
|
||||
},
|
||||
{
|
||||
fileToBeImported: "_metadata.ts",
|
||||
importName: "metadata",
|
||||
},
|
||||
],
|
||||
},
|
||||
isBookerApp
|
||||
)
|
||||
);
|
||||
schemasOutput.push(
|
||||
...getExportedObject("appDataSchemas", {
|
||||
// Import path must have / even for windows and not \
|
||||
@@ -312,6 +341,7 @@ function generateFiles() {
|
||||
["apps.browser.generated.tsx", browserOutput],
|
||||
["apps.schemas.generated.ts", schemasOutput],
|
||||
["apps.keys-schemas.generated.ts", appKeysSchemasOutput],
|
||||
["bookerApps.metadata.generated.ts", bookerMetadataOutput],
|
||||
];
|
||||
filesToGenerate.forEach(([fileName, output]) => {
|
||||
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
|
||||
@@ -347,3 +377,11 @@ if (isInWatchMode) {
|
||||
} else {
|
||||
generateFiles();
|
||||
}
|
||||
|
||||
function isBookerApp(app: App) {
|
||||
// Right now there are only two types of Apps that booker needs.
|
||||
// Note that currently payment apps' meta don't need to be accessed on booker. We just access from DB eventType.metadata
|
||||
// 1. It is a location app(e.g. any Conferencing App)
|
||||
// 2. It is a tag manager app(e.g. Google Analytics, GTM, Fathom)
|
||||
return !!(app.appData?.location || app.appData?.tag);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Script from "next/script";
|
||||
|
||||
import { getEventTypeAppData } from "@calcom/app-store/_utils/getEventTypeAppData";
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData";
|
||||
|
||||
import type { appDataSchemas } from "./apps.schemas.generated";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated";
|
||||
import { getAppAssetFullPath } from "./getAppAssetFullPath";
|
||||
import { getNormalizedAppMetadata } from "./getNormalizedAppMetadata";
|
||||
|
||||
type RawAppStoreMetaData = typeof rawAppStoreMetadata;
|
||||
type AppStoreMetaData = {
|
||||
@@ -10,18 +10,5 @@ type AppStoreMetaData = {
|
||||
|
||||
export const appStoreMetadata = {} as AppStoreMetaData;
|
||||
for (const [key, value] of Object.entries(rawAppStoreMetadata)) {
|
||||
const dirName = "dirName" in value ? value.dirName : value.slug;
|
||||
if (!dirName) {
|
||||
throw new Error(`Couldn't derive dirName for app ${key}`);
|
||||
}
|
||||
const metadata = (appStoreMetadata[key as keyof typeof appStoreMetadata] = {
|
||||
appData: null,
|
||||
dirName,
|
||||
__template: "",
|
||||
...value,
|
||||
} as AppStoreMetaData[keyof AppStoreMetaData]);
|
||||
metadata.logo = getAppAssetFullPath(metadata.logo, {
|
||||
dirName,
|
||||
isTemplate: metadata.isTemplate,
|
||||
});
|
||||
appStoreMetadata[key as keyof typeof appStoreMetadata] = getNormalizedAppMetadata(value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
This file is autogenerated using the command `yarn app-store:build --watch`.
|
||||
Don't modify this file manually.
|
||||
**/
|
||||
import around_config_json from "./around/config.json";
|
||||
import campfire_config_json from "./campfire/config.json";
|
||||
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
|
||||
import discord_config_json from "./discord/config.json";
|
||||
import eightxeight_config_json from "./eightxeight/config.json";
|
||||
import element_call_config_json from "./element-call/config.json";
|
||||
import facetime_config_json from "./facetime/config.json";
|
||||
import fathom_config_json from "./fathom/config.json";
|
||||
import ga4_config_json from "./ga4/config.json";
|
||||
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
|
||||
import gtm_config_json from "./gtm/config.json";
|
||||
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
|
||||
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||
import metapixel_config_json from "./metapixel/config.json";
|
||||
import mirotalk_config_json from "./mirotalk/config.json";
|
||||
import office365video_config_json from "./office365video/config.json";
|
||||
import ping_config_json from "./ping/config.json";
|
||||
import plausible_config_json from "./plausible/config.json";
|
||||
import riverside_config_json from "./riverside/config.json";
|
||||
import signal_config_json from "./signal/config.json";
|
||||
import sirius_video_config_json from "./sirius_video/config.json";
|
||||
import sylapsvideo_config_json from "./sylapsvideo/config.json";
|
||||
import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata";
|
||||
import telegram_config_json from "./telegram/config.json";
|
||||
import booking_pages_tag_config_json from "./templates/booking-pages-tag/config.json";
|
||||
import event_type_location_video_static_config_json from "./templates/event-type-location-video-static/config.json";
|
||||
import webex_config_json from "./webex/config.json";
|
||||
import whatsapp_config_json from "./whatsapp/config.json";
|
||||
import whereby_config_json from "./whereby/config.json";
|
||||
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
|
||||
|
||||
export const appStoreMetadata = {
|
||||
around: around_config_json,
|
||||
campfire: campfire_config_json,
|
||||
dailyvideo: dailyvideo__metadata_ts,
|
||||
discord: discord_config_json,
|
||||
eightxeight: eightxeight_config_json,
|
||||
"element-call": element_call_config_json,
|
||||
facetime: facetime_config_json,
|
||||
fathom: fathom_config_json,
|
||||
ga4: ga4_config_json,
|
||||
googlevideo: googlevideo__metadata_ts,
|
||||
gtm: gtm_config_json,
|
||||
huddle01video: huddle01video__metadata_ts,
|
||||
jitsivideo: jitsivideo__metadata_ts,
|
||||
metapixel: metapixel_config_json,
|
||||
mirotalk: mirotalk_config_json,
|
||||
office365video: office365video_config_json,
|
||||
ping: ping_config_json,
|
||||
plausible: plausible_config_json,
|
||||
riverside: riverside_config_json,
|
||||
signal: signal_config_json,
|
||||
sirius_video: sirius_video_config_json,
|
||||
sylapsvideo: sylapsvideo_config_json,
|
||||
tandemvideo: tandemvideo__metadata_ts,
|
||||
telegram: telegram_config_json,
|
||||
"booking-pages-tag": booking_pages_tag_config_json,
|
||||
"event-type-location-video-static": event_type_location_video_static_config_json,
|
||||
webex: webex_config_json,
|
||||
whatsapp: whatsapp_config_json,
|
||||
whereby: whereby_config_json,
|
||||
zoomvideo: zoomvideo__metadata_ts,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
// We have to import all the booker-apps config/metadata in here as without importing that we can't dig into their config and read their props
|
||||
// It isn't a significant bundle-size impact as we are only importing the metadata of only the booker apps, but when it becomes a problem we can figure out a solution
|
||||
import { appStoreMetadata as rawBookerAppsMetadata } from "./bookerApps.metadata.generated";
|
||||
import { getNormalizedAppMetadata } from "./getNormalizedAppMetadata";
|
||||
|
||||
type RawAppStoreMetaData = typeof rawBookerAppsMetadata;
|
||||
type AppStoreMetaData = {
|
||||
[key in keyof RawAppStoreMetaData]: AppMeta;
|
||||
};
|
||||
|
||||
export const appStoreMetadata = {} as AppStoreMetaData;
|
||||
for (const [key, value] of Object.entries(rawBookerAppsMetadata)) {
|
||||
appStoreMetadata[key as keyof typeof appStoreMetadata] = getNormalizedAppMetadata(value);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
// We have to import all the booker-apps config/metadata in here as without that we couldn't
|
||||
import type { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated";
|
||||
import { getAppAssetFullPath } from "./getAppAssetFullPath";
|
||||
|
||||
type RawAppStoreMetaData = typeof rawAppStoreMetadata;
|
||||
type AppStoreMetaData = {
|
||||
[key in keyof RawAppStoreMetaData]: AppMeta;
|
||||
};
|
||||
|
||||
export const getNormalizedAppMetadata = (appMeta: RawAppStoreMetaData[keyof RawAppStoreMetaData]) => {
|
||||
const dirName = "dirName" in appMeta ? appMeta.dirName : appMeta.slug;
|
||||
if (!dirName) {
|
||||
throw new Error(`Couldn't derive dirName for app ${appMeta.name}`);
|
||||
}
|
||||
const metadata = {
|
||||
appData: null,
|
||||
dirName,
|
||||
__template: "",
|
||||
...appMeta,
|
||||
} as AppStoreMetaData[keyof AppStoreMetaData];
|
||||
metadata.logo = getAppAssetFullPath(metadata.logo, {
|
||||
dirName,
|
||||
isTemplate: metadata.isTemplate,
|
||||
});
|
||||
return metadata;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { Ensure, Optional } from "@calcom/types/utils";
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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,11 +1,13 @@
|
||||
import { LazyMotion, domAnimation, m, AnimatePresence } from "framer-motion";
|
||||
import { LazyMotion, m, AnimatePresence } from "framer-motion";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useRef } from "react";
|
||||
import StickyBox from "react-sticky-box";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||
@@ -26,6 +28,7 @@ import { validateLayout } from "./utils/layout";
|
||||
import { getQueryParam } from "./utils/query-param";
|
||||
import { useBrandColors } from "./utils/use-brand-colors";
|
||||
|
||||
const loadFramerFeatures = () => import("./framer-features").then((res) => res.default);
|
||||
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
|
||||
const UnpublishedEntity = dynamic(() => import("@calcom/ui").then((mod) => mod.UnpublishedEntity));
|
||||
const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => mod.DatePicker), {
|
||||
@@ -73,6 +76,9 @@ const BookerComponent = ({
|
||||
// In Embed we give preference to embed configuration for the layout.If that's not set, we use the App configuration for the event layout
|
||||
// But if it's mobile view, there is only one layout supported which is 'mobile'
|
||||
const layout = isEmbed ? (isMobile ? "mobile" : validateLayout(embedUiConfig.layout) || _layout) : _layout;
|
||||
const columnViewExtraDays = useRef<number>(
|
||||
isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop
|
||||
);
|
||||
|
||||
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
|
||||
const selectedDate = useBookerStore((state) => state.selectedDate);
|
||||
@@ -86,9 +92,36 @@ const BookerComponent = ({
|
||||
shallow
|
||||
);
|
||||
|
||||
const date = dayjs(selectedDate).format("YYYY-MM-DD");
|
||||
const schedule = useScheduleForEvent({ prefetchNextMonth: true });
|
||||
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
|
||||
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
|
||||
);
|
||||
|
||||
const extraDays = isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop;
|
||||
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
|
||||
const animationScope = useBookerResizeAnimation(layout, bookerState);
|
||||
const totalWeekDays = 7;
|
||||
const addonDays =
|
||||
nonEmptyScheduleDays.length < extraDays
|
||||
? (extraDays - nonEmptyScheduleDays.length + 1) * totalWeekDays
|
||||
: nonEmptyScheduleDays.length === extraDays
|
||||
? totalWeekDays
|
||||
: 0;
|
||||
|
||||
//Taking one more avaliable slot(extraDays + 1) to claculate the no of days in between, that next and prev button need to shift.
|
||||
const availableSlots = nonEmptyScheduleDays.slice(0, extraDays + 1);
|
||||
if (nonEmptyScheduleDays.length !== 0)
|
||||
columnViewExtraDays.current =
|
||||
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 2], "day")) + addonDays;
|
||||
const prefetchNextMonth =
|
||||
dayjs(date).month() !== dayjs(date).add(columnViewExtraDays.current, "day").month();
|
||||
const monthCount =
|
||||
dayjs(date).add(1, "month").month() !== dayjs(date).add(columnViewExtraDays.current, "day").month()
|
||||
? 2
|
||||
: undefined;
|
||||
const nextSlots =
|
||||
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 1], "day")) + addonDays;
|
||||
|
||||
// I would expect isEmbed to be not needed here as it's handled in derived variable layout, but somehow removing it breaks the views.
|
||||
const defaultLayout = isEmbed
|
||||
@@ -213,8 +246,9 @@ const BookerComponent = ({
|
||||
)}>
|
||||
<Header
|
||||
enabledLayouts={bookerLayouts.enabledLayouts}
|
||||
extraDays={extraDays}
|
||||
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
|
||||
isMobile={isMobile}
|
||||
nextSlots={nextSlots}
|
||||
/>
|
||||
</BookerSection>
|
||||
<StickyOnDesktop
|
||||
@@ -280,7 +314,7 @@ const BookerComponent = ({
|
||||
layout === BookerLayouts.COLUMN_VIEW
|
||||
}
|
||||
className={classNames(
|
||||
"border-subtle rtl:border-default flex h-full w-full flex-col px-5 py-3 pb-0 rtl:border-r ltr:md:border-l",
|
||||
"border-subtle rtl:border-default flex h-full w-full flex-col overflow-x-auto px-5 py-3 pb-0 rtl:border-r ltr:md:border-l",
|
||||
layout === BookerLayouts.MONTH_VIEW &&
|
||||
"scroll-bar h-full overflow-auto md:w-[var(--booker-timeslots-width)]",
|
||||
layout !== BookerLayouts.MONTH_VIEW && "sticky top-0"
|
||||
@@ -290,6 +324,8 @@ const BookerComponent = ({
|
||||
<AvailableTimeSlots
|
||||
extraDays={extraDays}
|
||||
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
||||
prefetchNextMonth={prefetchNextMonth}
|
||||
monthCount={monthCount}
|
||||
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
|
||||
/>
|
||||
</BookerSection>
|
||||
@@ -319,7 +355,7 @@ export const Booker = (props: BookerProps) => {
|
||||
if (props.isAway) return <Away />;
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<LazyMotion strict features={loadFramerFeatures}>
|
||||
<BookerComponent {...props} />
|
||||
</LazyMotion>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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 { useSlotsForMultipleDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
import { useSlotsForAvailableDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
|
||||
import { classNames } from "@calcom/lib";
|
||||
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";
|
||||
@@ -13,6 +16,8 @@ import { useEvent, useScheduleForEvent } from "../utils/event";
|
||||
type AvailableTimeSlotsProps = {
|
||||
extraDays?: number;
|
||||
limitHeight?: boolean;
|
||||
prefetchNextMonth: boolean;
|
||||
monthCount: number | undefined;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
};
|
||||
|
||||
@@ -23,7 +28,14 @@ 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 }: 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);
|
||||
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
|
||||
@@ -31,6 +43,8 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
|
||||
const isEmbed = useIsEmbed();
|
||||
const event = useEvent();
|
||||
const date = selectedDate || dayjs().format("YYYY-MM-DD");
|
||||
const [layout] = useBookerStore((state) => [state.layout]);
|
||||
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const onTimeSelect = (
|
||||
@@ -57,29 +71,23 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
|
||||
};
|
||||
|
||||
const schedule = useScheduleForEvent({
|
||||
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
|
||||
prefetchNextMonth,
|
||||
monthCount,
|
||||
});
|
||||
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 = useMemo(
|
||||
() =>
|
||||
!extraDays
|
||||
? [date]
|
||||
: [
|
||||
// If NO date is selected yet, we show by default the upcomming `nextDays` days.
|
||||
date,
|
||||
...Array.from({ length: extraDays }).map((_, index) =>
|
||||
dayjs(date)
|
||||
.add(index + 1, "day")
|
||||
.format("YYYY-MM-DD")
|
||||
),
|
||||
],
|
||||
[date, extraDays]
|
||||
);
|
||||
const dates = !extraDays
|
||||
? [date]
|
||||
: nonEmptyScheduleDaysFromSelectedDate.length > 0
|
||||
? nonEmptyScheduleDaysFromSelectedDate.slice(0, extraDays)
|
||||
: [];
|
||||
|
||||
const isMultipleDates = dates.length > 1;
|
||||
const slotsPerDay = useSlotsForMultipleDates(dates, schedule?.data?.slots);
|
||||
const slotsPerDay = useSlotsForAvailableDates(dates, schedule?.data?.slots);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmbed) return;
|
||||
@@ -103,11 +111,16 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
|
||||
<AvailableTimes
|
||||
className="w-full"
|
||||
key={slots.date}
|
||||
showTimeFormatToggle={!isColumnView}
|
||||
onTimeSelect={onTimeSelect}
|
||||
date={dayjs(slots.date)}
|
||||
slots={slots.slots}
|
||||
onTimeSelect={onTimeSelect}
|
||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||
showTimeFormatToggle={!isMultipleDates}
|
||||
availableMonth={
|
||||
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
|
||||
? dayjs(slots.date).format("MMM")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -16,10 +16,12 @@ export function Header({
|
||||
extraDays,
|
||||
isMobile,
|
||||
enabledLayouts,
|
||||
nextSlots,
|
||||
}: {
|
||||
extraDays: number;
|
||||
isMobile: boolean;
|
||||
enabledLayouts: BookerLayouts[];
|
||||
nextSlots: number;
|
||||
}) {
|
||||
const { t, i18n } = useLocale();
|
||||
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
|
||||
@@ -56,8 +58,7 @@ export function Header({
|
||||
if (isMonthView) {
|
||||
return <LayoutToggleWithData />;
|
||||
}
|
||||
|
||||
const endDate = selectedDate.add(extraDays - 1, "days");
|
||||
const endDate = selectedDate.add(layout === BookerLayouts.COLUMN_VIEW ? extraDays : extraDays - 1, "days");
|
||||
|
||||
const isSameMonth = () => {
|
||||
return selectedDate.format("MMM") === endDate.format("MMM");
|
||||
@@ -91,7 +92,7 @@ export function Header({
|
||||
color="minimal"
|
||||
StartIcon={ChevronLeft}
|
||||
aria-label="Previous Day"
|
||||
onClick={() => addToSelectedDate(-extraDays)}
|
||||
onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? -nextSlots : -extraDays)}
|
||||
/>
|
||||
<Button
|
||||
className="group rtl:mr-1 rtl:rotate-180"
|
||||
@@ -99,7 +100,7 @@ export function Header({
|
||||
color="minimal"
|
||||
StartIcon={ChevronRight}
|
||||
aria-label="Next Day"
|
||||
onClick={() => addToSelectedDate(extraDays)}
|
||||
onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? nextSlots : extraDays)}
|
||||
/>
|
||||
{selectedDateMin3DaysDifference && (
|
||||
<Button
|
||||
|
||||
@@ -224,7 +224,7 @@ export const extraDaysConfig = {
|
||||
tablet: 4,
|
||||
},
|
||||
[BookerLayouts.COLUMN_VIEW]: {
|
||||
desktop: 4,
|
||||
desktop: 6,
|
||||
tablet: 2,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { domAnimation } from "framer-motion";
|
||||
|
||||
export default domAnimation;
|
||||
@@ -45,6 +45,7 @@ export const useScheduleForEvent = ({
|
||||
eventId,
|
||||
month,
|
||||
duration,
|
||||
monthCount,
|
||||
}: {
|
||||
prefetchNextMonth?: boolean;
|
||||
username?: string | null;
|
||||
@@ -52,6 +53,7 @@ export const useScheduleForEvent = ({
|
||||
eventId?: number | null;
|
||||
month?: string | null;
|
||||
duration?: number | null;
|
||||
monthCount?: number;
|
||||
} = {}) => {
|
||||
const { timezone } = useTimePreferences();
|
||||
const event = useEvent();
|
||||
@@ -70,6 +72,7 @@ export const useScheduleForEvent = ({
|
||||
eventId: event.data?.id ?? eventId,
|
||||
timezone,
|
||||
prefetchNextMonth,
|
||||
monthCount,
|
||||
rescheduleUid,
|
||||
month: monthFromStore ?? month,
|
||||
duration: durationFromStore ?? duration,
|
||||
|
||||
@@ -26,6 +26,7 @@ type AvailableTimesProps = {
|
||||
seatsPerTimeSlot?: number | null;
|
||||
showTimeFormatToggle?: boolean;
|
||||
className?: string;
|
||||
availableMonth?: string | undefined;
|
||||
selectedSlots?: string[];
|
||||
};
|
||||
|
||||
@@ -36,6 +37,7 @@ export const AvailableTimes = ({
|
||||
seatsPerTimeSlot,
|
||||
showTimeFormatToggle = true,
|
||||
className,
|
||||
availableMonth,
|
||||
selectedSlots,
|
||||
}: AvailableTimesProps) => {
|
||||
const { t, i18n } = useLocale();
|
||||
@@ -65,6 +67,7 @@ export const AvailableTimes = ({
|
||||
isMonthView ? "text-default text-sm" : "text-xs"
|
||||
)}>
|
||||
{date.format("DD")}
|
||||
{availableMonth && `, ${availableMonth}`}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { AppFlags } from "@calcom/features/flags/config";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function useFlags() {
|
||||
const query = trpc.viewer.features.map.useQuery(undefined, {
|
||||
initialData: process.env.NEXT_PUBLIC_IS_E2E
|
||||
? { "managed-event-types": true, organizations: true, teams: true }
|
||||
: {},
|
||||
: undefined,
|
||||
placeholderData: {},
|
||||
});
|
||||
return query.data;
|
||||
return query.data ?? ({} as AppFlags);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ type UseScheduleWithCacheArgs = {
|
||||
timezone?: string | null;
|
||||
prefetchNextMonth?: boolean;
|
||||
duration?: number | null;
|
||||
monthCount?: number | null;
|
||||
rescheduleUid?: string | null;
|
||||
isTeamEvent?: boolean;
|
||||
};
|
||||
@@ -22,11 +23,12 @@ export const useSchedule = ({
|
||||
eventId,
|
||||
prefetchNextMonth,
|
||||
duration,
|
||||
monthCount,
|
||||
rescheduleUid,
|
||||
isTeamEvent,
|
||||
}: UseScheduleWithCacheArgs) => {
|
||||
const monthDayjs = month ? dayjs(month) : dayjs();
|
||||
const nextMonthDayjs = monthDayjs.add(1, "month");
|
||||
const nextMonthDayjs = monthDayjs.add(monthCount ? monthCount : 1, "month");
|
||||
// Why the non-null assertions? All of these arguments are checked in the enabled condition,
|
||||
// and the query will not run if they are null. However, the check in `enabled` does
|
||||
// no satisfy typscript.
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Slots } from "./types";
|
||||
* @param date Format YYYY-MM-DD
|
||||
* @param scheduleCache Instance of useScheduleWithCache
|
||||
*/
|
||||
|
||||
export const useSlotsForDate = (date: string | null, slots?: Slots) => {
|
||||
const slotsForDate = useMemo(() => {
|
||||
if (!date || typeof slots === "undefined") return [];
|
||||
@@ -16,9 +17,9 @@ export const useSlotsForDate = (date: string | null, slots?: Slots) => {
|
||||
return slotsForDate;
|
||||
};
|
||||
|
||||
export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots) => {
|
||||
export const useSlotsForAvailableDates = (dates: (string | null)[], slots?: Slots) => {
|
||||
const slotsForDates = useMemo(() => {
|
||||
if (typeof slots === "undefined") return [];
|
||||
if (slots === undefined) return [];
|
||||
return dates
|
||||
.filter((date) => date !== null)
|
||||
.map((date) => ({
|
||||
@@ -26,6 +27,5 @@ export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots
|
||||
date,
|
||||
}));
|
||||
}, [dates, slots]);
|
||||
|
||||
return slotsForDates;
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -383,6 +383,7 @@ async function getBookings({
|
||||
);
|
||||
|
||||
const plainBookings = getUniqueBookings(
|
||||
// It's going to mess up the orderBy as we are concatenating independent queries results
|
||||
bookingsQueryUserId
|
||||
.concat(bookingsQueryAttendees)
|
||||
.concat(bookingsQueryTeamMember)
|
||||
@@ -399,6 +400,8 @@ async function getBookings({
|
||||
},
|
||||
},
|
||||
select: bookingSelect,
|
||||
// We need to get the sorted bookings here as well because plainBookings array is not correctly sorted
|
||||
orderBy,
|
||||
})
|
||||
).map((booking) => {
|
||||
// If seats are enabled and the event is not set to show attendees, filter out attendees that are not the current user
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { IncomingMessage } from "http";
|
||||
|
||||
import type { TGetScheduleInputSchema } from "./getSchedule.schema";
|
||||
import { getAvailableSlots } from "./util";
|
||||
|
||||
type GetScheduleOptions = {
|
||||
ctx: Record<string, unknown>;
|
||||
export type GetScheduleOptions = {
|
||||
ctx?: ContextForGetSchedule;
|
||||
input: TGetScheduleInputSchema;
|
||||
};
|
||||
|
||||
export const getScheduleHandler = async ({ input }: GetScheduleOptions) => {
|
||||
return await getAvailableSlots(input);
|
||||
interface ContextForGetSchedule extends Record<string, unknown> {
|
||||
req?: (IncomingMessage & { cookies: Partial<{ [key: string]: string }> }) | undefined;
|
||||
}
|
||||
|
||||
export const getScheduleHandler = async ({ ctx, input }: GetScheduleOptions) => {
|
||||
return await getAvailableSlots({ ctx, input });
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { CurrentSeats } from "@calcom/core/getUserAvailability";
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import isTimeOutOfBounds from "@calcom/lib/isOutOfBounds";
|
||||
import logger from "@calcom/lib/logger";
|
||||
@@ -19,6 +20,7 @@ import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { GetScheduleOptions } from "./getSchedule.handler";
|
||||
import type { TGetScheduleInputSchema } from "./getSchedule.schema";
|
||||
|
||||
export const checkIfIsAvailable = ({
|
||||
@@ -74,19 +76,27 @@ async function getEventTypeId({
|
||||
slug,
|
||||
eventTypeSlug,
|
||||
isTeamEvent,
|
||||
organizationDetails,
|
||||
}: {
|
||||
slug?: string;
|
||||
eventTypeSlug?: string;
|
||||
isTeamEvent: boolean;
|
||||
organizationDetails?: { currentOrgDomain: string | null; isValidOrgDomain: boolean };
|
||||
}) {
|
||||
if (!eventTypeSlug || !slug) return null;
|
||||
|
||||
let teamId;
|
||||
let userId;
|
||||
if (isTeamEvent) {
|
||||
teamId = await getTeamIdFromSlug(slug);
|
||||
teamId = await getTeamIdFromSlug(
|
||||
slug,
|
||||
organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false }
|
||||
);
|
||||
} else {
|
||||
userId = await getUserIdFromUsername(slug);
|
||||
userId = await getUserIdFromUsername(
|
||||
slug,
|
||||
organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false }
|
||||
);
|
||||
}
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
@@ -104,7 +114,10 @@ async function getEventTypeId({
|
||||
return eventType?.id;
|
||||
}
|
||||
|
||||
export async function getEventType(input: TGetScheduleInputSchema) {
|
||||
export async function getEventType(
|
||||
input: TGetScheduleInputSchema,
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const { eventTypeSlug, usernameList, isTeamEvent } = input;
|
||||
const eventTypeId =
|
||||
input.eventTypeId ||
|
||||
@@ -113,6 +126,7 @@ export async function getEventType(input: TGetScheduleInputSchema) {
|
||||
slug: usernameList?.[0],
|
||||
eventTypeSlug: eventTypeSlug,
|
||||
isTeamEvent,
|
||||
organizationDetails,
|
||||
}));
|
||||
|
||||
if (!eventTypeId) {
|
||||
@@ -223,12 +237,17 @@ export async function getDynamicEventType(input: TGetScheduleInputSchema) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getRegularOrDynamicEventType(input: TGetScheduleInputSchema) {
|
||||
export function getRegularOrDynamicEventType(
|
||||
input: TGetScheduleInputSchema,
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const isDynamicBooking = input.usernameList && input.usernameList.length > 1;
|
||||
return isDynamicBooking ? getDynamicEventType(input) : getEventType(input);
|
||||
return isDynamicBooking ? getDynamicEventType(input) : getEventType(input, organizationDetails);
|
||||
}
|
||||
|
||||
export async function getAvailableSlots(input: TGetScheduleInputSchema) {
|
||||
export async function getAvailableSlots({ input, ctx }: GetScheduleOptions) {
|
||||
const orgDetails = orgDomainConfig(ctx?.req?.headers.host ?? "");
|
||||
|
||||
if (input.debug === true) {
|
||||
logger.setSettings({ minLevel: "debug" });
|
||||
}
|
||||
@@ -236,7 +255,7 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
|
||||
logger.setSettings({ minLevel: "silly" });
|
||||
}
|
||||
const startPrismaEventTypeGet = performance.now();
|
||||
const eventType = await getRegularOrDynamicEventType(input);
|
||||
const eventType = await getRegularOrDynamicEventType(input, orgDetails);
|
||||
const endPrismaEventTypeGet = performance.now();
|
||||
logger.debug(
|
||||
`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms for event:${
|
||||
@@ -492,10 +511,15 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
|
||||
};
|
||||
}
|
||||
|
||||
async function getUserIdFromUsername(username: string) {
|
||||
async function getUserIdFromUsername(
|
||||
username: string,
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -504,10 +528,15 @@ async function getUserIdFromUsername(username: string) {
|
||||
return user?.id;
|
||||
}
|
||||
|
||||
async function getTeamIdFromSlug(slug: string) {
|
||||
async function getTeamIdFromSlug(
|
||||
slug: string,
|
||||
organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean }
|
||||
) {
|
||||
const { currentOrgDomain, isValidOrgDomain } = organizationDetails;
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug,
|
||||
parent: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -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 { useMemo, useState } from "react";
|
||||
import type { ITimezoneOption, ITimezone, Props as SelectProps } from "react-timezone-select";
|
||||
import BaseSelect, { allTimezones } from "react-timezone-select";
|
||||
import BaseSelect from "react-timezone-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { filterByCities, addCitiesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
|
||||
@@ -34,13 +34,6 @@ export function TimezoneSelect({
|
||||
});
|
||||
}, [components]);
|
||||
|
||||
// We use modifiedTimezones in place of the allTimezones object replacing any underscores in the curly braces
|
||||
// with spaces and removing the America/Detroit timezone, adding the America/New_York timezone instead.
|
||||
const modifiedTimezones = useMemo(() => {
|
||||
const { "America/Detroit": _, ...rest } = allTimezones;
|
||||
return { ...rest, "America/New_York": "New York" };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BaseSelect
|
||||
className={className}
|
||||
@@ -48,9 +41,8 @@ export function TimezoneSelect({
|
||||
isDisabled={isLoading}
|
||||
{...reactSelectProps}
|
||||
timezones={{
|
||||
...modifiedTimezones,
|
||||
...(data ? addCitiesToDropdown(data) : {}),
|
||||
...addCitiesToDropdown(cities),
|
||||
"America/Asuncion": "Asuncion",
|
||||
}}
|
||||
onInputChange={handleInputChange}
|
||||
{...props}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user