Compare commits

..

15 Commits

Author SHA1 Message Date
Syed Ali Shahbaz a7749288ee
Merge branch 'main' into feat/v2-managed-events 2024-01-12 18:26:01 +04:00
alishaz-polymath 83dfa0248b early review fixes ... 2024-01-12 18:03:59 +04:00
alishaz-polymath 2b279356d9 prettier fix 2024-01-11 15:29:49 +04:00
alishaz-polymath 922053a4c4 fix recurring 2024-01-11 15:15:16 +04:00
alishaz-polymath d3e1d12c8c lock private url for managed type to stop confusion 2024-01-11 14:21:56 +04:00
alishaz-polymath afd0d2e0cd lock hashed link until further notice 2024-01-11 14:12:54 +04:00
alishaz-polymath bcac078cee fix bugs 2024-01-10 18:00:09 +04:00
Syed Ali Shahbaz 409658a43d
Merge branch 'main' into feat/v2-managed-events 2024-01-10 16:44:14 +04:00
alishaz-polymath 6ebf167d4b WIP 2024-01-10 16:38:59 +04:00
Syed Ali Shahbaz b2c6122ed7
Merge branch 'main' into feat/v2-managed-events 2024-01-02 12:43:29 +04:00
alishaz-polymath 48d18a3925 wip 2024-01-02 12:39:25 +04:00
alishaz-polymath 1821c7aa3a Fix event limit interlinked switches 2023-12-15 22:37:47 +04:00
Syed Ali Shahbaz 3b6f0d3a53
Merge branch 'main' into feat/v2-managed-events 2023-12-13 17:06:36 +04:00
Syed Ali Shahbaz bc65859444
Merge branch 'main' into feat/v2-managed-events 2023-12-07 17:27:53 +04:00
alishaz-polymath dc6fcaae76 init 2023-11-10 17:29:28 +04:00
118 changed files with 683 additions and 1743 deletions

View File

@ -1,37 +0,0 @@
## PRIVACY POLICY
Last updated January 28, 2024
### Introduction
This is the privacy notice of MaroCalendar, a personal calendar booking service that is only used by me, Gustavo Maronato, to manage my personal and work calendars, and allow you to book events with me. Here, you'll find a description of how and why I might collect, store, and use your information when you book an event with me using this service.
The service is located at [https://cal.maronato.dev](https://cal.maronato.dev).
### Questions or concerns?
Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with my policies and practices, please do not access or book an event with me using this service.
## SUMMARY OF KEY POINTS
### What personal information do I process?
When you choose to book an event with me, you provide me with your name and email address.
### Do I process any sensitive personal information?
I do not process sensitive personal information.
### Do I receive any information from third parties?
I do not receive any information from third parties.
### How do I process your information?
When you book an event with me, I use your name and email address to send you an email with a calendar invite to the event you booked.
### In what situations and with which parties do I share personal information?
The information you submit is used to create a booking between myself and you. I do not share your information with any third parties.
### How do I keep your information safe?
I use reasonable and appropriate security measures to protect your personal information from loss, misuse, and unauthorized access, disclosure, alteration, and destruction.
### What are your rights?
You can cancel your booking at any time by clicking the link in the confirmation email you received when you booked the event.
### Google Calendar
I use a Google Calendar oAuth integration to automatically display to you what are my free time slots and manage the events you book on my calendar. You do not interact with this integration and you are not allowed to use your Google account to add this integration to your Google Calendar. This integration is only used by me to manage my calendar and is not shared with anyone else.

View File

@ -1,117 +0,0 @@
Terms of Service
----------------
Effective date: 01/28/2024
Introduction
------------
These are the terms of service for my personal calendar booking service, MaroCalendar. You may use this service to book events with me by providing your name, email address, and date/time preferences.
These Terms of Service (“Terms”, “Terms of Service”) govern your use of this service located at https://cal.maronato.dev operated by Gustavo Maronato.
You can also find it's privacy policy here https://git.maronato.dev/maronato/cal/src/branch/main/PRIVACY.md
And the source code here https://git.maronato.dev/maronato/cal
If you do not agree with (or cannot comply with) these terms, then you may not use the Service.
Thank you for being responsible.
Communications
--------------
By using this service to book an event with me, you agree to receive an email with the calendar invite. You may also receive a reminder email before the event, or a confirmation email if you reschedule or cancel the event.
Purchases
---------
There are no purchases on this service. You may use it to book events with me, but you will not be charged for it.
Contests, Sweepstakes and Promotions
------------------------------------
There are no contests, sweepstakes, or promotions on this service.
Subscriptions
-------------
There is no subscription on this service.
Fee Changes
-----------
There are no fees on this service.
Refunds
-------
This is a free service, so there are no refunds.
Content
-------
Our Service allows you to create an event with me by providing your name and email address. You are responsible for that information that you submit on or through Service, including its legality, reliability, and appropriateness.
By posting Content on or through Service, You represent and warrant that: (i) Content is yours (you own it) and/or you have the right to use it, and (ii) that the posting of your Content on or through Service does not violate the privacy rights, publicity rights, copyrights, contract rights or any other rights of any person or entity. I reserve the right to not meet with you.
Prohibited Uses
---------------
You may use Service only for lawful purposes and in accordance with Terms. You agree not to use Service:
* In any way that violates any applicable national or international law or regulation.
* For the purpose of exploiting, harming, or attempting to exploit or harm minors in any way by exposing them to inappropriate content or otherwise.
* To transmit, or procure the sending of, any advertising or promotional material, including any “junk mail”, “chain letter,” “spam,” or any other similar solicitation.
* To impersonate or attempt to impersonate Company, a Company employee, another user, or any other person or entity.
* In any way that infringes upon the rights of others, or in any way is illegal, threatening, fraudulent, or harmful, or in connection with any unlawful, illegal, fraudulent, or harmful purpose or activity.
* To engage in any other conduct that restricts or inhibits anyones use or enjoyment of Service, or which, as determined by us, may harm or offend Company or users of Service or expose them to liability.
Additionally, you agree not to:
* Use Service in any manner that could disable, overburden, damage, or impair Service or interfere with any other partys use of Service, including their ability to engage in real time activities through Service.
* Use any robot, spider, or other automatic device, process, or means to access Service for any purpose, including monitoring or copying any of the material on Service.
* Use any manual process to monitor or copy any of the material on Service or for any other unauthorized purpose without our prior written consent.
* Use any device, software, or routine that interferes with the proper working of Service.
* Introduce any viruses, trojan horses, worms, logic bombs, or other material which is malicious or technologically harmful.
* Attempt to gain unauthorized access to, interfere with, damage, or disrupt any parts of Service, the server on which Service is stored, or any server, computer, or database connected to Service.
* Attack Service via a denial-of-service attack or a distributed denial-of-service attack.
* Take any action that may damage or falsify Company rating.
* Otherwise attempt to interfere with the proper working of Service.
Analytics
---------
There is no analytics on this service.
No Use By Minors
----------------
Service is intended only for access and use by individuals at least eighteen (18) years old. By accessing or using any of Company, you warrant and represent that you are at least eighteen (18) years of age and with the full authority, right, and capacity to enter into this agreement and abide by all of the terms and conditions of Terms. If you are not at least eighteen (18) years old, you are prohibited from both the access and usage of Service.
Accounts
--------
You cannot create an account on this service. The only account that exists is mine.
Changes To Service
------------------
I reserve the right to withdraw or amend this Service, and any service or material I provide via Service, in my sole discretion without notice. I will not be liable if for any reason all or any part of Service is unavailable at any time or for any period. From time to time, I may restrict access to some parts of Service, or the entire Service, to visitors.
Amendments To Terms
-------------------
I may amend Terms at any time by posting the amended terms on this site. It is your responsibility to review these Terms periodically.
Acknowledgement
---------------
BY USING SERVICE OR OTHER SERVICES PROVIDED BY ME, YOU ACKNOWLEDGE THAT YOU HAVE READ THESE TERMS OF SERVICE AND AGREE TO BE BOUND BY THEM.
Contact Me
----------
If you have any questions about these terms of service, please contact me:
By email: support@maronato.dev

View File

@ -74,7 +74,7 @@ export async function patchHandler(req: NextApiRequest) {
},
},
});
if (slugAlreadyExists && data.slug !== _team.slug)
throw new HttpError({ statusCode: 409, message: "Team slug already exists" });

View File

@ -2,20 +2,16 @@ import type { GetServerSideProps, GetServerSidePropsContext } from "next";
import { notFound, redirect } from "next/navigation";
export const withAppDir =
<T extends Record<string, any>>(getServerSideProps: GetServerSideProps<T>) =>
async (context: GetServerSidePropsContext): Promise<T> => {
(getServerSideProps: GetServerSideProps) => async (context: GetServerSidePropsContext) => {
const ssrResponse = await getServerSideProps(context);
if ("redirect" in ssrResponse) {
redirect(ssrResponse.redirect.destination);
}
if ("notFound" in ssrResponse) {
notFound();
}
return {
...ssrResponse.props,
// includes dehydratedState required for future page trpcPropvider
...("trpcState" in ssrResponse.props && { dehydratedState: ssrResponse.props.trpcState }),
};
return ssrResponse.props;
};

View File

@ -1,12 +1,13 @@
import LegacyPage from "@pages/apps/categories/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () => {
@ -16,9 +17,11 @@ export const generateMetadata = async () => {
);
};
const getData = async (ctx: GetServerSidePropsContext) => {
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(ctx);
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | IncomingMessage
const session = await getServerSession({ req: ctx.req });
let appStore;

View File

@ -1,7 +1,6 @@
import AppsPage from "@pages/apps";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
@ -11,6 +10,8 @@ import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import { APP_NAME } from "@calcom/lib/constants";
import type { AppCategories } from "@calcom/prisma/enums";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () => {
@ -20,9 +21,11 @@ export const generateMetadata = async () => {
);
};
const getData = async (ctx: GetServerSidePropsContext) => {
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(ctx);
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
const session = await getServerSession({ req: ctx.req });
let appStore, userAdminTeams: UserAdminTeams;

View File

@ -1,12 +1,13 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { notFound } from "next/navigation";
import { z } from "zod";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { APP_NAME } from "@calcom/lib/constants";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssgInit } from "@server/lib/ssg";
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
@ -25,7 +26,7 @@ export const generateStaticParams = async () => {
return validStatuses.map((status) => ({ status }));
};
const getData = async (ctx: GetServerSidePropsContext) => {
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
const parsedParams = querySchema.safeParse(ctx.params);
if (!parsedParams.success) {

View File

@ -1,132 +0,0 @@
import LegacyPage, { type PageProps } from "@pages/d/[link]/[slug]";
import { withAppDir } from "app/AppDirSSRHOC";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
const pageProps = await getPageProps(
buildLegacyCtx(headers(), cookies(), params) as unknown as GetServerSidePropsContext
);
const { entity, booking, user, slug, isTeamEvent } = pageProps;
const rescheduleUid = booking?.uid;
const { trpc } = await import("@calcom/trpc");
const { data: event } = trpc.viewer.public.event.useQuery(
{ username: user ?? "", eventSlug: slug ?? "", isTeamEvent, org: entity.orgSlug ?? null },
{ refetchOnWindowFocus: false }
);
const profileName = event?.profile?.name ?? "";
const title = event?.title ?? "";
return await _generateMetadata(
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
(t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
);
};
async function getPageProps(context: GetServerSidePropsContext) {
const session = await getServerSession({ req: context.req });
const { link, slug } = paramsSchema.parse(context.params);
const { rescheduleUid, duration: queryDuration } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
const org = isValidOrgDomain ? currentOrgDomain : null;
const hashedLink = await prisma.hashedLink.findUnique({
where: {
link,
},
select: {
eventTypeId: true,
eventType: {
select: {
users: {
select: {
username: true,
},
},
team: {
select: {
id: true,
},
},
},
},
},
});
const username = hashedLink?.eventType.users[0]?.username;
if (!hashedLink || !username) {
return notFound();
}
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
if (!user) {
return notFound();
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
}
const isTeamEvent = !!hashedLink.eventType?.team?.id;
const ssr = await ssrInit(context);
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent, org });
if (!eventData) {
return notFound();
}
return {
entity: eventData.entity,
duration: getMultipleDurationValue(eventData.metadata?.multipleDuration, queryDuration, eventData.length),
booking,
away: user?.away,
user: username,
slug,
dehydratedState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
// Sending the team event from the server, because this template file
// is reused for both team and user events.
isTeamEvent,
hashedLink: link,
};
}
const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
// @ts-expect-error arg
const getData = withAppDir<PageProps>(getPageProps);
export default WithLayout({ getLayout: null, Page: LegacyPage, getData })<"P">;

View File

@ -1,19 +1,25 @@
import LegacyPage from "@pages/getting-started/[[...step]]";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
const getData = async (ctx: GetServerSidePropsContext) => {
const session = await getServerSession({ req: ctx.req });
const getData = async (ctx: ReturnType<typeof buildLegacyCtx>) => {
const req = { headers: headers(), cookies: cookies() };
//@ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest
const session = await getServerSession({ req });
if (!session?.user?.id) {
return redirect("/auth/login");
}
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(ctx);
await ssr.viewer.me.prefetch();
@ -48,7 +54,7 @@ const getData = async (ctx: GetServerSidePropsContext) => {
return {
dehydratedState: ssr.dehydrate(),
hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
hasPendingInvites: user.teams.find((team: any) => team.accepted === false) ?? false,
requiresLicense: false,
themeBasis: null,
};

View File

@ -1,167 +0,0 @@
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { redirect, notFound } from "next/navigation";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import PaymentPage from "@calcom/features/ee/payments/components/PaymentPage";
import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment";
import { APP_NAME } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
await _generateMetadata(
// the title does not contain the eventName as in the legacy page
(t) => `${t("payment")} | ${APP_NAME}`,
() => ""
);
const querySchema = z.object({
uid: z.string(),
});
async function getData(context: GetServerSidePropsContext) {
const session = await getServerSession({ req: context.req });
if (!session?.user?.id) {
return redirect("/auth/login");
}
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const { uid } = querySchema.parse(context.params);
const rawPayment = await prisma.payment.findFirst({
where: {
uid,
},
select: {
data: true,
success: true,
uid: true,
refunded: true,
bookingId: true,
appId: true,
amount: true,
currency: true,
paymentOption: true,
booking: {
select: {
id: true,
uid: true,
description: true,
title: true,
startTime: true,
endTime: true,
attendees: {
select: {
email: true,
name: true,
},
},
eventTypeId: true,
location: true,
status: true,
rejectionReason: true,
cancellationReason: true,
eventType: {
select: {
id: true,
title: true,
description: true,
length: true,
eventName: true,
requiresConfirmation: true,
userId: true,
metadata: true,
users: {
select: {
name: true,
username: true,
hideBranding: true,
theme: true,
},
},
team: {
select: {
name: true,
hideBranding: true,
},
},
price: true,
currency: true,
successRedirectUrl: true,
},
},
},
},
},
});
if (!rawPayment) {
return notFound();
}
const { data, booking: _booking, ...restPayment } = rawPayment;
const payment = {
...restPayment,
data: data as Record<string, unknown>,
};
if (!_booking) {
return notFound();
}
const { startTime, endTime, eventType, ...restBooking } = _booking;
const booking = {
...restBooking,
startTime: startTime.toString(),
endTime: endTime.toString(),
};
if (!eventType) {
return notFound();
}
if (eventType.users.length === 0 && !!!eventType.team) {
return notFound();
}
const [user] = eventType?.users.length
? eventType.users
: [{ name: null, theme: null, hideBranding: null, username: null }];
const profile = {
name: eventType.team?.name || user?.name || null,
theme: (!eventType.team?.name && user?.theme) || null,
hideBranding: eventType.team?.hideBranding || user?.hideBranding || null,
};
if (
([BookingStatus.CANCELLED, BookingStatus.REJECTED] as BookingStatus[]).includes(
booking.status as BookingStatus
)
) {
return redirect(`/booking/${booking.uid}`);
}
return {
user,
eventType: {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
},
booking,
dehydratedState: ssr.dehydrate(),
payment,
clientSecret: getClientSecretFromPayment(payment),
profile,
};
}
export default WithLayout({ getLayout: null, getData, Page: PaymentPage });

View File

@ -1,11 +0,0 @@
import LegacyPage, { WrappedAboutOrganizationPage } from "@pages/settings/organizations/[id]/about";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("about_your_organization"),
(t) => t("about_your_organization_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: WrappedAboutOrganizationPage });

View File

@ -1,11 +0,0 @@
import LegacyPage, { WrapperAddNewTeamsPage } from "@pages/settings/organizations/[id]/add-teams";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("create_your_teams"),
(t) => t("create_your_teams_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: WrapperAddNewTeamsPage });

View File

@ -1,35 +0,0 @@
import LegacyPage, {
buildWrappedOnboardTeamMembersPage,
} from "@pages/settings/organizations/[id]/onboard-admins";
import { type Params } from "app/_types";
import { _generateMetadata } from "app/_utils";
import { headers } from "next/headers";
import PageWrapper from "@components/PageWrapperAppDir";
type PageProps = Readonly<{
params: Params;
}>;
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("invite_organization_admins"),
(t) => t("invite_organization_admins_description")
);
const Page = ({ params }: PageProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper
getLayout={(page: React.ReactElement) => buildWrappedOnboardTeamMembersPage(params.id, page)}
requiresLicense={false}
nonce={nonce}
themeBasis={null}>
<LegacyPage />
</PageWrapper>
);
};
export default Page;

View File

@ -1,11 +0,0 @@
import LegacyPage, { WrappedSetPasswordPage } from "@pages/settings/organizations/[id]/set-password";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("set_a_password"),
(t) => t("set_a_password_description")
);
export default WithLayout({ Page: LegacyPage, getLayout: WrappedSetPasswordPage });

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/appearance";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("appearance"),
(t) => t("appearance_org_description")
);
export default Page;

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,10 +0,0 @@
import Page from "@pages/settings/billing/index";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("billing"),
(t) => t("manage_billing_description")
);
export default Page;

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/general";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("general"),
(t) => t("general_description")
);
export default Page;

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/members";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("organization_members"),
(t) => t("organization_description")
);
export default Page;

View File

@ -1,34 +0,0 @@
import LegacyPage, { WrappedCreateNewOrganizationPage } from "@pages/settings/organizations/new/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import { notFound } from "next/navigation";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("set_up_your_organization"),
(t) => t("organizations_description")
);
const getPageProps = async (context: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
// Check if organizations are enabled
if (flags["organizations"] !== true) {
return notFound();
}
const querySlug = context.query.slug as string;
return {
querySlug: querySlug ?? null,
};
};
export default WithLayout({
getLayout: WrappedCreateNewOrganizationPage,
Page: LegacyPage,
getData: getPageProps,
});

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/profile";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("profile"),
(t) => t("profile_org_description")
);
export default Page;

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/teams/pages/team-appearance-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("booking_appearance"),
(t) => t("appearance_team_description")
);
export default Page;

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("team_members"),
(t) => t("members_team_description")
);
export default Page;

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("profile"),
(t) => t("profile_team_description")
);
export default Page;

View File

@ -1,5 +0,0 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -1,11 +0,0 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/organizations/pages/settings/other-team-listing-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("org_admin_other_teams"),
(t) => t("org_admin_other_teams_description")
);
export default Page;

View File

@ -1,12 +1,13 @@
import OldPage from "@pages/teams/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
@ -15,12 +16,14 @@ export const generateMetadata = async () =>
(t) => t("create_manage_teams_collaborative")
);
async function getData(context: GetServerSidePropsContext) {
async function getData(context: ReturnType<typeof buildLegacyCtx>) {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const session = await getServerSession({
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | (IncomingMessage & { cookies: Partial<{ [key: string]: string; }>; })'.
req: context.req,
});

View File

@ -2,13 +2,14 @@ import OldPage from "@pages/video/[uid]";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import MarkdownIt from "markdown-it";
import type { GetServerSidePropsContext } from "next";
import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { APP_NAME } from "@calcom/lib/constants";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
export const generateMetadata = async () =>
@ -19,7 +20,8 @@ export const generateMetadata = async () =>
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
async function getData(context: GetServerSidePropsContext) {
async function getData(context: ReturnType<typeof buildLegacyCtx>) {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
const booking = await prisma.booking.findUnique({
@ -77,11 +79,12 @@ async function getData(context: GetServerSidePropsContext) {
endTime: booking.endTime.toString(),
});
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | (IncomingMessage & { cookies: Partial<{ [key: string]: string; }>; })'.
const session = await getServerSession({ req: context.req });
// set meetingPassword to null for guests
if (session?.user.id !== bookingObj.user?.id) {
bookingObj.references.forEach((bookRef) => {
bookingObj.references.forEach((bookRef: any) => {
bookRef.meetingPassword = null;
});
}

View File

@ -49,4 +49,5 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
};
}
// @ts-expect-error getData arg
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -47,4 +47,5 @@ async function getData(context: Omit<GetServerSidePropsContext, "res" | "resolve
};
}
// @ts-expect-error getData arg
export default WithLayout({ getData, Page: OldPage, getLayout: null })<"P">;

View File

@ -1,7 +1,8 @@
import LegacyPage from "@pages/video/no-meeting-found";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { type GetServerSidePropsContext } from "next";
import type { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
@ -11,7 +12,8 @@ export const generateMetadata = async () =>
(t) => t("no_meeting_found")
);
const getData = async (context: GetServerSidePropsContext) => {
const getData = async (context: ReturnType<typeof buildLegacyCtx>) => {
// @ts-expect-error Argument of type '{ query: Params; params: Params; req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
const ssr = await ssrInit(context);
return {

View File

@ -35,6 +35,7 @@ async function getProps(context: GetServerSidePropsContext) {
export const generateStaticParams = () => [];
// @ts-expect-error getData arg
export default WithLayout({ getLayout: null, getData: getProps, Page: LegacyPage })<"P">;
export const dynamic = "force-static";
// generate segments on demand

View File

@ -1,5 +1,4 @@
import type { LayoutProps, PageProps } from "app/_types";
import { type GetServerSidePropsContext } from "next";
import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
@ -9,16 +8,14 @@ import PageWrapper from "@components/PageWrapperAppDir";
type WithLayoutParams<T extends Record<string, any>> = {
getLayout: ((page: React.ReactElement) => React.ReactNode) | null;
Page?: (props: T) => React.ReactElement | null;
getData?: (arg: GetServerSidePropsContext) => Promise<T>;
getData?: (arg: ReturnType<typeof buildLegacyCtx>) => Promise<T>;
};
export function WithLayout<T extends Record<string, any>>({ getLayout, getData, Page }: WithLayoutParams<T>) {
return async <P extends "P" | "L">(p: P extends "P" ? PageProps : LayoutProps) => {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
const props = getData
? await getData(buildLegacyCtx(h, cookies(), p.params) as unknown as GetServerSidePropsContext)
: ({} as T);
const props = getData ? await getData(buildLegacyCtx(h, cookies(), p.params)) : ({} as T);
const children = "children" in p ? p.children : null;

View File

@ -1,17 +0,0 @@
import NotFoundPage from "@pages/404";
import { WithLayout } from "app/layoutHOC";
import type { GetStaticPropsContext } from "next";
import { ssgInit } from "@server/lib/ssg";
const getData = async (context: GetStaticPropsContext) => {
const ssg = await ssgInit(context);
return {
dehydratedState: ssg.dehydrate(),
};
};
export const dynamic = "force-static";
export default WithLayout({ getLayout: null, getData, Page: NotFoundPage });

View File

@ -1,6 +1,6 @@
import dynamic from "next/dynamic";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid";
@ -16,6 +16,7 @@ import {
allowDisablingAttendeeConfirmationEmails,
allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import type { EditableSchema } from "@calcom/features/form-builder/schema";
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
@ -107,11 +108,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
);
};
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const { isChildrenManagedEventType, isManagedEventType, shouldLockDisableProps, shouldLockIndicator } =
useLockedFieldsManager(eventType, formMethods, t);
const eventNamePlaceholder = getEventName({
...eventNameObject,
eventName: formMethods.watch("eventName"),
@ -119,9 +117,15 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
const requiresBookerEmailVerificationProps = shouldLockDisableProps("requiresBookerEmailVerification");
const hideCalendarNotesLocked = shouldLockDisableProps("hideCalendarNotes");
const lockTimeZoneToggleOnBookingPageLocked = shouldLockDisableProps("lockTimeZoneToggleOnBookingPage");
const closeEventNameTip = () => setShowEventNameTip(false);
// For the field 'eventName'
// const EventNameLabel = useLockedLabel("eventName");
// const EventNameSwitch = useLockedSwitch("eventName")();
const setEventName = (value: string) => formMethods.setValue("eventName", value);
return (
<div className="flex flex-col space-y-4">
@ -165,6 +169,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<TextField
label={t("event_name_in_calendar")}
type="text"
isDisabled={shouldLockDisableProps("eventName").disabled}
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
@ -173,6 +178,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Button
color="minimal"
size="sm"
{...(shouldLockDisableProps("eventName").disabled ? { disabled: true } : {})}
aria-label="edit custom name"
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
onClick={() => setShowEventNameTip((old) => !old)}>
@ -182,9 +188,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>
</div>
</div>
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
<div className="border-subtle space-y-6 rounded-lg border p-6">
<FormBuilder
title={t("booking_questions_title")}
@ -199,7 +203,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}
/>
</div>
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
@ -207,7 +210,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation}
/>
<Controller
name="requiresBookerEmailVerification"
control={formMethods.control}
@ -218,14 +220,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("requires_booker_email_verification")}
{...shouldLockDisableProps("requiresBookerEmailVerification")}
{...requiresBookerEmailVerificationProps}
description={t("description_requires_booker_email_verification")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
<Controller
name="hideCalendarNotes"
control={formMethods.control}
@ -236,14 +237,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_notes")}
{...shouldLockDisableProps("hideCalendarNotes")}
{...hideCalendarNotesLocked}
description={t("disable_notes_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
<Controller
name="successRedirectUrl"
control={formMethods.control}
@ -289,7 +289,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</>
)}
/>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
@ -308,7 +307,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Info className="ml-1.5 h-4 w-4 cursor-pointer" />
</a>
}
{...shouldLockDisableProps("hashedLinkCheck")}
{...(isManagedEventType || isChildrenManagedEventType ? { disabled: true } : {})}
{...(isChildrenManagedEventType ? { LockedIcon: shouldLockIndicator("hashedLink", false) } : {})}
description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible}
onCheckedChange={(e) => {
@ -350,7 +350,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)}
</div>
</SettingsToggle>
<Controller
name="seatsPerTimeSlotEnabled"
control={formMethods.control}
@ -444,7 +443,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("lock_timezone_toggle_on_booking_page")}
{...shouldLockDisableProps("lockTimeZoneToggleOnBookingPage")}
{...lockTimeZoneToggleOnBookingPageLocked}
description={t("description_lock_timezone_toggle_on_booking_page")}
checked={value}
onCheckedChange={(e) => onChange(e)}

View File

@ -1,6 +1,6 @@
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useFormContext } from "react-hook-form";
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
@ -8,6 +8,7 @@ import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCard
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen, Alert } from "@calcom/ui";
@ -73,9 +74,11 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
methods,
t
);
const appsDisableProps = shouldLockDisableProps("apps", { simple: true });
const lockedText = appsDisableProps.isLocked ? "locked" : "unlocked";
const appsWithTeamCredentials = eventTypeApps?.items.filter((app) => app.teams.length) || [];
const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => {
@ -130,12 +133,28 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
<>
<div>
<div className="before:border-0">
{isManagedEventType && (
{(isManagedEventType || isChildrenManagedEventType) && (
<Alert
severity="neutral"
severity={appsDisableProps.isLocked ? "neutral" : "green"}
className="mb-2"
title={t("locked_for_members")}
message={t("locked_apps_description")}
title={
<Trans i18nKey={`${lockedText}_${isManagedEventType ? "for_members" : "by_team_admins"}`}>
{lockedText[0].toUpperCase()}
{lockedText.slice(1)} {isManagedEventType ? "for members" : "by team admins"}
</Trans>
}
actions={<div className="flex h-full items-center">{appsDisableProps.LockedIcon}</div>}
message={
<Trans
i18nKey={`apps_${lockedText}_${
isManagedEventType ? "for_members" : "by_team_admins"
}_description`}>
{isManagedEventType ? "Members" : "You"}{" "}
{appsDisableProps.isLocked
? "will be able to see the active apps but will not be able to edit any app settings"
: "will be able to see the active apps and will be able to edit any app settings"}
</Trans>
}
/>
)}
{!isLoading && !installedApps?.length ? (
@ -144,9 +163,9 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
appsDisableProps.disabled ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
{t("locked_by_team_admin")}
</Button>
) : (
<Button target="_blank" color="secondary" href="/apps">
@ -177,7 +196,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
})}
</div>
</div>
{!shouldLockDisableProps("apps").disabled && (
{!appsDisableProps.disabled && (
<div className="bg-muted mt-6 rounded-md p-8">
{!isLoading && notInstalledApps?.length ? (
<>

View File

@ -1,4 +1,4 @@
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup } from "pages/event-types/[type]";
import { useState, memo, useEffect } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
@ -6,6 +6,7 @@ import { components } from "react-select";
import dayjs from "@calcom/dayjs";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { AvailabilityOption, FormValues } from "@calcom/features/eventtypes/lib/types";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
@ -17,13 +18,6 @@ import { ExternalLink, Globe } from "@calcom/ui/components/icon";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
export type AvailabilityOption = {
label: string;
value: number;
isDefault: boolean;
isManaged?: boolean;
};
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale();
@ -160,11 +154,9 @@ EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const { shouldLockIndicator, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const formMethods = useFormContext<FormValues>();
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
useLockedFieldsManager(eventType, formMethods, t);
const { watch, setValue, getValues } = useFormContext<FormValues>();
const watchSchedule = watch("schedule");
const [options, setOptions] = useState<AvailabilityOption[]>([]);
@ -239,7 +231,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
<div className="border-subtle rounded-t-md border p-6">
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
{t("availability")}
{shouldLockIndicator("availability")}
{(isManagedEventType || isChildrenManagedEventType) && shouldLockIndicator("availability")}
</label>
{isLoading && <SelectSkeletonLoader />}
{!isLoading && (
@ -250,6 +242,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
<Select
placeholder={t("select")}
options={options}
isDisabled={shouldLockDisableProps("availability").disabled}
isSearchable={false}
onChange={(selected) => {
field.onChange(selected?.value || null);

View File

@ -1,12 +1,14 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import type { Key } from "react";
import React, { useEffect, useState } from "react";
import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { SingleValue } from "react-select";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { classNames } from "@calcom/lib";
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
@ -140,6 +142,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue: periodType?.type,
});
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager(eventType, formMethods, t);
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
const onlyFirstAvailableSlotLocked = shouldLockDisableProps("onlyShowFirstAvailableSlot");
const periodTypeLocked = shouldLockDisableProps("periodType");
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
@ -162,7 +172,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<div className="border-subtle space-y-6 rounded-lg border p-6">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
<div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")}</Label>
<Label htmlFor="beforeBufferTime">
{t("before_event")}
{shouldLockIndicator("beforeBufferTime")}
</Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
@ -184,6 +197,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
onChange={(val) => {
if (val) onChange(val.value);
}}
isDisabled={shouldLockDisableProps("beforeBufferTime").disabled}
defaultValue={
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
}
@ -194,7 +208,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
<Label htmlFor="afterBufferTime">
{t("after_event")}
{shouldLockIndicator("afterBufferTime")}
</Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
@ -216,6 +233,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
onChange={(val) => {
if (val) onChange(val.value);
}}
isDisabled={shouldLockDisableProps("afterBufferTime").disabled}
defaultValue={
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
}
@ -228,11 +246,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
<div className="w-full">
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")}</Label>
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
<Label htmlFor="minimumBookingNotice">
{t("minimum_booking_notice")}
{shouldLockIndicator("minimumBookingNotice")}
</Label>
<MinimumBookingNoticeInput
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
{...formMethods.register("minimumBookingNotice")}
/>
</div>
<div className="w-full">
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
<Label htmlFor="slotInterval">
{t("slot_interval")}
{shouldLockIndicator("slotInterval")}
</Label>
<Controller
name="slotInterval"
control={formMethods.control}
@ -250,6 +277,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("slotInterval").disabled}
onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
@ -275,6 +303,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
toggleSwitchAtTheEnd={true}
labelClassName="text-sm"
title={t("limit_booking_frequency")}
{...bookingLimitsLocked}
description={t("limit_booking_frequency_description")}
checked={isChecked}
onCheckedChange={(active) => {
@ -292,7 +321,12 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
)}
childrenClassName="lg:ml-0">
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
<IntervalLimitsManager
disabled={bookingLimitsLocked.disabled}
propertyName="bookingLimits"
defaultLimit={1}
step={1}
/>
</div>
</SettingsToggle>
);
@ -310,6 +344,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
title={t("limit_booking_only_first_slot")}
description={t("limit_booking_only_first_slot_description")}
checked={isChecked}
{...onlyFirstAvailableSlotLocked}
onCheckedChange={(active) => {
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
}}
@ -337,6 +372,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
childrenClassName="lg:ml-0"
title={t("limit_total_booking_duration")}
description={t("limit_total_booking_duration_description")}
{...durationLimitsLocked}
checked={isChecked}
onCheckedChange={(active) => {
if (active) {
@ -351,6 +387,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<IntervalLimitsManager
propertyName="durationLimits"
defaultLimit={60}
disabled={durationLimitsLocked.disabled}
step={15}
textFieldSuffix={t("minutes")}
/>
@ -376,6 +413,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
childrenClassName="lg:ml-0"
title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")}
{...periodTypeLocked}
checked={isChecked}
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
@ -383,7 +421,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{PERIOD_TYPES.map((period) => {
{PERIOD_TYPES.filter((opt) =>
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
).map((period) => {
if (period.type === "UNLIMITED") return null;
return (
<div
@ -392,12 +432,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
)}
key={period.type}>
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
</RadioGroup.Item>
{!periodTypeLocked.disabled && (
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
</RadioGroup.Item>
)}
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
@ -407,12 +449,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
type="number"
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
disabled={periodTypeLocked.disabled}
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<Select
options={optionsPeriod}
isSearchable={false}
isDisabled={periodTypeLocked.disabled}
onChange={(opt) => {
formMethods.setValue(
"periodCountCalendarDays",
@ -437,6 +481,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
disabled={periodTypeLocked.disabled}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
startDate,
@ -468,6 +513,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
childrenClassName="lg:ml-0"
title={t("offset_toggle")}
description={t("offset_toggle_description")}
{...offsetStartLockedProps}
checked={offsetToggle}
onCheckedChange={(active) => {
setOffsetToggle(active);
@ -480,6 +526,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
required
type="number"
containerClassName="max-w-80"
{...offsetStartLockedProps}
label={t("offset_start")}
{...formMethods.register("offsetStart")}
addOnSuffix={<>{t("minutes")}</>}

View File

@ -2,7 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { ErrorMessage } from "@hookform/error-message";
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useEffect, useState } from "react";
import { Controller, useFormContext, useFieldArray } from "react-hook-form";
import type { MultiValue } from "react-select";
@ -11,6 +11,7 @@ import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@ -147,11 +148,7 @@ export const EventSetupTab = (
);
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
useLockedFieldsManager(eventType, formMethods, t);
const Locations = () => {
const { t } = useLocale();
@ -206,6 +203,7 @@ export const EventSetupTab = (
required
onChange={onChange}
value={value}
{...(shouldLockDisableProps("locations").disabled ? { disabled: true } : {})}
className="my-0"
{...rest}
/>
@ -225,6 +223,7 @@ export const EventSetupTab = (
return (
<PhoneInput
required
isDisabled={shouldLockDisableProps("locations").disabled}
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
value={value}
@ -295,16 +294,18 @@ export const EventSetupTab = (
}
}}
/>
<button
data-testid={`delete-locations.${index}.type`}
className="min-h-9 block h-9 px-2"
type="button"
onClick={() => remove(index)}
aria-label={t("remove")}>
<div className="h-4 w-4">
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
</div>
</button>
{!(shouldLockDisableProps("locations").disabled && isChildrenManagedEventType) && (
<button
data-testid={`delete-locations.${index}.type`}
className="min-h-9 block h-9 px-2"
type="button"
onClick={() => remove(index)}
aria-label={t("remove")}>
<div className="h-4 w-4">
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
</div>
</button>
)}
</div>
{eventLocationType?.organizerInputType && (
@ -336,6 +337,7 @@ export const EventSetupTab = (
<CheckboxField
name={`locations[${index}].displayLocationPublicly`}
data-testid="display-location"
isDisabled={shouldLockDisableProps("locations").disabled}
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) => {
@ -424,7 +426,8 @@ export const EventSetupTab = (
</a>
</p>
)}
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
{validLocations.length > 0 && !shouldLockDisableProps("locations").disabled && (
// && !isChildrenManagedEventType : Add this to hide add-location button only when location is disabled by Admin
<li>
<Button
data-testid="add-location"
@ -451,6 +454,8 @@ export const EventSetupTab = (
const lengthLockedProps = shouldLockDisableProps("length");
const descriptionLockedProps = shouldLockDisableProps("description");
const urlLockedProps = shouldLockDisableProps("slug");
const titleLockedProps = shouldLockDisableProps("title");
const urlPrefix = orgBranding
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
: `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
@ -462,14 +467,14 @@ export const EventSetupTab = (
<TextField
required
label={t("title")}
{...shouldLockDisableProps("title")}
{...(isManagedEventType || isChildrenManagedEventType ? titleLockedProps : {})}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
<div>
<Label>
<Label htmlFor="editor">
{t("description")}
{shouldLockIndicator("description")}
{(isManagedEventType || isChildrenManagedEventType) && shouldLockIndicator("description")}
</Label>
<DescriptionEditor
description={eventType?.description}
@ -479,7 +484,7 @@ export const EventSetupTab = (
<TextField
required
label={t("URL")}
{...shouldLockDisableProps("slug")}
{...(isManagedEventType || isChildrenManagedEventType ? urlLockedProps : {})}
defaultValue={eventType.slug}
addOnLeading={
<>
@ -562,7 +567,7 @@ export const EventSetupTab = (
<TextField
required
type="number"
{...lengthLockedProps}
{...(isManagedEventType || isChildrenManagedEventType ? lengthLockedProps : {})}
label={t("duration")}
defaultValue={eventType.length ?? 15}
{...formMethods.register("length")}
@ -592,14 +597,14 @@ export const EventSetupTab = (
</div>
)}
</div>
<div className="border-subtle rounded-lg border p-6">
<div>
<Skeleton as={Label} loadingClassName="w-16">
<Skeleton as={Label} loadingClassName="w-16" htmlFor="locations">
{t("location")}
{/*improve shouldLockIndicator function to also accept eventType and then conditionally render
based on Managed Event type or not.*/}
{shouldLockIndicator("locations")}
</Skeleton>
<Controller
name="locations"
control={formMethods.control}

View File

@ -1,6 +1,6 @@
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
@ -9,6 +9,7 @@ import type { Options } from "react-select";
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Label, Select } from "@calcom/ui";

View File

@ -2,12 +2,13 @@ import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useRouter } from "next/navigation";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
import type { FormValues, AvailabilityOption } from "@calcom/features/eventtypes/lib/types";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -51,8 +52,6 @@ import {
Loader,
} from "@calcom/ui/components/icon";
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
type Props = {
children: React.ReactNode;
eventType: EventTypeSetupProps["eventType"];
@ -168,8 +167,8 @@ function EventTypeSingleLayout({
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
formMethods,
t
);
// Define tab navigation here

View File

@ -4,8 +4,10 @@ import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { WebhookForm } from "@calcom/features/webhooks/components";
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
@ -20,6 +22,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const { t } = useLocale();
const utils = trpc.useContext();
const formMethods = useFormContext<FormValues>();
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
@ -96,8 +99,8 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
formMethods,
t
);
const webhookLockedStatus = shouldLockDisableProps("webhooks");
@ -161,7 +164,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
{t("locked_by_team_admin")}
</Button>
) : (
<NewWebhookButton />

View File

@ -1,10 +1,11 @@
import { useSession } from "next-auth/react";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Button, EmptyScreen, SettingsToggle } from "@calcom/ui";

View File

@ -1,8 +1,9 @@
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Frequency } from "@calcom/prisma/zod-utils";
@ -32,11 +33,7 @@ export default function RecurringEventController({
value: value.toString(),
}));
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const { shouldLockDisableProps } = useLockedFieldsManager(eventType, formMethods, t);
const recurringLocked = shouldLockDisableProps("recurringEvent");

View File

@ -1,13 +1,14 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { UnitTypeLongPlural } from "dayjs";
import { Trans } from "next-i18next";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup } from "pages/event-types/[type]";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type z from "zod";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@ -42,11 +43,7 @@ export default function RequiresConfirmationController({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [requiresConfirmation]);
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const { shouldLockDisableProps } = useLockedFieldsManager(eventType, formMethods, t);
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
const options = [

View File

@ -62,12 +62,8 @@ const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSR
// @TODO
const session = useSession();
// window.document.documentElement.lang can be empty in some cases, for instance when we rendering GlobalError (not-found) page.
const locale =
session?.data?.user.locale ?? typeof window !== "undefined"
? window.document.documentElement.lang || "en"
: "en";
session?.data?.user.locale ?? typeof window !== "undefined" ? window.document.documentElement.lang : "en";
useEffect(() => {
try {

View File

@ -1,5 +1,3 @@
"use client";
import type { GetStaticPropsContext } from "next";
import Link from "next/link";
import { usePathname } from "next/navigation";

View File

@ -1,5 +1,3 @@
"use client";
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
@ -18,7 +16,7 @@ import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
export type PageProps = Omit<inferSSRProps<typeof getServerSideProps>, "trpcState"> & EmbedProps;
type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type({
slug,

View File

@ -11,9 +11,9 @@ import { z } from "zod";
import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
import { getEventLocationType } from "@calcom/app-store/locations";
import { validateCustomEventName } from "@calcom/core/event";
import type { EventLocationType } from "@calcom/core/location";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -22,23 +22,16 @@ import { HttpError } from "@calcom/lib/http-error";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
import type { Prisma } from "@calcom/prisma/client";
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
import type {
BookerLayoutSettings,
customInputSchema,
EventTypeMetaDataSchema,
} from "@calcom/prisma/zod-utils";
import type { customInputSchema } from "@calcom/prisma/zod-utils";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
import { Form, showToast } from "@calcom/ui";
import { asStringOrThrow } from "@lib/asStringOrNull";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
import { ssrInit } from "@server/lib/ssr";
@ -84,68 +77,6 @@ const EventWebhooksTab = dynamic(() =>
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
export type FormValues = {
title: string;
eventTitle: string;
eventName: string;
slug: string;
isInstantEvent: boolean;
length: number;
offsetStart: number;
description: string;
disableGuests: boolean;
lockTimeZoneToggleOnBookingPage: boolean;
requiresConfirmation: boolean;
requiresBookerEmailVerification: boolean;
recurringEvent: RecurringEvent | null;
schedulingType: SchedulingType | null;
hidden: boolean;
hideCalendarNotes: boolean;
hashedLink: string | undefined;
locations: {
type: EventLocationType["type"];
address?: string;
attendeeAddress?: string;
link?: string;
hostPhoneNumber?: string;
displayLocationPublicly?: boolean;
phone?: string;
hostDefault?: string;
credentialId?: number;
teamName?: string;
}[];
customInputs: CustomInputParsed[];
schedule: number | null;
periodType: PeriodType;
periodDays: number;
periodCountCalendarDays: "1" | "0";
periodDates: { startDate: Date; endDate: Date };
seatsPerTimeSlot: number | null;
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
seatsPerTimeSlotEnabled: boolean;
minimumBookingNotice: number;
minimumBookingNoticeInDurationType: number;
beforeBufferTime: number;
afterBufferTime: number;
slotInterval: number | null;
metadata: z.infer<typeof EventTypeMetaDataSchema>;
destinationCalendar: {
integration: string;
externalId: string;
};
successRedirectUrl: string;
durationLimits?: IntervalLimit;
bookingLimits?: IntervalLimit;
onlyShowFirstAvailableSlot: boolean;
children: ChildrenEventType[];
hosts: { userId: number; isFixed: boolean }[];
bookingFields: z.infer<typeof eventTypeBookingFields>;
availability?: AvailabilityOption;
bookerLayouts: BookerLayoutSettings;
multipleDurationEnabled: boolean;
};
export type CustomInputParsed = typeof customInputSchema._output;
const querySchema = z.object({

View File

@ -1,8 +1,6 @@
"use client";
import { AboutOrganizationForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta, WizardLayout, WizardLayoutAppDir } from "@calcom/ui";
import { Meta, WizardLayout } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -17,7 +15,7 @@ const AboutOrganizationPage = () => {
</>
);
};
export const LayoutWrapper = (page: React.ReactElement) => {
const LayoutWrapper = (page: React.ReactElement) => {
return (
<WizardLayout currentStep={3} maxSteps={5}>
{page}
@ -25,14 +23,6 @@ export const LayoutWrapper = (page: React.ReactElement) => {
);
};
export const WrappedAboutOrganizationPage = (page: React.ReactElement) => {
return (
<WizardLayoutAppDir currentStep={3} maxSteps={5}>
{page}
</WizardLayoutAppDir>
);
};
AboutOrganizationPage.getLayout = LayoutWrapper;
AboutOrganizationPage.PageWrapper = PageWrapper;

View File

@ -1,12 +1,8 @@
"use client";
import type { AppProps as NextAppProps } from "next/app";
import { redirect } from "next/navigation";
import { AddNewTeamsForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta, WizardLayout } from "@calcom/ui";
import { WizardLayoutAppDir } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -37,17 +33,4 @@ AddNewTeamsPage.getLayout = (page: React.ReactElement, router: NextAppProps["rou
AddNewTeamsPage.PageWrapper = PageWrapper;
export const WrapperAddNewTeamsPage = (page: React.ReactElement) => {
return (
<WizardLayoutAppDir
currentStep={5}
maxSteps={5}
isOptionalCallback={() => {
redirect(`/event-types`);
}}>
{page}
</WizardLayoutAppDir>
);
};
export default AddNewTeamsPage;

View File

@ -1,11 +1,8 @@
"use client";
import type { AppProps as NextAppProps } from "next/app";
import { redirect } from "next/navigation";
import { AddNewOrgAdminsForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta, WizardLayout, WizardLayoutAppDir } from "@calcom/ui";
import { Meta, WizardLayout } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -36,22 +33,6 @@ OnboardTeamMembersPage.getLayout = (page: React.ReactElement, router: NextAppPro
</WizardLayout>
);
export const buildWrappedOnboardTeamMembersPage = (
id: string | string[] | undefined,
page: React.ReactElement
) => {
return (
<WizardLayoutAppDir
currentStep={4}
maxSteps={5}
isOptionalCallback={() => {
redirect(`/settings/organizations/${id}/add-teams`);
}}>
{page}
</WizardLayoutAppDir>
);
};
OnboardTeamMembersPage.PageWrapper = PageWrapper;
export default OnboardTeamMembersPage;

View File

@ -1,8 +1,6 @@
"use client";
import { SetPasswordForm } from "@calcom/features/ee/organizations/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta, WizardLayout, WizardLayoutAppDir } from "@calcom/ui";
import { Meta, WizardLayout } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
@ -25,14 +23,6 @@ const LayoutWrapper = (page: React.ReactElement) => {
);
};
export const WrappedSetPasswordPage = (page: React.ReactElement) => {
return (
<WizardLayoutAppDir currentStep={2} maxSteps={5}>
{page}
</WizardLayoutAppDir>
);
};
SetPasswordPage.getLayout = LayoutWrapper;
SetPasswordPage.PageWrapper = PageWrapper;

View File

@ -1,12 +1,10 @@
"use client";
import type { GetServerSidePropsContext } from "next";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { CreateANewOrganizationForm } from "@calcom/features/ee/organizations/components";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WizardLayout, Meta, WizardLayoutAppDir } from "@calcom/ui";
import { WizardLayout, Meta } from "@calcom/ui";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@ -29,14 +27,6 @@ const LayoutWrapper = (page: React.ReactElement) => {
);
};
export const WrappedCreateNewOrganizationPage = (page: React.ReactElement) => {
return (
<WizardLayoutAppDir currentStep={1} maxSteps={5}>
{page}
</WizardLayoutAppDir>
);
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);

View File

@ -435,7 +435,7 @@ test.describe("Reschedule for booking with seats", () => {
await page.locator('[data-testid="confirm_cancel"]').click();
await page.waitForResponse((res) => res.url().includes("api/cancel") && res.status() === 200);
await page.waitForLoadState("networkidle");
const oldBooking = await prisma.booking.findFirst({
where: { uid: booking.uid },

View File

@ -360,5 +360,5 @@ export async function doOnOrgDomain(
}
// When App directory is there, this is the 404 page text. We should work on fixing the 404 page as it changed due to app directory.
export const NotFoundPageTextAppDir = "This page does not exist.";
export const NotFoundPageText = "This page could not be found";
// export const NotFoundPageText = "ERROR 404";

View File

@ -11,9 +11,10 @@ import {
selectFirstAvailableTimeSlotNextMonth,
} from "./lib/testUtils";
test.afterEach(({ users }) => users.deleteAll());
test.afterAll(({ users }) => users.deleteAll());
test.setTimeout(120000);
test.describe("Managed Event Types tests", () => {
test.describe("Managed Event Types", () => {
test("Can create managed event type", async ({ page, users }) => {
// Creating the owner user of the team
const adminUser = await users.create();
@ -61,7 +62,7 @@ test.describe("Managed Event Types tests", () => {
await expect(page.locator('input[name="title"]')).toBeEditable();
await expect(page.locator('input[name="slug"]')).toBeEditable();
await expect(page.locator('input[name="length"]')).toBeEditable();
await adminUser.logout();
await adminUser.apiLogin();
});
await test.step("Managed event type exists for added member", async () => {
@ -73,7 +74,7 @@ test.describe("Managed Event Types tests", () => {
await page.locator('button[data-testid^="accept-invitation"]').click();
await page.getByText("Member").waitFor();
await memberUser.logout();
await page.goto("/auth/logout");
// Coming back as team owner to assign member user to managed event
await adminUser.apiLogin();
@ -107,7 +108,6 @@ test.describe("Managed Event Types tests", () => {
await test.step("Managed event type has locked fields for added member", async () => {
await adminUser.logout();
// Coming back as member user to see if there is a managed event present after assignment
await memberUser.apiLogin();
await page.goto("/event-types");
@ -118,6 +118,42 @@ test.describe("Managed Event Types tests", () => {
await expect(page.locator('input[name="title"]')).not.toBeEditable();
await expect(page.locator('input[name="slug"]')).not.toBeEditable();
await expect(page.locator('input[name="length"]')).not.toBeEditable();
await page.goto("/auth/logout");
});
await test.step("Managed event type provides discrete field lock/unlock state for admin", async () => {
await adminUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.waitForURL("event-types/**");
// Locked by default
const titleLockIndicator = page.getByTestId("locked-indicator-title");
await expect(titleLockIndicator).toBeVisible();
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(1);
// Proceed to unlock and check that it got unlocked
titleLockIndicator.click();
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(0);
await expect(titleLockIndicator.locator("[data-state='unchecked']")).toHaveCount(1);
// Save changes
await page.locator('[type="submit"]').click();
await page.waitForLoadState("networkidle");
await page.goto("/auth/logout");
});
await test.step("Managed event type shows discretionally unlocked field to member", async () => {
await memberUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.waitForURL("event-types/**");
await expect(page.locator('input[name="title"]')).toBeEditable();
});
});
});

View File

@ -1,68 +0,0 @@
import { expect } from "@playwright/test";
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "@calcom/web/playwright/lib/testUtils";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.describe("Payment", () => {
test.describe("user", () => {
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
test("should create a mock payment for a user", async ({ context, users, page }) => {
test.skip(process.env.MOCK_PAYMENT_APP_ENABLED === undefined, "Skipped as Stripe is not installed");
const user = await users.create();
await user.apiLogin();
await context.addCookies([
{
name: "x-calcom-future-routes-override",
value: "1",
url: "http://localhost:3000",
},
]);
await page.goto("/apps");
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("mock");
await page.getByTestId("install-app-button").click();
await page.waitForURL((url) => url.pathname.endsWith("/apps/installed/payment"));
await page.getByRole("link", { name: "Event Types" }).click();
await page.getByRole("link", { name: /^30 min/ }).click();
await page.getByTestId("vertical-tab-apps").click();
await page.locator("#event-type-form").getByRole("switch").click();
await page.getByPlaceholder("Price").click();
await page.getByPlaceholder("Price").fill("1");
await page.locator("#test-mock-payment-app-currency-id").click();
await page.getByTestId("select-option-USD").click();
await page.getByTestId("update-eventtype").click();
await page.goto(`${user.username}/30-min`);
await page.waitForLoadState("networkidle");
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await page.waitForURL((url) => url.pathname.includes("/payment/"));
const dataNextJsRouter = await page.evaluate(() =>
window.document.documentElement.getAttribute("data-nextjs-router")
);
expect(dataNextJsRouter).toEqual("app");
await page.getByText("Payment", { exact: true }).waitFor();
});
});
});

View File

@ -6,7 +6,7 @@ import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import {
NotFoundPageTextAppDir,
NotFoundPageText,
bookTimeSlot,
doOnOrgDomain,
fillStripeTestCheckout,
@ -40,7 +40,7 @@ test.describe("Teams A/B tests", () => {
expect(dataNextJsRouter).toEqual("app");
const locator = page.getByRole("heading", { name: "Teams", exact: true });
const locator = page.getByRole("heading", { name: "teams" });
await expect(locator).toBeVisible();
});
@ -389,7 +389,7 @@ test.describe("Teams - Org", () => {
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
await expect(page.locator(`text=${NotFoundPageText}`)).toBeVisible();
await doOnOrgDomain(
{
orgSlug: org.slug,

View File

@ -695,8 +695,10 @@
"assigned_to": "Assigned to",
"you_must_be_logged_in_to":"You must be logged in to {{url}}",
"start_assigning_members_above": "Start assigning members above",
"locked_fields_admin_description": "Members will not be able to edit this",
"locked_fields_member_description": "This option was locked by the team admin",
"locked_fields_admin_description": "Members can not edit",
"unlocked_fields_admin_description": "Members can edit",
"locked_fields_member_description": "Locked by the team admin",
"unlocked_fields_member_description": "Unlocked by team admin",
"url": "URL",
"hidden": "Hidden",
"readonly": "Readonly",
@ -1258,7 +1260,7 @@
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
"show_eventtype_on_profile": "Show on profile",
"show_eventtype_on_profile": "Show on Profile",
"embed": "Embed",
"new_username": "New username",
"current_username": "Current username",
@ -1868,10 +1870,16 @@
"requires_at_least_one_schedule": "You are required to have at least one schedule",
"default_conferencing_bulk_description": "Update the locations for the selected event types",
"locked_for_members": "Locked for members",
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
"locked_by_admin": "Locked by team admin",
"unlocked_for_members": "Unlocked for members",
"apps_locked_for_members_description": "Members will be able to see the active apps but will not be able to edit any app settings",
"apps_unlocked_for_members_description": "Members will be able to see the active apps and will be able to edit any app settings",
"apps_locked_by_team_admins_description": "You will be able to see the active apps but will not be able to edit any app settings",
"apps_unlocked_by_team_admins_description": "You will be able to see the active apps and will be able to edit any app settings",
"workflows_locked_for_members_description": "Members can not add their personal workflows to this event type. Members will be able to see the active team workflows but will not be able to edit any workflow settings.",
"workflows_unlocked_for_members_description": "Members will be able to add their personal workflows to this event type. Members will be able to see the active team workflows but will not be able to edit any workflow settings.",
"workflows_locked_by_team_admins_description": "You will be able to see the active team workflows but will not be able to edit any workflow settings or add your personal workflows to this event type.",
"workflows_unlocked_by_team_admins_description": "You will be able to enable/disable personal workflows on this event type. You will be able to see the active team workflows but will not be able to edit any team workflow settings.",
"locked_by_team_admin": "Locked by team admin",
"app_not_connected": "You have not connected a {{appName}} account.",
"connect_now": "Connect now",
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
@ -2150,7 +2158,10 @@
"overlay_my_calendar":"Overlay my calendar",
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
"join_event_location":"Join {{eventLocationType}}",
"locked": "Locked",
"unlocked": "Unlocked",
"join_event_location": "Join {{eventLocationType}}",
"join_meeting": "Join Meeting",
"troubleshooting":"Troubleshooting",
"calendars_were_checking_for_conflicts":"Calendars were checking for conflicts",
"availabilty_schedules":"Availability schedules",
@ -2223,4 +2234,4 @@
"create_entry": "Create entry",
"time_range": "Time range",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
}

View File

@ -1046,77 +1046,72 @@ export function mockCalendar(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const deleteEventCalls: any[] = [];
const app = appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata];
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockResolvedValue({
lib: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
CalendarService: function MockCalendarService() {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createEvent: async function (...rest: any[]): Promise<NewCalendarEventType> {
if (calendarData?.creationCrash) {
throw new Error("MockCalendarService.createEvent fake error");
}
const [calEvent, credentialId] = rest;
log.silly("mockCalendar.createEvent", JSON.stringify({ calEvent, credentialId }));
createEventCalls.push(rest);
return Promise.resolve({
type: app.type,
additionalInfo: {},
uid: "PROBABLY_UNUSED_UID",
// A Calendar is always expected to return an id.
id: normalizedCalendarData.create?.id || "FALLBACK_MOCK_CALENDAR_EVENT_ID",
iCalUID: normalizedCalendarData.create?.iCalUID,
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
password: "MOCK_PASSWORD",
url: "https://UNUSED_URL",
});
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateEvent: async function (...rest: any[]): Promise<NewCalendarEventType> {
if (calendarData?.updationCrash) {
throw new Error("MockCalendarService.updateEvent fake error");
}
const [uid, event, externalCalendarId] = rest;
log.silly("mockCalendar.updateEvent", JSON.stringify({ uid, event, externalCalendarId }));
// eslint-disable-next-line prefer-rest-params
updateEventCalls.push(rest);
return Promise.resolve({
type: app.type,
additionalInfo: {},
uid: "PROBABLY_UNUSED_UID",
iCalUID: normalizedCalendarData.update?.iCalUID,
const appMock = appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default];
appMock &&
`mockResolvedValue` in appMock &&
appMock.mockResolvedValue({
lib: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
CalendarService: function MockCalendarService() {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createEvent: async function (...rest: any[]): Promise<NewCalendarEventType> {
if (calendarData?.creationCrash) {
throw new Error("MockCalendarService.createEvent fake error");
}
const [calEvent, credentialId] = rest;
log.silly("mockCalendar.createEvent", JSON.stringify({ calEvent, credentialId }));
createEventCalls.push(rest);
return Promise.resolve({
type: app.type,
additionalInfo: {},
uid: "PROBABLY_UNUSED_UID",
// A Calendar is always expected to return an id.
id: normalizedCalendarData.create?.id || "FALLBACK_MOCK_CALENDAR_EVENT_ID",
iCalUID: normalizedCalendarData.create?.iCalUID,
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
password: "MOCK_PASSWORD",
url: "https://UNUSED_URL",
});
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
updateEvent: async function (...rest: any[]): Promise<NewCalendarEventType> {
if (calendarData?.updationCrash) {
throw new Error("MockCalendarService.updateEvent fake error");
}
const [uid, event, externalCalendarId] = rest;
log.silly("mockCalendar.updateEvent", JSON.stringify({ uid, event, externalCalendarId }));
// eslint-disable-next-line prefer-rest-params
updateEventCalls.push(rest);
return Promise.resolve({
type: app.type,
additionalInfo: {},
uid: "PROBABLY_UNUSED_UID",
iCalUID: normalizedCalendarData.update?.iCalUID,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: normalizedCalendarData.update?.uid || "FALLBACK_MOCK_ID",
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
password: "MOCK_PASSWORD",
url: "https://UNUSED_URL",
});
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deleteEvent: async (...rest: any[]) => {
log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest }));
// eslint-disable-next-line prefer-rest-params
deleteEventCalls.push(rest);
},
getAvailability: async (): Promise<EventBusyDate[]> => {
if (calendarData?.getAvailabilityCrash) {
throw new Error("MockCalendarService.getAvailability fake error");
}
return new Promise((resolve) => {
resolve(calendarData?.busySlots || []);
});
},
};
},
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: normalizedCalendarData.update?.uid || "FALLBACK_MOCK_ID",
// Password and URL seems useless for CalendarService, plan to remove them if that's the case
password: "MOCK_PASSWORD",
url: "https://UNUSED_URL",
});
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deleteEvent: async (...rest: any[]) => {
log.silly("mockCalendar.deleteEvent", JSON.stringify({ rest }));
// eslint-disable-next-line prefer-rest-params
deleteEventCalls.push(rest);
},
getAvailability: async (): Promise<EventBusyDate[]> => {
if (calendarData?.getAvailabilityCrash) {
throw new Error("MockCalendarService.getAvailability fake error");
}
return new Promise((resolve) => {
resolve(calendarData?.busySlots || []);
});
},
};
},
});
},
});
return {
createEventCalls,
deleteEventCalls,

View File

@ -109,7 +109,6 @@
"turbo": "^1.10.1"
},
"resolutions": {
"types-ramda": "0.29.4",
"@apidevtools/json-schema-ref-parser": "9.0.9",
"@types/node": "16.9.1",
"@types/react": "18.0.26",

View File

@ -1,5 +1,5 @@
import { usePathname, useSearchParams } from "next/navigation";
import { useState, useEffect, useMemo } from "react";
import { useRouter } from "next/router";
import { useState, useEffect } from "react";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
@ -21,13 +21,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
eventType,
eventTypeFormMetadata,
}) {
const searchParams = useSearchParams();
/** TODO "pathname" no longer contains square-bracket expressions. Rewrite the code relying on them if required. **/
const pathname = usePathname();
const asPath = useMemo(
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
[pathname, searchParams]
);
const { asPath } = useRouter();
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
const price = getAppData("price");
const currency = getAppData("currency");

View File

@ -29,7 +29,6 @@ export const EventTypeAddonMap = {
gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")),
matomo: dynamic(() => import("./matomo/components/EventTypeAppCardInterface")),
metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")),
"mock-payment-app": dynamic(() => import("./mock-payment-app/components/EventTypeAppCardInterface")),
paypal: dynamic(() => import("./paypal/components/EventTypeAppCardInterface")),
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),

View File

@ -17,7 +17,6 @@ import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appKeysSchema as make_zod_ts } from "./make/zod";
import { appKeysSchema as matomo_zod_ts } from "./matomo/zod";
import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod";
import { appKeysSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod";
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
import { appKeysSchema as paypal_zod_ts } from "./paypal/zod";
@ -56,7 +55,6 @@ export const appKeysSchemas = {
make: make_zod_ts,
matomo: matomo_zod_ts,
metapixel: metapixel_zod_ts,
"mock-payment-app": mock_payment_app_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,
paypal: paypal_zod_ts,

View File

@ -36,7 +36,6 @@ import make_config_json from "./make/config.json";
import matomo_config_json from "./matomo/config.json";
import metapixel_config_json from "./metapixel/config.json";
import mirotalk_config_json from "./mirotalk/config.json";
import mock_payment_app_config_json from "./mock-payment-app/config.json";
import n8n_config_json from "./n8n/config.json";
import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata";
import office365video_config_json from "./office365video/config.json";
@ -116,7 +115,6 @@ export const appStoreMetadata = {
matomo: matomo_config_json,
metapixel: metapixel_config_json,
mirotalk: mirotalk_config_json,
"mock-payment-app": mock_payment_app_config_json,
n8n: n8n_config_json,
office365calendar: office365calendar__metadata_ts,
office365video: office365video_config_json,

View File

@ -17,7 +17,6 @@ import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appDataSchema as make_zod_ts } from "./make/zod";
import { appDataSchema as matomo_zod_ts } from "./matomo/zod";
import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod";
import { appDataSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod";
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
import { appDataSchema as paypal_zod_ts } from "./paypal/zod";
@ -56,7 +55,6 @@ export const appDataSchemas = {
make: make_zod_ts,
matomo: matomo_zod_ts,
metapixel: metapixel_zod_ts,
"mock-payment-app": mock_payment_app_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,
paypal: paypal_zod_ts,

View File

@ -36,7 +36,6 @@ export const apiHandlers = {
matomo: import("./matomo/api"),
metapixel: import("./metapixel/api"),
mirotalk: import("./mirotalk/api"),
"mock-payment-app": import("./mock-payment-app/api"),
n8n: import("./n8n/api"),
office365calendar: import("./office365calendar/api"),
office365video: import("./office365video/api"),

View File

@ -41,12 +41,4 @@ const appStore = {
shimmervideo: () => import("./shimmervideo"),
};
const exportedAppStore: typeof appStore & {
["mock-payment-app"]?: () => Promise<typeof import("./mock-payment-app/index")>;
} = appStore;
if (process.env.MOCK_PAYMENT_APP_ENABLED !== undefined) {
exportedAppStore["mock-payment-app"] = () => import("./mock-payment-app/index");
}
export default exportedAppStore;
export default appStore;

View File

@ -1,8 +0,0 @@
---
items:
- 1.jpeg
- 2.jpeg
- 3.jpeg
---
{DESCRIPTION}

View File

@ -1,16 +0,0 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};
export default handler;

View File

@ -1 +0,0 @@
export { default as add } from "./add";

View File

@ -1,136 +0,0 @@
import { usePathname, useSearchParams } from "next/navigation";
import { useState, useMemo } from "react";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
import {
currencyOptions,
currencySymbols,
isAcceptedCurrencyCode,
} from "@calcom/app-store/paypal/lib/currencyOptions";
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Select, TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod";
import { paymentOptions } from "../zod";
type Option = { value: string; label: string };
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const searchParams = useSearchParams();
/** TODO "pathname" no longer contains square-bracket expressions. Rewrite the code relying on them if required. **/
const pathname = usePathname();
const asPath = useMemo(
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
[pathname, searchParams]
);
const { t } = useLocale();
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
const price = getAppData("price");
const currency = getAppData("currency");
const paymentOption = getAppData("paymentOption");
const enable = getAppData("enabled");
const [selectedCurrency, setSelectedCurrency] = useState(currencyOptions.find((c) => c.value === currency));
const [currencySymbol, setCurrencySymbol] = useState(
isAcceptedCurrencyCode(currency) ? currencySymbols[currency] : ""
);
const [requirePayment, setRequirePayment] = useState(enable);
const paymentOptionSelectValue = paymentOptions?.find((option) => paymentOption === option.value) || {
label: paymentOptions[0].label,
value: paymentOptions[0].value,
};
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
return (
<AppCard
returnTo={WEBAPP_URL + asPath}
app={app}
switchChecked={requirePayment}
switchOnClick={(enabled) => {
setRequirePayment(enabled);
}}
description={<>Add a mock payment to your events</>}>
<>
{recurringEventDefined ? (
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
) : (
requirePayment && (
<>
<div className="mt-2 block items-center sm:flex">
<TextField
id="test-mock-payment-app-price"
label="Price"
labelSrOnly
addOnLeading={currencySymbol}
addOnSuffix={currency}
step="0.01"
min="0.5"
type="number"
required
className="block w-full rounded-sm pl-2 text-sm"
placeholder="Price"
onChange={(e) => {
setAppData("price", Number(e.target.value) * 100);
if (selectedCurrency) {
setAppData("currency", selectedCurrency.value);
}
}}
value={price > 0 ? price / 100 : undefined}
/>
</div>
<div className="mt-5 w-60">
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
{t("currency")}
</label>
<Select
id="test-mock-payment-app-currency-id"
variant="default"
options={currencyOptions}
value={selectedCurrency}
className="text-black"
defaultValue={selectedCurrency}
onChange={(e) => {
if (e) {
setSelectedCurrency(e);
setCurrencySymbol(currencySymbols[e.value]);
setAppData("currency", e.value);
}
}}
/>
</div>
<div className="mt-4 w-60">
<label className="text-default mb-1 block text-sm font-medium" htmlFor="currency">
Payment option
</label>
<Select<Option>
defaultValue={
paymentOptionSelectValue
? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) }
: { ...paymentOptions[0], label: t(paymentOptions[0].label) }
}
options={paymentOptions.map((option) => {
return { ...option, label: t(option.label) || option.label };
})}
onChange={(input) => {
if (input) setAppData("paymentOption", input.value);
}}
className="mb-1 h-[38px] w-full"
isDisabled={false}
/>
</div>
</>
)
)}
</>
</AppCard>
);
};
export default EventTypeAppCard;

View File

@ -1,18 +0,0 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Mock Payment App",
"slug": "mock-payment-app",
"type": "mock-payment-app_payment",
"logo": "icon.svg",
"url": "https://example.com/link",
"variant": "payment",
"categories": ["payment"],
"publisher": "Intuita",
"email": "greg@intuita.io",
"description": "The mock payment app for tests",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"dirName": "mock-payment-app",
"extendsFeature": "EventType"
}

View File

@ -1,2 +0,0 @@
export * as api from "./api";
export * as lib from "./lib";

View File

@ -1,141 +0,0 @@
import type { Booking, Payment, Prisma, PaymentOption } from "@prisma/client";
import { v4 as uuidv4 } from "uuid";
import prisma from "@calcom/prisma";
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
export class PaymentService implements IAbstractPaymentService {
async create(
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
bookingId: Booking["id"]
) {
try {
const booking = await prisma.booking.findFirst({
select: {
uid: true,
title: true,
},
where: {
id: bookingId,
},
});
if (booking === null) {
throw new Error("Booking not found");
}
const uid = uuidv4();
console.log("CREATE payment");
const paymentData = await prisma.payment.create({
data: {
uid,
app: {
connect: {
slug: "mock-payment-app",
},
},
booking: {
connect: {
id: bookingId,
},
},
amount: payment.amount,
externalId: uid,
currency: payment.currency,
data: {} as Prisma.InputJsonValue,
fee: 0,
refunded: false,
success: false,
},
});
return paymentData;
} catch (error) {
console.error(error);
throw new Error("Payment could not be created");
}
}
async update(): Promise<Payment> {
throw new Error("Method not implemented.");
}
async refund(): Promise<Payment> {
throw new Error("Method not implemented.");
}
async collectCard(
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
bookingId: number,
_bookerEmail: string,
paymentOption: PaymentOption
): Promise<Payment> {
try {
const booking = await prisma.booking.findFirst({
select: {
uid: true,
title: true,
},
where: {
id: bookingId,
},
});
if (booking === null) {
throw new Error("Booking not found");
}
const uid = uuidv4();
const paymentData = await prisma.payment.create({
data: {
uid,
app: {
connect: {
slug: "paypal",
},
},
booking: {
connect: {
id: bookingId,
},
},
amount: payment.amount,
currency: payment.currency,
data: {} as Prisma.InputJsonValue,
fee: 0,
refunded: false,
success: false,
paymentOption,
externalId: "",
},
});
if (!paymentData) {
throw new Error();
}
return paymentData;
} catch (error) {
console.error(error);
throw new Error("Payment could not be created");
}
}
chargeCard(): Promise<Payment> {
throw new Error("Method not implemented.");
}
getPaymentPaidStatus(): Promise<string> {
throw new Error("Method not implemented.");
}
getPaymentDetails(): Promise<Payment> {
throw new Error("Method not implemented.");
}
afterPayment(): Promise<void> {
return Promise.resolve();
}
deletePayment(): Promise<boolean> {
return Promise.resolve(false);
}
isSetupAlready(): boolean {
return true;
}
}

View File

@ -1 +0,0 @@
export { PaymentService } from "./PaymentService";

View File

@ -1,14 +0,0 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/mock-payment-app",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "The mock payment app for tests"
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48"><title>blockchain</title><g class="nc-icon-wrapper"><path d="M32.874,10.405a.99.99,0,0,0-.353-.369l-8-4.889a1,1,0,0,0-1.042,0l-8,4.889a.99.99,0,0,0-.353.369L24,15.828Z" fill="#a0e6ee"></path><path d="M15.126,10.405a1,1,0,0,0-.126.484v8.878a1,1,0,0,0,.479.853l8,4.889a1,1,0,0,0,.521.147V15.828Z" fill="#6cc4f5"></path><path d="M32.874,10.405a1,1,0,0,1,.126.484v8.878a1,1,0,0,1-.479.853l-8,4.889a1,1,0,0,1-.521.147V15.828Z" fill="#2594d0"></path><path d="M47.874,19.589a.99.99,0,0,0-.353-.369l-8-4.889a1,1,0,0,0-1.042,0l-8,4.889a.99.99,0,0,0-.353.369L39,25.013Z" fill="#a0e6ee"></path><path d="M30.126,19.589a1,1,0,0,0-.126.485v8.878a1,1,0,0,0,.479.853l8,4.889a1,1,0,0,0,.521.147V25.013Z" fill="#6cc4f5"></path><path d="M47.874,19.589a1,1,0,0,1,.126.485v8.878a1,1,0,0,1-.479.853l-8,4.889a1,1,0,0,1-.521.147V25.013Z" fill="#2594d0"></path><path d="M17.874,19.589a.99.99,0,0,0-.353-.369l-8-4.889a1,1,0,0,0-1.042,0l-8,4.889a.99.99,0,0,0-.353.369L9,25.013Z" fill="#a0e6ee"></path><path d="M.126,19.589A1,1,0,0,0,0,20.074v8.878a1,1,0,0,0,.479.853l8,4.889A1,1,0,0,0,9,34.841V25.013Z" fill="#6cc4f5"></path><path d="M17.874,19.589a1,1,0,0,1,.126.485v8.878a1,1,0,0,1-.479.853l-8,4.889A1,1,0,0,1,9,34.841V25.013Z" fill="#2594d0"></path><path d="M32.874,29.577a.99.99,0,0,0-.353-.369l-8-4.889a1,1,0,0,0-1.042,0l-8,4.889a.99.99,0,0,0-.353.369L24,35Z" fill="#a0e6ee"></path><path d="M15.126,29.577a1,1,0,0,0-.126.484v8.878a1,1,0,0,0,.479.853l8,4.889a1,1,0,0,0,.521.147V35Z" fill="#6cc4f5"></path><path d="M32.874,29.577a1,1,0,0,1,.126.484v8.878a1,1,0,0,1-.479.853l-8,4.889a1,1,0,0,1-.521.147V35Z" fill="#2594d0"></path></g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,35 +0,0 @@
import { z } from "zod";
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
const paymentOptionSchema = z.object({
label: z.string(),
value: z.string(),
});
export const paymentOptionsSchema = z.array(paymentOptionSchema);
export const paymentOptions = [
{
label: "on_booking_option",
value: "ON_BOOKING",
},
];
type PaymentOption = (typeof paymentOptions)[number]["value"];
const VALUES: [PaymentOption, ...PaymentOption[]] = [
paymentOptions[0].value,
...paymentOptions.slice(1).map((option) => option.value),
];
export const paymentOptionEnum = z.enum(VALUES);
export const appDataSchema = eventTypeAppCardZod.merge(
z.object({
price: z.number(),
currency: z.string(),
paymentOption: z.string().optional(),
enabled: z.boolean().optional(),
})
);
export const appKeysSchema = z.object({});

View File

@ -1,5 +1,5 @@
import { usePathname, useSearchParams } from "next/navigation";
import { useState, useEffect, useMemo } from "react";
import { useRouter } from "next/router";
import { useState, useEffect } from "react";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
@ -24,13 +24,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
eventType,
eventTypeFormMetadata,
}) {
const searchParams = useSearchParams();
/** TODO "pathname" no longer contains square-bracket expressions. Rewrite the code relying on them if required. **/
const pathname = usePathname();
const asPath = useMemo(
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
[pathname, searchParams]
);
const { asPath } = useRouter();
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
const price = getAppData("price");

View File

@ -3,7 +3,7 @@ import { expect } from "@playwright/test";
import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
import { test } from "@calcom/web/playwright/lib/fixtures";
import { NotFoundPageTextAppDir, gotoRoutingLink } from "@calcom/web/playwright/lib/testUtils";
import { NotFoundPageText, gotoRoutingLink } from "@calcom/web/playwright/lib/testUtils";
import {
addForm,
@ -36,7 +36,7 @@ test.describe("Routing Forms", () => {
await page.goto(`apps/routing-forms/route-builder/${formId}`);
await disableForm(page);
await gotoRoutingLink({ page, formId });
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
await expect(page.locator(`text=${NotFoundPageText}`)).toBeVisible();
});
test("should be able to edit the form", async ({ page }) => {

View File

@ -597,8 +597,7 @@ async function handler(req: CustomRequest) {
// Posible to refactor TODO:
const paymentApp = (await appStore[
paymentAppCredential?.app?.dirName as keyof typeof appStore
]?.()) as PaymentApp;
]()) as PaymentApp;
if (!paymentApp?.lib?.PaymentService) {
console.warn(`payment App service of type ${paymentApp} is not implemented`);
return null;

View File

@ -1,27 +1,87 @@
// eslint-disable-next-line no-restricted-imports
import { get } from "lodash";
import React from "react";
import type { TFunction } from "next-i18next";
import { useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import type { UseFormReturn } from "react-hook-form";
import type z from "zod";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import type { Prisma } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import type { _EventTypeModel } from "@calcom/prisma/zod/eventtype";
import { Tooltip } from "@calcom/ui";
import { Lock } from "@calcom/ui/components/icon";
import { Tooltip, Badge, Switch } from "@calcom/ui";
import { Lock, Unlock } from "@calcom/ui/components/icon";
export const LockedIndicator = (label: string) => (
<Tooltip content={<>{label}</>}>
<div className="bg -mt-0.5 ml-1 inline-flex h-4 w-4 rounded-sm p-0.5">
<Lock className="text-subtle hover:text-muted h-3 w-3" />
</div>
</Tooltip>
);
export const LockedSwitch = (
isManagedEventType: boolean,
[isLocked, setIsLocked]: [boolean, Dispatch<SetStateAction<boolean>>],
fieldName: string,
setUnlockedFields: (fieldName: string, val: boolean | undefined) => void,
options = { simple: false }
) => {
return isManagedEventType ? (
<Switch
data-testid={`locked-indicator-${fieldName}`}
onCheckedChange={(enabled) => {
setIsLocked(enabled);
setUnlockedFields(fieldName, !enabled || undefined);
}}
checked={isLocked}
small={!options.simple}
/>
) : null;
};
export const LockedIndicator = (
isManagedEventType: boolean,
[isLocked, setIsLocked]: [boolean, Dispatch<SetStateAction<boolean>>],
t: TFunction,
fieldName: string,
setUnlockedFields: (fieldName: string, val: boolean | undefined) => void,
options = { simple: false }
) => {
const stateText = t(isLocked ? "locked" : "unlocked");
const tooltipText = t(
`${isLocked ? "locked" : "unlocked"}_fields_${isManagedEventType ? "admin" : "member"}_description`
);
return (
<Tooltip content={<>{tooltipText}</>}>
<div className="inline">
<Badge variant={isLocked ? "gray" : "green"} className="ml-2 transform gap-1.5 p-1">
{!options.simple && (
<>
{isLocked ? (
<Lock className="text-subtle h-3 w-3" />
) : (
<Unlock className="text-subtle h-3 w-3" />
)}
<span className="font-medium">{stateText}</span>
</>
)}
{isManagedEventType && (
<Switch
data-testid={`locked-indicator-${fieldName}`}
onCheckedChange={(enabled) => {
setIsLocked(enabled);
setUnlockedFields(fieldName, !enabled || undefined);
}}
checked={isLocked}
small={!options.simple}
/>
)}
</Badge>
</div>
</Tooltip>
);
};
const useLockedFieldsManager = (
eventType: Pick<z.infer<typeof _EventTypeModel>, "schedulingType" | "userId" | "metadata">,
adminLabel: string,
memberLabel: string
eventType: Pick<z.infer<typeof _EventTypeModel>, "schedulingType" | "userId" | "metadata" | "id">,
formMethods: UseFormReturn<FormValues>,
translate: TFunction
) => {
const fieldStates: Record<string, [boolean, Dispatch<SetStateAction<boolean>>]> = {};
const unlockedFields =
(eventType.metadata?.managedEventConfig?.unlockedFields !== undefined &&
eventType.metadata?.managedEventConfig?.unlockedFields) ||
@ -32,7 +92,22 @@ const useLockedFieldsManager = (
eventType.metadata?.managedEventConfig !== undefined &&
eventType.schedulingType !== SchedulingType.MANAGED;
const shouldLockIndicator = (fieldName: string) => {
const setUnlockedFields = (fieldName: string, val: boolean | undefined) => {
const path = "metadata.managedEventConfig.unlockedFields";
const metaUnlockedFields = formMethods.getValues(path);
if (!metaUnlockedFields) return;
if (val === undefined) {
delete metaUnlockedFields[fieldName as keyof typeof metaUnlockedFields];
formMethods.setValue(path, { ...metaUnlockedFields });
} else {
formMethods.setValue(path, {
...metaUnlockedFields,
[fieldName]: val,
});
}
};
const getLockedInitState = (fieldName: string): boolean => {
let locked = isManagedEventType || isChildrenManagedEventType;
// Supports "metadata.fieldName"
if (fieldName.includes(".")) {
@ -40,20 +115,75 @@ const useLockedFieldsManager = (
} else {
locked = locked && unlockedFields[fieldName as keyof Omit<Prisma.EventTypeSelect, "id">] === undefined;
}
return locked && LockedIndicator(isManagedEventType ? adminLabel : memberLabel);
return locked;
};
const shouldLockDisableProps = (fieldName: string) => {
const useShouldLockIndicator = (fieldName: string, options?: { simple: true }) => {
if (!fieldStates[fieldName]) {
// eslint-disable-next-line react-hooks/rules-of-hooks
fieldStates[fieldName] = useState(getLockedInitState(fieldName));
}
return LockedIndicator(
isManagedEventType,
fieldStates[fieldName],
translate,
fieldName,
setUnlockedFields,
options
);
};
const useLockedLabel = (fieldName: string, options?: { simple: true }) => {
if (!fieldStates[fieldName]) {
// eslint-disable-next-line react-hooks/rules-of-hooks
fieldStates[fieldName] = useState(getLockedInitState(fieldName));
}
const isLocked = fieldStates[fieldName][0];
return {
disabled:
!isManagedEventType &&
eventType.metadata?.managedEventConfig !== undefined &&
unlockedFields[fieldName as keyof Omit<Prisma.EventTypeSelect, "id">] === undefined,
LockedIcon: shouldLockIndicator(fieldName),
LockedIcon: useShouldLockIndicator(fieldName, options),
isLocked,
};
};
return { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType };
const useLockedSwitch = (fieldName: string, options = { simple: false }) => {
if (!fieldStates[fieldName]) {
// eslint-disable-next-line react-hooks/rules-of-hooks
fieldStates[fieldName] = useState(getLockedInitState(fieldName));
}
return () =>
LockedSwitch(isManagedEventType, fieldStates[fieldName], fieldName, setUnlockedFields, options);
};
const useShouldLockDisableProps = (fieldName: string, options?: { simple: true }) => {
if (!fieldStates[fieldName]) {
// eslint-disable-next-line react-hooks/rules-of-hooks
fieldStates[fieldName] = useState(getLockedInitState(fieldName));
}
return {
disabled:
!isManagedEventType &&
eventType.metadata?.managedEventConfig !== undefined &&
unlockedFields[fieldName as keyof Omit<Prisma.EventTypeSelect, "id">] === undefined,
LockedIcon: useShouldLockIndicator(fieldName, options),
isLocked: fieldStates[fieldName][0],
};
};
return {
shouldLockIndicator: useShouldLockIndicator,
shouldLockDisableProps: useShouldLockDisableProps,
useLockedLabel,
useLockedSwitch,
isManagedEventType,
isChildrenManagedEventType,
};
};
export default useLockedFieldsManager;

View File

@ -122,9 +122,7 @@ export default async function handleChildrenEventTypes({
// Define what values are expected to be changed from a managed event type
const allManagedEventTypePropsZod = _EventTypeModel.pick(allManagedEventTypeProps);
const managedEventTypeValues = allManagedEventTypePropsZod
.omit(unlockedManagedEventTypeProps)
.parse(eventType);
const managedEventTypeValues = allManagedEventTypePropsZod.parse(eventType);
// Check we are certainly dealing with a managed event type through its metadata
if (!managedEventTypeValues.metadata?.managedEventConfig)
@ -233,12 +231,13 @@ export default async function handleChildrenEventTypes({
},
data: {
...managedEventTypeValues,
locations: managedEventTypeValues.locations,
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
bookingLimits:
(managedEventTypeValues.bookingLimits as unknown as Prisma.InputJsonObject) ?? undefined,
onlyShowFirstAvailableSlot: managedEventTypeValues.onlyShowFirstAvailableSlot ?? false,
recurringEvent:
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? undefined,
(managedEventTypeValues.recurringEvent as unknown as Prisma.InputJsonValue) ?? null,
metadata: (managedEventTypeValues.metadata as Prisma.InputJsonValue) ?? undefined,
bookingFields: (managedEventTypeValues.bookingFields as Prisma.InputJsonValue) ?? undefined,
durationLimits: (managedEventTypeValues.durationLimits as Prisma.InputJsonValue) ?? undefined,

View File

@ -1,5 +1,3 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";

View File

@ -1,5 +1,3 @@
"use client";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";

View File

@ -1,5 +1,3 @@
"use client";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { UserListTable } from "@calcom/features/users/components/UserTable/UserListTable";

View File

@ -1,5 +1,3 @@
"use client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Meta } from "@calcom/ui";

View File

@ -1,5 +1,3 @@
"use client";
// import { debounce } from "lodash";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";

View File

@ -1,5 +1,3 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import type { Prisma } from "@prisma/client";
import { useSession } from "next-auth/react";

View File

@ -1,5 +1,3 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import type { Prisma } from "@prisma/client";
import { LinkIcon } from "lucide-react";

View File

@ -1,5 +1,3 @@
"use client";
import classNames from "classnames";
import dynamic from "next/dynamic";
import Head from "next/head";

Some files were not shown because too many files have changed in this diff Show More