Merge branch 'main' into connect-component

This commit is contained in:
Ryukemeister 2023-10-09 20:46:26 +05:30
commit eade205ed3
70 changed files with 2021 additions and 261 deletions

View File

@ -253,6 +253,8 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
#### Setting up your first user
##### Approach 1
1. Open [Prisma Studio](https://prisma.io/studio) to look at or modify the database content:
```sh
@ -264,6 +266,17 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
> New users are set on a `TRIAL` plan by default. You might want to adjust this behavior to your needs in the `packages/prisma/schema.prisma` file.
1. Open a browser to [http://localhost:3000](http://localhost:3000) and login with your just created, first user.
##### Approach 2
Seed the local db by running
```sh
cd packages/prisma
yarn db-seed
```
The above command will populate the local db with dummy users.
### E2E-Testing
Be sure to set the environment variable `NEXTAUTH_URL` to the correct value. If you are running locally, as the documentation within `.env.example` mentions, the value should be `http://localhost:3000`.

View File

@ -24,6 +24,11 @@ const hostSchema = _HostModel.pick({
userId: true,
});
export const childrenSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
});
export const schemaEventTypeBaseBodyParams = EventType.pick({
title: true,
description: true,
@ -45,6 +50,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
disableGuests: true,
hideCalendarNotes: true,
minimumBookingNotice: true,
parentId: true,
beforeEventBuffer: true,
afterEventBuffer: true,
teamId: true,
@ -56,7 +62,12 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
bookingLimits: true,
durationLimits: true,
})
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
.merge(
z.object({
children: z.array(childrenSchema).optional().default([]),
hosts: z.array(hostSchema).optional().default([]),
})
)
.partial()
.strict();
@ -73,6 +84,7 @@ const schemaEventTypeCreateParams = z
seatsShowAvailabilityCount: z.boolean().optional(),
bookingFields: eventTypeBookingFields.optional(),
scheduleId: z.number().optional(),
parentId: z.number().optional(),
})
.strict();
@ -125,6 +137,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
price: true,
currency: true,
slotInterval: true,
parentId: true,
successRedirectUrl: true,
description: true,
locations: true,
@ -137,6 +150,8 @@ export const schemaEventTypeReadPublic = EventType.pick({
durationLimits: true,
}).merge(
z.object({
children: z.array(childrenSchema).optional().default([]),
hosts: z.array(hostSchema).optional().default([]),
locations: z
.array(
z.object({

View File

@ -52,6 +52,7 @@ export async function getHandler(req: NextApiRequest) {
team: { select: { slug: true } },
users: true,
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
},
});
await checkPermissions(req, eventType);

View File

@ -46,6 +46,7 @@ async function getHandler(req: NextApiRequest) {
team: { select: { slug: true } },
users: true,
owner: { select: { username: true, id: true } },
children: { select: { id: true, userId: true } },
},
});
// this really should return [], but backwards compatibility..

View File

@ -6,7 +6,9 @@ import { defaultResponder } from "@calcom/lib/server";
import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type";
import checkParentEventOwnership from "./_utils/checkParentEventOwnership";
import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission";
import checkUserMembership from "./_utils/checkUserMembership";
import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
/**
@ -118,10 +120,13 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
* schedulingType:
* type: string
* description: The type of scheduling if a Team event. Required for team events only
* enum: [ROUND_ROBIN, COLLECTIVE]
* enum: [ROUND_ROBIN, COLLECTIVE, MANAGED]
* price:
* type: integer
* description: Price of the event type booking
* parentId:
* type: integer
* description: EventTypeId of the parent managed event
* currency:
* type: string
* description: Currency acronym. Eg- usd, eur, gbp, etc.
@ -276,6 +281,11 @@ async function postHandler(req: NextApiRequest) {
await checkPermissions(req);
if (parsedBody.parentId) {
await checkParentEventOwnership(parsedBody.parentId, userId);
await checkUserMembership(parsedBody.parentId, parsedBody.userId);
}
if (isAdmin && parsedBody.userId) {
data = { ...parsedBody, users: { connect: { id: parsedBody.userId } } };
}

View File

@ -0,0 +1,52 @@
import { HttpError } from "@calcom/lib/http-error";
/**
* Checks if a user, identified by the provided userId, has ownership (or admin rights) over
* the team associated with the event type identified by the parentId.
*
* @param parentId - The ID of the parent event type.
* @param userId - The ID of the user.
*
* @throws {HttpError} If the parent event type is not found,
* if the parent event type doesn't belong to any team,
* or if the user doesn't have ownership or admin rights to the associated team.
*/
export default async function checkParentEventOwnership(parentId: number, userId: number) {
const parentEventType = await prisma.eventType.findUnique({
where: {
id: parentId,
},
select: {
teamId: true,
},
});
if (!parentEventType) {
throw new HttpError({
statusCode: 404,
message: "Parent event type not found.",
});
}
if (!parentEventType.teamId) {
throw new HttpError({
statusCode: 400,
message: "This event type is not capable of having children",
});
}
const teamMember = await prisma.membership.findFirst({
where: {
teamId: parentEventType.teamId,
userId: userId,
OR: [{ role: "OWNER" }, { role: "ADMIN" }],
},
});
if (!teamMember) {
throw new HttpError({
statusCode: 403,
message: "User is not authorized to access the team to which the parent event type belongs.",
});
}
}

View File

@ -0,0 +1,52 @@
import { HttpError } from "@calcom/lib/http-error";
/**
* Checks if a user, identified by the provided userId, is a member of the team associated
* with the event type identified by the parentId.
*
* @param parentId - The ID of the event type.
* @param userId - The ID of the user.
*
* @throws {HttpError} If the event type is not found,
* if the event type doesn't belong to any team,
* or if the user isn't a member of the associated team.
*/
export default async function checkUserMembership(parentId: number, userId: number) {
const parentEventType = await prisma.eventType.findUnique({
where: {
id: parentId,
},
select: {
teamId: true,
},
});
if (!parentEventType) {
throw new HttpError({
statusCode: 404,
message: "Event type not found.",
});
}
if (!parentEventType.teamId) {
throw new HttpError({
statusCode: 400,
message: "This event type is not capable of having children.",
});
}
const teamMember = await prisma.membership.findFirst({
where: {
teamId: parentEventType.teamId,
userId: userId,
accepted: true,
},
});
if (!teamMember) {
throw new HttpError({
statusCode: 400,
message: "User is not a team member.",
});
}
}

View File

@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
import { slotsRouter } from "@calcom/trpc/server/routers/viewer/slots/_router";
import { TRPCError } from "@trpc/server";
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
@ -11,10 +11,10 @@ import { getHTTPStatusCodeFromError } from "@trpc/server/http";
async function handler(req: NextApiRequest, res: NextApiResponse) {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res });
const caller = viewerRouter.createCaller(ctx);
const caller = slotsRouter.createCaller(ctx);
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await caller.slots.getSchedule(req.query as any /* Let tRPC handle this */);
return await caller.getSchedule(req.query as any /* Let tRPC handle this */);
} catch (cause) {
if (cause instanceof TRPCError) {
const statusCode = getHTTPStatusCodeFromError(cause);

View File

@ -189,7 +189,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: `minutes ${t("minutes")}`,
label: `${minutes} ${t("minutes")}`,
value: minutes,
})),
];
@ -225,7 +225,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: `minutes ${t("minutes")}`,
label: `${minutes} ${t("minutes")}`,
value: minutes,
})),
];
@ -272,7 +272,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
value: -1,
},
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
label: `minutes ${t("minutes")}`,
label: `${minutes} ${t("minutes")}`,
value: minutes,
})),
];

View File

@ -294,7 +294,6 @@ export const EventSetupTab = (
const eventLabel =
location[eventLocationType.defaultValueVariable] || t(eventLocationType.label);
return (
<li
key={`${location.type}${index}`}

View File

@ -47,6 +47,11 @@ export default function RecurringEventController({
<Alert severity="warning" title={t("warning_payment_recurring_event")} />
) : (
<>
<Alert
className="mb-4"
severity="warning"
title="Experimental: Recurring Events are currently experimental and causes some issues sometimes when checking for availability. We are working on fixing this."
/>
<SettingsToggle
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames(

View File

@ -15,7 +15,7 @@ interface Props {
export default function AuthContainer(props: React.PropsWithChildren<Props>) {
return (
<div className="flex min-h-screen flex-col justify-center bg-[#f3f4f6] py-12 sm:px-6 lg:px-8">
<div className="bg-subtle dark:bg-darkgray-50 flex min-h-screen flex-col justify-center py-12 sm:px-6 lg:px-8">
<HeadSeo title={props.title} description={props.description} />
{props.showLogo && <Logo small inline={false} className="mx-auto mb-auto" />}
@ -28,7 +28,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
</div>
)}
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-default border-subtle mx-2 rounded-md border px-4 py-10 sm:px-10">
<div className="bg-default dark:bg-muted border-subtle mx-2 rounded-md border px-4 py-10 sm:px-10">
{props.children}
</div>
<div className="text-default mt-8 text-center text-sm">{props.footerText}</div>

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.3.4",
"version": "3.3.6",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@ -49,6 +49,7 @@
"@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-id": "^1.0.0",
"@radix-ui/react-popover": "^1.0.2",
"@radix-ui/react-radio-group": "^1.0.0",

View File

@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import {
@ -43,6 +44,7 @@ export default function Type({
hideBranding={isBrandingHidden}
isSEOIndexable={isSEOIndexable ?? true}
entity={entity}
bookingData={booking}
/>
<Booker
username={user}
@ -61,6 +63,7 @@ Type.isBookingPage = true;
Type.PageWrapper = PageWrapper;
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context);
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
@ -96,7 +99,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`);
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
} else if (bookingUid) {
booking = await getBookingForSeatedEvent(`${bookingUid}`);
}
@ -138,6 +141,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
}
async function getUserPageProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context);
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const username = usernames[0];
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
@ -168,7 +172,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`);
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
} else if (bookingUid) {
booking = await getBookingForSeatedEvent(`${bookingUid}`);
}

View File

@ -7,7 +7,7 @@ import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { TRPCError } from "@calcom/trpc/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
import { bookingsRouter } from "@calcom/trpc/server/routers/viewer/bookings/_router";
enum DirectAction {
ACCEPT = "accept",
@ -55,13 +55,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
try {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res }, sessionGetter);
const caller = viewerRouter.createCaller({
const caller = bookingsRouter.createCaller({
...ctx,
req,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.bookings.confirm({
await caller.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,

View File

@ -141,7 +141,6 @@ export default function Page({ requestId, isRequestExpired, csrfToken }: Props)
);
}
Page.isThemeSupported = false;
Page.PageWrapper = PageWrapper;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const id = context.params?.id as string;

View File

@ -126,8 +126,9 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
/>
<div className="space-y-2">
<Button
className="w-full justify-center"
className="w-full justify-center dark:bg-white dark:text-black"
type="submit"
color="primary"
disabled={loading}
aria-label={t("request_password_reset")}
loading={loading}>
@ -141,7 +142,6 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
);
}
ForgotPassword.isThemeSupported = false;
ForgotPassword.PageWrapper = PageWrapper;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {

View File

@ -225,7 +225,7 @@ export default function Login({
type="submit"
color="primary"
disabled={formState.isSubmitting}
className="w-full justify-center">
className="w-full justify-center dark:bg-white dark:text-black">
{twoFactorRequired ? t("submit") : t("sign_in")}
</Button>
</div>
@ -337,7 +337,6 @@ const _getServerSideProps = async function getServerSideProps(context: GetServer
};
};
Login.isThemeSupported = false;
Login.PageWrapper = PageWrapper;
export const getServerSideProps = withNonce(_getServerSideProps);

View File

@ -57,7 +57,6 @@ export function Logout(props: Props) {
);
}
Logout.isThemeSupported = false;
Logout.PageWrapper = PageWrapper;
export default Logout;

View File

@ -146,7 +146,7 @@ export default function Success(props: SuccessProps) {
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
const { requiresLoginToUpdate } = props;
function setIsCancellationMode(value: boolean) {
const _searchParams = new URLSearchParams(searchParams);
@ -532,7 +532,28 @@ export default function Success(props: SuccessProps) {
})}
</div>
</div>
{(!needsConfirmation || !userIsOwner) &&
{requiresLoginToUpdate && (
<>
<hr className="border-subtle mb-8" />
<div className="text-center">
<span className="text-emphasis ltr:mr-2 rtl:ml-2">{t("need_to_make_a_change")}</span>
{/* Login button but redirect to here */}
<span className="text-default inline">
<span className="underline" data-testid="reschedule-link">
<Link
href={`/auth/login?callbackUrl=${encodeURIComponent(
`/booking/${bookingInfo?.uid}`
)}`}
legacyBehavior>
{t("login")}
</Link>
</span>
</span>
</div>
</>
)}
{!requiresLoginToUpdate &&
(!needsConfirmation || !userIsOwner) &&
!isCancelled &&
(!isCancellationMode ? (
<>
@ -540,28 +561,30 @@ export default function Success(props: SuccessProps) {
<div className="text-center last:pb-0">
<span className="text-emphasis ltr:mr-2 rtl:ml-2">{t("need_to_make_a_change")}</span>
{!props.recurringBookings && (
<span className="text-default inline">
<span className="underline" data-testid="reschedule-link">
<Link
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
legacyBehavior>
{t("reschedule")}
</Link>
<>
{!props.recurringBookings && (
<span className="text-default inline">
<span className="underline" data-testid="reschedule-link">
<Link
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
legacyBehavior>
{t("reschedule")}
</Link>
</span>
<span className="mx-2">{t("or_lowercase")}</span>
</span>
<span className="mx-2">{t("or_lowercase")}</span>
</span>
)}
<button
data-testid="cancel"
className={classNames(
"text-default underline",
props.recurringBookings && "ltr:mr-2 rtl:ml-2"
)}
onClick={() => setIsCancellationMode(true)}>
{t("cancel")}
</button>
<button
data-testid="cancel"
className={classNames(
"text-default underline",
props.recurringBookings && "ltr:mr-2 rtl:ml-2"
)}
onClick={() => setIsCancellationMode(true)}>
{t("cancel")}
</button>
</>
</div>
</>
) : (
@ -1010,7 +1033,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context);
let tz: string | null = null;
let userTimeFormat: number | null = null;
let requiresLoginToUpdate = false;
if (session) {
const user = await ssr.viewer.me.fetch();
tz = user.timeZone;
@ -1022,9 +1045,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!parsedQuery.success) return { notFound: true };
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
const bookingInfoRaw = await prisma.booking.findFirst({
where: {
uid: await maybeGetBookingUidFromSeat(prisma, uid),
uid: maybeUid,
},
select: {
title: true,
@ -1088,6 +1112,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
requiresLoginToUpdate = true;
}
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
@ -1156,6 +1184,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
paymentStatus: payment,
...(tz && { tz }),
userTimeFormat,
requiresLoginToUpdate,
},
};
}

View File

@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
@ -27,6 +28,7 @@ export default function Type({
isTeamEvent,
entity,
duration,
hashedLink,
}: PageProps) {
return (
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
@ -46,6 +48,7 @@ export default function Type({
isTeamEvent={isTeamEvent}
entity={entity}
duration={duration}
hashedLink={hashedLink}
/>
</main>
);
@ -55,6 +58,7 @@ Type.PageWrapper = PageWrapper;
Type.isBookingPage = true;
async function getUserPageProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context);
const { link, slug } = paramsSchema.parse(context.params);
const { rescheduleUid, duration: queryDuration } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
@ -117,7 +121,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`);
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
}
const isTeamEvent = !!hashedLink.eventType?.team?.id;
@ -149,6 +153,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
// Sending the team event from the server, because this template file
// is reused for both team and user events.
isTeamEvent,
hashedLink: link,
},
};
}

View File

@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
import { URLSearchParams } from "url";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@ -12,11 +13,16 @@ export default function Type() {
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { uid: bookingId, seatReferenceUid } = z
const session = await getServerSession(context);
const { uid: bookingUid, seatReferenceUid } = z
.object({ uid: z.string(), seatReferenceUid: z.string().optional() })
.parse(context.query);
const uid = await maybeGetBookingUidFromSeat(prisma, bookingId);
const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
prisma,
bookingUid
);
const booking = await prisma.booking.findUnique({
where: {
uid,
@ -37,6 +43,21 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
},
seatsPerTimeSlot: true,
userId: true,
owner: {
select: {
id: true,
},
},
hosts: {
select: {
user: {
select: {
id: true,
},
},
},
},
},
},
dynamicEventSlugRef: true,
@ -53,7 +74,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}
if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
// TODO: Show something in UI to let user know that this booking is not rescheduleable.
// TODO: Show something in UI to let user know that this booking is not rescheduleable
return {
notFound: true,
} as {
@ -61,6 +82,33 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}
// if booking event type is for a seated event and no seat reference uid is provided, throw not found
if (booking?.eventType?.seatsPerTimeSlot && !maybeSeatReferenceUid) {
const userId = session?.user?.id;
if (!userId && !seatReferenceUid) {
return {
redirect: {
destination: `/auth/login?callbackUrl=/reschedule/${bookingUid}`,
permanent: false,
},
};
}
const userIsHost = booking?.eventType.hosts.find((host) => {
if (host.user.id === userId) return true;
});
const userIsOwnerOfEventType = booking?.eventType.owner?.id === userId;
if (!userIsHost && !userIsOwnerOfEventType) {
return {
notFound: true,
} as {
notFound: true;
};
}
}
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
const eventPage = `${
@ -72,7 +120,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}/${eventType?.slug}`;
const destinationUrl = new URLSearchParams();
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingId);
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingUid);
return {
redirect: {

View File

@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
@ -43,6 +44,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
const teamName = team.name || "Nameless Team";
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
const metadata = teamMetadataSchema.parse(team.metadata);
const orgBranding = useOrgBranding();
useEffect(() => {
telemetry.event(
@ -124,12 +126,6 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<li key={i} className="hover:bg-muted w-full">
<Link href={`/${ch.slug}`} className="flex items-center justify-between">
<div className="flex items-center px-5 py-5">
<Avatar
size="md"
imageSrc={`/team/${ch.slug}/avatar.png`}
alt="Team Logo"
className="inline-flex justify-center"
/>
<div className="ms-3 inline-block truncate">
<span className="text-default text-sm font-bold">{ch.name}</span>
<span className="text-subtle block text-xs">
@ -185,9 +181,11 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<div className="relative">
<Avatar
alt={teamName}
imageSrc={`${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${
team.slug
}/avatar.png`}
imageSrc={
!!team.parent && !!orgBranding
? `${orgBranding?.fullDomain}/org/${orgBranding?.slug}/avatar.png`
: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`
}
size="lg"
/>
</div>

View File

@ -2,6 +2,7 @@ import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
@ -37,6 +38,7 @@ export default function Type({
hideBranding={isBrandingHidden}
isTeamEvent
entity={entity}
bookingData={booking}
/>
<Booker
username={user}
@ -64,6 +66,7 @@ const paramsSchema = z.object({
// 1. Check if team exists, to show 404
// 2. If rescheduling, get the booking details
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerSession(context);
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
const { rescheduleUid, duration: queryDuration } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
@ -92,7 +95,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`);
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
}
const org = isValidOrgDomain ? currentOrgDomain : null;

View File

@ -46,7 +46,7 @@ export default function JoinCall(props: JoinCallPageProps) {
baseText: "#FFF",
border: "#292929",
mainAreaBg: "#111111",
mainAreaBgAccent: "#111111",
mainAreaBgAccent: "#1A1A1A",
mainAreaText: "#FFF",
supportiveText: "#FFF",
},

View File

@ -28,6 +28,7 @@ test.describe("Availablity tests", () => {
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
await page.locator('[data-testid="date-override-mark-unavailable"]').click();
await page.locator('[data-testid="add-override-submit-btn"]').click();
await page.locator('[data-testid="dialog-rejection"]').click();
await expect(page.locator('[data-testid="date-overrides-list"] > li')).toHaveCount(1);
await page.locator('[form="availability-form"][type="submit"]').click();
});

View File

@ -2,6 +2,7 @@ import { expect } from "@playwright/test";
import { uuid } from "short-uuid";
import { v4 as uuidv4 } from "uuid";
import { randomString } from "@calcom/lib/random";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
@ -96,7 +97,7 @@ test.describe("Booking with Seats", () => {
});
test(`Attendees can cancel a seated event time slot`, async ({ page, users, bookings }) => {
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
@ -143,6 +144,19 @@ test.describe("Booking with Seats", () => {
expect(attendeeIds).not.toContain(bookingAttendees[0].id);
});
await test.step("Attendee #2 shouldn't be able to cancel booking using only booking/uid", async () => {
await page.goto(`/booking/${booking.uid}`);
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
});
await test.step("Attendee #2 shouldn't be able to cancel booking using randomString for seatReferenceUId", async () => {
await page.goto(`/booking/${booking.uid}?seatReferenceUid=${randomString(10)}`);
// expect cancel button to don't be in the page
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
});
await test.step("All attendees cancelling should delete the booking for the user", async () => {
// The remaining 2 attendees cancel
for (let i = 1; i < bookingSeats.length; i++) {
@ -166,6 +180,47 @@ test.describe("Booking with Seats", () => {
expect(updatedBooking?.status).toBe(BookingStatus.CANCELLED);
});
});
test("Owner shouldn't be able to cancel booking without login in", async ({ page, bookings, users }) => {
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
]);
await page.goto(`/booking/${booking.uid}?cancel=true`);
await expect(page.locator("[text=Cancel]")).toHaveCount(0);
// expect login text to be in the page, not data-testid
await expect(page.locator("text=Login")).toHaveCount(1);
// click on login button text
await page.locator("text=Login").click();
// expect to be redirected to login page with query parameter callbackUrl
await expect(page).toHaveURL(/\/auth\/login\?callbackUrl=.*/);
await user.apiLogin();
// manual redirect to booking page
await page.goto(`/booking/${booking.uid}?cancel=true`);
// expect login button to don't be in the page
await expect(page.locator("text=Login")).toHaveCount(0);
// fill reason for cancellation
await page.fill('[data-testid="cancel_reason"]', "Double booked!");
// confirm cancellation
await page.locator('[data-testid="confirm_cancel"]').click();
await page.waitForLoadState("networkidle");
const updatedBooking = await prisma.booking.findFirst({
where: { id: booking.id },
});
expect(updatedBooking).not.toBeNull();
expect(updatedBooking?.status).toBe(BookingStatus.CANCELLED);
});
});
test.describe("Reschedule for booking with seats", () => {
@ -543,4 +598,113 @@ test.describe("Reschedule for booking with seats", () => {
.first();
await expect(foundFirstAttendeeAgain).toHaveCount(1);
});
test("Owner shouldn't be able to reschedule booking without login in", async ({
page,
bookings,
users,
}) => {
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
]);
const getBooking = await booking.self();
await page.goto(`/booking/${booking.uid}`);
await expect(page.locator('[data-testid="reschedule"]')).toHaveCount(0);
// expect login text to be in the page, not data-testid
await expect(page.locator("text=Login")).toHaveCount(1);
// click on login button text
await page.locator("text=Login").click();
// expect to be redirected to login page with query parameter callbackUrl
await expect(page).toHaveURL(/\/auth\/login\?callbackUrl=.*/);
await user.apiLogin();
// manual redirect to booking page
await page.goto(`/booking/${booking.uid}`);
// expect login button to don't be in the page
await expect(page.locator("text=Login")).toHaveCount(0);
// reschedule-link click
await page.locator('[data-testid="reschedule-link"]').click();
await selectFirstAvailableTimeSlotNextMonth(page);
// data displayed in form should be user owner
const nameElement = await page.locator("input[name=name]");
const name = await nameElement.inputValue();
expect(name).toBe(user.username);
//same for email
const emailElement = await page.locator("input[name=email]");
const email = await emailElement.inputValue();
expect(email).toBe(user.email);
// reason to reschedule input should be visible textfield with name rescheduleReason
const reasonElement = await page.locator("textarea[name=rescheduleReason]");
await expect(reasonElement).toBeVisible();
// expect to be redirected to reschedule page
await page.locator('[data-testid="confirm-reschedule-button"]').click();
// should wait for URL but that path starts with booking/
await page.waitForURL(/\/booking\/.*/);
await expect(page).toHaveURL(/\/booking\/.*/);
await page.waitForLoadState("networkidle");
const updatedBooking = await prisma.booking.findFirst({
where: { id: booking.id },
});
expect(updatedBooking).not.toBeNull();
expect(getBooking?.startTime).not.toBe(updatedBooking?.startTime);
expect(getBooking?.endTime).not.toBe(updatedBooking?.endTime);
expect(updatedBooking?.status).toBe(BookingStatus.ACCEPTED);
});
test("Owner shouldn't be able to reschedule when going directly to booking/rescheduleUid", async ({
page,
bookings,
users,
}) => {
const { booking, user } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "John Third", email: "third+seats@cal.com", timeZone: "Europe/Berlin" },
]);
const getBooking = await booking.self();
await page.goto(`/${user.username}/seats?rescheduleUid=${getBooking?.uid}&bookingUid=null`);
await selectFirstAvailableTimeSlotNextMonth(page);
// expect textarea with name notes to be visible
const notesElement = await page.locator("textarea[name=notes]");
await expect(notesElement).toBeVisible();
// expect button confirm instead of reschedule
await expect(page.locator('[data-testid="confirm-book-button"]')).toHaveCount(1);
// now login and try again
await user.apiLogin();
await page.goto(`/${user.username}/seats?rescheduleUid=${getBooking?.uid}&bookingUid=null`);
await selectFirstAvailableTimeSlotNextMonth(page);
await expect(page).toHaveTitle(/(?!.*reschedule).*/);
// expect button reschedule
await expect(page.locator('[data-testid="confirm-reschedule-button"]')).toHaveCount(1);
});
// @TODO: force 404 when rescheduleUid is not found
});

View File

@ -0,0 +1,239 @@
import { expect } from "@playwright/test";
import { randomString } from "@calcom/lib/random";
import prisma from "@calcom/prisma";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
const createTeamsAndMembership = async (userIdOne: number, userIdTwo: number) => {
const teamOne = await prisma.team.create({
data: {
name: "test-insights",
slug: `test-insights-${Date.now()}-${randomString(5)}}`,
},
});
const teamTwo = await prisma.team.create({
data: {
name: "test-insights-2",
slug: `test-insights-2-${Date.now()}-${randomString(5)}}`,
},
});
if (!userIdOne || !userIdTwo || !teamOne || !teamTwo) {
throw new Error("Failed to create test data");
}
// create memberships
await prisma.membership.create({
data: {
userId: userIdOne,
teamId: teamOne.id,
accepted: true,
role: "ADMIN",
},
});
await prisma.membership.create({
data: {
teamId: teamTwo.id,
userId: userIdOne,
accepted: true,
role: "ADMIN",
},
});
await prisma.membership.create({
data: {
teamId: teamOne.id,
userId: userIdTwo,
accepted: true,
role: "MEMBER",
},
});
await prisma.membership.create({
data: {
teamId: teamTwo.id,
userId: userIdTwo,
accepted: true,
role: "MEMBER",
},
});
return { teamOne, teamTwo };
};
test.afterAll(async ({ users }) => {
await users.deleteAll();
});
test.describe("Insights", async () => {
test("should be able to go to insights as admins", async ({ page, users }) => {
const user = await users.create();
const userTwo = await users.create();
await createTeamsAndMembership(user.id, userTwo.id);
await user.apiLogin();
// go to insights page
await page.goto("/insights");
await page.waitForLoadState("networkidle");
// expect url to have isAll and TeamId in query params
expect(page.url()).toContain("isAll=false");
expect(page.url()).toContain("teamId=");
});
test("should be able to go to insights as members", async ({ page, users }) => {
const user = await users.create();
const userTwo = await users.create();
await userTwo.apiLogin();
await createTeamsAndMembership(user.id, userTwo.id);
// go to insights page
await page.goto("/insights");
await page.waitForLoadState("networkidle");
// expect url to have isAll and TeamId in query params
expect(page.url()).toContain("isAll=false");
expect(page.url()).not.toContain("teamId=");
});
test("team select filter should have 2 teams and your account option only as member", async ({
page,
users,
}) => {
const user = await users.create();
const userTwo = await users.create();
await user.apiLogin();
await createTeamsAndMembership(user.id, userTwo.id);
// go to insights page
await page.goto("/insights");
await page.waitForLoadState("networkidle");
// get div from team select filter with this class flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1
await page.getByTestId("dashboard-shell").getByText("Team: test-insights").click();
await page
.locator('div[class="flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1"]')
.click();
const teamSelectFilter = await page.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]'
);
await expect(teamSelectFilter).toHaveCount(3);
});
test("Insights Organization should have isAll option true", async ({ users, page }) => {
const owner = await users.create(undefined, {
hasTeam: true,
isUnpublished: true,
isOrg: true,
hasSubteam: true,
});
await owner.apiLogin();
await page.goto("/insights");
await page.waitForLoadState("networkidle");
await page.getByTestId("dashboard-shell").getByText("All").nth(1).click();
const teamSelectFilter = await page.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]'
);
await expect(teamSelectFilter).toHaveCount(4);
});
test("should have all option in team-and-self filter as admin", async ({ page, users }) => {
const owner = await users.create();
const member = await users.create();
await createTeamsAndMembership(owner.id, member.id);
await owner.apiLogin();
await page.goto("/insights");
// get div from team select filter with this class flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1
await page.getByTestId("dashboard-shell").getByText("Team: test-insights").click();
await page
.locator('div[class="flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1"]')
.click();
const teamSelectFilter = await page.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]'
);
await expect(teamSelectFilter).toHaveCount(3);
});
test("should be able to switch between teams and self profile for insights", async ({ page, users }) => {
const owner = await users.create();
const member = await users.create();
await createTeamsAndMembership(owner.id, member.id);
await owner.apiLogin();
await page.goto("/insights");
// get div from team select filter with this class flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1
await page.getByTestId("dashboard-shell").getByText("Team: test-insights").click();
await page
.locator('div[class="flex flex-col gap-0.5 [&>*:first-child]:mt-1 [&>*:last-child]:mb-1"]')
.click();
const teamSelectFilter = await page.locator(
'div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]'
);
await expect(teamSelectFilter).toHaveCount(3);
// switch to self profile
await page.getByTestId("dashboard-shell").getByText("Your Account").click();
// switch to team 1
await page.getByTestId("dashboard-shell").getByText("test-insights").nth(0).click();
// switch to team 2
await page.getByTestId("dashboard-shell").getByText("test-insights-2").click();
});
test("should be able to switch between memberUsers", async ({ page, users }) => {
const owner = await users.create();
const member = await users.create();
await createTeamsAndMembership(owner.id, member.id);
await owner.apiLogin();
await page.goto("/insights");
await page.getByText("Add filter").click();
await page.getByRole("button", { name: "User" }).click();
// <div class="flex select-none truncate font-medium" data-state="closed">People</div>
await page.locator('div[class="flex select-none truncate font-medium"]').getByText("People").click();
await page
.locator('div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]')
.nth(0)
.click();
await page.waitForLoadState("networkidle");
await page
.locator('div[class="hover:bg-muted flex items-center py-2 pl-3 pr-2.5 hover:cursor-pointer"]')
.nth(1)
.click();
await page.waitForLoadState("networkidle");
// press escape button to close the filter
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Clear" }).click();
// expect for "Team: test-insight" text in page
expect(await page.locator("text=Team: test-insights").isVisible()).toBeTruthy();
});
});

View File

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 322 B

View File

@ -288,6 +288,7 @@
"when": "Wann",
"where": "Wo",
"add_to_calendar": "Zum Kalender hinzufügen",
"add_to_calendar_description": "Legen Sie fest, wo neue Termine hinzugefügt werden sollen, wenn Sie gebucht werden.",
"add_another_calendar": "Einen weiteren Kalender hinzufügen",
"other": "Sonstige",
"email_sign_in_subject": "Ihr Anmelde-Link für {{appName}}",
@ -599,6 +600,7 @@
"hide_book_a_team_member": "„Ein Teammitglied buchen“-Schaltfläche ausblenden",
"hide_book_a_team_member_description": "Blendet die „Ein Teammitglied buchen“-Schaltflächen auf Ihren öffentlichen Seiten aus.",
"danger_zone": "Achtung",
"account_deletion_cannot_be_undone": "Vorsicht. Löschen eines Kontos kann nicht rückgängig gemacht werden.",
"back": "Zurück",
"cancel": "Absagen",
"cancel_all_remaining": "Alle verbleibenden absagen",
@ -688,6 +690,7 @@
"people": "Personen",
"your_email": "Ihre E-Mail-Adresse",
"change_avatar": "Profilbild ändern",
"upload_avatar": "Avatar hochladen",
"language": "Sprache",
"timezone": "Zeitzone",
"first_day_of_week": "Erster Tag der Woche",
@ -1276,6 +1279,7 @@
"personal_cal_url": "Meine persönliche {{appName}}-URL",
"bio_hint": "Schreiben Sie eine kurze Beschreibung, welche auf Ihrer persönlichen Profil-Seite erscheinen wird.",
"user_has_no_bio": "Dieser Benutzer hat noch keine Bio hinzugefügt.",
"bio": "Biografie",
"delete_account_modal_title": "Account löschen",
"confirm_delete_account_modal": "Sind Sie sicher, dass Sie Ihr {{appName}}-Konto löschen möchten?",
"delete_my_account": "Meinen Account löschen",
@ -1530,6 +1534,7 @@
"problem_registering_domain": "Es gab ein Problem bei der Registrierung der Subdomain, bitte versuchen Sie es erneut oder kontaktieren Sie einen Administrator",
"team_publish": "Team veröffentlichen",
"number_text_notifications": "Telefonnummer (Textbenachrichtigungen)",
"number_sms_notifications": "Telefonnummer (SMS-Benachrichtigungen)",
"attendee_email_variable": "Teilnehmer E-Mail",
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie ...",
@ -1639,6 +1644,7 @@
"minimum_round_robin_hosts_count": "Anzahl der Veranstalter, die teilnehmen müssen",
"hosts": "Veranstalter",
"upgrade_to_enable_feature": "Sie müssen ein Team erstellen, um diese Funktion zu aktivieren. Klicken Sie hier, um ein Team zu erstellen.",
"orgs_upgrade_to_enable_feature": "Sie müssen auf unsere Enterprise-Lizenz aktualisieren, um diese Funktion zu aktivieren.",
"new_attendee": "Neuer Teilnehmer",
"awaiting_approval": "Wartet auf Genehmigung",
"requires_google_calendar": "Diese App erfordert eine Verbindung mit Google Calendar",
@ -1743,6 +1749,7 @@
"show_on_booking_page": "Auf der Buchungsseite anzeigen",
"get_started_zapier_templates": "Legen Sie mit Zapier-Vorlagen los",
"team_is_unpublished": "{{team}} ist unveröffentlicht",
"org_is_unpublished_description": "Dieser Organisations-Link ist derzeit nicht verfügbar. Bitte kontaktieren Sie den Organisations-Besitzer oder fragen Sie ihn, ob er ihn veröffentlicht.",
"team_is_unpublished_description": "Dieser {{entity}}-Link ist derzeit nicht verfügbar. Bitte kontaktieren Sie den {{entity}}-Besitzer oder fragen Sie ihn, ob er ihn veröffentlicht.",
"team_member": "Teammitglied",
"a_routing_form": "Ein Weiterleitungsformular",
@ -1877,6 +1884,7 @@
"edit_invite_link": "Linkeinstellungen bearbeiten",
"invite_link_copied": "Einladungslink kopiert",
"invite_link_deleted": "Einladungslink gelöscht",
"api_key_deleted": "API-Schlüssel gelöscht",
"invite_link_updated": "Einladungslink-Einstellungen gespeichert",
"link_expires_after": "Links verfallen nach...",
"one_day": "1 Tag",
@ -2011,7 +2019,13 @@
"attendee_last_name_variable": "Nachname des Teilnehmers",
"attendee_first_name_info": "Vorname der buchenden Person",
"attendee_last_name_info": "Nachname der buchenden Person",
"your_monthly_digest": "Ihre monatliche Statistik",
"member_name": "Mitgliedsname",
"most_popular_events": "Beliebteste Termine",
"summary_of_events_for_your_team_for_the_last_30_days": "Hier ist Ihre Zusammenfassung der beliebten Termine für Ihr Team {{teamName}} der letzten 30 Tage",
"me": "Ich",
"monthly_digest_email": "Monatliche Statistik E-Mail",
"monthly_digest_email_for_teams": "Monatliche Statistik E-Mail für Teams",
"verify_team_tooltip": "Verifizieren Sie Ihr Team, um das Senden von Nachrichten an Teilnehmer zu aktivieren",
"member_removed": "Mitglied entfernt",
"my_availability": "Meine Verfügbarkeit",
@ -2041,12 +2055,28 @@
"team_no_event_types": "Dieses Team hat keine Ereignistypen",
"seat_options_doesnt_multiple_durations": "Platzoption unterstützt mehrere Dauern nicht",
"include_calendar_event": "Kalenderereignis hinzufügen",
"oAuth": "OAuth",
"recently_added": "Kürzlich hinzugefügt",
"no_members_found": "Keine Mitglieder gefunden",
"event_setup_length_error": "Ereignis-Einrichtung: Die Dauer muss mindestens 1 Minute betragen.",
"availability_schedules": "Verfügbarkeitspläne",
"unauthorized": "Nicht authorisiert",
"access_cal_account": "{{clientName}} möchte auf Ihr {{appName}} Konto zugreifen",
"select_account_team": "Konto oder Team auswählen",
"allow_client_to": "Dies wird {{clientName}} erlauben",
"see_personal_info": "Ihre persönlichen Daten einzusehen, einschließlich persönlicher Informationen, die Sie öffentlich zugänglich gemacht haben",
"see_primary_email_address": "Ihre primäre E-Mail-Adresse einzusehen",
"connect_installed_apps": "Sich mit Ihren installierten Apps zu verbinden",
"access_event_type": "Lesen, Bearbeiten, Löschen Ihrer Ereignis-Typen",
"access_availability": "Lesen, Bearbeiten, Löschen Ihrer Verfügbarkeiten",
"access_bookings": "Lesen, Bearbeiten, Löschen Ihrer Termine",
"allow_client_to_do": "{{clientName}} zulassen, dies zu tun?",
"allow": "Zulassen",
"view_only_edit_availability_not_onboarded": "Dieser Benutzer hat das Onboarding noch nicht abgeschlossen. Sie können seine Verfügbarkeit erst festlegen, wenn er das Onboarding abgeschlossen hat.",
"view_only_edit_availability": "Sie sehen die Verfügbarkeit dieses Benutzers. Sie können nur Ihre eigene Verfügbarkeit bearbeiten.",
"edit_users_availability": "Benutzerverfügbarkeit bearbeiten: {{username}}",
"resend_invitation": "Einladung erneut senden",
"invitation_resent": "Die Einladung wurde erneut gesendet.",
"this_app_is_not_setup_already": "Diese App wurde noch nicht eingerichtet",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -1535,6 +1535,7 @@
"problem_registering_domain": "There was a problem with registering the subdomain, please try again or contact an administrator",
"team_publish": "Publish team",
"number_text_notifications": "Phone number (Text notifications)",
"number_sms_notifications": "Phone number (SMS notifications)",
"attendee_email_variable": "Attendee email",
"attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...",
@ -1618,6 +1619,7 @@
"date_overrides_mark_all_day_unavailable_other": "Mark unavailable on selected dates",
"date_overrides_add_btn": "Add Override",
"date_overrides_update_btn": "Update Override",
"date_successfully_added": "Date override added successfully",
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default",
"hide_eventtype_details": "Hide event type details",
@ -1658,7 +1660,7 @@
"no_recordings_found": "No recordings found",
"new_workflow_subtitle": "New workflow for...",
"reporting": "Reporting",
"reporting_feature": "See all incoming from data and download it as a CSV",
"reporting_feature": "See all incoming form data and download it as a CSV",
"teams_plan_required": "Teams plan required",
"routing_forms_are_a_great_way": "Routing forms are a great way to route your incoming leads to the right person. Upgrade to a Teams plan to access this feature.",
"choose_a_license": "Choose a license",

View File

@ -7,17 +7,32 @@
"second_other": "{{count}} segundo",
"upgrade_now": "Eguneratu orain",
"accept_invitation": "Onartu gonbidapena",
"calcom_explained": "{{appName}}-ek bilerak programatzeko azpiegitura eskaintzen du guztiontzat.",
"calcom_explained_new_user": "Bukatu zure {{appName}} kontua konfiguratzen! Bileren programazio-arazo guztiak konpontzeko urrats gutxi batzuk besterik ez zaizkizu geratzen.",
"have_any_questions": "Galderarik? Laguntzeko gaude.",
"reset_password_subject": "{{appName}}: Pasahitza berrezartzeko argibideak",
"verify_email_subject": "{{appName}}: egiaztatu zure kontua",
"check_your_email": "Begiratu zure emaila",
"verify_email_page_body": "Email bat bidali dugu {{email}} helbidera. Garrantzitsua da zure email helbidea egiaztatzea, {{appName}}-tik mezuak eta egutegiko eguneratzeak ahalik eta hobekien jasoko dituzula bermatzeko.",
"verify_email_banner_body": "Egiaztatu zure email helbidea mezuak eta egutegiko eguneratzeak ahalik eta hobekien jasoko dituzula bermatzeko",
"verify_email_email_header": "Egiaztatu zure email helbidea",
"verify_email_email_button": "Egiaztatu emaila",
"verify_email_email_body": "Mesedez, egiaztatu zure email helbidea beheko botoia sakatuz.",
"verify_email_by_code_email_body": "Mesedez, egiaztatu zure email helbidea beheko kodea erabiliz.",
"verify_email_email_link_text": "Hemen duzu esteka, botoiak sakatzea gustuko ez baduzu:",
"email_verification_code": "Sartu egiaztatze-kodea",
"email_verification_code_placeholder": "Sartu zure email helbidera bidalitako egiaztatze-kodea",
"incorrect_email_verification_code": "Egiaztatze-kodea ez da zuzena.",
"email_sent": "Email mezua zuzen bidali da",
"email_not_sent": "Errore bat gertatu da email mezua bidaltzerakoan",
"event_declined_subject": "Baztertua: {{title}} {{date}}(e)an",
"event_cancelled_subject": "Bertan behera: {{title}} {{date}}(e)an",
"event_request_declined": "Zure gertaera-eskaera baztertua izan da",
"event_request_declined_recurring": "Zure gertaera errepikari-eskaera baztertua izan da",
"event_request_cancelled": "Zure programatutako gertaera bertan behera utzi da",
"organizer": "Antolatzailea",
"need_to_reschedule_or_cancel": "Programazioa aldatu edo bertan behera utzi behar duzu?",
"no_options_available": "Ez dago aukerarik eskuragarri",
"cancellation_reason": "Bertan behera uztearen arrazoia (aukerakoa)",
"cancellation_reason_placeholder": "Zergatik utzi duzu bertan behera?",
"rejection_reason": "Errefusatzeko arrazoia",
@ -25,7 +40,11 @@
"rejection_reason_description": "Ziur zaude erreserba errefusatu nahi duzula? Erreserba-eskaera egin duen pertsonari jakinaraziko zaio. Arrazoi bat adieraz dezakezu behean.",
"rejection_confirmation": "Errefusatu erreserba",
"manage_this_event": "Kudeatu gertaera hau",
"invite_team_member": "Gonbidatu taldekidea",
"invite_team_individual_segment": "Gonbidatu norbanakoa",
"invite_team_notifcation_badge": "Gon.",
"your_event_has_been_scheduled": "Zure gertaera programatu da",
"your_event_has_been_scheduled_recurring": "Zure gertaera errepikaria programatu da",
"error_message": "Errore-mezua honakoa ian da: '{{errorMessage}}'",
"refund_failed_subject": "Itzulketak huts egin du: {{name}} - {{date}} - {{eventType}}",
"refund_failed": "Huts egin du itzulketak {{eventType}} gertaerarako, {{userName}}(r)ekin {{date}}(e)an.",
@ -37,26 +56,79 @@
"refunded": "Itzulita",
"payment": "Ordainketa",
"pay_now": "Ordaindu orain",
"still_waiting_for_approval": "Gertaera bat onarpenaren zain dago",
"event_is_still_waiting": "Gertaera-eskaera oraindik zain dago: {{attendeeName}} - {{date}} - {{eventType}}",
"no_more_results": "Emaitza gehiagorik ez",
"no_results": "Emaitzarik ez",
"load_more_results": "Kargatu emaitza gehiago",
"integration_meeting_id": "{{integrationName}} bileraren IDa: {{meetingId}}",
"confirmed_event_type_subject": "Baieztatua: {{eventType}} {{name}}(r)ekin {{date}}(e)an",
"new_event_request": "Gertaera berriaren eskaera: {{attendeeName}} - {{date}} - {{eventType}}",
"confirm_or_reject_request": "Baieztatu edo errefusatu eskaera",
"check_bookings_page_to_confirm_or_reject": "Begiratu zure erreserba-orrialdea erreserba baieztatu edo errefusatzeko.",
"event_awaiting_approval": "Gertaera bat zure onarpenaren zain dago",
"event_awaiting_approval_recurring": "Gertaera errepikari bat zure onarpenaren zain dago",
"someone_requested_an_event": "Norbaitek zure egutegian gertaera bat programatzeko eskaera egin du.",
"someone_requested_password_reset": "Norbaitek zure pasahitza aldatzeko esteka bat eskatu du.",
"password_reset_email_sent": "Email helbide hau gure sisteman baldin badago, berrezartzeko email mezu bat jaso behar zenuke.",
"password_reset_instructions": "Ez baduzu eskaera hau egin, segurua da email mezu honi kasurik ez egitea, eta zure pasahitza ez da aldatuko.",
"event_awaiting_approval_subject": "Onarpenaren zain: {{title}} {{date}}(e)an",
"event_still_awaiting_approval": "Gertaera bat zure onarpenaren zain dago oraindik",
"booking_submitted_subject": "Erreserba bidalita: {{title}} {{date}}(e)an",
"download_recording_subject": "Deskargatu grabaketa: {{title}} {{date}}(e)an",
"download_your_recording": "Deskargatu zure grabaketa",
"your_meeting_has_been_booked": "Zure bileraren erreserba egin da",
"event_type_has_been_rescheduled_on_time_date": "Zure {{title}} getaeraren programazioa aldatu egin da {{date}}(e)ra.",
"event_has_been_rescheduled": "Eguneratuta - Zure gertaeraren programazioa aldatu egin da",
"request_reschedule_subtitle": "{{organizer}}(e)k erreserba bertan behera utzi du eta beste denbora-tarte bat hautatzeko eskatu dizu.",
"request_reschedule_title_organizer": "Beste denbora-tarte bat hautatzeko eskatu diozu {{attendee}}(r)i",
"hi_user_name": "Kaixo {{name}}",
"ics_event_title": "{{eventType}} {{name}}(r)ekin",
"notes": "Oharrak",
"manage_my_bookings": "Kudeatu nire erreserbak",
"rejected_event_type_with_organizer": "Errefusatua: {{eventType}} {{organizer}}(r)ekin {{date}}(e)an",
"hi": "Kaixo",
"use_link_to_reset_password": "Erabili beheko esteka pasahitza berrezartzeko",
"hey_there": "Kaixo,",
"forgot_your_password_calcom": "Pasahitza ahaztu duzu? - {{appName}}",
"dismiss": "Alde batera utzi",
"no_data_yet": "Ez dago daturik",
"ping_test": "Ping testa",
"upcoming": "Laster",
"recurring": "Errepikariak",
"past": "Iraganekoak",
"choose_a_file": "Hautatu fitxategi bat...",
"upload_image": "Igo irudia",
"upload_target": "Igo {{target}}",
"no_target": "Ez dago {{target}}(r)ik",
"view_notifications": "Ikusi jakinarazpenak",
"view_public_page": "Ikusi orrialde publikoa",
"copy_public_page_link": "Kopiatu orrialde publikoaren esteka",
"sign_out": "Saioa itxi",
"add_another": "Gehitu beste bat",
"install_another": "Instalatu beste bat",
"unavailable": "Ez eskuragarri",
"set_work_schedule": "Ezarri zure laneko ordutegia",
"change_bookings_availability": "Aldatu noiz zauden prest erreserbak jasotzeko",
"select": "Hautatu...",
"text": "Testua",
"multiline_text": "Lerro ugaritako testua",
"number": "Zenbakia",
"checkbox": "Kontrol-laukia",
"is_required": "Derrigorrezkoa da",
"required": "Derrigorrezkoa",
"optional": "Hautazkoa",
"input_type": "Sarrera-mota",
"rejected": "Baztertua",
"unconfirmed": "Baieztatu gabea",
"guests": "Gonbidatuak",
"create_account": "Sortu kontua",
"confirm_password": "Baieztatu pasahitza",
"create_booking_link_with_calcom": "Sor ezazu zeure erreserba-esteka {{appName}}(e)kin",
"user_needs_to_confirm_or_reject_booking": "{{user}}(e)k erreserba baieztatu edo errefusatu behar du oraindik.",
"booking_submitted": "Zure erreserba bidali da",
"booking_confirmed": "Zure erreserba baieztatu da",
"bookerlayout_column_view": "Zutabea",
"back_to_bookings": "Itzuli erreserbatara",
"really_cancel_booking": "Benetan bertan behera utzi nahi duzu zure erreserba?",
"cannot_cancel_booking": "Ezin duzu erreserba hau bertan behera utzi",

View File

@ -1530,6 +1530,7 @@
"problem_registering_domain": "Un problème est survenu lors de l'enregistrement du sous-domaine, veuillez réessayer ou contacter un administrateur",
"team_publish": "Publier l'équipe",
"number_text_notifications": "Numéro de téléphone (notifications par SMS)",
"number_sms_notifications": "Numéro de téléphone (notifications par SMS)",
"attendee_email_variable": "Adresse e-mail du participant",
"attendee_email_info": "Adresse e-mail du participant",
"kbar_search_placeholder": "Saisissez une commande ou une recherche...",

View File

@ -0,0 +1 @@
{}

View File

@ -1530,6 +1530,7 @@
"problem_registering_domain": "Houve um problema ao registar o subdomínio. Tente novamente ou contacte um administrador",
"team_publish": "Publicar equipa",
"number_text_notifications": "Número de telefone (notificações de texto)",
"number_sms_notifications": "Número de telefone (notificações SMS)",
"attendee_email_variable": "E-mail do participante",
"attendee_email_info": "O e-mail do responsável pela reserva",
"kbar_search_placeholder": "Digite um comando ou pesquise...",

View File

@ -0,0 +1,97 @@
import { render, screen } from "@testing-library/react";
import type { CredentialOwner } from "types";
import { vi } from "vitest";
import type { RouterOutputs } from "@calcom/trpc";
import { DynamicComponent } from "./DynamicComponent";
import { EventTypeAppCard } from "./EventTypeAppCardInterface";
vi.mock("./DynamicComponent", async () => {
const actual = (await vi.importActual("./DynamicComponent")) as object;
return {
...actual,
DynamicComponent: vi.fn(() => <div>MockedDynamicComponent</div>),
};
});
afterEach(() => {
vi.clearAllMocks();
});
const getAppDataMock = vi.fn();
const setAppDataMock = vi.fn();
const mockProps = {
app: {
name: "TestApp",
slug: "testapp",
credentialOwner: {},
} as RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner },
eventType: {},
getAppData: getAppDataMock,
setAppData: setAppDataMock,
LockedIcon: <div>MockedIcon</div>,
disabled: false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
describe("Tests for EventTypeAppCard component", () => {
test("Should render DynamicComponent with correct slug", () => {
render(<EventTypeAppCard {...mockProps} />);
expect(DynamicComponent).toHaveBeenCalledWith(
expect.objectContaining({
slug: mockProps.app.slug,
}),
{}
);
expect(screen.getByText("MockedDynamicComponent")).toBeInTheDocument();
});
test("Should invoke getAppData and setAppData from context on render", () => {
render(
<EventTypeAppCard
{...mockProps}
value={{
getAppData: getAppDataMock(),
setAppData: setAppDataMock(),
}}
/>
);
expect(getAppDataMock).toHaveBeenCalled();
expect(setAppDataMock).toHaveBeenCalled();
});
test("Should render DynamicComponent with 'stripepayment' slug for stripe app", () => {
const stripeProps = {
...mockProps,
app: {
...mockProps.app,
slug: "stripe",
},
};
render(<EventTypeAppCard {...stripeProps} />);
expect(DynamicComponent).toHaveBeenCalledWith(
expect.objectContaining({
slug: "stripepayment",
}),
{}
);
expect(screen.getByText("MockedDynamicComponent")).toBeInTheDocument();
});
test("Should display error boundary message on child component error", () => {
(DynamicComponent as jest.Mock).mockImplementation(() => {
return Error("Mocked error from DynamicComponent");
});
render(<EventTypeAppCard {...mockProps} />);
const errorMessage = screen.getByText(`There is some problem with ${mockProps.app.name} App`);
expect(errorMessage).toBeInTheDocument();
});
});

View File

@ -122,9 +122,9 @@ function AlbySetupPage(props: IAlbySetupProps) {
const albyIcon = (
<>
<img className="h-16 w-16 dark:hidden" src="/api/app-store/alby/icon-borderless.svg" alt="Alby Icon" />
<img className="h-12 w-12 dark:hidden" src="/api/app-store/alby/icon-borderless.svg" alt="Alby Icon" />
<img
className="hidden h-16 w-16 dark:block"
className="hidden h-12 w-12 dark:block"
src="/api/app-store/alby/icon-borderless-dark.svg"
alt="Alby Icon"
/>

View File

@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
import { apiKeysRouter } from "@calcom/trpc/server/routers/viewer/apiKeys/_router";
import checkSession from "../../_utils/auth";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
@ -15,9 +15,9 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const appType = appConfig.type;
const ctx = await createContext({ req, res });
const caller = viewerRouter.createCaller(ctx);
const caller = apiKeysRouter.createCaller(ctx);
const apiKey = await caller.apiKeys.create({
const apiKey = await caller.create({
note: "Cal.ai",
expiresAt: null,
appId: "cal-ai",

View File

@ -284,9 +284,10 @@ export default class GoogleCalendarService implements Calendar {
const calendar = await this.authedCalendar();
const selectedCalendar = externalCalendarId
? externalCalendarId
: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
const selectedCalendar =
(externalCalendarId
? event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId
: undefined) || "primary";
try {
const evt = await calendar.events.update({
@ -337,14 +338,15 @@ export default class GoogleCalendarService implements Calendar {
async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise<void> {
const calendar = await this.authedCalendar();
const defaultCalendarId = "primary";
const calendarId = externalCalendarId
? externalCalendarId
: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
const selectedCalendar =
(externalCalendarId
? event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId
: undefined) || "primary";
try {
const event = await calendar.events.delete({
calendarId: calendarId ? calendarId : defaultCalendarId,
calendarId: selectedCalendar,
eventId: uid,
sendNotifications: false,
sendUpdates: "none",

View File

@ -91,7 +91,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
attendeeInputType: "attendeeAddress",
attendeeInputPlaceholder: "enter_address",
defaultValueVariable: "attendeeAddress",
iconUrl: "/map-pin.svg",
iconUrl: "/map-pin-dark.svg",
category: "in person",
},
{
@ -103,7 +103,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
// HACK:
variable: "locationAddress",
defaultValueVariable: "address",
iconUrl: "/map-pin.svg",
iconUrl: "/map-pin-dark.svg",
category: "in person",
},
{

View File

@ -0,0 +1,27 @@
import matchers from "@testing-library/jest-dom/matchers";
import { cleanup } from "@testing-library/react";
import { afterEach, expect, vi } from "vitest";
vi.mock("@calcom/lib/OgImages", async () => {
return {};
});
vi.mock("@calcom/lib/hooks/useLocale", () => ({
useLocale: () => {
return {
t: (str: string) => str,
};
},
}));
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
expect.extend(matchers);
afterEach(() => {
cleanup();
});

View File

@ -44,6 +44,7 @@ const BookerComponent = ({
isTeamEvent,
entity,
duration,
hashedLink,
}: BookerProps) => {
/**
* Prioritize dateSchedule load
@ -115,6 +116,7 @@ const BookerComponent = ({
columnViewExtraDays.current =
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 2], "day")) + addonDays;
const prefetchNextMonth =
layout === BookerLayouts.COLUMN_VIEW &&
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()
@ -284,6 +286,7 @@ const BookerComponent = ({
setSeatedEventData({ ...seatedEventData, bookingUid: undefined, attendees: undefined });
}
}}
hashedLink={hashedLink}
/>
</BookerSection>

View File

@ -41,11 +41,12 @@ import { FormSkeleton } from "./Skeleton";
type BookEventFormProps = {
onCancel?: () => void;
hashedLink?: string | null;
};
type DefaultValues = Record<string, unknown>;
export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
export const BookEventForm = ({ onCancel, hashedLink }: BookEventFormProps) => {
const [slotReservationId, setSlotReservationId] = useSlotReservationId();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation({
trpc: {
@ -114,6 +115,7 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
isRescheduling={isRescheduling}
eventQuery={eventQuery}
rescheduleUid={rescheduleUid}
hashedLink={hashedLink}
/>
);
};
@ -124,11 +126,13 @@ export const BookEventFormChild = ({
isRescheduling,
eventQuery,
rescheduleUid,
hashedLink,
}: BookEventFormProps & {
initialValues: DefaultValues;
isRescheduling: boolean;
eventQuery: ReturnType<typeof useEvent>;
rescheduleUid: string | null;
hashedLink?: string | null;
}) => {
const eventType = eventQuery.data;
const bookingFormSchema = z
@ -332,6 +336,7 @@ export const BookEventFormChild = ({
}),
{}
),
hashedLink,
};
if (eventQuery.data?.recurringEvent?.freq && recurringEventCount) {
@ -370,6 +375,7 @@ export const BookEventFormChild = ({
fields={eventType.bookingFields}
locations={eventType.locations}
rescheduleUid={rescheduleUid || undefined}
bookingData={bookingData}
/>
{(createBookingMutation.isError ||
createRecurringBookingMutation.isError ||
@ -399,8 +405,8 @@ export const BookEventFormChild = ({
type="submit"
color="primary"
loading={createBookingMutation.isLoading || createRecurringBookingMutation.isLoading}
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}>
{rescheduleUid
data-testid={rescheduleUid && bookingData ? "confirm-reschedule-button" : "confirm-book-button"}>
{rescheduleUid && bookingData
? t("reschedule")
: renderConfirmNotVerifyEmailButtonCond
? t("confirm")
@ -492,12 +498,18 @@ function useInitialFormValues({
});
const defaultUserValues = {
email: rescheduleUid
? bookingData?.attendees[0].email
: parsedQuery["email"] || session.data?.user?.email || "",
name: rescheduleUid
? bookingData?.attendees[0].name
: parsedQuery["name"] || session.data?.user?.name || "",
email:
rescheduleUid && bookingData && bookingData.attendees.length > 0
? bookingData?.attendees[0].email
: !!parsedQuery["email"]
? parsedQuery["email"]
: session.data?.user?.email ?? "",
name:
rescheduleUid && bookingData && bookingData.attendees.length > 0
? bookingData?.attendees[0].name
: !!parsedQuery["name"]
? parsedQuery["name"]
: session.data?.user?.name ?? session.data?.user?.username ?? "",
};
if (!isRescheduling) {
@ -521,14 +533,12 @@ function useInitialFormValues({
setDefaultValues(defaults);
}
if ((!rescheduleUid && !bookingData) || !bookingData?.attendees.length) {
return {};
}
const primaryAttendee = bookingData.attendees[0];
if (!primaryAttendee) {
if (!rescheduleUid && !bookingData) {
return {};
}
// We should allow current session user as default values for booking form
const defaults = {
responses: {} as Partial<z.infer<ReturnType<typeof getBookingResponsesSchema>>>,
};
@ -536,7 +546,7 @@ function useInitialFormValues({
const responses = eventType.bookingFields.reduce((responses, field) => {
return {
...responses,
[field.name]: bookingData.responses[field.name],
[field.name]: bookingData?.responses[field.name],
};
}, {});
defaults.responses = {

View File

@ -1,6 +1,7 @@
import { useFormContext } from "react-hook-form";
import type { LocationObject } from "@calcom/app-store/locations";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilderField";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -13,10 +14,12 @@ export const BookingFields = ({
locations,
rescheduleUid,
isDynamicGroupBooking,
bookingData,
}: {
fields: NonNullable<RouterOutputs["viewer"]["public"]["event"]>["bookingFields"];
locations: LocationObject[];
rescheduleUid?: string;
bookingData?: GetBookingType | null;
isDynamicGroupBooking: boolean;
}) => {
const { t } = useLocale();
@ -32,7 +35,9 @@ export const BookingFields = ({
// During reschedule by default all system fields are readOnly. Make them editable on case by case basis.
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
let readOnly =
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
(field.editable === "system" || field.editable === "system-but-optional") &&
!!rescheduleUid &&
bookingData !== null;
let hidden = !!field.hidden;
const fieldViews = field.views;
@ -42,6 +47,9 @@ export const BookingFields = ({
}
if (field.name === SystemField.Enum.rescheduleReason) {
if (bookingData === null) {
return null;
}
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
readOnly = false;
}
@ -64,8 +72,8 @@ export const BookingFields = ({
hidden = isDynamicGroupBooking ? true : !!field.hidden;
}
// We don't show `notes` field during reschedule
if (field.name === SystemField.Enum.notes && !!rescheduleUid) {
// We don't show `notes` field during reschedule but since it's a query param we better valid if rescheduleUid brought any bookingData
if (field.name === SystemField.Enum.notes && bookingData !== null) {
return null;
}

View File

@ -64,6 +64,10 @@ export interface BookerProps {
* otherwise, the default value is selected
*/
duration?: number | null;
/**
* Refers to the private link from event types page.
*/
hashedLink?: string | null;
}
export type BookerState = "loading" | "selecting_date" | "selecting_time" | "booking";

View File

@ -1,3 +1,4 @@
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { HeadSeo } from "@calcom/ui";
@ -14,10 +15,20 @@ interface BookerSeoProps {
teamSlug?: string | null;
name?: string | null;
};
bookingData?: GetBookingType | null;
}
export const BookerSeo = (props: BookerSeoProps) => {
const { eventSlug, username, rescheduleUid, hideBranding, isTeamEvent, entity, isSEOIndexable } = props;
const {
eventSlug,
username,
rescheduleUid,
hideBranding,
isTeamEvent,
entity,
isSEOIndexable,
bookingData,
} = props;
const { t } = useLocale();
const { data: event } = trpc.viewer.public.event.useQuery(
{ username, eventSlug, isTeamEvent, org: entity.orgSlug ?? null },
@ -29,7 +40,7 @@ export const BookerSeo = (props: BookerSeoProps) => {
const title = event?.title ?? "";
return (
<HeadSeo
title={`${rescheduleUid ? t("reschedule") : ""} ${title} | ${profileName}`}
title={`${rescheduleUid && !!bookingData ? t("reschedule") : ""} ${title} | ${profileName}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${title}`}
meeting={{
title: title,

View File

@ -96,7 +96,7 @@ export function AvailableEventLocations({ locations }: { locations: LocationObje
return filteredLocations.length > 1 ? (
<div className="flex flex-row items-center text-sm font-medium">
<img
src="/map-pin.svg"
src="/map-pin-dark.svg"
className={classNames("me-[10px] h-4 w-4 opacity-70 dark:invert")}
alt="map-pin"
/>

View File

@ -61,7 +61,7 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe
image: "logo" in profile && profile.logo ? `${profile.logo}` : undefined,
alt: profile.name || undefined,
href: profile.username
? `${CAL_URL}` + (pathname.indexOf("/team/") !== -1 ? "/team" : "") + `/${profile.username}`
? `${CAL_URL}${pathname.indexOf("/team/") !== -1 ? "/team" : ""}/${profile.username}`
: undefined,
});

View File

@ -18,6 +18,7 @@ type BookingOptions = {
metadata?: Record<string, string>;
bookingUid?: string;
seatReferenceUid?: string;
hashedLink?: string | null;
};
export const mapBookingToMutationInput = ({
@ -32,6 +33,7 @@ export const mapBookingToMutationInput = ({
metadata,
bookingUid,
seatReferenceUid,
hashedLink,
}: BookingOptions): BookingCreateBody => {
return {
...values,
@ -47,11 +49,10 @@ export const mapBookingToMutationInput = ({
language: language,
rescheduleUid,
metadata: metadata || {},
hasHashedBookingLink: false,
hasHashedBookingLink: hashedLink ? true : false,
bookingUid,
seatReferenceUid,
// hasHashedBookingLink,
// hashedLink,
hashedLink,
};
};

View File

@ -109,7 +109,7 @@ export const getBookingWithResponses = <
export default getBooking;
export const getBookingForReschedule = async (uid: string) => {
export const getBookingForReschedule = async (uid: string, userId?: number) => {
let rescheduleUid: string | null = null;
const theBooking = await prisma.booking.findFirst({
where: {
@ -117,8 +117,25 @@ export const getBookingForReschedule = async (uid: string) => {
},
select: {
id: true,
userId: true,
eventType: {
select: {
seatsPerTimeSlot: true,
hosts: {
select: {
userId: true,
},
},
owner: {
select: {
id: true,
},
},
},
},
},
});
let bookingSeatReferenceUid: number | null = null;
// If no booking is found via the uid, it's probably a booking seat
// that its being rescheduled, which we query next.
@ -144,11 +161,26 @@ export const getBookingForReschedule = async (uid: string) => {
},
});
if (bookingSeat) {
bookingSeatReferenceUid = bookingSeat.id;
rescheduleUid = bookingSeat.booking.uid;
attendeeEmail = bookingSeat.attendee.email;
}
}
// If we have the booking and not bookingSeat, we need to make sure the booking belongs to the userLoggedIn
// Otherwise, we return null here.
let hasOwnershipOnBooking = false;
if (theBooking && theBooking?.eventType?.seatsPerTimeSlot && bookingSeatReferenceUid === null) {
const isOwnerOfBooking = theBooking.userId === userId;
const isHostOfEventType = theBooking?.eventType?.hosts.some((host) => host.userId === userId);
const isUserIdInBooking = theBooking.userId === userId;
if (!isOwnerOfBooking && !isHostOfEventType && !isUserIdInBooking) return null;
hasOwnershipOnBooking = true;
}
// If we don't have a booking and no rescheduleUid, the ID is invalid,
// and we return null here.
if (!theBooking && !rescheduleUid) return null;
@ -161,6 +193,8 @@ export const getBookingForReschedule = async (uid: string) => {
...booking,
attendees: rescheduleUid
? booking.attendees.filter((attendee) => attendee.email === attendeeEmail)
: hasOwnershipOnBooking
? []
: booking.attendees,
};
};

View File

@ -136,6 +136,19 @@ async function handler(req: CustomRequest) {
throw new HttpError({ statusCode: 400, message: "User not found" });
}
// If the booking is a seated event and there is no seatReferenceUid we should validate that logged in user is host
if (bookingToDelete.eventType?.seatsPerTimeSlot && !seatReferenceUid) {
const userIsHost = bookingToDelete.eventType.hosts.find((host) => {
if (host.user.id === userId) return true;
});
const userIsOwnerOfEventType = bookingToDelete.eventType.owner?.id === userId;
if (!userIsHost && !userIsOwnerOfEventType) {
throw new HttpError({ statusCode: 401, message: "User not a host of this event" });
}
}
// get webhooks
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";

View File

@ -996,7 +996,7 @@ async function handler(
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
const organizerMetadata = metadataParseResult.success ? metadataParseResult.data : undefined;
if (organizerMetadata) {
if (organizerMetadata?.defaultConferencingApp?.appSlug) {
const app = getAppFromSlug(organizerMetadata?.defaultConferencingApp?.appSlug);
locationBodyString = app?.appData?.location?.type || locationBodyString;
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
@ -2148,6 +2148,7 @@ async function handler(
id: originalRescheduledBooking.id,
},
data: {
rescheduled: true,
status: BookingStatus.CANCELLED,
},
});

View File

@ -176,30 +176,37 @@ const ProfileView = () => {
mutation.mutate({ id: team.id, ...variables });
}
}}>
<div className="flex items-center">
<Controller
control={form.control}
name="logo"
render={({ field: { value } }) => (
<>
<Avatar alt="" imageSrc={getPlaceholderAvatar(value, team?.name as string)} size="lg" />
<div className="ms-4">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("update")}
handleAvatarChange={(newLogo) => {
form.setValue("logo", newLogo);
}}
imageSrc={value}
/>
</div>
</>
)}
/>
</div>
<hr className="border-subtle my-8" />
{!team.parent && (
<>
<div className="flex items-center">
<Controller
control={form.control}
name="logo"
render={({ field: { value } }) => (
<>
<Avatar
alt=""
imageSrc={getPlaceholderAvatar(value, team?.name as string)}
size="lg"
/>
<div className="ms-4">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("update")}
handleAvatarChange={(newLogo) => {
form.setValue("logo", newLogo);
}}
imageSrc={value}
/>
</div>
</>
)}
/>
</div>
<hr className="border-subtle my-8" />
</>
)}
<Controller
control={form.control}

View File

@ -114,17 +114,19 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
selectedFilter,
isAll,
dateRange,
initialConfig,
} = newConfigFilters;
const [startTime, endTime] = dateRange || [null, null];
const newSearchParams = new URLSearchParams(searchParams);
const newSearchParams = new URLSearchParams(searchParams.toString());
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
if (value !== undefined && value !== null) newSearchParams.set(key, value.toString());
}
setParamsIfDefined("memberUserId", selectedMemberUserId);
setParamsIfDefined("teamId", selectedTeamId);
setParamsIfDefined("userId", selectedUserId);
setParamsIfDefined("teamId", selectedTeamId || initialConfig?.teamId);
setParamsIfDefined("userId", selectedUserId || initialConfig?.userId);
setParamsIfDefined("eventTypeId", selectedEventTypeId);
setParamsIfDefined("isAll", isAll);
setParamsIfDefined("isAll", isAll || initialConfig?.isAll);
setParamsIfDefined("startTime", startTime?.toISOString());
setParamsIfDefined("endTime", endTime?.toISOString());
setParamsIfDefined("filter", selectedFilter?.[0]);

View File

@ -22,6 +22,11 @@ export const TeamAndSelfList = () => {
const { data, isSuccess } = trpc.viewer.insights.teamListForUser.useQuery(undefined, {
// Teams don't change that frequently
refetchOnWindowFocus: false,
trpc: {
context: {
skipBatch: true,
},
},
});
useEffect(() => {
@ -48,6 +53,7 @@ export const TeamAndSelfList = () => {
} else if (session.data?.user.id) {
// default to user
setConfigFilters({
selectedUserId: session.data?.user.id,
initialConfig: {
teamId: null,
userId: session.data?.user.id,

View File

@ -3,7 +3,6 @@ import { useForm } from "react-hook-form";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
@ -15,6 +14,7 @@ import {
DialogHeader,
DialogClose,
Switch,
showToast,
Form,
Button,
} from "@calcom/ui";
@ -23,15 +23,11 @@ import DatePicker from "../../calendars/DatePicker";
import type { TimeRange } from "./Schedule";
import { DayRanges } from "./Schedule";
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
const DateOverrideForm = ({
value,
workingHours,
excludedDates,
onChange,
onClose = noop,
}: {
workingHours?: WorkingHours[];
onChange: (newValue: TimeRange[]) => void;
@ -137,14 +133,10 @@ const DateOverrideForm = ({
})
: datesInRanges
);
onClose();
setSelectedDates([]);
}}
className="p-6 sm:flex sm:p-0 md:flex-col lg:flex-col xl:flex-row">
<div
className={classNames(
selectedDates[0] && "sm:border-subtle w-full sm:border-r sm:pr-6",
"sm:p-4 md:p-8"
)}>
<div className="sm:border-subtle w-full sm:border-r sm:p-4 sm:pr-6 md:p-8">
<DialogHeader title={t("date_overrides_dialog_title")} />
<DatePicker
excludedDates={excludedDates}
@ -160,39 +152,48 @@ const DateOverrideForm = ({
locale={isLocaleReady ? i18n.language : "en"}
/>
</div>
{selectedDates[0] && (
<div className="relative mt-8 flex w-full flex-col sm:mt-0 sm:p-4 md:p-8">
<div className="mb-4 flex-grow space-y-4">
<p className="text-medium text-emphasis text-sm">{t("date_overrides_dialog_which_hours")}</p>
<div>
{datesUnavailable ? (
<p className="text-subtle border-default rounded border p-2 text-sm">
{t("date_overrides_unavailable")}
</p>
) : (
<DayRanges name="range" />
)}
<div className="relative mt-8 flex w-full flex-col sm:mt-0 sm:p-4 md:p-8">
{selectedDates[0] ? (
<>
<div className="mb-4 flex-grow space-y-4">
<p className="text-medium text-emphasis text-sm">{t("date_overrides_dialog_which_hours")}</p>
<div>
{datesUnavailable ? (
<p className="text-subtle border-default rounded border p-2 text-sm">
{t("date_overrides_unavailable")}
</p>
) : (
<DayRanges name="range" />
)}
</div>
<Switch
label={t("date_overrides_mark_all_day_unavailable_one")}
checked={datesUnavailable}
onCheckedChange={setDatesUnavailable}
data-testid="date-override-mark-unavailable"
/>
</div>
<Switch
label={t("date_overrides_mark_all_day_unavailable_one")}
checked={datesUnavailable}
onCheckedChange={setDatesUnavailable}
data-testid="date-override-mark-unavailable"
/>
<div className="mt-4 flex flex-row-reverse sm:mt-0">
<Button
className="ml-2"
color="primary"
type="submit"
onClick={() => {
showToast(t("date_successfully_added"), "success");
}}
disabled={selectedDates.length === 0}
data-testid="add-override-submit-btn">
{value ? t("date_overrides_update_btn") : t("date_overrides_add_btn")}
</Button>
<DialogClose />
</div>
</>
) : (
<div className="bottom-7 right-8 flex flex-row-reverse sm:absolute">
<DialogClose />
</div>
<div className="mt-4 flex flex-row-reverse sm:mt-0">
<Button
className="ml-2"
color="primary"
type="submit"
disabled={selectedDates.length === 0}
data-testid="add-override-submit-btn">
{value ? t("date_overrides_update_btn") : t("date_overrides_add_btn")}
</Button>
<DialogClose onClick={onClose} />
</div>
</div>
)}
)}
</div>
</Form>
);
};
@ -220,7 +221,7 @@ const DateOverrideInputDialog = ({
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{Trigger}</DialogTrigger>
<DialogContent enableOverflow={enableOverflow} size="md" className="p-0 md:w-auto">
<DialogContent enableOverflow={enableOverflow} size="md" className="p-0">
<DateOverrideForm
excludedDates={excludedDates}
{...passThroughProps}

View File

@ -375,11 +375,13 @@ const SettingsSidebarContainer = ({
<ChevronRight className="h-4 w-4" />
)}
</div>
<img
src={getPlaceholderAvatar(team.logo, team?.name as string)}
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
alt={team.name || "Team logo"}
/>
{!team.parentId && (
<img
src={getPlaceholderAvatar(team.logo, team?.name as string)}
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
alt={team.name || "Team logo"}
/>
)}
<p className="w-1/2 truncate">{team.name}</p>
{!team.accepted && (
<Badge className="ms-3" variant="orange">
@ -517,11 +519,13 @@ const SettingsSidebarContainer = ({
<ChevronRight className="h-4 w-4" />
)}
</div>
<img
src={getPlaceholderAvatar(otherTeam.logo, otherTeam?.name as string)}
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
alt={otherTeam.name || "Team logo"}
/>
{!otherTeam.parentId && (
<img
src={getPlaceholderAvatar(otherTeam.logo, otherTeam?.name as string)}
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
alt={otherTeam.name || "Team logo"}
/>
)}
<p className="w-1/2 truncate">{otherTeam.name}</p>
</div>
</CollapsibleTrigger>

View File

@ -1008,7 +1008,7 @@ function MainContainer({
<main className="bg-default relative z-0 flex-1 focus:outline-none">
{/* show top navigation for md and smaller (tablet and phones) */}
{TopNavContainerProp}
<div className="max-w-full px-2 py-4 md:py-12 lg:px-6">
<div className="max-w-full px-2 py-4 lg:px-6">
<ErrorBoundary>
{!props.withoutMain ? <ShellMain {...props}>{props.children}</ShellMain> : props.children}
</ErrorBoundary>

View File

@ -7,7 +7,7 @@ export const useUrlMatchesCurrentUrl = (url: string) => {
// It can certainly have null value https://nextjs.org/docs/app/api-reference/functions/use-pathname#:~:text=usePathname%20can%20return%20null%20when%20a%20fallback%20route%20is%20being%20rendered%20or%20when%20a%20pages%20directory%20page%20has%20been%20automatically%20statically%20optimized%20by%20Next.js%20and%20the%20router%20is%20not%20ready.
const pathname = usePathname() as null | string;
const searchParams = useSearchParams();
const query = searchParams.toString();
const query = searchParams?.toString();
let pathnameWithQuery;
if (query) {
pathnameWithQuery = `${pathname}?${query}`;

View File

@ -15,6 +15,6 @@ export async function maybeGetBookingUidFromSeat(prisma: PrismaClient, uid: stri
},
},
});
if (bookingSeat) return bookingSeat.booking.uid;
return uid;
if (bookingSeat) return { uid: bookingSeat.booking.uid, seatReferenceUid: uid };
return { uid };
}

View File

@ -0,0 +1,35 @@
-- View: public.BookingsTimeStatus
-- DROP VIEW public."BookingsTimeStatus";
CREATE OR REPLACE VIEW public."BookingTimeStatus"
AS
SELECT "Booking".id,
"Booking".uid,
"Booking"."eventTypeId",
"Booking".title,
"Booking".description,
"Booking"."startTime",
"Booking"."endTime",
"Booking"."createdAt",
"Booking".location,
"Booking".paid,
"Booking".status,
"Booking".rescheduled,
"Booking"."userId",
"et"."teamId",
"et"."length" as "eventLength",
CASE
WHEN "Booking".rescheduled IS TRUE THEN 'rescheduled'::text
WHEN "Booking".status = 'cancelled'::"BookingStatus" AND "Booking".rescheduled IS NULL THEN 'cancelled'::text
WHEN "Booking"."endTime" < now() THEN 'completed'::text
WHEN "Booking"."endTime" > now() THEN 'uncompleted'::text
ELSE NULL::text
END AS "timeStatus",
"et"."parentId" as "eventParentId"
FROM "Booking"
LEFT JOIN "EventType" et ON "Booking"."eventTypeId" = et.id
LEFT JOIN "Membership" mb ON "mb"."userId" = "Booking"."userId";

View File

@ -36,10 +36,10 @@ export function DataTableToolbar<TData>({
const isFiltered = table.getState().columnFilters.length > 0;
return (
<div className="flex items-center justify-end space-x-2">
<div className="bg-default sticky top-[3rem] z-10 flex items-center justify-end space-x-2 py-4 md:top-0">
{searchKey && (
<Input
className="max-w-64 mb-0 mr-auto"
className="max-w-64 mb-0 mr-auto rounded-md"
placeholder="Search"
value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
onChange={(event) => table.getColumn(searchKey)?.setFilterValue(event.target.value)}

View File

@ -102,21 +102,9 @@ export function DataTable<TData, TValue>({
searchKey={searchKey}
tableCTA={tableCTA}
/>
<div
className="border-subtle rounded-md border"
ref={tableContainerRef}
onScroll={onScroll}
style={{
height: "calc(100vh - 30vh)",
overflow: "auto",
}}>
<div className="border-subtle border" ref={tableContainerRef} onScroll={onScroll}>
<Table>
<TableHeader
style={{
position: "sticky",
top: 0,
zIndex: 1,
}}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {

View File

@ -187,7 +187,7 @@ export function DialogClose(
return (
<DialogPrimitive.Close asChild {...props.dialogCloseProps}>
{/* This will require the i18n string passed in */}
<Button color={props.color || "minimal"} {...props}>
<Button data-testid="dialog-rejection" color={props.color || "minimal"} {...props}>
{props.children ? props.children : t("Close")}
</Button>
</DialogPrimitive.Close>

View File

@ -1,22 +1,20 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import { Canvas, Meta, Story } from "@storybook/addon-docs";
import {
Examples,
Example,
Note,
Title,
CustomArgsTable,
VariantRow,
VariantsTable,
} from "@calcom/storybook/components";
import { Select, UnstyledSelect } from "../select";
import { InputFieldWithSelect } from "./Input";
import { InputField } from "./Input";
import { InputFieldWithSelect } from "./InputFieldWithSelect";
import { InputField } from "./TextField";
<Meta title="UI/Form/Input Field" component={InputField} />
<Title title="Inputs" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
<Title title="Inputs" suffix="Brief" subtitle="Version 2.0 — Last Update: 24 Aug 2023" />
## Definition

View File

@ -0,0 +1,134 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs/blocks";
import {
Title,
VariantRow,
VariantsTable,
CustomArgsTable,
Examples,
Example,
} from "@calcom/storybook/components";
import { Plus } from "@calcom/ui/components/icon";
import HorizontalTabs from "../HorizontalTabs";
<Meta title="UI/Navigation/HorizontalTabs" component={HorizontalTabs} />
<Title title="Horizontal Tabs" suffix="Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
## Definition
The HorizontalTabs component is a user interface element used for displaying a horizontal set of tabs, often employed for navigation or organization purposes within a web application.
## Structure
The HorizontalTabs component is designed to work alongside the HorizontalTabItem component, which represents individual tabs within the tab bar.
export const tabs = [
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
disabled: false,
linkShallow: true,
linkScroll: true,
icon: Plus,
},
{
name: "Tab 2",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab2",
disabled: false,
linkShallow: true,
linkScroll: true,
avatar: "Avatar",
},
{
name: "Tab 3",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab3",
disabled: true,
linkShallow: true,
linkScroll: true,
},
];
<CustomArgsTable of={HorizontalTabs} />
<Examples title="Default">
<Example title="Default">
<HorizontalTabs
tabs={[
{
name: "tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
},
]}
/>
</Example>
<Example title="With avatar">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
avatar: "Avatar",
},
]}
/>
</Example>
<Example title="With icon">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
icon: Plus,
},
]}
/>
</Example>
<Example title="Disabled">
<HorizontalTabs
tabs={[
{
name: "Tab 1",
href: "?path=/story/ui-navigation-horizontaltabs--horizontal-tabs/tab1",
disabled: true,
},
]}
/>
</Example>
</Examples>
## HorizontalTabs Story
<Canvas>
<Story
name="Horizontal Tabs"
args={{
name: "Tab 1",
href: "/tab1",
disabled: false,
className: "",
linkShallow: true,
linkScroll: true,
icon: "",
avatar: "",
}}
argTypes={{
name: { control: "text", description: "Tab name" },
href: { control: "text", description: "Tab link" },
disabled: { control: "boolean", description: "Whether the tab is disabled" },
className: { control: "text", description: "Additional CSS class" },
linkShallow: { control: "boolean", description: "Whether to use shallow links" },
linkScroll: { control: "boolean", description: "Whether to scroll to links" },
icon: { control: "text", description: "SVGComponent icon" },
avatar: { control: "text", description: "Avatar image URL" },
}}>
{(...props) => (
<VariantsTable titles={["Default"]} columnMinWidth={150}>
<VariantRow>
<HorizontalTabs tabs={tabs} className="overflow-hidden" actions={<button>Click me</button>} />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>

View File

@ -0,0 +1,158 @@
import { Meta, Story } from "@storybook/addon-docs/blocks";
import {
Title,
CustomArgsTable,
Examples,
Example,
VariantsTable,
VariantsRow,
} from "@calcom/storybook/components";
import { Plus } from "@calcom/ui/components/icon";
import VerticalTabs from "../VerticalTabs";
<Meta title="UI/Navigation/VerticalTabs" component={VerticalTabs} />
<Title title="Vertical Tabs Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
## Definition
The VerticalTabs component is a user interface element utilized to present a vertical set of tabs, commonly employed for navigation or organizing content within a web application.
## Structure
The VerticalTabs component is designed to complement the HorizontalTabItem component, which represents individual tabs within the tab bar. This combination allows for creating intuitive navigation experiences and organized content presentation.
export const tabs = [
{
name: "Tab 1",
href: "/tab1",
disabled: false,
linkShallow: true,
linkScroll: true,
disableChevron: true,
icon: Plus,
},
{
name: "Tab 2",
href: "/tab2",
disabled: false,
linkShallow: true,
linkScroll: true,
avatar: "Avatar",
},
{
name: "Tab 3",
href: "/tab3",
disabled: true,
linkShallow: true,
linkScroll: true,
},
];
<CustomArgsTable of={VerticalTabs} />
<Examples title="Default">
<Example title="Default">
<VerticalTabs
tabs={[
{
name: "tab 1",
href: "/tab1",
},
]}
/>
</Example>
<Example title="Disabled chevron">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
disableChevron: true,
},
]}
/>
</Example>
<Example title="With icon">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
icon: Plus,
},
]}
/>
</Example>
<Example title="Disabled">
<VerticalTabs
tabs={[
{
name: "Tab 1",
href: "/tab1",
disabled: true,
},
]}
/>
</Example>
</Examples>
## VerticalTabs Story
<Canvas>
<Story
name="Vertical Tabs"
args={{
name: "Tab 1",
info: "Tab information",
icon: Plus,
disabled: false,
children: [
{
name: "Sub Tab 1",
href: "/sub-tab1",
disabled: false,
className: "sub-tab",
},
],
textClassNames: "",
className: "",
isChild: false,
hidden: false,
disableChevron: true,
href: "/tab1",
isExternalLink: true,
linkShallow: true,
linkScroll: true,
avatar: "",
iconClassName: "",
}}
argTypes={{
name: { control: "text", description: "Tab name" },
info: { control: "text", description: "Tab information" },
icon: { control: "object", description: "SVGComponent icon" },
disabled: { control: "boolean", description: "Whether the tab is disabled" },
children: { control: "object", description: "Array of child tabs" },
textClassNames: { control: "text", description: "Additional text class names" },
className: { control: "text", description: "Additional CSS class" },
isChild: { control: "boolean", description: "Whether the tab is a child tab" },
hidden: { control: "boolean", description: "Whether the tab is hidden" },
disableChevron: { control: "boolean", description: "Whether to disable the chevron" },
href: { control: "text", description: "Tab link" },
isExternalLink: { control: "boolean", description: "Whether the link is external" },
linkShallow: { control: "boolean", description: "Whether to use shallow links" },
linkScroll: { control: "boolean", description: "Whether to scroll to links" },
avatar: { control: "text", description: "Avatar image URL" },
iconClassName: { control: "text", description: "Additional icon class name" },
}}>
{(...props) => (
<VariantsTable titles={["Default"]} columnMinWidth={150}>
<VariantRow>
<VerticalTabs tabs={tabs} className="overflow-hidden" />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>

View File

@ -4,7 +4,7 @@ import { classNames } from "@calcom/lib";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="w-full overflow-auto">
<div className="w-full overflow-auto md:overflow-visible">
<table ref={ref} className={classNames("w-full caption-bottom text-sm", className)} {...props} />
</div>
)
@ -13,7 +13,11 @@ Table.displayName = "Table";
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<thead ref={ref} className={classNames("[&_tr]:bg-subtle [&_tr]:border-b", className)} {...props} />
<thead
ref={ref}
className={classNames("[&_tr]:bg-subtle md:sticky md:top-[4.25rem] md:z-10 [&_tr]:border-b", className)}
{...props}
/>
)
);
TableHeader.displayName = "TableHeader";

View File

@ -37,6 +37,15 @@ const workspaces = packagedEmbedTestsOnly
setupFiles: ["packages/ui/components/test-setup.ts"],
},
},
{
test: {
globals: true,
name: "EventTypeAppCardInterface components",
include: ["packages/app-store/_components/**/*.{test,spec}.{ts,js,tsx}"],
environment: "jsdom",
setupFiles: ["packages/app-store/test-setup.ts"],
},
},
];
export default defineWorkspace(workspaces);

547
yarn.lock
View File

@ -91,6 +91,13 @@ __metadata:
languageName: node
linkType: hard
"@alloc/quick-lru@npm:^5.2.0":
version: 5.2.0
resolution: "@alloc/quick-lru@npm:5.2.0"
checksum: bdc35758b552bcf045733ac047fb7f9a07c4678b944c641adfbd41f798b4b91fffd0fdc0df2578d9b0afc7b4d636aa6e110ead5d6281a2adc1ab90efd7f057f8
languageName: node
linkType: hard
"@ampproject/remapping@npm:^2.2.0":
version: 2.2.1
resolution: "@ampproject/remapping@npm:2.2.1"
@ -3183,7 +3190,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.21.0":
"@babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.21.0":
version: 7.23.1
resolution: "@babel/runtime@npm:7.23.1"
dependencies:
@ -3535,15 +3542,13 @@ __metadata:
"@calcom/ui": "*"
"@types/node": 16.9.1
"@types/react": 18.0.26
"@types/react-dom": ^18.0.9
"@types/react-dom": 18.0.9
eslint: ^8.34.0
eslint-config-next: ^13.2.1
next: ^13.4.6
next-auth: ^4.22.1
postcss: ^8.4.18
next: ^13.2.1
next-auth: ^4.20.1
react: ^18.2.0
react-dom: ^18.2.0
tailwindcss: ^3.3.1
typescript: ^4.9.4
languageName: unknown
linkType: soft
@ -3637,7 +3642,7 @@ __metadata:
"@calcom/ui": "*"
"@headlessui/react": ^1.5.0
"@heroicons/react": ^1.0.6
"@prisma/client": ^5.0.0
"@prisma/client": ^4.13.0
"@tailwindcss/forms": ^0.5.2
"@types/node": 16.9.1
"@types/react": 18.0.26
@ -3645,21 +3650,21 @@ __metadata:
chart.js: ^3.7.1
client-only: ^0.0.1
eslint: ^8.34.0
next: ^13.4.6
next-auth: ^4.22.1
next-i18next: ^13.2.2
next: ^13.2.1
next-auth: ^4.20.1
next-i18next: ^11.3.0
postcss: ^8.4.18
prisma: ^5.0.0
prisma: ^4.13.0
prisma-field-encryption: ^1.4.0
react: ^18.2.0
react-chartjs-2: ^4.0.1
react-dom: ^18.2.0
react-hook-form: ^7.43.3
react-live-chat-loader: ^2.8.1
react-live-chat-loader: ^2.7.3
swr: ^1.2.2
tailwindcss: ^3.3.1
tailwindcss: ^3.2.1
typescript: ^4.9.4
zod: ^3.22.2
zod: ^3.20.2
languageName: unknown
linkType: soft
@ -3991,6 +3996,15 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/intercom@workspace:packages/app-store/intercom":
version: 0.0.0-use.local
resolution: "@calcom/intercom@workspace:packages/app-store/intercom"
dependencies:
"@calcom/lib": "*"
"@calcom/types": "*"
languageName: unknown
linkType: soft
"@calcom/jitsivideo@workspace:packages/app-store/jitsivideo":
version: 0.0.0-use.local
resolution: "@calcom/jitsivideo@workspace:packages/app-store/jitsivideo"
@ -4491,6 +4505,7 @@ __metadata:
"@radix-ui/react-collapsible": ^1.0.0
"@radix-ui/react-dialog": ^1.0.4
"@radix-ui/react-dropdown-menu": ^2.0.5
"@radix-ui/react-hover-card": ^1.0.7
"@radix-ui/react-id": ^1.0.0
"@radix-ui/react-popover": ^1.0.2
"@radix-ui/react-radio-group": ^1.0.0
@ -7768,6 +7783,13 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:13.5.4":
version: 13.5.4
resolution: "@next/env@npm:13.5.4"
checksum: 95ec7108bc88a01fed5389fb33e4b9eb34937908859d9f0aa87930c660f4395d90dafe10e54830faae5bc0a1b799be544c6455a2c8054499569d1e9296369076
languageName: node
linkType: hard
"@next/eslint-plugin-next@npm:13.2.1":
version: 13.2.1
resolution: "@next/eslint-plugin-next@npm:13.2.1"
@ -7784,6 +7806,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-darwin-arm64@npm:13.5.4"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-darwin-x64@npm:13.4.6"
@ -7791,6 +7820,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-darwin-x64@npm:13.5.4"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-arm64-gnu@npm:13.4.6"
@ -7798,6 +7834,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-linux-arm64-gnu@npm:13.5.4"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-arm64-musl@npm:13.4.6"
@ -7805,6 +7848,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-linux-arm64-musl@npm:13.5.4"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-x64-gnu@npm:13.4.6"
@ -7812,6 +7862,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-linux-x64-gnu@npm:13.5.4"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-x64-musl@npm:13.4.6"
@ -7819,6 +7876,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-linux-x64-musl@npm:13.5.4"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-arm64-msvc@npm:13.4.6"
@ -7826,6 +7890,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-win32-arm64-msvc@npm:13.5.4"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-ia32-msvc@npm:13.4.6"
@ -7833,6 +7904,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-win32-ia32-msvc@npm:13.5.4"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-x64-msvc@npm:13.4.6"
@ -7840,6 +7918,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.5.4":
version: 13.5.4
resolution: "@next/swc-win32-x64-msvc@npm:13.5.4"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0":
version: 1.1.0
resolution: "@noble/curves@npm:1.1.0"
@ -8147,17 +8232,17 @@ __metadata:
languageName: node
linkType: hard
"@prisma/client@npm:^5.0.0":
version: 5.3.1
resolution: "@prisma/client@npm:5.3.1"
"@prisma/client@npm:^4.13.0":
version: 4.16.2
resolution: "@prisma/client@npm:4.16.2"
dependencies:
"@prisma/engines-version": 5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59
"@prisma/engines-version": 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81
peerDependencies:
prisma: "*"
peerDependenciesMeta:
prisma:
optional: true
checksum: 8017b721a231ab7b2b0f932507b02bf075aae2c6f6e630a69d7089ff33bab44fa50b50dd8a81655b7202092ffc19717d484ae5f183fc4f2a1822b0d228991a7c
checksum: 38e1356644a764946c69c8691ea4bbed0ba37739d833a435625bd5435912bed4b9bdd7c384125f3a4ab8128faf566027985c0f0840a42741c338d72e40b5d565
languageName: node
linkType: hard
@ -8219,6 +8304,13 @@ __metadata:
languageName: node
linkType: hard
"@prisma/engines-version@npm:4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81":
version: 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81
resolution: "@prisma/engines-version@npm:4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81"
checksum: b42c6abe7c1928e546f15449e40ffa455701ef2ab1f62973628ecb4e19ff3652e34609a0d83196d1cbd0864adb44c55e082beec852b11929acf1c15fb57ca45a
languageName: node
linkType: hard
"@prisma/engines-version@npm:5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f":
version: 5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f
resolution: "@prisma/engines-version@npm:5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f"
@ -8226,10 +8318,10 @@ __metadata:
languageName: node
linkType: hard
"@prisma/engines-version@npm:5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59":
version: 5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59
resolution: "@prisma/engines-version@npm:5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59"
checksum: c1adf540c9330a54a000c3005a4621c5d8355f2e3b159121587d33f82bf5992f567ac453f0ce76dd48fac427ec4dbc55942228fcee10d522b0d2c03bddbe422a
"@prisma/engines@npm:4.16.2":
version: 4.16.2
resolution: "@prisma/engines@npm:4.16.2"
checksum: f423e6092c3e558cd089a68ae87459fba7fd390c433df087342b3269c3b04163965b50845150dfe47d01f811781bfff89d5ae81c95ca603c59359ab69ebd810f
languageName: node
linkType: hard
@ -8247,13 +8339,6 @@ __metadata:
languageName: node
linkType: hard
"@prisma/engines@npm:5.3.1":
version: 5.3.1
resolution: "@prisma/engines@npm:5.3.1"
checksum: a231adad60ac42569b560ea9782bc181818d8ad15e65283d1317bda5d7aa754e5b536a3f9365ce1eda8961e1eff4eca5978c456fa9764a867fe4339d123e7371
languageName: node
linkType: hard
"@prisma/extension-accelerate@npm:^0.6.2":
version: 0.6.2
resolution: "@prisma/extension-accelerate@npm:0.6.2"
@ -8952,6 +9037,34 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-hover-card@npm:^1.0.7":
version: 1.0.7
resolution: "@radix-ui/react-hover-card@npm:1.0.7"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-dismissable-layer": 1.0.5
"@radix-ui/react-popper": 1.1.3
"@radix-ui/react-portal": 1.0.4
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-controllable-state": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 812c348d8331348774b0460cd9058fdb34e0a4e167cc3ab7350d60d0ac374c673e8159573919da299f58860b8eeb9d43c21ccb679cf6db70f5db0386359871ef
languageName: node
linkType: hard
"@radix-ui/react-id@npm:0.1.5":
version: 0.1.5
resolution: "@radix-ui/react-id@npm:0.1.5"
@ -9140,6 +9253,35 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-popper@npm:1.1.3":
version: 1.1.3
resolution: "@radix-ui/react-popper@npm:1.1.3"
dependencies:
"@babel/runtime": ^7.13.10
"@floating-ui/react-dom": ^2.0.0
"@radix-ui/react-arrow": 1.0.3
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-callback-ref": 1.0.1
"@radix-ui/react-use-layout-effect": 1.0.1
"@radix-ui/react-use-rect": 1.0.1
"@radix-ui/react-use-size": 1.0.1
"@radix-ui/rect": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: b18a15958623f9222b6ed3e24b9fbcc2ba67b8df5a5272412f261de1592b3f05002af1c8b94c065830c3c74267ce00cf6c1d70d4d507ec92ba639501f98aa348
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:0.1.4":
version: 0.1.4
resolution: "@radix-ui/react-portal@npm:0.1.4"
@ -9187,6 +9329,26 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-portal@npm:1.0.4"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-primitive": 1.0.3
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: c4cf35e2f26a89703189d0eef3ceeeb706ae0832e98e558730a5e929ca7c72c7cb510413a24eca94c7732f8d659a1e81942bec7b90540cb73ce9e4885d040b64
languageName: node
linkType: hard
"@radix-ui/react-presence@npm:1.0.0":
version: 1.0.0
resolution: "@radix-ui/react-presence@npm:1.0.0"
@ -12049,6 +12211,15 @@ __metadata:
languageName: node
linkType: hard
"@swc/helpers@npm:0.5.2":
version: 0.5.2
resolution: "@swc/helpers@npm:0.5.2"
dependencies:
tslib: ^2.4.0
checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258
languageName: node
linkType: hard
"@szmarczak/http-timer@npm:^4.0.5":
version: 4.0.6
resolution: "@szmarczak/http-timer@npm:4.0.6"
@ -23897,6 +24068,13 @@ __metadata:
languageName: node
linkType: hard
"i18next-fs-backend@npm:^1.1.4":
version: 1.2.0
resolution: "i18next-fs-backend@npm:1.2.0"
checksum: da74d20f2b007f8e34eaf442fa91ad12aaff3b9891e066c6addd6d111b37e370c62370dfbc656730ab2f8afd988f2e7ea1c48301ebb19ccb716fb5965600eddf
languageName: node
linkType: hard
"i18next-fs-backend@npm:^2.1.1":
version: 2.1.3
resolution: "i18next-fs-backend@npm:2.1.3"
@ -23904,6 +24082,15 @@ __metadata:
languageName: node
linkType: hard
"i18next@npm:^21.8.13":
version: 21.10.0
resolution: "i18next@npm:21.10.0"
dependencies:
"@babel/runtime": ^7.17.2
checksum: f997985e2d4d15a62a0936a82ff6420b97f3f971e776fe685bdd50b4de0cb4dc2198bc75efe6b152844794ebd5040d8060d6d152506a687affad534834836d81
languageName: node
linkType: hard
"i18next@npm:^23.2.3":
version: 23.2.3
resolution: "i18next@npm:23.2.3"
@ -24576,7 +24763,7 @@ __metadata:
languageName: node
linkType: hard
"is-core-module@npm:^2.11.0":
"is-core-module@npm:^2.11.0, is-core-module@npm:^2.13.0":
version: 2.13.0
resolution: "is-core-module@npm:2.13.0"
dependencies:
@ -26711,6 +26898,13 @@ __metadata:
languageName: node
linkType: hard
"lilconfig@npm:^2.1.0":
version: 2.1.0
resolution: "lilconfig@npm:2.1.0"
checksum: 8549bb352b8192375fed4a74694cd61ad293904eee33f9d4866c2192865c44c4eb35d10782966242634e0cbc1e91fe62b1247f148dc5514918e3a966da7ea117
languageName: node
linkType: hard
"limiter@npm:^1.1.5":
version: 1.1.5
resolution: "limiter@npm:1.1.5"
@ -29155,6 +29349,31 @@ __metadata:
languageName: node
linkType: hard
"next-auth@npm:^4.20.1":
version: 4.23.2
resolution: "next-auth@npm:4.23.2"
dependencies:
"@babel/runtime": ^7.20.13
"@panva/hkdf": ^1.0.2
cookie: ^0.5.0
jose: ^4.11.4
oauth: ^0.9.15
openid-client: ^5.4.0
preact: ^10.6.3
preact-render-to-string: ^5.1.19
uuid: ^8.3.2
peerDependencies:
next: ^12.2.5 || ^13
nodemailer: ^6.6.5
react: ^17.0.2 || ^18
react-dom: ^17.0.2 || ^18
peerDependenciesMeta:
nodemailer:
optional: true
checksum: 4820fdc8d9f066afd2dfe64012d7aba727fd7b82fec3a94e85ea5c1651cb4bf532d8742bfd253d9910055833f00c1c8f8f17212661f7648ecff4dd1f3e002e80
languageName: node
linkType: hard
"next-auth@npm:^4.22.1":
version: 4.22.1
resolution: "next-auth@npm:4.22.1"
@ -29204,6 +29423,24 @@ __metadata:
languageName: node
linkType: hard
"next-i18next@npm:^11.3.0":
version: 11.3.0
resolution: "next-i18next@npm:11.3.0"
dependencies:
"@babel/runtime": ^7.18.6
"@types/hoist-non-react-statics": ^3.3.1
core-js: ^3
hoist-non-react-statics: ^3.3.2
i18next: ^21.8.13
i18next-fs-backend: ^1.1.4
react-i18next: ^11.18.0
peerDependencies:
next: ">= 10.0.0"
react: ">= 16.8.0"
checksum: fbce97a4fbf9ad846c08652471a833c7f173c3e7ddc7cafa1423625b4a684715bb85f76ae06fe9cbed3e70f12b8e78e2459e5bc1a3c3f5c517743f17648f8939
languageName: node
linkType: hard
"next-i18next@npm:^13.2.2":
version: 13.3.0
resolution: "next-i18next@npm:13.3.0"
@ -29285,6 +29522,61 @@ __metadata:
languageName: node
linkType: hard
"next@npm:^13.2.1":
version: 13.5.4
resolution: "next@npm:13.5.4"
dependencies:
"@next/env": 13.5.4
"@next/swc-darwin-arm64": 13.5.4
"@next/swc-darwin-x64": 13.5.4
"@next/swc-linux-arm64-gnu": 13.5.4
"@next/swc-linux-arm64-musl": 13.5.4
"@next/swc-linux-x64-gnu": 13.5.4
"@next/swc-linux-x64-musl": 13.5.4
"@next/swc-win32-arm64-msvc": 13.5.4
"@next/swc-win32-ia32-msvc": 13.5.4
"@next/swc-win32-x64-msvc": 13.5.4
"@swc/helpers": 0.5.2
busboy: 1.6.0
caniuse-lite: ^1.0.30001406
postcss: 8.4.31
styled-jsx: 5.1.1
watchpack: 2.4.0
peerDependencies:
"@opentelemetry/api": ^1.1.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
dependenciesMeta:
"@next/swc-darwin-arm64":
optional: true
"@next/swc-darwin-x64":
optional: true
"@next/swc-linux-arm64-gnu":
optional: true
"@next/swc-linux-arm64-musl":
optional: true
"@next/swc-linux-x64-gnu":
optional: true
"@next/swc-linux-x64-musl":
optional: true
"@next/swc-win32-arm64-msvc":
optional: true
"@next/swc-win32-ia32-msvc":
optional: true
"@next/swc-win32-x64-msvc":
optional: true
peerDependenciesMeta:
"@opentelemetry/api":
optional: true
sass:
optional: true
bin:
next: dist/bin/next
checksum: f8e964ee9bbabd0303f9d807c9193833fcc47960be029c3721db9a5a35cc4ff690313e30fc6ee497f959a9141048957dddf6eb038b4a23c78c8762b0cd9d0ae0
languageName: node
linkType: hard
"next@npm:^13.4.6":
version: 13.4.6
resolution: "next@npm:13.4.6"
@ -31318,6 +31610,19 @@ __metadata:
languageName: node
linkType: hard
"postcss-import@npm:^15.1.0":
version: 15.1.0
resolution: "postcss-import@npm:15.1.0"
dependencies:
postcss-value-parser: ^4.0.0
read-cache: ^1.0.0
resolve: ^1.1.7
peerDependencies:
postcss: ^8.0.0
checksum: 7bd04bd8f0235429009d0022cbf00faebc885de1d017f6d12ccb1b021265882efc9302006ba700af6cab24c46bfa2f3bc590be3f9aee89d064944f171b04e2a3
languageName: node
linkType: hard
"postcss-js@npm:^4.0.0":
version: 4.0.0
resolution: "postcss-js@npm:4.0.0"
@ -31329,6 +31634,17 @@ __metadata:
languageName: node
linkType: hard
"postcss-js@npm:^4.0.1":
version: 4.0.1
resolution: "postcss-js@npm:4.0.1"
dependencies:
camelcase-css: ^2.0.1
peerDependencies:
postcss: ^8.4.21
checksum: 5c1e83efeabeb5a42676193f4357aa9c88f4dc1b3c4a0332c132fe88932b33ea58848186db117cf473049fc233a980356f67db490bd0a7832ccba9d0b3fd3491
languageName: node
linkType: hard
"postcss-load-config@npm:^3.1.4":
version: 3.1.4
resolution: "postcss-load-config@npm:3.1.4"
@ -31347,6 +31663,24 @@ __metadata:
languageName: node
linkType: hard
"postcss-load-config@npm:^4.0.1":
version: 4.0.1
resolution: "postcss-load-config@npm:4.0.1"
dependencies:
lilconfig: ^2.0.5
yaml: ^2.1.1
peerDependencies:
postcss: ">=8.0.9"
ts-node: ">=9.0.0"
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
checksum: b61f890499ed7dcda1e36c20a9582b17d745bad5e2b2c7bc96942465e406bc43ae03f270c08e60d1e29dab1ee50cb26970b5eb20c9aae30e066e20bd607ae4e4
languageName: node
linkType: hard
"postcss-loader@npm:^4.2.0":
version: 4.3.0
resolution: "postcss-loader@npm:4.3.0"
@ -31473,6 +31807,17 @@ __metadata:
languageName: node
linkType: hard
"postcss-nested@npm:^6.0.1":
version: 6.0.1
resolution: "postcss-nested@npm:6.0.1"
dependencies:
postcss-selector-parser: ^6.0.11
peerDependencies:
postcss: ^8.2.14
checksum: 7ddb0364cd797de01e38f644879189e0caeb7ea3f78628c933d91cc24f327c56d31269384454fc02ecaf503b44bfa8e08870a7c4cc56b23bc15640e1894523fa
languageName: node
linkType: hard
"postcss-pseudo-companion-classes@npm:^0.1.1":
version: 0.1.1
resolution: "postcss-pseudo-companion-classes@npm:0.1.1"
@ -31527,6 +31872,17 @@ __metadata:
languageName: node
linkType: hard
"postcss@npm:8.4.31":
version: 8.4.31
resolution: "postcss@npm:8.4.31"
dependencies:
nanoid: ^3.3.6
picocolors: ^1.0.0
source-map-js: ^1.0.2
checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea
languageName: node
linkType: hard
"postcss@npm:^7.0.14, postcss@npm:^7.0.26, postcss@npm:^7.0.32, postcss@npm:^7.0.36, postcss@npm:^7.0.5, postcss@npm:^7.0.6":
version: 7.0.39
resolution: "postcss@npm:7.0.39"
@ -31856,14 +32212,15 @@ __metadata:
languageName: node
linkType: hard
"prisma@npm:^5.0.0":
version: 5.3.1
resolution: "prisma@npm:5.3.1"
"prisma@npm:^4.13.0":
version: 4.16.2
resolution: "prisma@npm:4.16.2"
dependencies:
"@prisma/engines": 5.3.1
"@prisma/engines": 4.16.2
bin:
prisma: build/index.js
checksum: e825adbcb4eec81de276de5507fb7e5486db7788c8c9de36ba6ed73f9e87d9f56b64d0e183a31dc6b80f6737ae1fbcdb110aac44ab89299af646aeb966655bef
prisma2: build/index.js
checksum: 1d0ed616abd7f8de22441e333b976705f1cb05abcb206965df3fc6a7ea03911ef467dd484a4bc51fdc6cff72dd9857b9852be5f232967a444af0a98c49bfdb76
languageName: node
linkType: hard
@ -32764,6 +33121,24 @@ __metadata:
languageName: node
linkType: hard
"react-i18next@npm:^11.18.0":
version: 11.18.6
resolution: "react-i18next@npm:11.18.6"
dependencies:
"@babel/runtime": ^7.14.5
html-parse-stringify: ^3.0.1
peerDependencies:
i18next: ">= 19.0.0"
react: ">= 16.8.0"
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 624c0a0313fac4e0d18560b83c99a8bd0a83abc02e5db8d01984e0643ac409d178668aa3a4720d01f7a0d9520d38598dcbff801d6f69a970bae67461de6cd852
languageName: node
linkType: hard
"react-i18next@npm:^12.2.0":
version: 12.3.1
resolution: "react-i18next@npm:12.3.1"
@ -32887,7 +33262,7 @@ __metadata:
languageName: node
linkType: hard
"react-live-chat-loader@npm:^2.8.1":
"react-live-chat-loader@npm:^2.7.3, react-live-chat-loader@npm:^2.8.1":
version: 2.8.1
resolution: "react-live-chat-loader@npm:2.8.1"
peerDependencies:
@ -34139,6 +34514,19 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:^1.22.2":
version: 1.22.6
resolution: "resolve@npm:1.22.6"
dependencies:
is-core-module: ^2.13.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
checksum: d13bf66d4e2ee30d291491f16f2fa44edd4e0cefb85d53249dd6f93e70b2b8c20ec62f01b18662e3cd40e50a7528f18c4087a99490048992a3bb954cf3201a5b
languageName: node
linkType: hard
"resolve@npm:^2.0.0-next.3":
version: 2.0.0-next.3
resolution: "resolve@npm:2.0.0-next.3"
@ -34188,6 +34576,19 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@^1.22.2#~builtin<compat/resolve>":
version: 1.22.6
resolution: "resolve@patch:resolve@npm%3A1.22.6#~builtin<compat/resolve>::version=1.22.6&hash=c3c19d"
dependencies:
is-core-module: ^2.13.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
checksum: 9d3b3c67aefd12cecbe5f10ca4d1f51ea190891096497c43f301b086883b426466918c3a64f1bbf1788fabb52b579d58809614006c5d0b49186702b3b8fb746a
languageName: node
linkType: hard
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>":
version: 2.0.0-next.3
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.3#~builtin<compat/resolve>::version=2.0.0-next.3&hash=c3c19d"
@ -36319,6 +36720,24 @@ __metadata:
languageName: node
linkType: hard
"sucrase@npm:^3.32.0":
version: 3.34.0
resolution: "sucrase@npm:3.34.0"
dependencies:
"@jridgewell/gen-mapping": ^0.3.2
commander: ^4.0.0
glob: 7.1.6
lines-and-columns: ^1.1.6
mz: ^2.7.0
pirates: ^4.0.1
ts-interface-checker: ^0.1.9
bin:
sucrase: bin/sucrase
sucrase-node: bin/sucrase-node
checksum: 61860063bdf6103413698e13247a3074d25843e91170825a9752e4af7668ffadd331b6e99e92fc32ee5b3c484ee134936f926fa9039d5711fafff29d017a2110
languageName: node
linkType: hard
"superagent@npm:^5.1.1":
version: 5.3.1
resolution: "superagent@npm:5.3.1"
@ -36680,6 +37099,39 @@ __metadata:
languageName: node
linkType: hard
"tailwindcss@npm:^3.2.1":
version: 3.3.3
resolution: "tailwindcss@npm:3.3.3"
dependencies:
"@alloc/quick-lru": ^5.2.0
arg: ^5.0.2
chokidar: ^3.5.3
didyoumean: ^1.2.2
dlv: ^1.1.3
fast-glob: ^3.2.12
glob-parent: ^6.0.2
is-glob: ^4.0.3
jiti: ^1.18.2
lilconfig: ^2.1.0
micromatch: ^4.0.5
normalize-path: ^3.0.0
object-hash: ^3.0.0
picocolors: ^1.0.0
postcss: ^8.4.23
postcss-import: ^15.1.0
postcss-js: ^4.0.1
postcss-load-config: ^4.0.1
postcss-nested: ^6.0.1
postcss-selector-parser: ^6.0.11
resolve: ^1.22.2
sucrase: ^3.32.0
bin:
tailwind: lib/cli.js
tailwindcss: lib/cli.js
checksum: 0195c7a3ebb0de5e391d2a883d777c78a4749f0c532d204ee8aea9129f2ed8e701d8c0c276aa5f7338d07176a3c2a7682c1d0ab9c8a6c2abe6d9325c2954eb50
languageName: node
linkType: hard
"tailwindcss@npm:^3.3.1":
version: 3.3.1
resolution: "tailwindcss@npm:3.3.1"
@ -40575,6 +41027,13 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.1.1, yaml@npm:^2.3.1":
version: 2.3.2
resolution: "yaml@npm:2.3.2"
checksum: acd80cc24df12c808c6dec8a0176d404ef9e6f08ad8786f746ecc9d8974968c53c6e8a67fdfabcc5f99f3dc59b6bb0994b95646ff03d18e9b1dcd59eccc02146
languageName: node
linkType: hard
"yaml@npm:^2.2.1":
version: 2.3.1
resolution: "yaml@npm:2.3.1"
@ -40582,13 +41041,6 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.3.1":
version: 2.3.2
resolution: "yaml@npm:2.3.2"
checksum: acd80cc24df12c808c6dec8a0176d404ef9e6f08ad8786f746ecc9d8974968c53c6e8a67fdfabcc5f99f3dc59b6bb0994b95646ff03d18e9b1dcd59eccc02146
languageName: node
linkType: hard
"yargs-parser@npm:^18.1.2, yargs-parser@npm:^18.1.3":
version: 18.1.3
resolution: "yargs-parser@npm:18.1.3"
@ -40822,6 +41274,13 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.20.2":
version: 3.22.4
resolution: "zod@npm:3.22.4"
checksum: 80bfd7f8039b24fddeb0718a2ec7c02aa9856e4838d6aa4864335a047b6b37a3273b191ef335bf0b2002e5c514ef261ffcda5a589fb084a48c336ffc4cdbab7f
languageName: node
linkType: hard
"zod@npm:^3.21.4, zod@npm:^3.22.2":
version: 3.22.2
resolution: "zod@npm:3.22.2"