feat: validate `CalProvider` api keys (#12672)
* fix: signup nit (#12585) * Disable submit on empty form * Fix submit --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com> * New Crowdin translations by Github Action * fix: Signup options are not disabled (#12610) * fix: Signup options are not disabled * fixes/signup disabling suggested changes done * chore: signup and login improvements --------- Co-authored-by: Udit Takkar <udit222001@gmail.com> * fix: typo in @calcom/emails readme (#12615) * fix: handle reschedule request for dynamic meetings (#12275) * chore: [app-router-migration-1] migrate the pages in `settings/admin` to the app directory (#12561) Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com> Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> * chore: added cursor-pointer to img upload (#12624) * added cursor-pointer to img upload * nit * feat: add clear filters option in bookings page (#12629) * add clear filters option * fix vscode settings.json * use removeAllQueryParams() * fix yarn lock * remove toggleoption * feat: display long durations in hours on booking (#12631) Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> * New Crowdin translations by Github Action * fix: workaround for future app dir routes * feat: Allow only first slot to be booked (#12636) Co-authored-by: Morgan Vernay <morgan@cal.com> * New Crowdin translations by Github Action * feat: add matomo analytics app (#12646) * chore: Sentry Wrapper with Performance and Error Tracing (#12642) * add wrapper for sentry and update functions in 'getUserAvailability'. Update tracesSampleRate to 1.0 * Make Sentry Wrapper utilize parent transaction, if it exists. * Update wrapper for functions to inherit parameters from the child function * add comment of when to use the wrapper * check for sentry before wrapping, if not call unwrapped function * refactored wrapper to have async and sync separate functions that utilize helpers for common behaviour * update type of args to unknown * fixed types of returns from wrapped functions --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> * validate api keys to set error and key states for api keys * organize error messages into one place * set error messages from errors file instead of hardcoding value * fix incorrect constant name --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: Pratik Kumar <70286186+Pratik-Kumar-621@users.noreply.github.com> Co-authored-by: Udit Takkar <udit222001@gmail.com> Co-authored-by: Samyabrata Maji <116789799+samyabrata-maji@users.noreply.github.com> Co-authored-by: Manpreet Singh <manpoffc@gmail.com> Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com> Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Varun Prahlad Balani <varunprahladbalani@gmail.com> Co-authored-by: Mike Zhou <mikezhoudev@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Haran Rajkumar <haranrajkumar97@gmail.com> Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Harshith Pabbati <pabbatiharshith@gmail.com> Co-authored-by: Brendan Woodward <73412688+bwoody13@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
parent
f0401e1f86
commit
ffba6d6d66
|
@ -290,3 +290,4 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
|||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
||||
APP_ROUTER_SETTINGS_ADMIN_ENABLED=1
|
|
@ -24,6 +24,8 @@ runs:
|
|||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: yarn build
|
||||
- run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn build
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
|
|
|
@ -60,6 +60,7 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
|
|||
successRedirectUrl: true,
|
||||
locations: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
})
|
||||
.merge(
|
||||
|
@ -147,6 +148,7 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
|||
seatsShowAvailabilityCount: true,
|
||||
bookingFields: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
}).merge(
|
||||
z.object({
|
||||
|
|
|
@ -5,6 +5,7 @@ import z from "zod";
|
|||
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
|
@ -27,7 +28,6 @@ export const abTestMiddlewareFactory =
|
|||
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
|
||||
|
||||
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
|
||||
|
||||
const enabled = route !== null ? route[1] || override : false;
|
||||
|
||||
if (pathname.includes("future") || !enabled) {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import type { TRPCContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx);
|
|
@ -8,33 +8,8 @@ import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
|
|||
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
||||
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
||||
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
||||
import { ENDPOINTS } from "@calcom/trpc/react/shared";
|
||||
|
||||
const ENDPOINTS = [
|
||||
"admin",
|
||||
"apiKeys",
|
||||
"appRoutingForms",
|
||||
"apps",
|
||||
"auth",
|
||||
"availability",
|
||||
"appBasecamp3",
|
||||
"bookings",
|
||||
"deploymentSetup",
|
||||
"eventTypes",
|
||||
"features",
|
||||
"insights",
|
||||
"payments",
|
||||
"public",
|
||||
"saml",
|
||||
"slots",
|
||||
"teams",
|
||||
"organizations",
|
||||
"users",
|
||||
"viewer",
|
||||
"webhook",
|
||||
"workflows",
|
||||
"appsRouter",
|
||||
"googleWorkspace",
|
||||
] as const;
|
||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export type Params = {
|
||||
[param: string]: string | string[] | undefined;
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { headers } from "next/headers";
|
||||
import { type ReactElement } from "react";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
|
||||
|
||||
type WrapperWithLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import Page from "@pages/settings/admin/apps/[category]";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("apps"),
|
||||
(t) => t("admin_apps_description")
|
||||
);
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,10 @@
|
|||
import Page from "@pages/settings/admin/apps/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("apps"),
|
||||
(t) => t("admin_apps_description")
|
||||
);
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,10 @@
|
|||
import Page from "@pages/settings/admin/flags";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Feature Flags",
|
||||
() => "Here you can toggle your Cal.com instance features."
|
||||
);
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,10 @@
|
|||
import Page from "@pages/settings/admin/impersonation";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("admin"),
|
||||
(t) => t("impersonation")
|
||||
);
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,10 @@
|
|||
import Page from "@pages/settings/admin/oAuth/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "OAuth",
|
||||
() => "Add new OAuth Clients"
|
||||
);
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,10 @@
|
|||
import Page from "@pages/settings/admin/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Admin",
|
||||
() => "admin_description"
|
||||
);
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,20 @@
|
|||
// pages containing layout (e.g., /availability/[schedule].tsx) are supposed to go under (no-layout) folder
|
||||
import { headers } from "next/headers";
|
||||
import { type ReactElement } from "react";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type WrapperWithoutLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default async function WrapperWithoutLayout({ children }: WrapperWithoutLayoutProps) {
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import Page from "@pages/settings/admin/oAuth/oAuthView";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "OAuth",
|
||||
() => "Add new OAuth Clients"
|
||||
);
|
||||
|
||||
export default Page;
|
|
@ -0,0 +1,21 @@
|
|||
import { headers } from "next/headers";
|
||||
import { type ReactElement } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type WrapperWithLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import Page from "@pages/settings/admin/organizations/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("organizations"),
|
||||
(t) => t("orgs_page_description")
|
||||
);
|
||||
|
||||
export default function AppPage() {
|
||||
// @ts-expect-error FIXME Property 'Component' is incompatible with index signature
|
||||
return <Page />;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import Page from "@pages/settings/admin/users/[id]/edit";
|
||||
import { getServerCaller } from "app/_trpc/serverClient";
|
||||
import { type Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { z } from "zod";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const userIdSchema = z.object({ id: z.coerce.number() });
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Params }) => {
|
||||
const input = userIdSchema.safeParse(params);
|
||||
|
||||
let title = "";
|
||||
if (!input.success) {
|
||||
title = "Editing user";
|
||||
} else {
|
||||
const req = {
|
||||
headers: headers(),
|
||||
cookies: cookies(),
|
||||
};
|
||||
|
||||
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest'
|
||||
const data = await getServerCaller({ req, prisma }).viewer.users.get({ userId: input.data.id });
|
||||
const { user } = data;
|
||||
title = `Editing user: ${user.username}`;
|
||||
}
|
||||
|
||||
return await _generateMetadata(
|
||||
() => title,
|
||||
() => "Here you can edit a current user."
|
||||
);
|
||||
};
|
||||
|
||||
export default function AppPage() {
|
||||
// @ts-expect-error FIXME AppProps | undefined' does not satisfy the constraint 'PageProps'
|
||||
return <Page />;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import Page from "@pages/settings/admin/users/add";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Add new user",
|
||||
() => "Here you can add a new user."
|
||||
);
|
||||
|
||||
export default function AppPage() {
|
||||
// @ts-expect-error FIXME AppProps | undefined' does not satisfy the constraint 'PageProps'
|
||||
return <Page />;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import Page from "@pages/settings/admin/users/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Users",
|
||||
() => "A list of all the users in your account including their name, title, email and role."
|
||||
);
|
||||
|
||||
export default function AppPage() {
|
||||
// @ts-expect-error FIXME Property 'Component' is incompatible with index signature
|
||||
return <Page />;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
"use client";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { ComponentProps } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import type Shell from "@calcom/features/shell/Shell";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { ErrorBoundary } from "@calcom/ui";
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
const pathname = usePathname();
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
// Force redirect on component level
|
||||
useEffect(() => {
|
||||
if (session.data && session.data.user.role !== UserPermissionRole.ADMIN) {
|
||||
router.replace("/settings/my-account/profile");
|
||||
}
|
||||
}, [session, router]);
|
||||
|
||||
const isAppsPage = pathname?.startsWith("/settings/admin/apps");
|
||||
return (
|
||||
<SettingsLayout {...rest}>
|
||||
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
|
||||
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getLayout = (page: React.ReactElement) => <AdminLayout>{page}</AdminLayout>;
|
|
@ -298,6 +298,29 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="onlyShowFirstAvailableSlot"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => {
|
||||
const isChecked = value;
|
||||
return (
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
labelClassName="text-sm"
|
||||
title={t("limit_booking_only_first_slot")}
|
||||
description={t("limit_booking_only_first_slot_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
|
||||
isChecked && "rounded-b-none"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="durationLimits"
|
||||
control={formMethods.control}
|
||||
|
|
|
@ -101,6 +101,8 @@ export const config = {
|
|||
"/apps/routing_forms/:path*",
|
||||
"/event-types",
|
||||
"/future/event-types/",
|
||||
"/settings/admin/:path*",
|
||||
"/future/settings/admin/:path*",
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -231,6 +231,9 @@ const nextConfig = {
|
|||
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
|
||||
// by next.js will be dropped. Doesn't make much sense, but how it is
|
||||
fs: false,
|
||||
// ignore module resolve errors caused by the server component bundler
|
||||
"pg-native": false,
|
||||
"superagent-proxy": false,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,7 +13,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
res.setHeader("Content-Type", "text/html");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||
res.write(
|
||||
renderEmail("MonthlyDigestEmail", {
|
||||
await renderEmail("MonthlyDigestEmail", {
|
||||
language: t,
|
||||
Created: 12,
|
||||
Completed: 13,
|
||||
|
|
|
@ -251,6 +251,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
|
|||
<Button
|
||||
color="secondary"
|
||||
className="w-full justify-center"
|
||||
disabled={formState.isSubmitting}
|
||||
data-testid="google"
|
||||
StartIcon={FaGoogle}
|
||||
onClick={async (e) => {
|
||||
|
|
|
@ -131,6 +131,7 @@ export type FormValues = {
|
|||
successRedirectUrl: string;
|
||||
durationLimits?: IntervalLimit;
|
||||
bookingLimits?: IntervalLimit;
|
||||
onlyShowFirstAvailableSlot: boolean;
|
||||
children: ChildrenEventType[];
|
||||
hosts: { userId: number; isFixed: boolean }[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
|
@ -250,6 +251,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
description: eventType.description ?? undefined,
|
||||
schedule: eventType.schedule || undefined,
|
||||
bookingLimits: eventType.bookingLimits || undefined,
|
||||
onlyShowFirstAvailableSlot: eventType.onlyShowFirstAvailableSlot || undefined,
|
||||
durationLimits: eventType.durationLimits || undefined,
|
||||
length: eventType.length,
|
||||
hidden: eventType.hidden,
|
||||
|
@ -429,6 +431,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
bookingLimits,
|
||||
onlyShowFirstAvailableSlot,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
locations,
|
||||
|
@ -491,6 +494,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
beforeEventBuffer: beforeBufferTime,
|
||||
afterEventBuffer: afterBufferTime,
|
||||
bookingLimits,
|
||||
onlyShowFirstAvailableSlot,
|
||||
durationLimits,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
|
@ -532,6 +536,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
bookingLimits,
|
||||
onlyShowFirstAvailableSlot,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
locations,
|
||||
|
@ -584,6 +589,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
beforeEventBuffer: beforeBufferTime,
|
||||
afterEventBuffer: afterBufferTime,
|
||||
bookingLimits,
|
||||
onlyShowFirstAvailableSlot,
|
||||
durationLimits,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
"use client";
|
||||
export { default } from "./[category]";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRef } from "react";
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import UsersEditView from "@calcom/features/ee/users/pages/users-edit-view";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import UsersAddView from "@calcom/features/ee/users/pages/users-add-view";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import UsersListingView from "@calcom/features/ee/users/pages/users-listing-view";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
|
|
|
@ -122,21 +122,23 @@ function UsernameField({
|
|||
/>
|
||||
{(!formState.isSubmitting || !formState.isSubmitted) && (
|
||||
<div className="text-gray text-default flex items-center text-sm">
|
||||
<p className="flex items-center text-sm ">
|
||||
<div className="text-sm ">
|
||||
{usernameTaken ? (
|
||||
<div className="text-error">
|
||||
<div className="text-error flex items-center">
|
||||
<Info className="mr-1 inline-block h-4 w-4" />
|
||||
{t("already_in_use_error")}
|
||||
<p>{t("already_in_use_error")}</p>
|
||||
</div>
|
||||
) : premium ? (
|
||||
<div data-testid="premium-username-warning">
|
||||
<div data-testid="premium-username-warning" className="flex items-center">
|
||||
<StarIcon className="mr-1 inline-block h-4 w-4" />
|
||||
{t("premium_username", {
|
||||
price: getPremiumPlanPriceValue(),
|
||||
})}
|
||||
<p>
|
||||
{t("premium_username", {
|
||||
price: getPremiumPlanPriceValue(),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -161,6 +163,7 @@ export default function Signup({
|
|||
}: SignupProps) {
|
||||
const [premiumUsername, setPremiumUsername] = useState(false);
|
||||
const [usernameTaken, setUsernameTaken] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
|
||||
const searchParams = useCompatSearchParams();
|
||||
const telemetry = useTelemetry();
|
||||
|
@ -245,21 +248,21 @@ export default function Signup({
|
|||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}>
|
||||
<div className="bg-muted 2xl:border-subtle grid max-h-[800px] w-full max-w-[1440px] grid-cols-1 grid-rows-1 lg:grid-cols-2 2xl:rounded-lg 2xl:border ">
|
||||
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
|
||||
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
||||
<div className="flex w-full flex-col px-4 py-6 sm:px-16 md:px-24 2xl:px-28">
|
||||
<div className="flex w-full flex-col px-4 pt-6 sm:px-16 md:px-20 2xl:px-28">
|
||||
{/* Header */}
|
||||
{errors.apiError && (
|
||||
<Alert severity="error" message={errors.apiError?.message} data-testid="signup-error-message" />
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="font-cal text-[28px] ">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="font-cal text-[28px] leading-none ">
|
||||
{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
|
||||
</h1>
|
||||
{IS_CALCOM ? (
|
||||
<p className="text-subtle text-base font-medium leading-6">{t("cal_signup_description")}</p>
|
||||
<p className="text-subtle text-base font-medium leading-5">{t("cal_signup_description")}</p>
|
||||
) : (
|
||||
<p className="text-subtle text-base font-medium leading-6">
|
||||
<p className="text-subtle text-base font-medium leading-5">
|
||||
{t("calcom_explained", {
|
||||
appName: APP_NAME,
|
||||
})}
|
||||
|
@ -318,6 +321,9 @@ export default function Signup({
|
|||
disabled={
|
||||
!!formMethods.formState.errors.username ||
|
||||
!!formMethods.formState.errors.email ||
|
||||
!formMethods.getValues("email") ||
|
||||
!formMethods.getValues("password") ||
|
||||
isSubmitting ||
|
||||
usernameTaken
|
||||
}>
|
||||
{premiumUsername && !usernameTaken
|
||||
|
@ -344,11 +350,22 @@ export default function Signup({
|
|||
<Button
|
||||
color="secondary"
|
||||
disabled={!!formMethods.formState.errors.username || premiumUsername}
|
||||
loading={isGoogleLoading}
|
||||
StartIcon={() => (
|
||||
<>
|
||||
<img
|
||||
className={classNames("text-subtle mr-2 h-4 w-4", premiumUsername && "opacity-50")}
|
||||
src="/google-icon.svg"
|
||||
alt=""
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
className={classNames(
|
||||
"w-full justify-center rounded-md text-center",
|
||||
formMethods.formState.errors.username ? "opacity-50" : ""
|
||||
)}
|
||||
onClick={async () => {
|
||||
setIsGoogleLoading(true);
|
||||
const username = formMethods.getValues("username");
|
||||
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
const GOOGLE_AUTH_URL = `${baseUrl}/auth/sso/google`;
|
||||
|
@ -362,11 +379,6 @@ export default function Signup({
|
|||
}
|
||||
router.push(GOOGLE_AUTH_URL);
|
||||
}}>
|
||||
<img
|
||||
className={classNames("text-emphasis mr-2 h-5 w-5", premiumUsername && "opacity-50")}
|
||||
src="/google-icon.svg"
|
||||
alt=""
|
||||
/>
|
||||
Google
|
||||
</Button>
|
||||
) : null}
|
||||
|
@ -376,7 +388,9 @@ export default function Signup({
|
|||
disabled={
|
||||
!!formMethods.formState.errors.username ||
|
||||
!!formMethods.formState.errors.email ||
|
||||
premiumUsername
|
||||
premiumUsername ||
|
||||
isSubmitting ||
|
||||
isGoogleLoading
|
||||
}
|
||||
className={classNames(
|
||||
"w-full justify-center rounded-md text-center",
|
||||
|
@ -390,6 +404,7 @@ export default function Signup({
|
|||
}
|
||||
if (!formMethods.getValues("email")) {
|
||||
formMethods.trigger("email");
|
||||
|
||||
return;
|
||||
}
|
||||
const username = formMethods.getValues("username");
|
||||
|
@ -410,17 +425,20 @@ export default function Signup({
|
|||
)}
|
||||
</div>
|
||||
{/* Already have an account & T&C */}
|
||||
<div className="mt-6">
|
||||
<div className="mt-10 flex h-full flex-col justify-end text-xs">
|
||||
<div className="flex flex-col text-sm">
|
||||
<Link href="/auth/login" className="text-emphasis hover:underline">
|
||||
{t("already_have_account")}
|
||||
</Link>
|
||||
<div className="flex gap-1">
|
||||
<p className="text-subtle">{t("already_have_account")}</p>
|
||||
<Link href="/auth/login" className="text-emphasis hover:underline">
|
||||
{t("sign_in")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-subtle">
|
||||
By signing up, you agree to our{" "}
|
||||
<Link className="text-emphasis hover:underline" href={`${WEBSITE_URL}/terms`}>
|
||||
Terms of Service{" "}
|
||||
Terms{" "}
|
||||
</Link>
|
||||
<span>and</span>{" "}
|
||||
<span>&</span>{" "}
|
||||
<Link className="text-emphasis hover:underline" href={`${WEBSITE_URL}/privacy`}>
|
||||
Privacy Policy.
|
||||
</Link>
|
||||
|
@ -428,7 +446,12 @@ export default function Signup({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-subtle border-subtle hidden w-full flex-col justify-between rounded-l-2xl py-12 pl-12 lg:flex">
|
||||
<div
|
||||
className="border-subtle hidden w-full flex-col justify-between rounded-l-2xl border py-12 pl-12 lg:flex"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(162.05% 170% at 109.58% 35%, rgba(102, 117, 147, 0.7) 0%, rgba(212, 212, 213, 0.4) 100%) ",
|
||||
}}>
|
||||
{IS_CALCOM && (
|
||||
<div className="mb-12 mr-12 grid h-full w-full grid-cols-4 gap-4 ">
|
||||
<div className="">
|
||||
|
@ -443,7 +466,7 @@ export default function Signup({
|
|||
</div>
|
||||
)}
|
||||
<div
|
||||
className="rounded-2xl border-y border-l border-dashed border-[#D1D5DB5A] py-[6px] pl-[6px]"
|
||||
className="border-default rounded-bl-2xl rounded-br-none rounded-tl-2xl border-dashed py-[6px] pl-[6px]"
|
||||
style={{
|
||||
backgroundColor: "rgba(236,237,239,0.9)",
|
||||
}}>
|
||||
|
|
|
@ -48,8 +48,6 @@ test.describe("Organization", () => {
|
|||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await expect(newPage.locator(".text-red-700")).toHaveCount(3); // 3 password hints
|
||||
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await newPage.waitForURL("/getting-started?from=signup");
|
||||
|
@ -78,8 +76,8 @@ test.describe("Organization", () => {
|
|||
await inviteLinkPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await expect(inviteLinkPage.locator(".text-red-700")).toHaveCount(4); // email + 3 password hints
|
||||
const button = inviteLinkPage.locator("button[type=submit][disabled]");
|
||||
await expect(button).toBeVisible(); // email + 3 password hints
|
||||
|
||||
// Happy path
|
||||
await inviteLinkPage.locator("input[name=email]").fill(`rick@domain-${Date.now()}.com`);
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Settings/admin A/B tests", () => {
|
||||
test("should point to the /future/settings/admin page", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/settings/admin");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Feature Flags" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
});
|
|
@ -51,8 +51,10 @@ test.describe("Team", () => {
|
|||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await expect(newPage.locator('[data-testid="hint-error"]')).toHaveCount(3);
|
||||
const button = newPage.locator("button[type=submit][disabled]");
|
||||
await expect(button).toBeVisible(); // email + 3 password hints
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await newPage.waitForURL("/getting-started?from=signup");
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "تمكين التطبيقات",
|
||||
"enable_apps_description": "تمكين التطبيقات التي يمكن للمستخدمين دمجها مع {{appName}}",
|
||||
"purchase_license": "شراء ترخيص",
|
||||
"already_have_account": "هل لديك حساب بالفعل؟",
|
||||
"already_have_key": "لدي مفتاح بالفعل:",
|
||||
"already_have_key_suggestion": "يرجى نسخ متغير البيئة الحالي CALCOM_LICENSE_KEY هنا.",
|
||||
"app_is_enabled": "تم تمكين {{appName}}",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Povolit aplikace",
|
||||
"enable_apps_description": "Zapněte aplikace, které mohou uživatelé integrovat s aplikací {{appName}}",
|
||||
"purchase_license": "Kupte si licenci",
|
||||
"already_have_account": "Už máte účet?",
|
||||
"already_have_key": "Klíč již mám:",
|
||||
"already_have_key_suggestion": "Zkopírujte sem svou stávající proměnnou prostředí CALCOM_LICENSE_KEY.",
|
||||
"app_is_enabled": "Aplikace {{appName}} je povolena",
|
||||
|
|
|
@ -1367,6 +1367,7 @@
|
|||
"disabled_calendar": "Hvis du har en anden kalender installeret vil nye bookinger blive tilføjet til den. Hvis ikke, så forbind en ny kalender, så du ikke går glip af nye bookinger.",
|
||||
"enable_apps": "Aktivér Apps",
|
||||
"purchase_license": "Køb en licens",
|
||||
"already_have_account": "Har du allerede en konto?",
|
||||
"already_have_key": "Jeg har allerede en nøgle:",
|
||||
"already_have_key_suggestion": "Kopiér venligst din eksisterende CALCOM_LICENSE_KEY environment variabel her.",
|
||||
"app_is_enabled": "{{appName}} er aktiveret",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Apps aktivieren",
|
||||
"enable_apps_description": "Apps aktivieren, die Benutzer mit {{appName}} integrieren können",
|
||||
"purchase_license": "Lizenz kaufen",
|
||||
"already_have_account": "Haben Sie bereits ein Konto?",
|
||||
"already_have_key": "Ich habe bereits einen Schlüssel:",
|
||||
"already_have_key_suggestion": "Bitte fügen Sie Ihre vorhandene CALCOM_LICENSE_KEY Umgebungsvariable hier ein.",
|
||||
"app_is_enabled": "{{appName}} ist aktiviert",
|
||||
|
|
|
@ -274,5 +274,6 @@
|
|||
"event_name_tooltip": "Το όνομα που θα εμφανίζεται στα ημερολόγια",
|
||||
"label": "Ετικέτα",
|
||||
"edit": "Επεξεργασία",
|
||||
"disable_guests": "Απενεργοποίηση επισκεπτών"
|
||||
"disable_guests": "Απενεργοποίηση επισκεπτών",
|
||||
"already_have_account": "Έχετε ήδη λογαριασμό;"
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
"cannot_repackage_codebase": "You can not repackage or sell the codebase",
|
||||
"acquire_license": "Acquire a commercial license to remove these terms by emailing",
|
||||
"terms_summary": "Summary of terms",
|
||||
"signing_up_terms":"By signing up, you agree to our <2>Terms of Service</2> and <3>Privacy Policy</3>.",
|
||||
"signing_up_terms":"By signing up you agree to our <2>Terms</2> & <3>Privacy Policy</3>.",
|
||||
"open_env": "Open .env and agree to our License",
|
||||
"env_changed": "I've changed my .env",
|
||||
"accept_license": "Accept License",
|
||||
|
@ -673,6 +673,7 @@
|
|||
"default_duration": "Default duration",
|
||||
"default_duration_no_options": "Please choose available durations first",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "Minutes",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Cycle meetings between multiple team members.",
|
||||
|
@ -1480,6 +1481,8 @@
|
|||
"report_app": "Report app",
|
||||
"limit_booking_frequency": "Limit booking frequency",
|
||||
"limit_booking_frequency_description": "Limit how many times this event can be booked",
|
||||
"limit_booking_only_first_slot": "Limit booking only first slot",
|
||||
"limit_booking_only_first_slot_description": "Allow only the first slot of every day to be booked",
|
||||
"limit_total_booking_duration": "Limit total booking duration",
|
||||
"limit_total_booking_duration_description": "Limit total amount of time that this event can be booked",
|
||||
"add_limit": "Add Limit",
|
||||
|
@ -1607,7 +1610,7 @@
|
|||
"enable_apps": "Enable Apps",
|
||||
"enable_apps_description": "Enable apps that users can integrate with {{appName}}",
|
||||
"purchase_license": "Purchase a License",
|
||||
"already_have_account":"I already have an account",
|
||||
"already_have_account":"Already have an account?",
|
||||
"already_have_key": "I already have a key:",
|
||||
"already_have_key_suggestion": "Please copy your existing CALCOM_LICENSE_KEY environment variable here.",
|
||||
"app_is_enabled": "{{appName}} is enabled",
|
||||
|
@ -1865,6 +1868,7 @@
|
|||
"looking_for_more_analytics": "Looking for more analytics?",
|
||||
"looking_for_more_insights": "Looking for more Insights?",
|
||||
"add_filter": "Add filter",
|
||||
"remove_filters": "Clear all filters",
|
||||
"select_user": "Select User",
|
||||
"select_event_type": "Select Event Type",
|
||||
"select_date_range": "Select Date Range",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Activar aplicaciones",
|
||||
"enable_apps_description": "Habilite las aplicaciones que los usuarios pueden integrar con {{appName}}",
|
||||
"purchase_license": "Compre una licencia",
|
||||
"already_have_account": "¿Ya tienes una cuenta?",
|
||||
"already_have_key": "Ya tengo una clave:",
|
||||
"already_have_key_suggestion": "Copie aquí su variable de entorno CALCOM_LICENSE_KEY existente.",
|
||||
"app_is_enabled": "{{appName}} está activada",
|
||||
|
|
|
@ -752,6 +752,7 @@
|
|||
"enter_email_or_username": "Sartu email edo erabiltzaile izen bat",
|
||||
"team_name_taken": "Izen hau dagoeneko hartua dago",
|
||||
"must_enter_team_name": "Taldearentzat izen bat behar da",
|
||||
"already_have_account": "Baduzu kontua dagoeneko?",
|
||||
"fill_this_field": "Mesedez, bete ezazu eremu hau",
|
||||
"options": "Aukerak",
|
||||
"add_an_option": "Gehitu aukera bat",
|
||||
|
|
|
@ -660,6 +660,7 @@
|
|||
"default_duration": "Durée par défaut",
|
||||
"default_duration_no_options": "Veuillez d'abord choisir les durées disponibles",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "minutes",
|
||||
"round_robin": "Round-robin",
|
||||
"round_robin_description": "Alternez vos rendez-vous entre plusieurs membres d'équipe.",
|
||||
|
@ -1843,6 +1844,7 @@
|
|||
"looking_for_more_analytics": "Vous cherchez plus d'analyses ?",
|
||||
"looking_for_more_insights": "Vous cherchez plus de statistiques ?",
|
||||
"add_filter": "Ajouter un filtre",
|
||||
"remove_filters": "Effacer les filtres",
|
||||
"select_user": "Sélectionner un utilisateur",
|
||||
"select_event_type": "Sélectionner un type d'événement",
|
||||
"select_date_range": "Sélectionner une plage de dates",
|
||||
|
@ -2085,11 +2087,17 @@
|
|||
"overlay_my_calendar": "Superposer mon calendrier",
|
||||
"overlay_my_calendar_toc": "En vous connectant à votre calendrier, vous acceptez notre politique de confidentialité et nos conditions d'utilisation. Vous pouvez révoquer cet accès à tout moment.",
|
||||
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
|
||||
"calendars_were_checking_for_conflicts": "Calendriers dont nous vérifions les conflits",
|
||||
"availabilty_schedules": "Horaires de disponibilité",
|
||||
"manage_calendars": "Gérer les calendriers",
|
||||
"manage_availability_schedules": "Gérer les horaires de disponibilité",
|
||||
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
|
||||
"number_in_international_format": "Veuillez entrer le numéro au format international.",
|
||||
"extensive_whitelabeling": "Marque blanche étendue",
|
||||
"unlimited_teams": "Équipes illimitées",
|
||||
"troubleshooter_tooltip": "Ouvrez l'outil de dépannage et déterminez ce qui ne va pas avec votre planning",
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"troubleshooter": "Dépannage",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "הפעלת אפליקציות",
|
||||
"enable_apps_description": "הפעל/הפעילי אפליקציות שמשתמשים יוכלו לשלב עם {{appName}}",
|
||||
"purchase_license": "רכוש רישיון",
|
||||
"already_have_account": "כבר יש לך חשבון?",
|
||||
"already_have_key": "כבר יש לי מפתח:",
|
||||
"already_have_key_suggestion": "אנא העתק את משתנה הסביבה CALCOM_LICENSE_KEY הקיים שלך לכאן.",
|
||||
"app_is_enabled": "האפליקציה {{appName}} מופעלת",
|
||||
|
|
|
@ -333,5 +333,6 @@
|
|||
"dark": "Tamna",
|
||||
"automatically_adjust_theme": "Automatski prilagodite temu na temelju preferencija pozvanih osoba",
|
||||
"user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije",
|
||||
"full_name": "Puno ime"
|
||||
"full_name": "Puno ime",
|
||||
"already_have_account": "Već imate račun?"
|
||||
}
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Abilita app",
|
||||
"enable_apps_description": "Abilita le app che gli utenti possono integrare con {{appName}}",
|
||||
"purchase_license": "Acquista una licenza",
|
||||
"already_have_account": "Hai già un account?",
|
||||
"already_have_key": "Ho già una chiave:",
|
||||
"already_have_key_suggestion": "Copia qui la variabile di ambiente CALCOM_LICENSE_KEY esistente.",
|
||||
"app_is_enabled": "{{appName}} è abilitato",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "アプリを有効にする",
|
||||
"enable_apps_description": "ユーザーが {{appName}} と連携できるアプリを有効にします",
|
||||
"purchase_license": "ライセンスを購入",
|
||||
"already_have_account": "既にアカウントをお持ちですか?",
|
||||
"already_have_key": "すでにキーを持っています:",
|
||||
"already_have_key_suggestion": "ここに既存の CALCOM_LICENSE_KEY 環境変数をコピーしてください。",
|
||||
"app_is_enabled": "{{appName}} は有効です",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "앱 활성화",
|
||||
"enable_apps_description": "사용자가 {{appName}} 앱과 통합할 수 있는 앱 활성화",
|
||||
"purchase_license": "라이선스 구매",
|
||||
"already_have_account": "이미 계정이 있으신가요?",
|
||||
"already_have_key": "이미 키가 있습니다:",
|
||||
"already_have_key_suggestion": "기존 CALCOM_LICENSE_KEY 환경 변수를 여기에 복사하십시오.",
|
||||
"app_is_enabled": "{{appName}} 앱이 활성화되었습니다",
|
||||
|
|
|
@ -124,5 +124,6 @@
|
|||
"already_have_an_account": "Vai jums jau ir konts?",
|
||||
"create_account": "Izveidot Kontu",
|
||||
"confirm_password": "Apstiprināt paroli",
|
||||
"confirm_auth_change": "Šis mainīs veidu, kā jūs autorizējaties"
|
||||
"confirm_auth_change": "Šis mainīs veidu, kā jūs autorizējaties",
|
||||
"already_have_account": "Vai jums jau ir konts?"
|
||||
}
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Apps inschakelen",
|
||||
"enable_apps_description": "Schakel apps in die gebruikers kunnen integreren met {{appName}}",
|
||||
"purchase_license": "Koop een licentie",
|
||||
"already_have_account": "Heeft u al een account?",
|
||||
"already_have_key": "Ik heb al een code:",
|
||||
"already_have_key_suggestion": "Kopieer hier uw bestaande CALCOM_LICENSE_KEY-omgevingsvariabele.",
|
||||
"app_is_enabled": "{{appName}} is ingeschakeld",
|
||||
|
|
|
@ -1338,6 +1338,7 @@
|
|||
"app_disabled_subject": "{{appName}} har blitt deaktivert",
|
||||
"navigate_installed_apps": "Gå til installerte apper",
|
||||
"enable_apps": "Aktiver Apper",
|
||||
"already_have_account": "Har du allerede en bruker?",
|
||||
"app_is_enabled": "{{appName}} er aktivert",
|
||||
"app_is_disabled": "{{appName}} er deaktivert",
|
||||
"disable_app": "Deaktiver App",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Włącz aplikacje",
|
||||
"enable_apps_description": "Włącz aplikacje, które użytkownicy mogą zintegrować z aplikacją {{appName}}",
|
||||
"purchase_license": "Kup licencję",
|
||||
"already_have_account": "Masz już konto?",
|
||||
"already_have_key": "Mam już klucz:",
|
||||
"already_have_key_suggestion": "Skopiuj istniejącą zmienną środowiskową CALCOM_LICENSE_KEY tutaj.",
|
||||
"app_is_enabled": "Aplikacja {{appName}} jest włączona",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Ativar aplicativos",
|
||||
"enable_apps_description": "Ative aplicativos que os usuários podem integrar com o {{appName}}",
|
||||
"purchase_license": "Comprar uma licença",
|
||||
"already_have_account": "Já tem uma conta?",
|
||||
"already_have_key": "Já tenho uma chave:",
|
||||
"already_have_key_suggestion": "Copie a variável de ambiente CALCOM_LICENSE_KEY existente aqui.",
|
||||
"app_is_enabled": "{{appName}} foi ativado",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Ativar aplicações",
|
||||
"enable_apps_description": "Ativar aplicações que os utilizadores podem integrar com {{appName}}",
|
||||
"purchase_license": "Adquira uma Licença",
|
||||
"already_have_account": "Já tem uma conta?",
|
||||
"already_have_key": "Eu já tenho uma chave:",
|
||||
"already_have_key_suggestion": "Por favor, copie a sua variável de ambiente CALCOM_LICENSE_KEY existente aqui.",
|
||||
"app_is_enabled": "{{appName}} foi ativada",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Activează aplicații",
|
||||
"enable_apps_description": "Activați aplicațiile pe care utilizatorii le pot integra cu {{appName}}",
|
||||
"purchase_license": "Achiziționați o licență",
|
||||
"already_have_account": "Aveți deja un cont?",
|
||||
"already_have_key": "Am deja o cheie:",
|
||||
"already_have_key_suggestion": "Copiați aici variabila de mediu CALCOM_LICENSE_KEY existentă.",
|
||||
"app_is_enabled": "{{appName}} este activat",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Включить приложения",
|
||||
"enable_apps_description": "Выберите приложения, которые пользователи смогут интегрировать с {{appName}}",
|
||||
"purchase_license": "Купить лицензию",
|
||||
"already_have_account": "Уже есть аккаунт?",
|
||||
"already_have_key": "У меня уже есть ключ:",
|
||||
"already_have_key_suggestion": "Скопируйте сюда переменную окружения CALCOM_LICENSE_KEY.",
|
||||
"app_is_enabled": "{{appName}} включено",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Omogući aplikacije",
|
||||
"enable_apps_description": "Omogući aplikacije koje korisnici mogu da integrišu sa aplikacijom {{appName}}",
|
||||
"purchase_license": "Kupovina licence",
|
||||
"already_have_account": "Već imate nalog?",
|
||||
"already_have_key": "Već imam ključ:",
|
||||
"already_have_key_suggestion": "Kopirajte ovde postojeću CALCOM_LICENSE_KEY promenljivu okruženja.",
|
||||
"app_is_enabled": "Aplikacija {{appName}} je omogućena",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Aktivera appar",
|
||||
"enable_apps_description": "Aktivera appar som användare kan integrera med {{appName}}",
|
||||
"purchase_license": "Köp en licens",
|
||||
"already_have_account": "Har du redan ett konto?",
|
||||
"already_have_key": "Jag har redan en nyckel:",
|
||||
"already_have_key_suggestion": "Kopiera din befintliga CALCOM_LICENSE_KEY-miljövariabel här.",
|
||||
"app_is_enabled": "{{appName}} har aktiverats",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Uygulamaları Etkinleştir",
|
||||
"enable_apps_description": "Kullanıcıların {{appName}} ile entegre edebileceği uygulamaları etkinleştirin",
|
||||
"purchase_license": "Lisans satın alın",
|
||||
"already_have_account": "Zaten bir hesabınız var mı?",
|
||||
"already_have_key": "Zaten bir anahtarım var:",
|
||||
"already_have_key_suggestion": "Lütfen mevcut CALCOM_LICENSE_KEY ortam değişkeninizi buraya kopyalayın.",
|
||||
"app_is_enabled": "{{appName}} etkinleştirildi",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Увімкнути додатки",
|
||||
"enable_apps_description": "Активуйте застосунки, які користувачі зможуть інтегрувати із застосунком {{appName}}",
|
||||
"purchase_license": "Придбати ліцензію",
|
||||
"already_have_account": "Уже маєте обліковий запис?",
|
||||
"already_have_key": "У мене вже є ключ:",
|
||||
"already_have_key_suggestion": "Скопіюйте сюди наявну змінну оточення CALCOM_LICENSE_KEY.",
|
||||
"app_is_enabled": "{{appName}} увімкнено",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "Kích hoạt ứng dụng",
|
||||
"enable_apps_description": "Kích hoạt những ứng dụng mà người dùng có thể tích hợp với {{appName}}",
|
||||
"purchase_license": "Mua một giấy phép",
|
||||
"already_have_account": "Bạn đã có sẵn tài khoản?",
|
||||
"already_have_key": "Tôi đã có một khoá:",
|
||||
"already_have_key_suggestion": "Vui lòng sao chép biến số môi trường CALCOM_LICENSE_KEY hiện có của bạn tại đây.",
|
||||
"app_is_enabled": "{{appName}} đã được kích hoạt",
|
||||
|
|
|
@ -1582,6 +1582,7 @@
|
|||
"enable_apps": "启用应用",
|
||||
"enable_apps_description": "启用用户可以与 {{appName}} 集成的应用",
|
||||
"purchase_license": "购买许可证",
|
||||
"already_have_account": "已经有帐号?",
|
||||
"already_have_key": "我已经有密钥:",
|
||||
"already_have_key_suggestion": "请将您现有的 CALCOM_LICENSE_KEY 环境变量复制到此处。",
|
||||
"app_is_enabled": "{{appName}} 已启用",
|
||||
|
|
|
@ -1581,6 +1581,7 @@
|
|||
"enable_apps": "啟用應用程式",
|
||||
"enable_apps_description": "啟用使用者可與 {{appName}} 整合的應用程式",
|
||||
"purchase_license": "購買授權",
|
||||
"already_have_account": "已經有帳號嗎?",
|
||||
"already_have_key": "我已經有金鑰:",
|
||||
"already_have_key_suggestion": "請複製您目前的 CALCOM_LICENSE_KEY 環境變數至此處。",
|
||||
"app_is_enabled": "已啟用 {{appName}}",
|
||||
|
|
|
@ -2,4 +2,5 @@ import * as Sentry from "@sentry/nextjs";
|
|||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalend
|
|||
import { parse } from "node-html-parser";
|
||||
import type { VEvent } from "node-ical";
|
||||
import ical from "node-ical";
|
||||
import { expect } from "vitest";
|
||||
import { expect, vi } from "vitest";
|
||||
import "vitest-fetch-mock";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
@ -547,7 +547,7 @@ export function expectCalendarEventCreationFailureEmails({
|
|||
);
|
||||
}
|
||||
|
||||
export function expectSuccessfulRoudRobinReschedulingEmails({
|
||||
export function expectSuccessfulRoundRobinReschedulingEmails({
|
||||
emails,
|
||||
newOrganizer,
|
||||
prevOrganizer,
|
||||
|
@ -557,32 +557,38 @@ export function expectSuccessfulRoudRobinReschedulingEmails({
|
|||
prevOrganizer: { email: string; name: string };
|
||||
}) {
|
||||
if (newOrganizer !== prevOrganizer) {
|
||||
// new organizer should recieve scheduling emails
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
heading: "new_event_scheduled",
|
||||
to: `${newOrganizer.email}`,
|
||||
},
|
||||
`${newOrganizer.email}`
|
||||
);
|
||||
vi.waitFor(() => {
|
||||
// new organizer should recieve scheduling emails
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
heading: "new_event_scheduled",
|
||||
to: `${newOrganizer.email}`,
|
||||
},
|
||||
`${newOrganizer.email}`
|
||||
);
|
||||
});
|
||||
|
||||
// old organizer should recieve cancelled emails
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
heading: "event_request_cancelled",
|
||||
to: `${prevOrganizer.email}`,
|
||||
},
|
||||
`${prevOrganizer.email}`
|
||||
);
|
||||
vi.waitFor(() => {
|
||||
// old organizer should recieve cancelled emails
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
heading: "event_request_cancelled",
|
||||
to: `${prevOrganizer.email}`,
|
||||
},
|
||||
`${prevOrganizer.email}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// organizer should recieve rescheduled emails
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
heading: "event_has_been_rescheduled",
|
||||
to: `${newOrganizer.email}`,
|
||||
},
|
||||
`${newOrganizer.email}`
|
||||
);
|
||||
vi.waitFor(() => {
|
||||
// organizer should recieve rescheduled emails
|
||||
expect(emails).toHaveEmail(
|
||||
{
|
||||
heading: "event_has_been_rescheduled",
|
||||
to: `${newOrganizer.email}`,
|
||||
},
|
||||
`${newOrganizer.email}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
"prismock": "^1.21.1",
|
||||
"tsc-absolute": "^1.0.0",
|
||||
"typescript": "^4.9.4",
|
||||
"vitest": "^0.34.3",
|
||||
"vitest": "^0.34.6",
|
||||
"vitest-fetch-mock": "^0.2.2",
|
||||
"vitest-mock-extended": "^1.1.3"
|
||||
},
|
||||
|
|
|
@ -27,6 +27,7 @@ export const EventTypeAddonMap = {
|
|||
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
|
||||
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
|
||||
gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")),
|
||||
matomo: dynamic(() => import("./matomo/components/EventTypeAppCardInterface")),
|
||||
metapixel: dynamic(() => import("./metapixel/components/EventTypeAppCardInterface")),
|
||||
paypal: dynamic(() => import("./paypal/components/EventTypeAppCardInterface")),
|
||||
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
|
||||
|
|
|
@ -15,6 +15,7 @@ import { appKeysSchema as intercom_zod_ts } from "./intercom/zod";
|
|||
import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
|
||||
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 office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
|
@ -51,6 +52,7 @@ export const appKeysSchemas = {
|
|||
jitsivideo: jitsivideo_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
make: make_zod_ts,
|
||||
matomo: matomo_zod_ts,
|
||||
metapixel: metapixel_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
office365video: office365video_zod_ts,
|
||||
|
|
|
@ -33,6 +33,7 @@ import intercom_config_json from "./intercom/config.json";
|
|||
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
|
||||
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 n8n_config_json from "./n8n/config.json";
|
||||
|
@ -109,6 +110,7 @@ export const appStoreMetadata = {
|
|||
jitsivideo: jitsivideo__metadata_ts,
|
||||
larkcalendar: larkcalendar__metadata_ts,
|
||||
make: make_config_json,
|
||||
matomo: matomo_config_json,
|
||||
metapixel: metapixel_config_json,
|
||||
mirotalk: mirotalk_config_json,
|
||||
n8n: n8n_config_json,
|
||||
|
|
|
@ -15,6 +15,7 @@ import { appDataSchema as intercom_zod_ts } from "./intercom/zod";
|
|||
import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
|
||||
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 office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
|
@ -51,6 +52,7 @@ export const appDataSchemas = {
|
|||
jitsivideo: jitsivideo_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
make: make_zod_ts,
|
||||
matomo: matomo_zod_ts,
|
||||
metapixel: metapixel_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
office365video: office365video_zod_ts,
|
||||
|
|
|
@ -33,6 +33,7 @@ export const apiHandlers = {
|
|||
jitsivideo: import("./jitsivideo/api"),
|
||||
larkcalendar: import("./larkcalendar/api"),
|
||||
make: import("./make/api"),
|
||||
matomo: import("./matomo/api"),
|
||||
metapixel: import("./metapixel/api"),
|
||||
mirotalk: import("./mirotalk/api"),
|
||||
n8n: import("./n8n/api"),
|
||||
|
|
|
@ -15,6 +15,7 @@ import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
|
|||
import gtm_config_json from "./gtm/config.json";
|
||||
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
|
||||
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||
import matomo_config_json from "./matomo/config.json";
|
||||
import metapixel_config_json from "./metapixel/config.json";
|
||||
import mirotalk_config_json from "./mirotalk/config.json";
|
||||
import office365video_config_json from "./office365video/config.json";
|
||||
|
@ -48,6 +49,7 @@ export const appStoreMetadata = {
|
|||
gtm: gtm_config_json,
|
||||
huddle01video: huddle01video__metadata_ts,
|
||||
jitsivideo: jitsivideo__metadata_ts,
|
||||
matomo: matomo_config_json,
|
||||
metapixel: metapixel_config_json,
|
||||
mirotalk: mirotalk_config_json,
|
||||
office365video: office365video_config_json,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
items:
|
||||
- 1.png
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
|
@ -0,0 +1,16 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default as add } from "./add";
|
|
@ -0,0 +1,47 @@
|
|||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { TextField } from "@calcom/ui";
|
||||
|
||||
import type { appDataSchema } from "../zod";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const matomoUrl = getAppData("MATOMO_URL");
|
||||
const siteId = getAppData("SITE_ID");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
app={app}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<TextField
|
||||
name="Matomo URL"
|
||||
placeholder="Enter your Matomo URL here"
|
||||
value={matomoUrl}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
setAppData("MATOMO_URL", e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
name="Site ID"
|
||||
placeholder="Enter your Site ID"
|
||||
value={siteId}
|
||||
onChange={(e) => {
|
||||
setAppData("SITE_ID", e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppCard;
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "Matomo",
|
||||
"slug": "matomo",
|
||||
"type": "matomo_analytics",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://cal.com/",
|
||||
"variant": "analytics",
|
||||
"categories": ["analytics"],
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
"description": "Google Analytics alternative that protects your data and your customers' privacy",
|
||||
"extendsFeature": "EventType",
|
||||
"appData": {
|
||||
"tag": {
|
||||
"scripts": [
|
||||
{
|
||||
"src": "{MATOMO_URL}/matomo.js",
|
||||
"attrs": {}
|
||||
},
|
||||
{
|
||||
"content": "var _paq = window._paq || [];\n _paq.push(['trackPageView']);\n _paq.push(['enableLinkTracking']);\n (function() {\n var u='{MATOMO_URL}/'; \n _paq.push(['setTrackerUrl', u+'matomo.php']);\n _paq.push(['setSiteId', '{SITE_ID}']); \n var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];\n g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);\n })();"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "booking-pages-tag"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * as api from "./api";
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/matomo",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "Google Analytics alternative that protects your data and your customers' privacy"
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 370 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.6 KiB |
|
@ -0,0 +1,12 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
|
||||
|
||||
export const appDataSchema = eventTypeAppCardZod.merge(
|
||||
z.object({
|
||||
MATOMO_URL: z.string().optional(),
|
||||
SITE_ID: z.string().optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const appKeysSchema = z.object({});
|
|
@ -25,14 +25,14 @@ export default class ResponseEmail extends BaseEmail {
|
|||
this.toAddresses = toAddresses;
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
const toAddresses = this.toAddresses;
|
||||
const subject = `${this.form.name} has a new response`;
|
||||
return {
|
||||
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||
to: toAddresses.join(","),
|
||||
subject,
|
||||
html: renderEmail("ResponseEmail", {
|
||||
html: await renderEmail("ResponseEmail", {
|
||||
form: this.form,
|
||||
orderedResponses: this.orderedResponses,
|
||||
subject,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
|
|
|
@ -25,6 +25,7 @@ import type {
|
|||
} from "@calcom/types/Calendar";
|
||||
|
||||
import { getBusyTimes, getBusyTimesForLimitChecks } from "./getBusyTimes";
|
||||
import monitorCallbackAsync, { monitorCallbackSync } from "./sentryWrapper";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["getUserAvailability"] });
|
||||
const availabilitySchema = z
|
||||
|
@ -41,7 +42,13 @@ const availabilitySchema = z
|
|||
})
|
||||
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
||||
|
||||
const getEventType = async (id: number) => {
|
||||
const getEventType = async (
|
||||
...args: Parameters<typeof _getEventType>
|
||||
): Promise<ReturnType<typeof _getEventType>> => {
|
||||
return monitorCallbackAsync(_getEventType, ...args);
|
||||
};
|
||||
|
||||
const _getEventType = async (id: number) => {
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
|
@ -86,7 +93,11 @@ const getEventType = async (id: number) => {
|
|||
|
||||
type EventType = Awaited<ReturnType<typeof getEventType>>;
|
||||
|
||||
const getUser = (where: Prisma.UserWhereInput) =>
|
||||
const getUser = (...args: Parameters<typeof _getUser>): ReturnType<typeof _getUser> => {
|
||||
return monitorCallbackSync(_getUser, ...args);
|
||||
};
|
||||
|
||||
const _getUser = (where: Prisma.UserWhereInput) =>
|
||||
prisma.user.findFirst({
|
||||
where,
|
||||
select: {
|
||||
|
@ -99,7 +110,13 @@ const getUser = (where: Prisma.UserWhereInput) =>
|
|||
|
||||
type User = Awaited<ReturnType<typeof getUser>>;
|
||||
|
||||
export const getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) =>
|
||||
export const getCurrentSeats = (
|
||||
...args: Parameters<typeof _getCurrentSeats>
|
||||
): ReturnType<typeof _getCurrentSeats> => {
|
||||
return monitorCallbackSync(_getCurrentSeats, ...args);
|
||||
};
|
||||
|
||||
const _getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) =>
|
||||
prisma.booking.findMany({
|
||||
where: {
|
||||
eventTypeId,
|
||||
|
@ -122,8 +139,14 @@ export const getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Da
|
|||
|
||||
export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
|
||||
|
||||
export const getUserAvailability = async (
|
||||
...args: Parameters<typeof _getUserAvailability>
|
||||
): Promise<ReturnType<typeof _getUserAvailability>> => {
|
||||
return monitorCallbackAsync(_getUserAvailability, ...args);
|
||||
};
|
||||
|
||||
/** This should be called getUsersWorkingHoursAndBusySlots (...and remaining seats, and final timezone) */
|
||||
export const getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseAndEverythingElse(
|
||||
const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseAndEverythingElse(
|
||||
query: {
|
||||
withSource?: boolean;
|
||||
username?: string;
|
||||
|
@ -305,7 +328,13 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
|||
};
|
||||
};
|
||||
|
||||
const getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => {
|
||||
const getPeriodStartDatesBetween = (
|
||||
...args: Parameters<typeof _getPeriodStartDatesBetween>
|
||||
): ReturnType<typeof _getPeriodStartDatesBetween> => {
|
||||
return monitorCallbackSync(_getPeriodStartDatesBetween, ...args);
|
||||
};
|
||||
|
||||
const _getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => {
|
||||
const dates = [];
|
||||
let startDate = dayjs(dateFrom).startOf(period);
|
||||
const endDate = dayjs(dateTo).endOf(period);
|
||||
|
@ -378,6 +407,12 @@ class LimitManager {
|
|||
}
|
||||
|
||||
const getBusyTimesFromLimits = async (
|
||||
...args: Parameters<typeof _getBusyTimesFromLimits>
|
||||
): Promise<ReturnType<typeof _getBusyTimesFromLimits>> => {
|
||||
return monitorCallbackAsync(_getBusyTimesFromLimits, ...args);
|
||||
};
|
||||
|
||||
const _getBusyTimesFromLimits = async (
|
||||
bookingLimits: IntervalLimit | null,
|
||||
durationLimits: IntervalLimit | null,
|
||||
dateFrom: Dayjs,
|
||||
|
@ -450,6 +485,12 @@ const getBusyTimesFromLimits = async (
|
|||
};
|
||||
|
||||
const getBusyTimesFromBookingLimits = async (
|
||||
...args: Parameters<typeof _getBusyTimesFromBookingLimits>
|
||||
): Promise<ReturnType<typeof _getBusyTimesFromBookingLimits>> => {
|
||||
return monitorCallbackAsync(_getBusyTimesFromBookingLimits, ...args);
|
||||
};
|
||||
|
||||
const _getBusyTimesFromBookingLimits = async (
|
||||
bookings: EventBusyDetails[],
|
||||
bookingLimits: IntervalLimit,
|
||||
dateFrom: Dayjs,
|
||||
|
@ -504,6 +545,12 @@ const getBusyTimesFromBookingLimits = async (
|
|||
};
|
||||
|
||||
const getBusyTimesFromDurationLimits = async (
|
||||
...args: Parameters<typeof _getBusyTimesFromDurationLimits>
|
||||
): Promise<ReturnType<typeof _getBusyTimesFromDurationLimits>> => {
|
||||
return monitorCallbackAsync(_getBusyTimesFromDurationLimits, ...args);
|
||||
};
|
||||
|
||||
const _getBusyTimesFromDurationLimits = async (
|
||||
bookings: EventBusyDetails[],
|
||||
durationLimits: IntervalLimit,
|
||||
dateFrom: Dayjs,
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
import type { Span, Transaction } from "@sentry/types";
|
||||
|
||||
/*
|
||||
WHEN TO USE
|
||||
We ran a script that performs a simple mathematical calculation within a loop of 1000000 iterations.
|
||||
Our results were: Plain execution time: 441, Monitored execution time: 8094.
|
||||
This suggests that using these wrappers within large loops can incur significant overhead and is thus not recommended.
|
||||
|
||||
For smaller loops, the cost incurred may not be very significant on an absolute scale
|
||||
considering that a million monitored iterations only took roughly 8 seconds when monitored.
|
||||
*/
|
||||
|
||||
const setUpMonitoring = (name: string) => {
|
||||
// Attempt to retrieve the current transaction from Sentry's scope
|
||||
let transaction = Sentry.getCurrentHub().getScope()?.getTransaction();
|
||||
|
||||
// Check if there's an existing transaction, if not, start a new one
|
||||
if (!transaction) {
|
||||
transaction = Sentry.startTransaction({
|
||||
op: name,
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
|
||||
// Start a new span in the current transaction
|
||||
const span = transaction.startChild({
|
||||
op: name,
|
||||
description: `Executing ${name}`,
|
||||
});
|
||||
return [transaction, span];
|
||||
};
|
||||
|
||||
// transaction will always be Transaction, since returned in a list with Span type must be listed as either or here
|
||||
const finishMonitoring = (transaction: Transaction | Span, span: Span) => {
|
||||
// Attempt to retrieve the current transaction from Sentry's scope
|
||||
span.finish();
|
||||
|
||||
// If this was a new transaction, finish it
|
||||
if (!Sentry.getCurrentHub().getScope()?.getTransaction()) {
|
||||
transaction.finish();
|
||||
}
|
||||
};
|
||||
|
||||
const monitorCallbackAsync = async <T extends (...args: any[]) => any>(
|
||||
cb: T,
|
||||
...args: Parameters<T>
|
||||
): Promise<ReturnType<T>> => {
|
||||
// Check if Sentry set
|
||||
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return (await cb(...args)) as ReturnType<T>;
|
||||
|
||||
const [transaction, span] = setUpMonitoring(cb.name);
|
||||
|
||||
try {
|
||||
const result = await cb(...args);
|
||||
return result as ReturnType<T>;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
throw error;
|
||||
} finally {
|
||||
finishMonitoring(transaction, span);
|
||||
}
|
||||
};
|
||||
|
||||
const monitorCallbackSync = <T extends (...args: any[]) => any>(
|
||||
cb: T,
|
||||
...args: Parameters<T>
|
||||
): ReturnType<T> => {
|
||||
// Check if Sentry set
|
||||
if (!process.env.NEXT_PUBLIC_SENTRY_DSN) return cb(...args) as ReturnType<T>;
|
||||
|
||||
const [transaction, span] = setUpMonitoring(cb.name);
|
||||
|
||||
try {
|
||||
const result = cb(...args);
|
||||
return result as ReturnType<T>;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
throw error;
|
||||
} finally {
|
||||
finishMonitoring(transaction, span);
|
||||
}
|
||||
};
|
||||
|
||||
export default monitorCallbackAsync;
|
||||
export { monitorCallbackSync };
|
|
@ -8,7 +8,7 @@
|
|||
```ts
|
||||
import { renderEmail } from "@calcom/emails";
|
||||
|
||||
renderEmail("TeamInviteEmail", */{
|
||||
renderEmail("TeamInviteEmail", {
|
||||
language: t,
|
||||
from: "teampro@example.com",
|
||||
to: "pro@example.com",
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import * as ReactDOMServer from "react-dom/server";
|
||||
|
||||
import * as templates from "./templates";
|
||||
|
||||
function renderEmail<K extends keyof typeof templates>(
|
||||
async function renderEmail<K extends keyof typeof templates>(
|
||||
template: K,
|
||||
props: React.ComponentProps<(typeof templates)[K]>
|
||||
) {
|
||||
const Component = templates[template];
|
||||
const ReactDOMServer = (await import("react-dom/server")).default;
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
|
|
|
@ -24,7 +24,7 @@ export default class BaseEmail {
|
|||
return dayjs(time).tz(this.getTimezone()).locale(this.getLocale()).format(format);
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
return {};
|
||||
}
|
||||
public async sendEmail() {
|
||||
|
@ -38,21 +38,20 @@ export default class BaseEmail {
|
|||
if (process.env.INTEGRATION_TEST_MODE === "true") {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-expect-error
|
||||
setTestEmail(this.getNodeMailerPayload());
|
||||
setTestEmail(await this.getNodeMailerPayload());
|
||||
console.log(
|
||||
"Skipped Sending Email as process.env.NEXT_PUBLIC_UNIT_TESTS is set. Emails are available in globalThis.testEmails"
|
||||
);
|
||||
return new Promise((r) => r("Skipped sendEmail for Unit Tests"));
|
||||
}
|
||||
|
||||
const payload = this.getNodeMailerPayload();
|
||||
const payload = await this.getNodeMailerPayload();
|
||||
const parseSubject = z.string().safeParse(payload?.subject);
|
||||
const payloadWithUnEscapedSubject = {
|
||||
headers: this.getMailerOptions().headers,
|
||||
...payload,
|
||||
...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }),
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) =>
|
||||
createTransport(this.getMailerOptions().transport).sendMail(
|
||||
payloadWithUnEscapedSubject,
|
||||
|
@ -69,7 +68,6 @@ export default class BaseEmail {
|
|||
).catch((e) => console.error("sendEmail", e));
|
||||
return new Promise((resolve) => resolve("send mail async"));
|
||||
}
|
||||
|
||||
protected getMailerOptions() {
|
||||
return {
|
||||
transport: serverConfig.transport,
|
||||
|
@ -77,7 +75,6 @@ export default class BaseEmail {
|
|||
headers: serverConfig.headers,
|
||||
};
|
||||
}
|
||||
|
||||
protected printNodeMailerError(error: Error): void {
|
||||
/** Don't clog the logs with unsent emails in E2E */
|
||||
if (process.env.NEXT_PUBLIC_IS_E2E) return;
|
||||
|
|
|
@ -23,14 +23,14 @@ export default class AccountVerifyEmail extends BaseEmail {
|
|||
this.verifyAccountInput = passwordEvent;
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`,
|
||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||
subject: this.verifyAccountInput.language("verify_email_subject", {
|
||||
appName: APP_NAME,
|
||||
}),
|
||||
html: renderEmail("VerifyAccountEmail", this.verifyAccountInput),
|
||||
html: await renderEmail("VerifyAccountEmail", this.verifyAccountInput),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,12 +22,12 @@ export default class AdminOrganizationNotification extends BaseEmail {
|
|||
this.input = input;
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
return {
|
||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||
to: this.input.instanceAdmins.map((admin) => admin.email).join(","),
|
||||
subject: `${this.input.t("admin_org_notification_email_subject")}`,
|
||||
html: renderEmail("AdminOrganizationNotificationEmail", {
|
||||
html: await renderEmail("AdminOrganizationNotificationEmail", {
|
||||
orgSlug: this.input.orgSlug,
|
||||
webappIPAddress: this.input.webappIPAddress,
|
||||
language: this.input.t,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user