diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86837c2f94..7b477aef5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,11 +94,13 @@ To develop locally: 6. Setup Node If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project: + ```sh nvm use ``` - + You first might need to install the specific version and then use it: + ```sh nvm install && nvm use ``` @@ -134,6 +136,7 @@ yarn test-e2e ``` #### Resolving issues + ##### E2E test browsers not installed Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`: @@ -157,4 +160,4 @@ If you get errors, be sure to fix them before committing. - Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating your PR. - If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). - Be sure to fill the PR Template accordingly. -- Review [App Contribution Guidelines](./packages/app-store/CONTRIBUTING.md) when building integrations \ No newline at end of file +- Review [App Contribution Guidelines](./packages/app-store/CONTRIBUTING.md) when building integrations diff --git a/README.md b/README.md index a2fe71fa81..1f64390a7a 100644 --- a/README.md +++ b/README.md @@ -144,17 +144,20 @@ Here is what you need to be able to run Cal.com. ``` 4. Set up your `.env` file + - Duplicate `.env.example` to `.env` - Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file. - Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file. 5. Setup Node If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project: + ```sh nvm use ``` - + You first might need to install the specific version and then use it: + ```sh nvm install && nvm use ``` @@ -234,6 +237,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env ``` 1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development + > **_NOTE:_** Required when `E2E_TEST_MAILHOG_ENABLED` is "1" ```sh @@ -273,6 +277,7 @@ yarn playwright show-report test-results/reports/playwright-html-report ``` #### Resolving issues + ##### E2E test browsers not installed Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`: @@ -492,9 +497,8 @@ following 4. Select Basecamp 4 as the product to integrate with. 5. Set the Redirect URL for OAuth `/api/integrations/basecamp3/callback` replacing Cal.com URL with the URI at which your application runs. 6. Click on done and copy the Client ID and secret into the `BASECAMP3_CLIENT_ID` and `BASECAMP3_CLIENT_SECRET` fields. -7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`. -For example, `Cal.com (support@cal.com)`. - +7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`. + For example, `Cal.com (support@cal.com)`. ### Obtaining HubSpot Client ID and Secret @@ -529,6 +533,7 @@ For example, `Cal.com (support@cal.com)`. ### Obtaining Zoho Calendar Client ID and Secret [Follow these steps](./packages/app-store/zohocalendar/) + ### Obtaining Zoho Bigin Client ID and Secret [Follow these steps](./packages/app-store/zoho-bigin/) diff --git a/apps/ai/README.md b/apps/ai/README.md index adcf03ba57..95316cd356 100644 --- a/apps/ai/README.md +++ b/apps/ai/README.md @@ -4,9 +4,9 @@ Welcome to the first stage of Cal.ai! This app lets you chat with your calendar via email: - - Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?" - - List and rearrange your bookings eg. "Cancel my next meeting" - - Answer basic questions about your busiest times eg. "How does my Tuesday look?" +- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?" +- List and rearrange your bookings eg. "Cancel my next meeting" +- Answer basic questions about your busiest times eg. "How does my Tuesday look?" The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings. @@ -24,10 +24,10 @@ If you haven't yet, please run the [root setup](/README.md) steps. Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. You'll need: - - An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4 - - A [SendGrid API key](https://app.sendgrid.com/settings/api_keys) - - A default sender email (for example, `ai@cal.dev`) - - The Cal.ai's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts)) +- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4 +- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys) +- A default sender email (for example, `ai@cal.dev`) +- The Cal.ai's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts)) To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`. diff --git a/apps/ai/src/utils/agent.ts b/apps/ai/src/utils/agent.ts index f5694160cf..1ef3319198 100644 --- a/apps/ai/src/utils/agent.ts +++ b/apps/ai/src/utils/agent.ts @@ -74,18 +74,19 @@ ${ ? `The email references the following @usernames and emails: ${users .map( (u) => - (u.id ? `, id: ${u.id}` : "id: (non user)") + - (u.username - ? u.type === "fromUsername" - ? `, username: @${u.username}` - : ", username: REDACTED" - : ", (no username)") + - (u.email - ? u.type === "fromEmail" - ? `, email: ${u.email}` - : ", email: REDACTED" - : ", (no email)") + - ";" + `${ + (u.id ? `, id: ${u.id}` : "id: (non user)") + + (u.username + ? u.type === "fromUsername" + ? `, username: @${u.username}` + : ", username: REDACTED" + : ", (no username)") + + (u.email + ? u.type === "fromEmail" + ? `, email: ${u.email}` + : ", email: REDACTED" + : ", (no email)") + };` ) .join("\n")}` : "" diff --git a/apps/api/test/lib/bookings/_post.test.ts b/apps/api/test/lib/bookings/_post.test.ts index 5e4b17f23d..a36b3b70fc 100644 --- a/apps/api/test/lib/bookings/_post.test.ts +++ b/apps/api/test/lib/bookings/_post.test.ts @@ -1,3 +1,5 @@ +import prismaMock from "../../../../../tests/libs/__mocks__/prisma"; + import type { Request, Response } from "express"; import type { NextApiRequest, NextApiResponse } from "next"; import { createMocks } from "node-mocks-http"; @@ -8,7 +10,6 @@ import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder"; import prisma from "@calcom/prisma"; -import prismaMock from "../../../../../tests/libs/__mocks__/prisma"; import handler from "../../../pages/api/bookings/_post"; type CustomNextApiRequest = NextApiRequest & Request; diff --git a/apps/web/components/apps/AppPage.tsx b/apps/web/components/apps/AppPage.tsx index 84b4102dbd..05c095caa1 100644 --- a/apps/web/components/apps/AppPage.tsx +++ b/apps/web/components/apps/AppPage.tsx @@ -265,8 +265,8 @@ export const AppPage = ({ {price !== 0 && ( - {feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar} - {feeType === "monthly" && "/" + t("month")} + {feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar} + {feeType === "monthly" && `/${t("month")}`} )} @@ -286,7 +286,7 @@ export const AppPage = ({ currency: "USD", useGrouping: false, }).format(price)} - {feeType === "monthly" && "/" + t("month")} + {feeType === "monthly" && `/${t("month")}`} )} @@ -323,7 +323,7 @@ export const AppPage = ({ target="_blank" rel="noreferrer" className="text-emphasis font-normal no-underline hover:underline" - href={"mailto:" + email}> + href={`mailto:${email}`}> {email} diff --git a/apps/web/components/apps/CalendarListContainer.tsx b/apps/web/components/apps/CalendarListContainer.tsx index 87bce77cdc..b2c8135119 100644 --- a/apps/web/components/apps/CalendarListContainer.tsx +++ b/apps/web/components/apps/CalendarListContainer.tsx @@ -130,7 +130,7 @@ function ConnectedCalendarsList(props: Props) { title={t("something_went_wrong")} message={ - {item.integration.name}:{" "} + {item.integration.name}:{" "} {t("calendar_error")} } diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 86cbb4daa8..e3f4fa7b22 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -380,7 +380,7 @@ function BookingListItem(booking: BookingItemProps) { - + {/* Time and Badges for mobile */}
@@ -576,7 +576,7 @@ const FirstAttendee = ({ e.stopPropagation()}> {user.name} @@ -590,7 +590,7 @@ type AttendeeProps = { const Attendee = ({ email, name }: AttendeeProps) => { return ( - e.stopPropagation()}> + e.stopPropagation()}> {name || email} ); diff --git a/apps/web/components/dialog/EditLocationDialog.tsx b/apps/web/components/dialog/EditLocationDialog.tsx index c1be998af3..b891235c65 100644 --- a/apps/web/components/dialog/EditLocationDialog.tsx +++ b/apps/web/components/dialog/EditLocationDialog.tsx @@ -121,7 +121,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid URL for ${eventLocationType.label}. ${ - sampleUrl ? "Sample URL: " + sampleUrl : "" + sampleUrl ? `Sample URL: ${sampleUrl}` : "" }`, }); } diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index 9379776d51..11b1366ea0 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -67,7 +67,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { - bookingFields[name] = name + " input"; + bookingFields[name] = `${name} input`; }); const eventNameObject: EventNameObjectType = { diff --git a/apps/web/components/eventtype/EventLimitsTab.tsx b/apps/web/components/eventtype/EventLimitsTab.tsx index 504bb01db3..2d422c7344 100644 --- a/apps/web/components/eventtype/EventLimitsTab.tsx +++ b/apps/web/components/eventtype/EventLimitsTab.tsx @@ -189,7 +189,7 @@ export const EventLimitsTab = ({ eventType }: Pick ({ - label: minutes + " " + t("minutes"), + label: `minutes ${t("minutes")}`, value: minutes, })), ]; @@ -225,7 +225,7 @@ export const EventLimitsTab = ({ eventType }: Pick ({ - label: minutes + " " + t("minutes"), + label: `minutes ${t("minutes")}`, value: minutes, })), ]; @@ -272,7 +272,7 @@ export const EventLimitsTab = ({ eventType }: Pick ({ - label: minutes + " " + t("minutes"), + label: `minutes ${t("minutes")}`, value: minutes, })), ]; diff --git a/apps/web/components/eventtype/EventTypeSingleLayout.tsx b/apps/web/components/eventtype/EventTypeSingleLayout.tsx index be3953160d..c0913422b9 100644 --- a/apps/web/components/eventtype/EventTypeSingleLayout.tsx +++ b/apps/web/components/eventtype/EventTypeSingleLayout.tsx @@ -247,7 +247,7 @@ function EventTypeSingleLayout({ return ( diff --git a/apps/web/components/team/screens/Team.tsx b/apps/web/components/team/screens/Team.tsx index 1bad87cf43..2b1c055915 100644 --- a/apps/web/components/team/screens/Team.tsx +++ b/apps/web/components/team/screens/Team.tsx @@ -22,7 +22,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n return (
- +

{member.name}

diff --git a/apps/web/lib/app-providers.tsx b/apps/web/lib/app-providers.tsx index 2c81796c72..a3f712ed91 100644 --- a/apps/web/lib/app-providers.tsx +++ b/apps/web/lib/app-providers.tsx @@ -179,14 +179,14 @@ function getThemeProviderProps({ ); } - const appearanceIdSuffix = themeBasis ? ":" + themeBasis : ""; + const appearanceIdSuffix = themeBasis ? `:${themeBasis}` : ""; const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined; let embedExplicitlySetThemeSuffix = ""; if (typeof window !== "undefined") { const embedTheme = window.getEmbedTheme(); if (embedTheme) { - embedExplicitlySetThemeSuffix = ":" + embedTheme; + embedExplicitlySetThemeSuffix = `:${embedTheme}`; } } diff --git a/apps/web/lib/csp.ts b/apps/web/lib/csp.ts index 4896e9b2c8..830ad7ffff 100644 --- a/apps/web/lib/csp.ts +++ b/apps/web/lib/csp.ts @@ -21,7 +21,7 @@ function getCspPolicy(nonce: string) { script-src ${ IS_PRODUCTION ? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic not supporting strict-dynamic - "'nonce-" + nonce + "' 'strict-dynamic' 'self' 'unsafe-inline' https:" + `'nonce-${nonce}' 'strict-dynamic' 'self' 'unsafe-inline' https:` : // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes "'unsafe-inline' 'unsafe-eval' https: http:" }; diff --git a/apps/web/lib/withEmbedSsr.tsx b/apps/web/lib/withEmbedSsr.tsx index 599704236e..9ee8ebe1e4 100644 --- a/apps/web/lib/withEmbedSsr.tsx +++ b/apps/web/lib/withEmbedSsr.tsx @@ -15,14 +15,9 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) { const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base"); // Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace - const newDestinationUrl = - destinationUrlObj.pathname + - "/embed?" + - destinationUrlObj.searchParams.toString() + - "&layout=" + - layout + - "&embed=" + - embed; + const newDestinationUrl = `${ + destinationUrlObj.pathname + }/embed?${destinationUrlObj.searchParams.toString()}&layout=${layout}&embed=${embed}`; return { ...ssrResponse, diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 5922803442..262e1f6e5a 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -21,11 +21,11 @@ process.env.NEXT_PUBLIC_CALCOM_VERSION = version; // So we can test deploy previews preview if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) { - process.env.NEXT_PUBLIC_WEBAPP_URL = "https://" + process.env.VERCEL_URL; + process.env.NEXT_PUBLIC_WEBAPP_URL = `https://${process.env.VERCEL_URL}`; } // Check for configuration of NEXTAUTH_URL before overriding if (!process.env.NEXTAUTH_URL && process.env.NEXT_PUBLIC_WEBAPP_URL) { - process.env.NEXTAUTH_URL = process.env.NEXT_PUBLIC_WEBAPP_URL + "/api/auth"; + process.env.NEXTAUTH_URL = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/auth`; } if (!process.env.NEXT_PUBLIC_WEBSITE_URL) { process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL; diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index 797684352e..763cd86ccf 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -125,7 +125,7 @@ export function UserPage(props: InferGetServerSidePropsType
-

😴{" " + t("user_away")}

+

😴{` ${t("user_away")}`}

{t("user_away_description") as string}

diff --git a/apps/web/pages/api/teams/googleworkspace/add.ts b/apps/web/pages/api/teams/googleworkspace/add.ts index 2955a03454..7d2f3d4763 100644 --- a/apps/web/pages/api/teams/googleworkspace/add.ts +++ b/apps/web/pages/api/teams/googleworkspace/add.ts @@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(400).json({ message: "Google client_secret missing." }); // use differnt callback to normal calendar connection - const redirect_uri = WEBAPP_URL + "/api/teams/googleworkspace/callback"; + const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`; const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const authUrl = oAuth2Client.generateAuthUrl({ diff --git a/apps/web/pages/api/teams/googleworkspace/callback.ts b/apps/web/pages/api/teams/googleworkspace/callback.ts index 07b17cfaa8..8b44c5b827 100644 --- a/apps/web/pages/api/teams/googleworkspace/callback.ts +++ b/apps/web/pages/api/teams/googleworkspace/callback.ts @@ -36,7 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (!client_secret || typeof client_secret !== "string") return res.status(400).json({ message: "Google client_secret missing." }); - const redirect_uri = WEBAPP_URL + "/api/teams/googleworkspace/callback"; + const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`; const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); if (!code) { @@ -54,11 +54,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }); if (!teamId) { - res.redirect(getSafeRedirectUrl(WEBAPP_URL + "/settings") ?? `${WEBAPP_URL}/teams`); + res.redirect(getSafeRedirectUrl(`${WEBAPP_URL}/settings`) ?? `${WEBAPP_URL}/teams`); } res.redirect( - getSafeRedirectUrl(WEBAPP_URL + `/settings/teams/${teamId}/members?inviteModal=true&bulk=true`) ?? + getSafeRedirectUrl(`${WEBAPP_URL}/settings/teams/${teamId}/members?inviteModal=true&bulk=true`) ?? `${WEBAPP_URL}/teams` ); } diff --git a/apps/web/pages/apps/categories/index.tsx b/apps/web/pages/apps/categories/index.tsx index 936e98804b..7b40ade47b 100644 --- a/apps/web/pages/apps/categories/index.tsx +++ b/apps/web/pages/apps/categories/index.tsx @@ -31,7 +31,7 @@ export default function Apps({ categories }: inferSSRProps (
diff --git a/apps/web/pages/auth/sso/[provider].tsx b/apps/web/pages/auth/sso/[provider].tsx index a95be8c182..f83c5e455b 100644 --- a/apps/web/pages/auth/sso/[provider].tsx +++ b/apps/web/pages/auth/sso/[provider].tsx @@ -30,12 +30,12 @@ export default function Provider(props: SSOProviderPageProps) { const email = searchParams?.get("email"); if (!email) { - router.push("/auth/error?error=" + "Email not provided"); + router.push(`/auth/error?error=Email not provided`); return; } if (!props.isSAMLLoginEnabled) { - router.push("/auth/error?error=" + "SAML login not enabled"); + router.push(`/auth/error?error=SAML login not enabled`); return; } @@ -56,7 +56,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const providerParam = asStringOrNull(context.query.provider); const emailParam = asStringOrNull(context.query.email); const usernameParam = asStringOrNull(context.query.username); - const successDestination = "/getting-started" + (usernameParam ? `?username=${usernameParam}` : ""); + const successDestination = `/getting-started${usernameParam ? `?username=${usernameParam}` : ""}`; if (!providerParam) { throw new Error(`File is not named sso/[provider]`); } @@ -120,7 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => if (error) { return { redirect: { - destination: "/auth/error?error=" + error, + destination: `/auth/error?error=${error}`, permanent: false, }, }; diff --git a/apps/web/pages/auth/verify.tsx b/apps/web/pages/auth/verify.tsx index 12d2c4c4f5..8f6193d5ef 100644 --- a/apps/web/pages/auth/verify.tsx +++ b/apps/web/pages/auth/verify.tsx @@ -120,7 +120,7 @@ export default function Verify() { ? "Your payment failed" : sessionId ? "Payment successful!" - : "Verify your email" + " | " + APP_NAME} + : `Verify your email | ${APP_NAME}`}
diff --git a/apps/web/pages/availability/[schedule].tsx b/apps/web/pages/availability/[schedule].tsx index d4dd83c381..da393efe05 100644 --- a/apps/web/pages/availability/[schedule].tsx +++ b/apps/web/pages/availability/[schedule].tsx @@ -151,7 +151,7 @@ export default function Availability() { return (
+ download={`${props.eventType.title}.ics`}> + data-testid={`event-type-title-${type.id}`}> {type.title} {group.profile.slug ? ( + data-testid={`event-type-slug-${type.id}`}> {`/${ type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder") }/${type.slug}`} @@ -177,13 +177,13 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
+ data-testid={`event-type-title-${type.id}`}> {type.title} {group.profile.slug ? ( + data-testid={`event-type-slug-${type.id}`}> {`/${group.profile.slug}/${type.slug}`} ) : null} @@ -479,7 +479,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL )} - +
- + {active && ( -