merge with main

This commit is contained in:
Alan 2023-09-06 12:53:00 -07:00
commit 9d1ebbebd1
158 changed files with 3140 additions and 2196 deletions

View File

@ -5,6 +5,15 @@ plugins.push(withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
/** @type {import("next").NextConfig} */
const nextConfig = {
async redirects() {
return [
{
source: "/",
destination: "https://cal.com/ai",
permanent: true,
},
];
},
i18n: {
defaultLocale: "en",
locales: ["en"],

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/ai",
"version": "0.1.0",
"version": "1.0.1",
"private": true,
"author": "Cal.com Inc.",
"dependencies": {

View File

@ -45,6 +45,7 @@ export const POST = async (request: NextRequest) => {
select: {
email: true,
id: true,
timeZone: true,
credentials: {
select: {
appId: true,
@ -55,15 +56,17 @@ export const POST = async (request: NextRequest) => {
where: { email: envelope.from },
});
if (!signature || !user?.email || !user?.id) {
// User is not a cal.com user or is using an unverified email.
if (!signature || !user) {
await sendEmail({
html: `Thanks for your interest in Cal AI! To get started, Make sure you have a <a href="https://cal.com/signup" target="_blank">cal.com</a> account with this email address.`,
subject: `Re: ${body.subject}`,
text: "Sorry, you are not authorized to use this service. Please verify your email address and try again.",
to: user?.email || "",
text: `Thanks for your interest in Cal AI! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`,
to: envelope.from,
from: aiEmail,
});
return new NextResponse();
return new NextResponse("ok");
}
const credential = user.credentials.find((c) => c.appId === env.APP_ID)?.key;
@ -93,8 +96,8 @@ export const POST = async (request: NextRequest) => {
fetchAvailability({
apiKey,
userId: user.id,
dateFrom: now,
dateTo: now,
dateFrom: now(user.timeZone),
dateTo: now(user.timeZone),
}),
]);
@ -120,7 +123,7 @@ export const POST = async (request: NextRequest) => {
return new NextResponse("Error fetching event types. Please try again.", { status: 400 });
}
const { timeZone, workingHours } = availability;
const { workingHours } = availability;
const appHost = getHostFromHeaders(request.headers);
@ -135,7 +138,7 @@ export const POST = async (request: NextRequest) => {
user: {
email: user.email,
eventTypes,
timeZone,
timeZone: user.timeZone,
workingHours,
},
}),

View File

@ -52,9 +52,8 @@ const createBooking = async ({
method: "POST",
});
if (response.status === 401) {
throw new Error("Unauthorized");
}
// Let GPT handle this. This will happen when wrong event type id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();

View File

@ -31,7 +31,8 @@ const cancelBooking = async ({
method: "DELETE",
});
if (response.status === 401) throw new Error("Unauthorized");
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();

View File

@ -35,9 +35,7 @@ export const fetchAvailability = async ({
const response = await fetch(url);
if (response.status === 401) {
throw new Error("Unauthorized");
}
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();

View File

@ -41,7 +41,8 @@ const editBooking = async ({
method: "PATCH",
});
if (response.status === 401) throw new Error("Unauthorized");
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();

View File

@ -44,7 +44,7 @@ const agent = async (input: string, user: User, apiKey: string, userId: number)
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
Tools will always handle times in UTC, but times sent to the user should be formatted per that user's timezone.
Current UTC time is: ${now}
The current time in the user's timezone is: ${now(user.timeZone)}
The user's time zone is: ${user.timeZone}
The user's event types are: ${user.eventTypes
.map((e: EventType) => `ID: ${e.id}, Title: ${e.title}, Length: ${e.length}`)

View File

@ -1 +1,5 @@
export default new Date().toISOString();
export default function now(timeZone: string) {
return new Date().toLocaleString("en-US", {
timeZone,
});
}

View File

@ -98,14 +98,7 @@ const UserProfile = () => {
return (
<form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end">
{user && (
<Avatar
alt={user.username || "user avatar"}
gravatarFallbackMd5={user.emailMd5}
size="lg"
imageSrc={imageSrc}
/>
)}
{user && <Avatar alt={user.username || "user avatar"} size="lg" imageSrc={imageSrc} />}
<input
ref={avatarRef}
type="hidden"

View File

@ -32,7 +32,7 @@ function getCspPolicy(nonce: string) {
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
} app.cal.com;
font-src 'self';
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
img-src 'self' ${WEBAPP_URL} https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
connect-src 'self'
`;
}

View File

@ -1,11 +0,0 @@
import crypto from "crypto";
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
if (!email && !md5) return "";
if (email && !md5) {
md5 = crypto.createHash("md5").update(email).digest("hex");
}
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
};

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.2.7.1",
"version": "3.2.8",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",

View File

@ -187,7 +187,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
res.setHeader("Content-Type", response.headers.get("content-type") as string);
res.setHeader("Cache-Control", "s-maxage=86400");
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
res.send(buffer);
} catch (error) {
res.statusCode = 404;

View File

@ -1,13 +1,11 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
import { defaultAvatarSrc } from "@lib/profile";
const querySchema = z
.object({
username: z.string(),
@ -69,18 +67,15 @@ async function getIdentityData(req: NextApiRequest) {
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const identity = await getIdentityData(req);
const img = identity?.avatar;
// We cache for one day
res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
// If image isn't set or links to this route itself, use default avatar
if (!img) {
if (identity?.org) {
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(302, {
Location: defaultAvatarSrc({
md5: crypto
.createHash("md5")
.update(identity?.email || "guest@example.com")
.digest("hex"),
}),
Location: AVATAR_FALLBACK,
});
return res.end();

View File

@ -52,6 +52,8 @@ export default function Login({
totpEmail,
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
const searchParams = useSearchParams();
const isTeamInvite = searchParams.get("teamInvite");
const { t } = useLocale();
const router = useRouter();
const formSchema = z
@ -95,7 +97,9 @@ export default function Login({
callbackUrl = safeCallbackUrl || "";
const LoginFooter = (
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
<a
href={callbackUrl !== "" ? `${WEBSITE_URL}/signup?callbackUrl=${callbackUrl}` : `${WEBSITE_URL}/signup`}
className="text-brand-500 font-medium">
{t("dont_have_an_account")}
</a>
);
@ -184,6 +188,9 @@ export default function Login({
? LoginFooter
: null
}>
{isTeamInvite && (
<Alert severity="info" message={t("signin_or_signup_to_accept_invite")} className="mb-4 mt-4" />
)}
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} noValidate data-testid="login-form">
<div>

View File

@ -376,9 +376,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
bookerLayouts,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
length,
...input
} = values;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@ -396,7 +399,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@ -410,6 +413,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
@ -467,9 +471,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
multipleDurationEnabled,
length,
...input
} = values;
if (!Number(length)) throw new Error(t("event_setup_length_error"));
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@ -487,7 +494,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!length && !metadata?.multipleDuration?.includes(length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@ -496,6 +503,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
length,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,

View File

@ -425,7 +425,7 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
)}
/>
)}
{isManagedEventType && (
{isManagedEventType && type?.children && type.children?.length > 0 && (
<AvatarGroup
className="relative right-3 top-1"
size="sm"

View File

@ -406,7 +406,7 @@ const ProfileForm = ({
name="avatar"
render={({ field: { value } }) => (
<>
<Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
<Avatar alt="" imageSrc={value} size="lg" />
<div className="ms-4">
<ImageUploader
target="avatar"

View File

@ -13,6 +13,7 @@ import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
@ -34,6 +35,23 @@ type FormValues = z.infer<typeof signupSchema>;
type SignupProps = inferSSRProps<typeof getServerSideProps>;
const getSafeCallbackUrl = (url: string | null) => {
if (!url) return null;
let callbackUrl = url;
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
}
const safeCallbackUrl = getSafeRedirectUrl(callbackUrl);
return safeCallbackUrl;
};
export default function Signup({ prepopulateFormValues, token, orgSlug }: SignupProps) {
const searchParams = useSearchParams();
const telemetry = useTelemetry();
@ -55,6 +73,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
throw new Error(err.message);
}
};
const callbackUrl = getSafeCallbackUrl(searchParams.get("callbackUrl"));
const signUp: SubmitHandler<FormValues> = async (data) => {
await fetch("/api/auth/signup", {
@ -72,13 +91,10 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
.then(async () => {
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
await signIn<"credentials">("credentials", {
...data,
callbackUrl: `${
searchParams?.get("callbackUrl")
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
: `${WEBAPP_URL}/${verifyOrGettingStarted}`
}?from=signup`,
callbackUrl: `${callbackUrl ? callbackUrl : `${WEBAPP_URL}/${verifyOrGettingStarted}`}?from=signup`,
});
})
.catch((err) => {
@ -157,9 +173,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
className="w-full justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: searchParams?.get("callbackUrl")
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
: `${WEBAPP_URL}/getting-started`,
callbackUrl: callbackUrl ? callbackUrl : `${WEBAPP_URL}/getting-started`,
})
}>
{t("login_instead")}

View File

@ -1,9 +1,11 @@
import type { GetServerSidePropsContext } from "next";
import { getLayout } from "@calcom/features/MainLayout";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { TeamsListing } from "@calcom/features/ee/teams/components";
import { ShellMain } from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
@ -41,6 +43,21 @@ function Teams() {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const session = await getServerSession({ req: context.req, res: context.res });
const token = context.query?.token;
const resolvedUrl = context.resolvedUrl;
const callbackUrl = token ? getSafeRedirectUrl(`${WEBAPP_URL}${resolvedUrl}`) : null;
if (!session) {
return {
redirect: {
destination: callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}&teamInvite=true` : "/auth/login",
permanent: false,
},
props: {},
};
}
return { props: { trpcState: ssr.dehydrate() } };
};

View File

@ -0,0 +1,61 @@
import { v4 as uuidv4 } from "uuid";
import { prisma } from "@calcom/prisma";
type Route = {
id: string;
action: {
type: string;
value: string;
};
isFallback: boolean;
queryValue: {
id: string;
type: string;
};
};
export const createRoutingFormsFixture = () => {
return {
async create({
userId,
teamId,
name,
fields,
routes = [],
}: {
name: string;
userId: number;
teamId: number | null;
routes?: Route[];
fields: {
type: string;
label: string;
identifier?: string;
required: boolean;
}[];
}) {
return await prisma.app_RoutingForms_Form.create({
data: {
name,
userId,
teamId,
routes: [
...routes,
// Add a fallback route always, this is taken care of tRPC route normally but do it manually while running the query directly.
{
id: "898899aa-4567-489a-bcde-f1823f708646",
action: { type: "customPageMessage", value: "Fallback Message" },
isFallback: true,
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
],
fields: fields.map((f) => ({
id: uuidv4(),
...f,
})),
},
});
},
};
};

View File

@ -10,6 +10,7 @@ import type { ExpectedUrlDetails } from "../../../../playwright.config";
import { createBookingsFixture } from "../fixtures/bookings";
import { createEmbedsFixture, createGetActionFiredDetails } from "../fixtures/embeds";
import { createPaymentsFixture } from "../fixtures/payments";
import { createRoutingFormsFixture } from "../fixtures/routingForms";
import { createServersFixture } from "../fixtures/servers";
import { createUsersFixture } from "../fixtures/users";
@ -23,6 +24,7 @@ export interface Fixtures {
servers: ReturnType<typeof createServersFixture>;
prisma: typeof prisma;
emails?: API;
routingForms: ReturnType<typeof createRoutingFormsFixture>;
}
declare global {
@ -71,6 +73,9 @@ export const test = base.extend<Fixtures>({
prisma: async ({}, use) => {
await use(prisma);
},
routingForms: async ({}, use) => {
await use(createRoutingFormsFixture());
},
emails: async ({}, use) => {
if (IS_MAILHOG_ENABLED) {
const mailhogAPI = mailhog();

View File

@ -1,3 +1,4 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
@ -9,6 +10,9 @@ import {
gotoRoutingLink,
} from "./lib/testUtils";
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
test.afterEach(({ users }) => users.deleteAll());
test.describe("BOOKING_CREATED", async () => {
@ -55,8 +59,6 @@ test.describe("BOOKING_CREATED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body: any = request.body;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@ -187,8 +189,6 @@ test.describe("BOOKING_REJECTED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@ -311,8 +311,6 @@ test.describe("BOOKING_REQUESTED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@ -391,54 +389,136 @@ test.describe("BOOKING_REQUESTED", async () => {
});
test.describe("FORM_SUBMITTED", async () => {
test("can submit a form and get a submission event", async ({ page, users }) => {
test("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
await page.goto("/settings/teams/new");
await page.waitForLoadState("networkidle");
const teamName = `${user.username}'s Team`;
// Create a new team
await page.locator('input[name="name"]').fill(teamName);
await page.locator('input[name="slug"]').fill(teamName);
await page.locator('button[type="submit"]').click();
await page.locator("text=Publish team").click();
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
await page.waitForLoadState("networkidle");
await page.waitForLoadState("networkidle");
await page.goto(`/settings/developer/webhooks/new`);
// Add webhook
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([page.click("[type=submit]"), page.goForward()]);
await page.click("[type=submit]");
// Page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
await page.waitForLoadState("networkidle");
await page.goto("/routing-forms/forms");
await page.click('[data-testid="new-routing-form"]');
// Choose to create the Form for the user(which is the first option) and not the team
await page.click('[data-testid="option-0"]');
await page.fill("input[name]", "TEST FORM");
await page.click('[data-testid="add-form"]');
await page.waitForSelector('[data-testid="add-field"]');
const url = page.url();
const formId = new URL(url).pathname.split("/").at(-1);
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
teamId: null,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
await gotoRoutingLink({ page, formId: formId });
await gotoRoutingLink({ page, formId: form.id });
const fieldName = "name";
await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe");
page.click('button[type="submit"]');
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
expect(body).toEqual({
triggerEvent: "FORM_SUBMITTED",
createdAt: dynamic,
payload: {
formId: form.id,
formName: form.name,
teamId: null,
responses: {
name: {
value: "John Doe",
},
},
},
name: "John Doe",
});
webhookReceiver.close();
});
test("on submitting team form, triggers team webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
const teamId = await clickFirstTeamWebhookCta(page);
// Add webhook
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.click("[type=submit]");
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
teamId: teamId,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
await gotoRoutingLink({ page, formId: form.id });
const fieldName = "name";
await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe");
page.click('button[type="submit"]');
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
expect(body).toEqual({
triggerEvent: "FORM_SUBMITTED",
createdAt: dynamic,
payload: {
formId: form.id,
formName: form.name,
teamId,
responses: {
name: {
value: "John Doe",
},
},
},
name: "John Doe",
});
webhookReceiver.close();
});
});
async function clickFirstTeamWebhookCta(page: Page) {
await page.click('[data-testid="new_webhook"]');
await page.click('[data-testid="option-team-1"]');
await page.waitForURL((u) => u.pathname === "/settings/developer/webhooks/new");
const url = page.url();
const teamId = Number(new URL(url).searchParams.get("teamId")) as number;
return teamId;
}

View File

@ -0,0 +1 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><rect width="32" height="32" rx="16" fill="#E5E7EB"/><path d="M16 21.333A6.667 6.667 0 1 0 16 8a6.667 6.667 0 0 0 0 13.333Z" fill="#1F2937"/><path d="M26.667 32a10.667 10.667 0 1 0-21.334 0" fill="#1F2937"/><path d="M16 21.333A6.667 6.667 0 1 0 16 8a6.667 6.667 0 0 0 0 13.333Zm0 0A10.667 10.667 0 0 1 26.667 32M16 21.333A10.666 10.666 0 0 0 5.333 32" stroke="#1F2937" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g><defs><clipPath id="a"><rect width="32" height="32" rx="16" fill="#fff"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 635 B

View File

@ -13,7 +13,7 @@
"reset_password_subject": "{{appName}}: إرشادات إعادة تعيين كلمة المرور",
"verify_email_subject": "{{appName}}: تأكيد حسابك",
"check_your_email": "تحقق من بريدك الإلكتروني",
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم من {{appName}}.",
"verify_email_page_body": "أرسلنا رسالة إلكترونية إلى {{email}}. من المهم تأكيد عنوان بريدك الإلكتروني لضمان تسليم البريد الإلكتروني والتقويم من {{appName}}.",
"verify_email_banner_body": "قم بتأكيد عنوان بريدك الإلكتروني لضمان أفضل تسليم للبريد الإلكتروني والتقويم",
"verify_email_email_header": "تأكيد عنوان بريدك الإلكتروني",
"verify_email_email_button": "تأكيد البريد الإلكتروني",
@ -130,7 +130,7 @@
"upgrade_banner_action": "قم بالترقية هنا",
"team_upgraded_successfully": "تمت ترقية فريقك بنجاح!",
"org_upgrade_banner_description": "شكرًا لك على تجربة خطة Organization الجديدة التي نقدمها. لقد لاحظنا أن منظمتك \"{{teamName}}\" بحاجة إلى الترقية.",
"org_upgraded_successfully": "تمت ترقية منظمتك بنجاح!",
"org_upgraded_successfully": "تمت ترقية خطة Organization بنجاح!",
"use_link_to_reset_password": "استخدم الرابط أدناه لإعادة تعيين كلمة المرور",
"hey_there": "مرحبًا،",
"forgot_your_password_calcom": "نسيت كلمة المرور الخاصة بك؟ - {{appName}}",
@ -550,7 +550,7 @@
"team_description": "بضع جمل عن فريقك. ستظهر على صفحة رابط فريقك.",
"org_description": "بضع جمل عن منظمتك. ستظهر على صفحة رابط منظمتك.",
"members": "الأعضاء",
"organization_members": "أعضاء المنظمة",
"organization_members": "أعضاء Organization",
"member": "العضو",
"number_member_one": "{{count}} عضو",
"number_member_other": "{{count}} من الأعضاء",
@ -1652,9 +1652,8 @@
"delete_sso_configuration_confirmation_description": "هل تريد بالتأكيد حذف تكوين {{connectionType}}؟ لن يتمكن أعضاء فريقك الذين يستخدمون معلومات تسجيل الدخول إلى {{connectionType}} من الوصول إلى Cal.com بعد الآن.",
"organizer_timezone": "منظم المناطق الزمنية",
"email_user_cta": "عرض الدعوة",
"email_no_user_invite_heading": "تمت دعوتك للانضمام إلى {{appName}} {{entity}}",
"email_no_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_user_invite_subheading": "لقد دعاك {{invitedBy}} للانضمام إلى {{entity}} `{{teamName}}` على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت و{{entity}} من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_user_invite_subheading_team": "دعاك {{invitedBy}} للانضمام إلى فريقه {{teamName}} على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_no_user_invite_steps_intro": "سنرشدك خلال بضع خطوات قصيرة وستستمتع بجدول زمني خالٍ من التوتر مع {{entity}} في لمح البصر.",
"email_no_user_step_one": "اختر اسم المستخدم الخاص بك",
"email_no_user_step_two": "ربط حساب التقويم الخاص بك",
@ -1871,7 +1870,6 @@
"first_event_type_webhook_description": "قم بإنشاء أول شبكة ويب هوك لهذا النوع من الأحداث",
"install_app_on": "تثبيت التطبيق على",
"create_for": "إنشاء من أجل",
"setup_organization": "إعداد منظمة",
"organization_banner_description": "إنشاء بيئات حيث يمكن لفرقك إنشاء تطبيقات مشتركة ومهام سير العمل وأنواع الأحداث باستخدام الجدولة الدوارة والجماعية.",
"organization_banner_title": "إدارة المنظمات ذات الفرق المتعددة",
"set_up_your_organization": "إعداد منظمتك",

View File

@ -118,7 +118,7 @@
"team_info": "Informace o týmu",
"request_another_invitation_email": "Pokud si nepřejete použít e-mail {{toEmail}} pro {{appName}} nebo už účet na {{appName}} máte, požádejte prosím o další pozvánku na jiný e-mail.",
"you_have_been_invited": "Byli jste pozváni do týmu {{teamName}}",
"user_invited_you": "Uživatel {{user}} vás pozval, abyste se připojili k subjektu {{entity}} {{team}} v aplikaci {{appName}}",
"user_invited_you": "Uživatel {{user}} vás pozval, abyste se připojili k {{entity}} {{team}} v aplikaci {{appName}}",
"hidden_team_member_title": "V tomto týmu nejste viditelní",
"hidden_team_member_message": "Vaše místo není zaplacené. Buď přejděte na tarif Pro, nebo informujte vlastníka týmu, že za vaše místo může zaplatit.",
"hidden_team_owner_message": "Pro využívání týmů potřebujete tarif Pro, budete skrytí, než přejdete na vyšší verzi.",
@ -130,7 +130,7 @@
"upgrade_banner_action": "Upgradovat zde",
"team_upgraded_successfully": "Váš tým byl úspěšně přešel na vyšší verzi!",
"org_upgrade_banner_description": "Děkujeme, že jste vyzkoušeli náš tarif Organizace. Všimli jsme si, že vaše organizace „{{teamName}}“ vyžaduje upgrade.",
"org_upgraded_successfully": "Vaše organizace byla upgradována!",
"org_upgraded_successfully": "Váš upgrade na tarif Organization byl úspěšně dokončen!",
"use_link_to_reset_password": "Pro obnovení hesla použijte odkaz níž",
"hey_there": "Zdravíme,",
"forgot_your_password_calcom": "Zapomněli jste heslo? - {{appName}}",
@ -307,7 +307,7 @@
"layout": "Rozvržení",
"bookerlayout_default_title": "Výchozí zobrazení",
"bookerlayout_description": "Můžete si jich vybrat více a vaši rezervující si mohou zobrazení přepínat.",
"bookerlayout_user_settings_title": "Rozvržení rezervace",
"bookerlayout_user_settings_title": "Rozvržení rezervací",
"bookerlayout_user_settings_description": "Můžete si jich vybrat více a vaši rezervující si mohou zobrazení přepínat. Lze nadefinovat pro každou událost zvlášť.",
"bookerlayout_month_view": "Měsíční",
"bookerlayout_week_view": "Týdenní",
@ -403,7 +403,7 @@
"recording_ready": "Odkaz ke stažení záznamu je připraven",
"booking_created": "Rezervace vytvořena",
"booking_rejected": "Rezervace byla zamítnuta",
"booking_requested": "Byla vyžádána rezervace",
"booking_requested": "Váš požadavek na rezervaci byl úspěšně odeslán",
"meeting_ended": "Schůzka skončila",
"form_submitted": "Formulář byl odeslán",
"event_triggers": "Eventy na základě akce",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Opravdu chcete odstranit konfiguraci {{connectionType}}? Členové vašeho týmu, kteří používají přihlášení {{connectionType}}, ztratí přístup k webu Cal.com.",
"organizer_timezone": "Časové pásmo organizátora",
"email_user_cta": "Zobrazit pozvánku",
"email_no_user_invite_heading": "Byli jste pozváni, abyste se připojili k subjektu {{entity}} v aplikaci {{appName}}",
"email_no_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového ping-pongu.",
"email_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k týmu {{teamName}} subjektu {{entity}} v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu subjektu {{entity}} umožňuje plánovat schůzky bez e-mailového pingpongu.",
"email_user_invite_subheading_team": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu „{{teamName}}“ v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového pingpongu.",
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým subjektem {{entity}} užívat plánování bez stresu.",
"email_no_user_step_one": "Vyberte si uživatelské jméno",
"email_no_user_step_two": "Připojte svůj účet kalendáře",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Vytvořte svůj první webhook pro tento typ události",
"install_app_on": "Nainstalovat aplikaci v",
"create_for": "Vytvořit pro",
"setup_organization": "Nastavení organizace",
"organization_banner_description": "Vytvořte prostředí, ve kterém budou moct vaše týmy vytvářet sdílené aplikace, pracovní postupy a typy událostí s kolektivním plánováním nebo plánováním typu round-robin.",
"organization_banner_title": "Správa organizace s více týmy",
"set_up_your_organization": "Nastavení organizace",
@ -1889,12 +1887,12 @@
"admin_username": "Uživatelské jméno správce",
"organization_name": "Název organizace",
"organization_url": "Adresa URL organizace",
"organization_verify_header": "Ověřte e-mailovou adresu organizace",
"organization_verify_header": "Ověřte svou e-mailovou adresu organizace",
"organization_verify_email_body": "Pomocí uvedeného kódu ověřte vaši e-mailovou adresu, abyste mohli pokračovat v nastavení vaší organizace.",
"additional_url_parameters": "Další parametry adresy URL",
"about_your_organization": "Informace o vaší organizaci",
"about_your_organization_description": "Organizace jsou sdílená prostředí, ve kterých můžete vytvořit několik týmů se sdílenými členy, typy událostí, aplikacemi, pracovními postupy a mnoha dalšími věcmi.",
"create_your_teams": "Vytvoření týmů",
"create_your_teams": "Vytvořit tým",
"create_your_teams_description": "Přidejte do své organizace členy a pusťte se do společného plánování",
"invite_organization_admins": "Pozvěte správce své organizace",
"invite_organization_admins_description": "Tito správci budou mít přístup ke všem týmům ve vaší organizaci. Správce a členy týmů můžete přidat později.",
@ -1934,11 +1932,11 @@
"404_the_org": "Organizace",
"404_the_team": "Tým",
"404_claim_entity_org": "Vyžádejte si subdoménu pro svou organizaci",
"404_claim_entity_team": "Vyžádejte si tento tým a začněte kolektivně spravovat rozvrhy",
"404_claim_entity_team": "Vyžádejte si přístup k tomuto týmu a začněte kolektivně spravovat rozvrhy",
"insights_all_org_filter": "Všechny aplikace",
"insights_team_filter": "Tým: {{teamName}}",
"insights_user_filter": "Uživatel: {{userName}}",
"insights_subtitle": "Zobrazte si přehled o rezervacích napříč vašimi událostmi",
"insights_subtitle": "Zobrazte si Insight rezervací napříč vašimi událostmi",
"custom_plan": "Vlastní plán",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -1679,9 +1679,8 @@
"delete_sso_configuration_confirmation_description": "Sind Sie sicher, dass Sie die {{connectionType}}-Konfiguration löschen möchten? Ihre Teammitglieder, die sich über {{connectionType}} anmelden, werden keinen Zugriff mehr auf Cal.com haben.",
"organizer_timezone": "Zeitzone des Veranstalters",
"email_user_cta": "Einladung anzeigen",
"email_no_user_invite_heading": "Sie wurden eingeladen, einem {{entity}} in {{appName}} beizutreten",
"email_no_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem Team in {{appName}} beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne ständiges hin und her zu planen.",
"email_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem {{entity}} „{{teamName}}“ in {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrem {{entity}} ermöglicht, Meetings ohne ständiges hin und her zu planen.",
"email_user_invite_subheading_team": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
"email_no_user_invite_steps_intro": "Wir begleiten Sie durch ein paar Schritte, danach können Sie in kürzester Zeit stressfreie Planung mit Ihrem {{entity}} genießen.",
"email_no_user_step_one": "Wählen Sie Ihren Benutzernamen",
"email_no_user_step_two": "Verbinden Sie Ihr Kalenderkonto",
@ -1902,7 +1901,6 @@
"install_app_on": "App installieren auf",
"create_for": "Erstellen für",
"currency": "Währung",
"setup_organization": "Eine Organization einrichten",
"organization_banner_description": "Schaffen Sie Umgebungen, in der Teams gemeinsame Apps, Workflows und Termintypen mit Round Robin und kollektiver Terminplanung erstellen können.",
"organization_banner_title": "Verwalten Sie Organizations mit mehreren Teams",
"set_up_your_organization": "Ihre Organization einrichten",

View File

@ -45,6 +45,7 @@
"invite_team_individual_segment": "Invite individual",
"invite_team_bulk_segment": "Bulk import",
"invite_team_notifcation_badge": "Inv.",
"signin_or_signup_to_accept_invite": "You need to Sign in or Sign up to see team invitation.",
"your_event_has_been_scheduled": "Your event has been scheduled",
"your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled",
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
@ -1053,12 +1054,15 @@
"how_you_want_add_cal_site": "How do you want to add {{appName}} to your site?",
"choose_ways_put_cal_site": "Choose one of the following ways to put {{appName}} on your site.",
"setting_up_zapier": "Setting up your Zapier integration",
"setting_up_make": "Setting up your Make integration",
"generate_api_key": "Generate API key",
"generate_api_key_description": "Generate an API key to use with {{appName}} at",
"your_unique_api_key": "Your unique API key",
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"make_setup_instructions": "<0>Go to <1><0>Make Invite Link</0></1> and install the Cal.com app.</0><1>Log into your Make account and create a new Scenario.</1><2>Select Cal.com as your Trigger app. Also choose a Trigger event.</2><3>Choose your account and then enter your Unique API Key.</3><4>Test your Trigger.</4><5>You're set!</5>",
"install_zapier_app": "Please first install the Zapier App in the app store.",
"install_make_app": "Please first install the Make App in the app store.",
"connect_apple_server": "Connect to Apple Server",
"calendar_url": "Calendar URL",
"apple_server_generate_password": "Generate an app specific password to use with {{appName}} at",
@ -1684,9 +1688,11 @@
"delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.",
"organizer_timezone": "Organizer timezone",
"email_user_cta": "View Invitation",
"email_no_user_invite_heading": "Youve been invited to join a {{appName}} {{entity}}",
"email_no_user_invite_heading_team": "Youve been invited to join a {{appName}} team",
"email_no_user_invite_heading_org": "Youve been invited to join a {{appName}} organization",
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
"email_user_invite_subheading": "{{invitedBy}} has invited you to join their {{entity}} `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your {{entity}} to schedule meetings without the email tennis.",
"email_user_invite_subheading_team": "{{invitedBy}} has invited you to join their team `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
"email_user_invite_subheading_org": "{{invitedBy}} has invited you to join their organization `{{teamName}}` on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your organization to schedule meetings without the email tennis.",
"email_no_user_invite_steps_intro": "Well walk you through a few short steps and youll be enjoying stress free scheduling with your {{entity}} in no time.",
"email_no_user_step_one": "Choose your username",
"email_no_user_step_two": "Connect your calendar account",
@ -1909,7 +1915,6 @@
"install_app_on": "Install app on",
"create_for": "Create for",
"currency": "Currency",
"setup_organization": "Setup an Organization",
"organization_banner_description": "Create an environments where your teams can create shared apps, workflows and event types with round-robin and collective scheduling.",
"organization_banner_title": "Manage organizations with multiple teams",
"set_up_your_organization": "Set up your organization",
@ -2038,5 +2043,7 @@
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
"include_calendar_event": "Include calendar event",
"recently_added":"Recently added",
"no_members_found": "No members found",
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "¿Está seguro de que desea eliminar la configuración de {{connectionType}}? Los miembros de su equipo que utilicen el inicio de sesión {{connectionType}} ya no podrán acceder a Cal.com.",
"organizer_timezone": "Zona horaria del organizador",
"email_user_cta": "Ver la invitación",
"email_no_user_invite_heading": "Lo invitaron a unirse a {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} lo invitó a unirse a su equipo en {{appName}}. {{appName}} es el programador de eventos que les permite a usted y a su equipo programar reuniones sin necesidad de enviar correos electrónicos.",
"email_user_invite_subheading": "{{invitedBy}} lo ha invitado a unirse a su {{entity}} \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su {{entity}} programar reuniones sin necesidad de correos electrónicos.",
"email_user_invite_subheading_team": "{{invitedBy}} lo ha invitado a unirse a su equipo \"{{teamName}}\" en {{appName}}. {{appName}} es el planificador de eventos que le permite a usted y a su equipo programar reuniones sin correos electrónicos de ida y vuelta.",
"email_no_user_invite_steps_intro": "Lo guiaremos a través de unos pocos pasos y disfrutará de una programación sin estrés con su {{entity}} en muy poco tiempo.",
"email_no_user_step_one": "Elija su nombre de usuario",
"email_no_user_step_two": "Conecte su cuenta de calendario",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Cree su primer webhook para este tipo de evento",
"install_app_on": "Instalar aplicación en",
"create_for": "Crear para",
"setup_organization": "Configurar una organización",
"organization_banner_description": "Cree entornos en los que sus equipos puedan crear aplicaciones, flujos de trabajo y tipos de eventos compartidos con programación por turnos y colectiva.",
"organization_banner_title": "Gestione organizaciones con varios equipos",
"set_up_your_organization": "Configure su organización",

View File

@ -45,6 +45,7 @@
"invite_team_individual_segment": "Inviter une personne",
"invite_team_bulk_segment": "Importation multiple",
"invite_team_notifcation_badge": "Inv.",
"signin_or_signup_to_accept_invite": "Vous devez vous connecter ou vous inscrire pour voir l'invitation d'équipe.",
"your_event_has_been_scheduled": "Votre événement a été planifié",
"your_event_has_been_scheduled_recurring": "Votre événement récurrent a été planifié",
"accept_our_license": "Acceptez notre licence en changeant la variable .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> en « {{agree}} ».",
@ -1053,12 +1054,15 @@
"how_you_want_add_cal_site": "Comment souhaitez-vous ajouter {{appName}} à votre site ?",
"choose_ways_put_cal_site": "Choisissez l'une des méthodes suivantes pour mettre {{appName}} sur votre site.",
"setting_up_zapier": "Configuration de votre intégration Zapier",
"setting_up_make": "Configuration de votre intégration Make",
"generate_api_key": "Générer une clé API",
"generate_api_key_description": "Générer une clé API à utiliser avec {{appName}} sur",
"your_unique_api_key": "Votre clé API unique",
"copy_safe_api_key": "Copiez cette clé API et conservez-la dans un endroit sûr. Si vous perdez cette clé, vous devrez en générer une nouvelle.",
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application Trigger. Choisissez également un événement Trigger.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre Trigger.</3><4>Vous êtes prêt !</4>",
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
"make_setup_instructions": "<0>Connectez-vous à votre compte Make et créez un nouveau Scénario.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
"install_zapier_app": "Veuillez d'abord installer l'application Zapier dans l'App Store.",
"install_make_app": "Veuillez d'abord installer l'application Make dans l'App Store.",
"connect_apple_server": "Se connecter au serveur d'Apple",
"calendar_url": "Lien du calendrier",
"apple_server_generate_password": "Générez un mot de passe pour application à utiliser avec {{appName}} sur",
@ -1116,7 +1120,7 @@
"add_new_workflow": "Ajouter un nouveau workflow",
"reschedule_event_trigger": "lorsque l'événement est replanifié",
"trigger": "Déclencheur",
"triggers": "Déclencher",
"triggers": "Déclencheurs",
"action": "Action",
"workflows_to_automate_notifications": "Créez des workflows pour automatiser les notifications et les rappels.",
"workflow_name": "Nom du workflow",
@ -1684,9 +1688,8 @@
"delete_sso_configuration_confirmation_description": "Voulez-vous vraiment supprimer la configuration {{connectionType}} ? Les membres de votre équipe utilisant la connexion {{connectionType}} ne pourront plus accéder à Cal.com.",
"organizer_timezone": "Fuseau horaire de l'organisateur",
"email_user_cta": "Voir l'invitation",
"email_no_user_invite_heading": "Vous avez été invité à rejoindre une {{entity}} {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
"email_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son {{entity}} « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre {{entity}} d'organiser des rendez-vous sans échanges d'e-mails.",
"email_user_invite_subheading_team": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre {{entity}} en un rien de temps.",
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
"email_no_user_step_two": "Connectez votre compte de calendrier",
@ -1909,7 +1912,6 @@
"install_app_on": "Installer lapplication sur",
"create_for": "Créer pour",
"currency": "Devise",
"setup_organization": "Configurer une organisation",
"organization_banner_description": "Créez un environnement où vos équipes peuvent créer des applications partagées, des workflows et des types d'événements avec une planification round-robin et collective.",
"organization_banner_title": "Gérer les organisations avec plusieurs équipes",
"set_up_your_organization": "Configurer votre organisation",
@ -2037,5 +2039,7 @@
"team_no_event_types": "Cette équipe n'a aucun type d'événement",
"seat_options_doesnt_multiple_durations": "L'option par place ne prend pas en charge les durées multiples",
"include_calendar_event": "Inclure l'événement du calendrier",
"recently_added": "Ajouté récemment",
"no_members_found": "Aucun membre trouvé",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "אתה בטוח שאתה רוצה למחוק את הגדרות {{connectionType}}? חברי הצוות שלך שמשתמשים ב- {{connectionType}} להזדהות לא יוכלו להשתמש בו להכנס ל- Cal.com.",
"organizer_timezone": "מארגן אזורי זמן",
"email_user_cta": "צפה בהזמנה",
"email_no_user_invite_heading": "הוזמנת להצטרף ל{{entity}} של {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} הזמין אותך להצטרף לצוות שלו ב- {{appName}}. {{appName}} הינה מתזמן זימונים שמאפשר לך ולצוות שלך לזמן פגישות בלי כל הפינג פונג במיילים.",
"email_user_invite_subheading": "{{invitedBy}} הזמין/ה אותך להצטרף ל-{{entity}} שלו/ה בשם ״{{teamName}}״ באפליקציה {{appName}}. אפליקציית {{appName}} היא כלי לקביעת מועדים לאירועים שמאפשר לך ול-{{entity}} שלך לתזמן פגישות בלי הצורך לנהל התכתבויות ארוכות בדוא״ל.",
"email_user_invite_subheading_team": "{{invitedBy}} הזמין/ה אותך להצטרף לצוות שלו/ה בשם '{{teamName}}' באפליקציה {{appName}}. אפליקציית {{appName}} היא כלי לקביעת מועדים לאירועים שמאפשר לך ולצוות שלך לתזמן פגישות בלי כל הפינג פונג במיילים.",
"email_no_user_invite_steps_intro": "נדריך אותך במספר קטן של צעדים ותוכל/י להתחיל ליהנות מקביעת מועדים עם ה-{{entity}} שלך במהירות ובלי בעיות.",
"email_no_user_step_one": "בחר שם משתמש",
"email_no_user_step_two": "קשר את לוח השנה שלך",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג האירוע הזה",
"install_app_on": "התקנת האפליקציה ב-",
"create_for": "צור/צרי עבור",
"setup_organization": "הגדרת Organization",
"organization_banner_description": "צור/צרי סביבות שבהן הצוותים שלך יוכלו ליצור אפליקציות, תהליכי עבודה וסוגי אירועים משותפים, עם תכונות כמו סבב וקביעת מועדים שיתופית.",
"organization_banner_title": "ניהול ארגונים עם צוותים מרובים",
"set_up_your_organization": "הגדרת הארגון שלך",

View File

@ -403,7 +403,7 @@
"recording_ready": "Il link per scaricare la registrazione è pronto",
"booking_created": "Prenotazione Creata",
"booking_rejected": "Prenotazione Rifiutata",
"booking_requested": "Prenotazione Richiesta",
"booking_requested": "Richiesta di prenotazione inviata",
"meeting_ended": "Riunione terminata",
"form_submitted": "Modulo inviato",
"event_triggers": "Attivatori Evento",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Eliminare la configurazione {{connectionType}}? I membri del tuo team che utilizzano l'accesso {{connectionType}} non saranno più in grado di accedere a Cal.com.",
"organizer_timezone": "Fuso orario organizzatore",
"email_user_cta": "Visualizza invito",
"email_no_user_invite_heading": "Hai ricevuto un invito a partecipare a un/una {{entity}} di {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} ti ha invitato a unirti al suo team su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette al tuo team di pianificare le riunioni senza scambiare decine di e-mail.",
"email_user_invite_subheading": "{{invitedBy}} ti ha invitato a partecipare al/alla {{entity}} `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e al tuo/tua {{entity}} di pianificare le riunioni senza scambiarvi decine di e-mail.",
"email_user_invite_subheading_team": "{{invitedBy}} ti ha invitato a unirti al suo team `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e al tuo team di pianificare le riunioni senza scambiare decine di e-mail.",
"email_no_user_invite_steps_intro": "Ti assisteremo nei passaggi iniziali e potrai rapidamente pianificare eventi per il/la {{entity}} senza fatica.",
"email_no_user_step_one": "Scegli il tuo nome utente",
"email_no_user_step_two": "Collega il tuo account di calendario",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Crea il primo webhook per questo tipo di evento",
"install_app_on": "Installa l'app su",
"create_for": "Crea per",
"setup_organization": "Imposta un'organizzazione",
"organization_banner_description": "Crea ambienti dove i tuoi team potranno creare e condividere applicazioni, flussi di lavoro e tipi di eventi con pianificazioni di gruppo e round robin.",
"organization_banner_title": "Gestisci organizzazioni con più team",
"set_up_your_organization": "Imposta la tua organizzazione",
@ -1889,7 +1887,7 @@
"admin_username": "Nome utente dell'amministratore",
"organization_name": "Nome dell'organizzazione",
"organization_url": "URL dell'organizzazione",
"organization_verify_header": "Verifica l'e-mail della tua organizzazione",
"organization_verify_header": "Verifica il tuo indirizzo e-mail",
"organization_verify_email_body": "Usa il codice sottostante per verificare il tuo indirizzo e-mail e proseguire la configurazione dell'organizzazione.",
"additional_url_parameters": "Parametri aggiuntivi dell'URL",
"about_your_organization": "Informazioni sulla tua organizzazione",

View File

@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "{{connectionType}} の構成を削除してもよろしいですか?{{connectionType}} でのログインを使用しているチームメンバーは、Cal.com にアクセスできなくなります。",
"organizer_timezone": "主催者のタイムゾーン",
"email_user_cta": "招待を表示",
"email_no_user_invite_heading": "{{appName}} の {{entity}} への参加に招待されました",
"email_no_user_invite_subheading": "{{invitedBy}} は {{appName}} のチームに参加するようあなたを招待しました。{{appName}} は、イベント調整スケジューラーです。チームと延々とメールのやりとりをすることなくミーティングのスケジュール設定を行うことができます。",
"email_user_invite_subheading": "{{invitedBy}} から {{appName}} の {{entity}} \"{{teamName}}\" に参加するよう招待されました。{{appName}} はイベント調整スケジューラーであり、{{entity}} 内で延々とメールのやりとりを続けることなくミーティングのスケジュールを設定できます。",
"email_user_invite_subheading_team": "{{invitedBy}}から{{appName}}の「{{teamName}}」に参加するよう招待されました。{{appName}}はイベント調整スケジューラーで、チーム内で延々とメールのやりとりをすることなく、ミーティングのスケジュールを設定できます。",
"email_no_user_invite_steps_intro": "いくつかの短い手順を踏むだけで、すぐに {{entity}} のストレスフリーなスケジュール設定をお楽しみいただけます。",
"email_no_user_step_one": "ユーザー名を選択",
"email_no_user_step_two": "カレンダーアカウントを接続",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "このイベントの種類の最初の Webhook を作成",
"install_app_on": "次のアカウントにアプリをインストール",
"create_for": "作成対象",
"setup_organization": "組織をセットアップする",
"organization_banner_description": "チームがラウンドロビンスケジューリングや一括スケジューリングを活用して、共有アプリ、ワークフロー、イベントの種類を作成できる環境を作成します。",
"organization_banner_title": "複数のチームを持つ組織を管理する",
"set_up_your_organization": "組織をセットアップする",

View File

@ -21,7 +21,7 @@
"verify_email_email_link_text": "버튼 클릭이 마음에 들지 않는 경우 다음 링크를 사용하세요.",
"email_sent": "이메일이 성공적으로 전송되었습니다",
"event_declined_subject": "거절됨: {{date}} {{title}}",
"event_cancelled_subject": "취소됨: {{title}} 날짜 {{date}}",
"event_cancelled_subject": "취소됨: {{title}}, 날짜 {{date}}",
"event_request_declined": "회의 요청이 거절되었습니다.",
"event_request_declined_recurring": "되풀이 이벤트 요청이 거부되었습니다",
"event_request_cancelled": "예약된 일정이 취소되었습니다",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "{{connectionType}} 구성을 삭제하시겠습니까? {{connectionType}} 로그인을 사용하는 팀원은 더 이상 Cal.com에 액세스할 수 없습니다.",
"organizer_timezone": "주최자 시간대",
"email_user_cta": "초대 보기",
"email_no_user_invite_heading": "{{appName}} {{entity}}에 참여 초대를 받으셨습니다",
"email_no_user_invite_subheading": "{{invitedBy}} 님이 귀하를 {{appName}}에서 자신의 팀에 초대했습니다. {{appName}} 앱은 귀하와 귀하의 팀이 이메일을 주고 받지 않고 회의 일정을 잡을 수 있게 해주는 이벤트 정리 스케줄러입니다.",
"email_user_invite_subheading": "{{invitedBy}} 님이 {{appName}}에서 자신의 {{entity}} `{{teamName}}`에 귀하의 참여를 초대했습니다. {{appName}}은(는) 사용자와 {{entity}}이(가) 이메일을 주고 받지 않고도 회의 일정을 잡을 수 있게 해주는 이벤트 조정 스케줄러입니다.",
"email_user_invite_subheading_team": "{{invitedBy}}님이 {{appName}}에서 자신의 `{{teamName}}` 팀에 가입하도록 당신을 초대했습니다. {{appName}}은 유저와 팀이 이메일을 주고 받지 않고도 회의 일정을 잡을 수 있게 하는 이벤트 조율 스케줄러입니다.",
"email_no_user_invite_steps_intro": "안내해 드릴 몇 가지 간단한 단계만 거치면 곧 스트레스 없이 편리하게 {{entity}} 일정을 관리할 수 있게 될 것입니다.",
"email_no_user_step_one": "사용자 이름 선택",
"email_no_user_step_two": "캘린더 계정 연결",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "이 이벤트 유형에 대한 첫 번째 웹훅 만들기",
"install_app_on": "앱 설치 대상",
"create_for": "작성 대상",
"setup_organization": "조직 설정",
"organization_banner_description": "팀이 라운드 로빈 및 공동 예약을 통해 공유 앱, 워크플로 및 이벤트 유형을 생성할 수 있는 환경을 만듭니다.",
"organization_banner_title": "여러 팀으로 조직 관리",
"set_up_your_organization": "조직 설정",

View File

@ -1663,9 +1663,8 @@
"delete_sso_configuration_confirmation_description": "Weet u zeker dat u de {{connectionType}}-configuratie wilt verwijderen? Uw teamleden die {{connectionType}}-aanmelding gebruiken hebben niet langer toegang tot Cal.com.",
"organizer_timezone": "Tijdzone organisator",
"email_user_cta": "Uitnodiging weergeven",
"email_no_user_invite_heading": "U bent uitgenodigd om lid te worden van {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} heeft u uitgenodigd om lid te worden van zijn team op {{appName}}. {{appName}} is de gebeurtenissenplanner die u en uw team in staat stelt vergaderingen te plannen zonder heen en weer te e-mailen.",
"email_user_invite_subheading": "{{invitedBy}} heeft u uitgenodigd om lid te worden van zijn {{entity}} '{{teamName}}' op {{appName}}. {{appName}} is de gebeurtenissenplanner die u en uw {{entity}} in staat stelt afspraken te plannen zonder heen en weer te e-mailen.",
"email_user_invite_subheading_team": "{{invitedBy}} heeft u uitgenodigd om lid te worden van zijn team '{{teamName}}' op {{appName}}. {{appName}} is de gebeurtenissenplanner die u en uw team in staat stelt afspraken te plannen zonder heen en weer te e-mailen.",
"email_no_user_invite_steps_intro": "We doorlopen samen een paar korte stappen en in een mum van tijd kunt u genieten van een stressvrije planning met uw {{entity}}.",
"email_no_user_step_one": "Kies uw gebruikersnaam",
"email_no_user_step_two": "Koppel uw agenda-account",
@ -1885,7 +1884,6 @@
"first_event_type_webhook_description": "Maak uw eerste webhook voor dit gebeurtenistype",
"install_app_on": "App installeren op",
"create_for": "Maken voor",
"setup_organization": "Een organisatie instellen",
"organization_banner_description": "Maak een omgeving waarin uw teams gedeelde apps, werkstromen en gebeurtenistypen kunnen maken met round-robin en collectieve planning.",
"organization_banner_title": "Beheer organisaties met meerdere teams",
"set_up_your_organization": "Stel uw organisatie in",

View File

@ -12,7 +12,7 @@
"have_any_questions": "Masz pytania? Jesteśmy tutaj, aby pomóc.",
"reset_password_subject": "{{appName}}: Instrukcje resetowania hasła",
"verify_email_subject": "{{appName}}: zweryfikuj swoje konto",
"check_your_email": "Sprawdź swój adres e-mail",
"check_your_email": "Sprawdź swoją pocztę e-mail",
"verify_email_page_body": "Wysłaliśmy wiadomość e-mail na adres {{email}}. Weryfikacja adresu e-mail jest ważna, ponieważ umożliwia zagwarantowanie prawidłowego działania kalendarza i dostarczania wiadomości e-mail z aplikacji {{appName}}.",
"verify_email_banner_body": "Potwierdź adres e-mail, aby upewnić się, że wiadomości e-mail i powiadomienia z kalendarza będą do Ciebie docierać.",
"verify_email_email_header": "Zweryfikuj swój adres e-mail",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Czy na pewno chcesz usunąć konfigurację {{connectionType}}? Członkowie zespołu, którzy używają logowania {{connectionType}} nie będą już mogli uzyskać dostępu do Cal.com.",
"organizer_timezone": "Strefa czasowa organizatora",
"email_user_cta": "Wyświetl zaproszenie",
"email_no_user_invite_heading": "Zaproszono Cię do dołączenia do aplikacji {{appName}} ({{entity}})",
"email_no_user_invite_subheading": "Użytkownik {{invitedBy}} zaprasza Cię do dołączenia do jego zespołu w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi Tobie i Twojemu zespołowi planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
"email_user_invite_subheading": "{{invitedBy}} zaprasza Cię do dołączenia do: {{entity}} „{{teamName}}” w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi {{entity}} planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
"email_user_invite_subheading_team": "Użytkownik {{invitedBy}} zaprasza Cię do dołączenia do jego zespołu „{{teamName}}” w aplikacji {{appName}}. Aplikacja {{appName}} to terminarz do planowania wydarzeń, który umożliwi Tobie i Twojemu zespołowi planowanie spotkań bez czasochłonnej wymiany wiadomości e-mail.",
"email_no_user_invite_steps_intro": "Przeprowadzimy Cię przez kilka krótkich kroków i wkrótce w ramach {{entity}} zaczniesz z przyjemnością korzystać z bezstresowego planowania.",
"email_no_user_step_one": "Wybierz nazwę użytkownika",
"email_no_user_step_two": "Połącz swoje konto kalendarza",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Utwórz swój pierwszy element webhook dla tego typu wydarzenia",
"install_app_on": "Zainstaluj aplikację na koncie",
"create_for": "Utwórz dla",
"setup_organization": "Skonfiguruj organizację",
"organization_banner_description": "Utwórz środowisko, w którym zespoły będą mogły tworzyć wspólne aplikacje, przepływy pracy i typy wydarzeń przy użyciu algorytmu karuzelowego lub zbiorowego ustalania harmonogramu.",
"organization_banner_title": "Zarządzaj organizacjami z wieloma zespołami",
"set_up_your_organization": "Skonfiguruj swoją organizację",
@ -1889,7 +1887,7 @@
"admin_username": "Nazwa użytkownika administratora",
"organization_name": "Nazwa organizacji",
"organization_url": "Adres URL organizacji",
"organization_verify_header": "Zweryfikuj adres e-mail swojej organizacji",
"organization_verify_header": "Zweryfikuj swój adres e-mail w organizacji",
"organization_verify_email_body": "Użyj poniższego kodu, aby zweryfikować adres e-mail i kontynuować konfigurację swojej organizacji.",
"additional_url_parameters": "Dodatkowe parametry adresu URL",
"about_your_organization": "Informacje o Twojej organizacji",

View File

@ -1675,9 +1675,8 @@
"delete_sso_configuration_confirmation_description": "Você tem certeza que deseja remover a configuração de {{connectionType}}? Os membros do seu time que utilizam {{connectionType}} para fazer login não conseguirão acessar o Cal.com.",
"organizer_timezone": "Fuso horário do organizador",
"email_user_cta": "Ver convite",
"email_no_user_invite_heading": "Você recebeu um convite para ingressar em {{entity}} de {{appName}}",
"email_no_user_invite_subheading": "Você recebeu um convite de {{invitedBy}} para ingressar em sua equipe em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que sua equipe agende reuniões sem precisar trocar e-mails.",
"email_user_invite_subheading": "Você recebeu um convite de {{invitedBy}} para ingressar na equipe \"{{teamName}}\" de {{entity}} em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que {{entity}} agende reuniões sem precisar trocar e-mails.",
"email_user_invite_subheading_team": "Você recebeu um convite de {{invitedBy}} para ingressar na equipe \"{{teamName}}\" em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que sua equipe agende reuniões sem precisar trocar e-mails.",
"email_no_user_invite_steps_intro": "Orientaremos ao longo de alguns passos rápidos para que você comece logo a agendar eventos com {{entity}} sem preocupação.",
"email_no_user_step_one": "Escolha seu nome de usuário",
"email_no_user_step_two": "Conecte com sua conta de calendário",
@ -1898,7 +1897,6 @@
"install_app_on": "Instalar aplicativo em",
"create_for": "Criar para",
"currency": "Moeda",
"setup_organization": "Definir uma Organização",
"organization_banner_description": "Crie um ambiente onde suas equipes podem criar tipos de evento, fluxos de trabalho e aplicativos compartilhados com agendamento coletivo e round robin.",
"organization_banner_title": "Gerenciar organizações com múltiplas equipes",
"set_up_your_organization": "Configurar sua organização",

View File

@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Tem a certeza que pretende eliminar a configuração {{connectionType}}? Os membros da sua equipa que utilizem {{connectionType}} para o início de sessão deixarão de poder aceder a Cal.com.",
"organizer_timezone": "Fuso horário do organizador",
"email_user_cta": "Ver convite",
"email_no_user_invite_heading": "Foi convidado para se juntar a um {{entity}} {{appName}}",
"email_no_user_invite_subheading": "Recebeu um convite de {{invitedBy}} para fazer parte da respetiva equipa em {{appName}}. {{appName}} é um ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua equipa agendar reuniões sem o pingue-pongue de e-mails.",
"email_user_invite_subheading": "Recebeu um convite de {{invitedBy}} para fazer parte da sua {{entity}} `{{teamName}}` em {{appName}}. {{appName}} é uma ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua {{entity}} agendar reuniões sem o pingue-pongue de mensagens eletrónicas.",
"email_user_invite_subheading_team": "Recebeu um convite de {{invitedBy}} para fazer parte da equipa `{{teamName}}` em {{appName}}. {{appName}} é uma ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua equipa agendar reuniões sem o pingue-pongue de mensagens eletrónicas.",
"email_no_user_invite_steps_intro": "Vamos ajudar com alguns pequenos passos para que você e a sua {{entity}} possam desfrutar rapidamente de uma gestão de agendamentos sem complicações.",
"email_no_user_step_one": "Escolha o seu nome de utilizador",
"email_no_user_step_two": "Associe a sua conta de calendário",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Crie o seu primeiro webhook para este tipo de evento",
"install_app_on": "Instalar aplicação em",
"create_for": "Criar para",
"setup_organization": "Configurar uma Organização",
"organization_banner_description": "Crie um ambiente onde as suas equipas possam criar aplicações partilhadas, fluxos de trabalho e tipos de eventos com distribuição equilibrada e agendamento coletivo.",
"organization_banner_title": "Faça a gestão de múltiplas organizações com múltiplas equipas",
"set_up_your_organization": "Configurar a sua organização",

View File

@ -118,7 +118,7 @@
"team_info": "Informații echipă",
"request_another_invitation_email": "Dacă preferați să nu utilizați {{toEmail}} ca e-mail {{appName}} sau aveți deja un cont {{appName}}, vă rugăm să solicitați o altă invitație la acel e-mail.",
"you_have_been_invited": "Ați fost invitat să vă alăturați echipei {{teamName}}",
"user_invited_you": "{{user}} v-a invitat să faceți parte din {{entity}} {{team}} de pe {{appName}}",
"user_invited_you": "{{user}} v-a invitat să faceți parte din {{entity}} {{team}} pe {{appName}}",
"hidden_team_member_title": "Sunteți ascuns în această echipă",
"hidden_team_member_message": "Licența dvs. nu este plătită. Fie faceți upgrade la Pro, fie anunțați proprietarul echipei că vă poate plăti licența.",
"hidden_team_owner_message": "Aveți nevoie de un cont Pro pentru a utiliza echipe. Sunteți ascuns până când faceți upgrade.",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Sigur doriți să ștergeți configurația {{connectionType}}? Membrii echipei dvs. care utilizează conectarea prin {{connectionType}} nu vor mai putea accesa Cal.com.",
"organizer_timezone": "Fusul orar al organizatorului",
"email_user_cta": "Vizualizare invitație",
"email_no_user_invite_heading": "Ați fost invitat să faceți parte dintr-o {{entity}} {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} v-a invitat să faceți parte din echipa sa de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor, care vă permite dvs. și echipei dvs. să programați ședințe fără a face ping-pong prin e-mail.",
"email_user_invite_subheading": "{{invitedBy}} v-a invitat să faceți parte din {{entity}} „{{teamName}}” de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor, prin care dvs. și {{entity}} dvs. puteți să programați ședințe fără a face ping-pong prin e-mail.",
"email_user_invite_subheading_team": "{{invitedBy}} v-a invitat să faceți parte din echipa sa „{{teamName}}” de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor, care vă permite dvs. și echipei dvs. să programați ședințe fără a face ping-pong prin e-mail.",
"email_no_user_invite_steps_intro": "Vom parcurge împreună câțiva pași simpli și vă veți bucura alături de {{entity}} dvs. de programări fără probleme, în cel mai scurt timp.",
"email_no_user_step_one": "Alegeți-vă numele de utilizator",
"email_no_user_step_two": "Conectați-vă contul de calendar",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Creați primul dvs. webhook pentru acest tip de eveniment",
"install_app_on": "Instalați aplicația în",
"create_for": "Creare pentru",
"setup_organization": "Configurarați o organizație",
"organization_banner_description": "Dezvoltați medii în care echipele dvs. să poată crea aplicații, fluxuri de lucru și tipuri de evenimente comune, folosind programarea colectivă sau cu alocare prin rotație.",
"organization_banner_title": "Gestionați organizații cu mai multe echipe",
"set_up_your_organization": "Configurați organizația",

View File

@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Удалить конфигурацию {{connectionType}}? Участники команды, входящие в Cal.com через {{connectionType}}, больше не смогут получить к нему доступ.",
"organizer_timezone": "Часовой пояс организатора",
"email_user_cta": "Посмотреть приглашение",
"email_no_user_invite_heading": "Вас пригласили в {{entity}} {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} пригласил(-а) вас в команду в {{appName}}. {{appName}} — это гибкий планировщик событий, с помощью которого пользователи и целые команды могут планировать встречи без утомительной переписки по электронной почте.",
"email_user_invite_subheading": "{{invitedBy}} пригласил(-а) вас в {{entity}} `{{teamName}}` в {{appName}}. {{appName}} — это гибкий планировщик событий, с помощью которого вы и ваша {{entity}} можете планировать встречи без утомительной переписки по электронной почте.",
"email_user_invite_subheading_team": "{{invitedBy}} пригласил(а) вас в команду в `{{teamName}}` в приложении {{appName}}. {{appName}} — это гибкий планировщик событий, с помощью которого пользователи и целые команды могут планировать встречи без утомительной переписки по электронной почте.",
"email_no_user_invite_steps_intro": "Всего несколько шагов — и вы сможете оперативно и без стресса планировать встречи в рамках вашей {{entity}}.",
"email_no_user_step_one": "Выберите имя пользователя",
"email_no_user_step_two": "Подключите аккаунт календаря",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Создайте первый вебхук для этого типа событий",
"install_app_on": "Установить приложение в",
"create_for": "Создать для",
"setup_organization": "Настроите организацию",
"organization_banner_description": "Создавайте рабочие среды, в рамках которых ваши команды смогут создавать общие приложения, рабочие процессы и типы событий с назначением участников по очереди и коллективным планированием.",
"organization_banner_title": "Управляйте организациями с несколькими командами",
"set_up_your_organization": "Настройте организацию",

View File

@ -1660,9 +1660,8 @@
"delete_sso_configuration_confirmation_description": "Da li ste sigurni da želite da izbrišete {{connectionType}} konfiguraciju? Članovi vašeg tima koji koriste {{connectionType}} prijavljivanje više neće moći da pristupe Cal.com-u.",
"organizer_timezone": "Vremenska zona organizatora",
"email_user_cta": "Prikaži pozivnicu",
"email_no_user_invite_heading": "Pozvani ste da se pridružite {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} vas je pozvao/la da se pridružite njihovom timu u {{appName}}. {{appName}} je planer za koordinaciju događaja koji omogućava vama i vašem timu da zakazujete sastanke bez dopisivanja imejlovima.",
"email_user_invite_subheading": "{{invitedBy}} vas je pozvao/la da se pridružite {{entity}} `{{teamName}}` na {{appName}}. {{appName}} je uređivač rasporeda koji omogućava vama i vašem {{entity}} da zakažete sastanke bez razmene imejlova.",
"email_user_invite_subheading_team": "{{invitedBy}} vas je pozvao/la da se pridružite njihovom timu `{{teamName}}` u {{appName}}. {{appName}} je planer za koordinaciju događaja koji omogućava vama i vašem timu da zakazujete sastanke bez dopisivanja imejlovima.",
"email_no_user_invite_steps_intro": "Provešćemo vas u nekoliko kratkiih koraka i za tren ćete uživati u zakazivanju bez stresa sa vašim {{entity}}.",
"email_no_user_step_one": "Izaberite korisničko ime",
"email_no_user_step_two": "Povežite se sa nalogom kalendara",
@ -1879,7 +1878,6 @@
"first_event_type_webhook_description": "Napravite svoj prvi webhook za ovaj tip događaja",
"install_app_on": "Instaliraj aplikaciju",
"create_for": "Napravi za",
"setup_organization": "Postavi organizaciju",
"organization_banner_description": "Kreirajte okruženje gde vaši timovi mogu da postave deljene aplikacije, radne tokove i vrste događaja sa kružnom dodelom i zajedničko zakazivanje.",
"organization_banner_title": "Upravljajte organizacijama sa više timova",
"set_up_your_organization": "Postavite svoju organizaciju",

View File

@ -21,7 +21,7 @@
"verify_email_email_link_text": "Här är länken om du inte gillar att klicka på knappar:",
"email_sent": "E-postmeddelandet har skickats",
"event_declined_subject": "Avvisades: {{title}} kl. {{date}}",
"event_cancelled_subject": "Avbröts: {{title}} {{date}}",
"event_cancelled_subject": "Avbokad: {{title}} {{date}}",
"event_request_declined": "Din bokningsförfrågan har avbjöts",
"event_request_declined_recurring": "Din återkommande händelse har avböjts",
"event_request_cancelled": "Din schemalagda bokning ställdes in",
@ -306,7 +306,7 @@
"password_has_been_reset_login": "Ditt lösenord har återställts. Du kan nu logga in med ditt nyskapade lösenord.",
"layout": "Layout",
"bookerlayout_default_title": "Standardvy",
"bookerlayout_description": "Du kan välja flera och dina bokare kan byta vy.",
"bookerlayout_description": "Du kan välja flera och dina bokare kan byta vyer.",
"bookerlayout_user_settings_title": "Bokningslayout",
"bookerlayout_user_settings_description": "Du kan välja flera och bokare kan byta vy. Detta kan åsidosättas för varje enskild händelse.",
"bookerlayout_month_view": "Månad",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Vill du verkligen ta bort {{connectionType}}-konfigurationen? Dina teammedlemmar som använder {{connectionType}}-inloggning kan inte längre komma åt Cal.com.",
"organizer_timezone": "Organisatörens tidszon",
"email_user_cta": "Visa inbjudan",
"email_no_user_invite_heading": "Du har blivit inbjuden att gå med i en/ett {{appName}}-{{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} har bjudit in dig att gå med i sitt team på {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
"email_user_invite_subheading": "{{invitedBy}} har bjudit in dig att gå med i deras {{entity}} '{{teamName}}' på {{appName}}. {{appName}} är schemaläggaren som gör det möjligt för dig och din/ditt {{entity}} att schemalägga möten utan att skicka e-post fram och tillbaka.",
"email_user_invite_subheading_team": "{{invitedBy}} har bjudit in dig att gå med i sitt team {{teamName}} den {{appName}}. {{appName}} är en händelsejonglerande schemaläggare som du och ditt team kan använda för att planera möten utan att skicka e-post fram och tillbaka.",
"email_no_user_invite_steps_intro": "Vi guidar dig genom några korta steg och du kommer att kunna schemalägga med din/ditt {{entity}} på nolltid utan stress.",
"email_no_user_step_one": "Välj ditt användarnamn",
"email_no_user_step_two": "Anslut ditt kalenderkonto",
@ -1687,7 +1686,7 @@
"attendee_no_longer_attending": "En deltagare deltar inte längre i din händelse",
"attendee_no_longer_attending_subtitle": "{{name}} har avbokat. Detta innebär att en plats har blivit ledig för denna tid",
"create_event_on": "Skapa händelse i",
"create_routing_form_on": "Skapa omdirigeringsformulär",
"create_routing_form_on": "Skapa omdirigeringsformulär",
"default_app_link_title": "Skapa en standardapplänk",
"default_app_link_description": "Om du skapar en standardapplänk kan alla händelsetyper som nyligen skapats använda den applänk som du har angett.",
"organizer_default_conferencing_app": "Arrangörens standardapp",
@ -1878,18 +1877,17 @@
"first_event_type_webhook_description": "Skapa din första webhook för denna händelsetyp",
"install_app_on": "Installera appen på",
"create_for": "Skapa för",
"setup_organization": "Konfigurera en organisation",
"organization_banner_description": "Skapa en miljö där dina team kan skapa delade appar, arbetsflöden och händelsetyper med round-robin och kollektiv schemaläggning.",
"organization_banner_description": "Skapa miljöer där dina team kan skapa delade appar, arbetsflöden och händelsetyper med round-robin och kollektiv schemaläggning.",
"organization_banner_title": "Hantera organisationer med flera team",
"set_up_your_organization": "Konfigurera din organisation",
"organizations_description": "Organisationer är delade miljöer där team kan skapa delade händelsetyper, appar, arbetsflöden och mycket mer.",
"must_enter_organization_name": "Ett organisationsnamn måste anges",
"must_enter_organization_admin_email": "En organisations e-postadress måste anges",
"admin_email": "Din organisations e-postadress",
"admin_email": "Din e-postadress i organisationen",
"admin_username": "Administratörens användarnamn",
"organization_name": "Organisationens namn",
"organization_url": "Organisationens URL",
"organization_verify_header": "Verifiera din organisations e-postadress",
"organization_verify_header": "Verifiera din e-postadress i organisationen",
"organization_verify_email_body": "Använd koden nedan för att verifiera din e-postadress så att du kan fortsätta att konfigurera din organisation.",
"additional_url_parameters": "Ytterligare URL-parametrar",
"about_your_organization": "Om din organisation",
@ -1940,5 +1938,5 @@
"insights_user_filter": "Användare: {{userName}}",
"insights_subtitle": "Visa Insights-bokningar för dina händelser",
"custom_plan": "Anpassad plan",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Lägg till dina nya strängar här ovanför ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -130,7 +130,7 @@
"upgrade_banner_action": "Buradan yükseltin",
"team_upgraded_successfully": "Ekibiniz başarıyla yükseltildi!",
"org_upgrade_banner_description": "Yeni Organization planımızı denediğiniz için teşekkür ederiz. \"{{teamName}}\" adlı Kuruluşunuzun planının yükseltilmesi gerektiğini fark ettik.",
"org_upgraded_successfully": "Kuruluşunuz başarıyla yükseltildi!",
"org_upgraded_successfully": "Organization'ınız başarıyla yükseltildi!",
"use_link_to_reset_password": "Şifrenizi sıfırlamak için aşağıdaki bağlantıyı kullanın",
"hey_there": "Selam,",
"forgot_your_password_calcom": "Şifrenizi mi unuttunuz? - {{appName}}",
@ -551,7 +551,7 @@
"team_description": "Birkaç cümleyle ekibinizden bahsedin. Bu, ekibinizin URL sayfasında görünecektir.",
"org_description": "Birkaç cümleyle kuruluşunuzdan bahsedin. Bu, kuruluşunuzun URL sayfasında görünecektir.",
"members": "Üyeler",
"organization_members": "Kuruluş üyeleri",
"organization_members": "Organization üyeleri",
"member": "Üye",
"number_member_one": "{{count}} üye",
"number_member_other": "{{count}} üye",
@ -662,7 +662,7 @@
"event_type_updated_successfully": "{{eventTypeTitle}} etkinlik türü başarıyla güncellendi",
"event_type_deleted_successfully": "Etkinlik türü başarıyla silindi",
"hours": "Saat",
"people": "İnsan",
"people": "İnsanlar",
"your_email": "E-postanız",
"change_avatar": "Avatarı değiştir",
"language": "Dil",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "{{connectionType}} yapılandırmasını silmek istediğinizden emin misiniz? {{connectionType}} girişini kullanan ekip üyeleriniz artık Cal.com'a erişemeyecek.",
"organizer_timezone": "Organizatörün saat dilimi",
"email_user_cta": "Daveti görüntüle",
"email_no_user_invite_heading": "{{appName}} {{entity}} grubuna katılmaya davet edildiniz",
"email_no_user_invite_subheading": "{{invitedBy}}, sizi {{appName}} ekibine katılmaya davet etti. {{appName}}, size ve ekibinize e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
"email_user_invite_subheading": "{{invitedBy}}, sizi {{appName}} uygulamasındaki {{entity}} `{{teamName}}` ekibine katılmaya davet etti. {{appName}}, size ve {{entity}} ekibinize e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
"email_user_invite_subheading_team": "{{invitedBy}}, sizi {{appName}} uygulamasındaki `{{teamName}}` ekibine katılmaya davet etti. {{appName}}, size ve ekibinize e-posta iletişimine ihtiyaç duymadan toplantı planlama yapma olanağı sağlayan bir etkinlik planlayıcıdır.",
"email_no_user_invite_steps_intro": "{{entity}} ekibinizle birlikte kısa sürede ve sorunsuz planlama yapmanın keyfini çıkarmanız için birkaç adımda size rehberlik edeceğiz.",
"email_no_user_step_one": "Kullanıcı adınızı seçin",
"email_no_user_step_two": "Takvim hesabınızı bağlayın",
@ -1876,9 +1875,8 @@
"connect_google_workspace": "Google Workspace'i bağlayın",
"google_workspace_admin_tooltip": "Bu özelliği kullanmak için Çalışma Alanı Yöneticisi olmalısınız",
"first_event_type_webhook_description": "Bu etkinlik türü için ilk web kancanızı oluşturun",
"install_app_on": "Uygulamayı şurada yükle:",
"install_app_on": "Uygulamayı şuraya yükle:",
"create_for": "Oluşturun",
"setup_organization": "Kuruluş oluşturun",
"organization_banner_description": "Ekiplerinizin, döngüsel ve toplu planlama ile paylaşılan uygulamalar, iş akışları ve olay türleri oluşturabileceği bir ortam oluşturun.",
"organization_banner_title": "Kuruluşları birden çok ekiple yönetin",
"set_up_your_organization": "Kuruluşunuzu düzenleyin",

View File

@ -551,7 +551,7 @@
"team_description": "Кілька речень про вашу команду. Ця інформація з’явиться на сторінці за URL-адресою вашої команди.",
"org_description": "Кілька речень про вашу організацію. Ця інформація з’явиться на сторінці за URL-адресою вашої організації.",
"members": "Учасники",
"organization_members": "Учасники організації",
"organization_members": "Учасники Organization",
"member": "Учасник",
"number_member_one": "{{count}} учасник",
"number_member_other": "Учасників: {{count}}",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Справді видалити конфігурацію {{connectionType}}? Учасники вашої команди, які входять через {{connectionType}}, не зможуть ввійти в Cal.com.",
"organizer_timezone": "Часовий пояс організатора",
"email_user_cta": "Переглянути запрошення",
"email_no_user_invite_heading": "Вас запрошено приєднатися: {{entity}} в застосунку {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} запрошує вас приєднатися до команди в {{appName}}. {{appName}} — планувальник подій, який дає змогу вам і вашій команді планувати зустрічі без тривалої переписки електронною поштою.",
"email_user_invite_subheading": "{{invitedBy}} запрошує вас приєднатися до {{entity}} «{{teamName}}» в застосунку {{appName}}. {{appName}} — планувальник подій, завдяки якому ви й ваша {{entity}} можете планувати наради без довгого листування.",
"email_user_invite_subheading_team": "{{invitedBy}} запрошує вас приєднатися до команди «{{teamName}}» в {{appName}}. {{appName}} — планувальник подій, який дає змогу вам і вашій команді планувати зустрічі без тривалої переписки електронною поштою.",
"email_no_user_invite_steps_intro": "Ми ознайомимо вас із застосунком, і вже скоро ви й ваша {{entity}} насолоджуватиметеся плануванням без жодного клопоту.",
"email_no_user_step_one": "Виберіть ім’я користувача",
"email_no_user_step_two": "Підключіть обліковий запис календаря",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Створіть свій перший вебгук для цього типу заходів",
"install_app_on": "Установити застосунок у",
"create_for": "Створити для",
"setup_organization": "Налаштувати організацію",
"organization_banner_description": "Підготуйте середовища, де ваші команди можуть створювати спільні застосунки, робочі процеси й типи заходів із циклічним і колективним плануванням.",
"organization_banner_title": "Керування організаціями з кількома командами",
"set_up_your_organization": "Налаштуйте свою організацію",
@ -1938,7 +1936,7 @@
"insights_all_org_filter": "Усі",
"insights_team_filter": "Команда: {{teamName}}",
"insights_user_filter": "Користувач: {{userName}}",
"insights_subtitle": "Переглядайте аналітичні дані про бронювання для своїх заходів",
"insights_subtitle": "Переглядайте дані Insights про бронювання для своїх заходів",
"custom_plan": "Користувацький план",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Додайте нові рядки вище ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "Bạn có chắc chắn muốn xóa cấu hình {{connectionType}} không? Các thành viên trong nhóm của bạn sử dụng thông tin đăng nhập {{connectionType}} sẽ không thể truy cập vào Cal.com được nữa.",
"organizer_timezone": "Múi giờ của người tổ chức",
"email_user_cta": "Xem lời mời",
"email_no_user_invite_heading": "Bạn đã được mời gia nhập {{entity}} {{appName}}",
"email_no_user_invite_subheading": "{{invitedBy}} đã mời bạn gia nhập nhóm của họ trên {{appName}}. {{appName}} là công cụ lên lịch sắp xếp sự kiện cho phép bạn và nhóm bạn lên lịch các cuộc gặp mà không cần trao đổi email nhiều.",
"email_user_invite_subheading": "{{invitedBy}} đã mời bạn gia nhập {{entity}} '{{teamName}}' của họ trên {{appName}}. {{appName}} là công cụ lên lịch sắp xếp sự kiện cho phép bạn và {{entity}} của bạn lên lịch các cuộc gặp mà không cần trao đổi email nhiều.",
"email_user_invite_subheading_team": "{{invitedBy}} đã mời bạn gia nhập nhóm '{{teamName}}' của họ trên {{appName}}. {{appName}} là công cụ lên lịch sắp xếp sự kiện cho phép bạn và nhóm bạn lên lịch các cuộc gặp mà không cần trao đổi email nhiều.",
"email_no_user_invite_steps_intro": "Chúng tôi sẽ hướng dẫn cho bạn qua vài bước nhỏ và bạn sẽ tận hưởng được ngay cảm giác thoải mái không căng thẳng trong việc lên lịch cùng {{entity}} của mình.",
"email_no_user_step_one": "Chọn tên người dùng của bạn",
"email_no_user_step_two": "Kết nối tài khoản lịch của bạn",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "Tạo webhook đầu tiên của bạn cho loại sự kiện này",
"install_app_on": "Cài đặt ứng dụng cho",
"create_for": "Tạo cho",
"setup_organization": "Thiết lập một Tổ chức",
"organization_banner_description": "Tạo môi trường tại đó các nhóm của bạn có thể tạo những ứng dụng, tiến độ công việc và loại sự kiện mà có thể chia sẻ được, với chức năng đặt lịch dạng round-robin và tập thể.",
"organization_banner_title": "Quản lý các tổ chức có nhiều nhóm",
"set_up_your_organization": "Thiết lập tổ chức của bạn",

View File

@ -1663,9 +1663,8 @@
"delete_sso_configuration_confirmation_description": "确定要删除 {{connectionType}} 配置吗?使用 {{connectionType}} 登录的团队成员将无法再访问 Cal.com。",
"organizer_timezone": "组织者时区",
"email_user_cta": "查看邀请",
"email_no_user_invite_heading": "您已被邀请加入 {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} 已邀请您加入他们在 {{appName}} 上的团队。{{appName}} 是一个活动安排调度程序,让您和您的团队无需通过电子邮件沟通即可安排会议。",
"email_user_invite_subheading": "{{invitedBy}} 已邀请您加入他们在 {{appName}} 上的 {{entity}}“{{teamName}}”。{{appName}} 是一个活动日程安排程序,让您和您的 {{entity}} 无需通过电子邮件沟通即可安排会议。",
"email_user_invite_subheading_team": "{{invitedBy}} 已邀请您加入他们在 {{appName}} 上的团队“{{teamName}}”。{{appName}} 是一个活动安排调度程序,让您和您的团队无需通过电子邮件沟通即可安排会议。",
"email_no_user_invite_steps_intro": "我们将引导您完成几个简短步骤,您将立即与您的 {{entity}} 一起享受无压力的日程安排。",
"email_no_user_step_one": "选择您的用户名",
"email_no_user_step_two": "连接您的日历账户",
@ -1882,7 +1881,6 @@
"first_event_type_webhook_description": "为此活动类型创建第一个 Webhook",
"install_app_on": "安装应用的账户",
"create_for": "创建",
"setup_organization": "设置组织",
"organization_banner_description": "创建环境,以便让您的团队可以在其中通过轮流和集体日程安排来创建共享的应用、工作流程和活动类型。",
"organization_banner_title": "管理具有多个团队的组织",
"set_up_your_organization": "设置您的组织",

View File

@ -89,7 +89,7 @@
"event_has_been_rescheduled": "已更新 - 已經重新預定活動時間",
"request_reschedule_subtitle": "{{organizer}} 已取消預約,並要求您選擇其他時間。",
"request_reschedule_title_organizer": "您已要求 {{attendee}} 重新預約",
"request_reschedule_subtitle_organizer": "您已取消預約,{{attendee}} 應和您重新挑選新的預約時間。",
"request_reschedule_subtitle_organizer": "您已取消預約,{{attendee}} 應和您重新挑選預約時間。",
"rescheduled_event_type_subject": "已送出重新預定的申請:在 {{date}} 與 {{name}} 的 {{eventType}}",
"requested_to_reschedule_subject_attendee": "需進行重新預約操作:請為與 {{name}} 的 {{eventType}} 預訂新時間",
"hi_user_name": "哈囉 {{name}}",
@ -306,7 +306,7 @@
"password_has_been_reset_login": "密碼已經重置。現在即可使用新建立的密碼登入。",
"layout": "版面",
"bookerlayout_default_title": "預設檢視",
"bookerlayout_description": "您可以選取多個版面,以便預約者切換檢視。",
"bookerlayout_description": "您可以選取多種檢視方式供預約者切換。",
"bookerlayout_user_settings_title": "預約版面",
"bookerlayout_user_settings_description": "您可以選取多個版面,以便預約者切換檢視。您可根據不同活動來覆寫此設定。",
"bookerlayout_month_view": "月",
@ -1659,9 +1659,8 @@
"delete_sso_configuration_confirmation_description": "確定要刪除 {{connectionType}} 設定?使用 {{connectionType}} 登入的團隊成員將再也無法存取 Cal.com。",
"organizer_timezone": "主辦者時區",
"email_user_cta": "檢視邀請",
"email_no_user_invite_heading": "您已獲邀加入 {{appName}} {{entity}}",
"email_no_user_invite_subheading": "{{invitedBy}} 已邀請您加入對方在 {{appName}} 上的團隊。{{appName}} 是活動多工排程工具,可讓您和您的團隊無須透過繁複的電子郵件往來就能輕鬆預定會議。",
"email_user_invite_subheading": "{{invitedBy}} 已邀請您加入對方在 {{appName}} 上的「{{teamName}}」{{entity}}。{{appName}} 為一款靈活的日程管理工具,可讓您和 {{entity}} 輕鬆預定會議,而無須透過繁複的電子郵件往來。",
"email_user_invite_subheading_team": "{{invitedBy}} 已邀請您加入對方在 {{appName}} 上的「{{teamName}}」團隊。{{appName}} 為一款活動多功能排程工具,可讓您和團隊無須透過繁複的電子郵件往來,就能輕鬆預定會議。",
"email_no_user_invite_steps_intro": "我們會逐步引導您完成幾個簡短的步驟,然後您就能馬上與 {{entity}} 享受沒有壓力的排程預定功能。",
"email_no_user_step_one": "選擇使用者名稱",
"email_no_user_step_two": "連結行事曆帳號",
@ -1687,7 +1686,7 @@
"attendee_no_longer_attending": "一位與會者不再參加您的活動",
"attendee_no_longer_attending_subtitle": "{{name}} 已取消,這代表此時段空出了一個座位",
"create_event_on": "活動建立日期",
"create_routing_form_on": "引導表單的建立位置",
"create_routing_form_on": "引導表單的建立帳戶",
"default_app_link_title": "設定預設應用程式連結",
"default_app_link_description": "設定預設應用程式連結,即可讓所有全新建立的活動類型使用您設定的應用程式連結。",
"organizer_default_conferencing_app": "主辦者的預設應用程式",
@ -1841,7 +1840,7 @@
"invite_link_copied": "邀請連結已複製",
"invite_link_deleted": "邀請連結已刪除",
"invite_link_updated": "邀請連結設定已儲存",
"link_expires_after": "連結設定效期…",
"link_expires_after": "連結效期設為…",
"one_day": "1 天",
"seven_days": "7 天",
"thirty_days": "30 天",
@ -1878,7 +1877,6 @@
"first_event_type_webhook_description": "為此活動類型建立第一個 Webhook",
"install_app_on": "應用程式安裝帳號",
"create_for": "建立",
"setup_organization": "設定組織",
"organization_banner_description": "建立環境,供團隊利用輪流制和全體預定功能建立共用的應用程式、工作流程和活動類型。",
"organization_banner_title": "管理有多個團隊的組織",
"set_up_your_organization": "設定組織",
@ -1938,7 +1936,7 @@
"insights_all_org_filter": "所有應用程式",
"insights_team_filter": "團隊:{{teamName}}",
"insights_user_filter": "使用者:{{userName}}",
"insights_subtitle": "查看所有活動的預約洞見",
"insights_subtitle": "查看所有活動的預約 Insight",
"custom_plan": "自訂方案",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 請在此處新增您的字串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

20
apps/web/test/fixtures/fixtures.ts vendored Normal file
View File

@ -0,0 +1,20 @@
// my-test.ts
import { test as base } from "vitest";
import { getTestEmails } from "@calcom/lib/testEmails";
export interface Fixtures {
emails: ReturnType<typeof getEmailsFixture>;
}
export const test = base.extend<Fixtures>({
emails: async ({}, use) => {
await use(getEmailsFixture());
},
});
function getEmailsFixture() {
return {
get: getTestEmails,
};
}

View File

@ -1,23 +1,14 @@
import type {
EventType as PrismaEventType,
User as PrismaUser,
Booking as PrismaBooking,
App as PrismaApp,
} from "@prisma/client";
import CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
import { diff } from "jest-diff";
import { v4 as uuidv4 } from "uuid";
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
import { getDate, getGoogleCalendarCredential, createBookingScenario} from "../utils/bookingScenario";
// TODO: Mock properly
prismaMock.eventType.findUnique.mockResolvedValue(null);
@ -129,6 +120,7 @@ const TestData = {
},
users: {
example: {
name: "Example",
username: "example",
defaultScheduleId: 1,
email: "example@example.com",
@ -151,63 +143,6 @@ const TestData = {
},
};
type App = {
slug: string;
dirName: string;
};
type InputCredential = typeof TestData.credentials.google;
type InputSelectedCalendar = typeof TestData.selectedCalendars.google;
type InputUser = typeof TestData.users.example & { id: number } & {
credentials?: InputCredential[];
selectedCalendars?: InputSelectedCalendar[];
schedules: {
id: number;
name: string;
availability: {
userId: number | null;
eventTypeId: number | null;
days: number[];
startTime: Date;
endTime: Date;
date: string | null;
}[];
timeZone: string;
}[];
};
type InputEventType = {
id: number;
title?: string;
length?: number;
offsetStart?: number;
slotInterval?: number;
minimumBookingNotice?: number;
users?: { id: number }[];
hosts?: { id: number }[];
schedulingType?: SchedulingType;
beforeEventBuffer?: number;
afterEventBuffer?: number;
};
type InputBooking = {
userId?: number;
eventTypeId: number;
startTime: string;
endTime: string;
title?: string;
status: BookingStatus;
attendees?: { email: string }[];
};
type InputHost = {
id: number;
userId: number;
eventTypeId: number;
isFixed: boolean;
};
const cleanup = async () => {
await prisma.eventType.deleteMany();
@ -241,7 +176,6 @@ describe("getSchedule", () => {
]);
const scenarioData = {
hosts: [],
eventTypes: [
{
id: 1,
@ -350,7 +284,6 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T06:15:00.000Z`,
},
],
hosts: [],
});
// Day Plus 2 is completely free - It only has non accepted bookings
@ -449,7 +382,6 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHours],
},
],
hosts: [],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
@ -550,7 +482,6 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHours],
},
],
hosts: [],
});
const { dateString: todayDateString } = getDate();
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
@ -634,7 +565,6 @@ describe("getSchedule", () => {
selectedCalendars: [TestData.selectedCalendars.google],
},
],
hosts: [],
apps: [TestData.apps.googleCalendar],
};
@ -710,7 +640,6 @@ describe("getSchedule", () => {
},
],
apps: [TestData.apps.googleCalendar],
hosts: [],
};
createBookingScenario(scenarioData);
@ -768,7 +697,6 @@ describe("getSchedule", () => {
selectedCalendars: [TestData.selectedCalendars.google],
},
],
hosts: [],
apps: [TestData.apps.googleCalendar],
};
@ -834,7 +762,6 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)],
},
],
hosts: [],
};
createBookingScenario(scenarioData);
@ -913,15 +840,6 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T04:15:00.000Z`,
},
],
hosts: [
// This user is a host of our Collective event
{
id: 1,
eventTypeId: 1,
userId: 101,
isFixed: true,
},
],
});
// Requesting this user's availability for their
@ -1022,7 +940,6 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T05:45:00.000Z`,
},
],
hosts: [],
});
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
@ -1162,7 +1079,6 @@ describe("getSchedule", () => {
endTime: `${plus3DateString}T04:15:00.000Z`,
},
],
hosts: [],
});
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
input: {
@ -1224,219 +1140,3 @@ describe("getSchedule", () => {
});
});
function getGoogleCalendarCredential() {
return {
type: "google_calendar",
key: {
scope:
"https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly",
token_type: "Bearer",
expiry_date: 1656999025367,
access_token: "ACCESS_TOKEN",
refresh_token: "REFRESH_TOKEN",
},
};
}
function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
const baseEventType = {
title: "Base EventType Title",
slug: "base-event-type-slug",
timeZone: null,
beforeEventBuffer: 0,
afterEventBuffer: 0,
schedulingType: null,
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
periodStartDate: new Date("2022-01-21T09:03:48.000Z"),
periodEndDate: new Date("2022-01-21T09:03:48.000Z"),
periodCountCalendarDays: false,
periodDays: 30,
seatsPerTimeSlot: null,
metadata: {},
minimumBookingNotice: 0,
offsetStart: 0,
};
const foundEvents: Record<number, boolean> = {};
const eventTypesWithUsers = eventTypes.map((eventType) => {
if (!eventType.slotInterval && !eventType.length) {
throw new Error("eventTypes[number]: slotInterval or length must be defined");
}
if (foundEvents[eventType.id]) {
throw new Error(`eventTypes[number]: id ${eventType.id} is not unique`);
}
foundEvents[eventType.id] = true;
const users =
eventType.users?.map((userWithJustId) => {
return usersStore.find((user) => user.id === userWithJustId.id);
}) || [];
return {
...baseEventType,
...eventType,
users,
};
});
logger.silly("TestData: Creating EventType", eventTypes);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.eventType.findUnique.mockImplementation(({ where }) => {
return new Promise((resolve) => {
const eventType = eventTypesWithUsers.find((e) => e.id === where.id) as unknown as PrismaEventType & {
users: PrismaUser[];
};
resolve(eventType);
});
});
}
async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[]) {
logger.silly("TestData: Creating Bookings", bookings);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.booking.findMany.mockImplementation((findManyArg) => {
const where = findManyArg?.where || {};
return new Promise((resolve) => {
resolve(
bookings
// We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed
.filter((booking) => {
/**
* A user is considered busy within a given time period if there
* is a booking they own OR host. This function mocks some of the logic
* for each condition. For details see the following ticket:
* https://github.com/calcom/cal.com/issues/6374
*/
// ~~ FIRST CONDITION ensures that this booking is owned by this user
// and that the status is what we want
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const statusIn = where.OR[0].status?.in || [];
const firstConditionMatches =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
statusIn.includes(booking.status) && booking.userId === where.OR[0].userId;
// We return this booking if either condition is met
return firstConditionMatches;
})
.map((booking) => ({
uid: uuidv4(),
title: "Test Booking Title",
...booking,
eventType: eventTypes.find((eventType) => eventType.id === booking.eventTypeId),
})) as unknown as PrismaBooking[]
);
});
});
}
function addUsers(users: InputUser[]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.user.findUniqueOrThrow.mockImplementation((findUniqueArgs) => {
return new Promise((resolve) => {
resolve({
email: `IntegrationTestUser${findUniqueArgs?.where.id}@example.com`,
} as unknown as PrismaUser);
});
});
prismaMock.user.findMany.mockResolvedValue(
users.map((user) => {
return {
...user,
username: `IntegrationTestUser${user.id}`,
email: `IntegrationTestUser${user.id}@example.com`,
};
}) as unknown as PrismaUser[]
);
}
type ScenarioData = {
// TODO: Support multiple bookings and add tests with that.
bookings?: InputBooking[];
users: InputUser[];
hosts: InputHost[];
credentials?: InputCredential[];
apps?: App[];
selectedCalendars?: InputSelectedCalendar[];
eventTypes: InputEventType[];
calendarBusyTimes?: {
start: string;
end: string;
}[];
};
function createBookingScenario(data: ScenarioData) {
logger.silly("TestData: Creating Scenario", data);
addUsers(data.users);
const eventType = addEventTypes(data.eventTypes, data.users);
if (data.apps) {
prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]);
// FIXME: How do we know which app to return?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.app.findUnique.mockImplementation(({ where: { slug: whereSlug } }) => {
return new Promise((resolve) => {
if (!data.apps) {
resolve(null);
return;
}
resolve((data.apps.find(({ slug }) => slug == whereSlug) as PrismaApp) || null);
});
});
}
data.bookings = data.bookings || [];
addBookings(data.bookings, data.eventTypes);
return {
eventType,
};
}
/**
* This fn indents to dynamically compute day, month, year for the purpose of testing.
* We are not using DayJS because that's actually being tested by this code.
* - `dateIncrement` adds the increment to current day
* - `monthIncrement` adds the increment to current month
* - `yearIncrement` adds the increment to current year
*/
const getDate = (param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {}) => {
let { dateIncrement, monthIncrement, yearIncrement } = param;
dateIncrement = dateIncrement || 0;
monthIncrement = monthIncrement || 0;
yearIncrement = yearIncrement || 0;
let _date = new Date().getDate() + dateIncrement;
let year = new Date().getFullYear() + yearIncrement;
// Make it start with 1 to match with DayJS requiremet
let _month = new Date().getMonth() + monthIncrement + 1;
// If last day of the month(As _month is plus 1 already it is going to be the 0th day of next month which is the last day of current month)
const lastDayOfMonth = new Date(year, _month, 0).getDate();
const numberOfDaysForNextMonth = +_date - +lastDayOfMonth;
if (numberOfDaysForNextMonth > 0) {
_date = numberOfDaysForNextMonth;
_month = _month + 1;
}
if (_month === 13) {
_month = 1;
year = year + 1;
}
const date = _date < 10 ? "0" + _date : _date;
const month = _month < 10 ? "0" + _month : _month;
return {
date,
month,
year,
dateString: `${year}-${month}-${date}`,
};
};

View File

@ -0,0 +1,742 @@
import type {
EventType as PrismaEventType,
User as PrismaUser,
Booking as PrismaBooking,
App as PrismaApp,
} from "@prisma/client";
import type { Prisma } from "@prisma/client";
import type { WebhookTriggerEvents } from "@prisma/client";
import { v4 as uuidv4 } from "uuid";
import { expect } from "vitest";
import "vitest-fetch-mock";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import logger from "@calcom/lib/logger";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { Fixtures } from "@calcom/web/test/fixtures/fixtures";
import appStoreMock from "../../../../tests/libs/__mocks__/app-store";
import i18nMock from "../../../../tests/libs/__mocks__/libServerI18n";
import prismaMock from "../../../../tests/libs/__mocks__/prisma";
type App = {
slug: string;
dirName: string;
};
type InputWebhook = {
appId: string | null;
userId?: number | null;
teamId?: number | null;
eventTypeId?: number;
active: boolean;
eventTriggers: WebhookTriggerEvents[];
subscriberUrl: string;
};
/**
* Data to be mocked
*/
type ScenarioData = {
// hosts: { id: number; eventTypeId?: number; userId?: number; isFixed?: boolean }[];
/**
* Prisma would return these eventTypes
*/
eventTypes: InputEventType[];
/**
* Prisma would return these users
*/
users: InputUser[];
/**
* Prisma would return these apps
*/
apps?: App[];
bookings?: InputBooking[];
webhooks?: InputWebhook[];
};
type InputCredential = typeof TestData.credentials.google;
type InputSelectedCalendar = typeof TestData.selectedCalendars.google;
type InputUser = typeof TestData.users.example & { id: number } & {
credentials?: InputCredential[];
selectedCalendars?: InputSelectedCalendar[];
schedules: {
id: number;
name: string;
availability: {
userId: number | null;
eventTypeId: number | null;
days: number[];
startTime: Date;
endTime: Date;
date: string | null;
}[];
timeZone: string;
}[];
};
type InputEventType = {
id: number;
title?: string;
length?: number;
offsetStart?: number;
slotInterval?: number;
minimumBookingNotice?: number;
/**
* These user ids are `ScenarioData["users"]["id"]`
*/
users?: { id: number }[];
hosts?: { id: number }[];
schedulingType?: SchedulingType;
beforeEventBuffer?: number;
afterEventBuffer?: number;
requiresConfirmation?: boolean;
};
type InputBooking = {
userId?: number;
eventTypeId: number;
startTime: string;
endTime: string;
title?: string;
status: BookingStatus;
attendees?: { email: string }[];
};
const Timezones = {
"+5:30": "Asia/Kolkata",
"+6:00": "Asia/Dhaka",
};
function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
const baseEventType = {
title: "Base EventType Title",
slug: "base-event-type-slug",
timeZone: null,
beforeEventBuffer: 0,
afterEventBuffer: 0,
schedulingType: null,
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
periodStartDate: new Date("2022-01-21T09:03:48.000Z"),
periodEndDate: new Date("2022-01-21T09:03:48.000Z"),
periodCountCalendarDays: false,
periodDays: 30,
seatsPerTimeSlot: null,
metadata: {},
minimumBookingNotice: 0,
offsetStart: 0,
};
const foundEvents: Record<number, boolean> = {};
const eventTypesWithUsers = eventTypes.map((eventType) => {
if (!eventType.slotInterval && !eventType.length) {
throw new Error("eventTypes[number]: slotInterval or length must be defined");
}
if (foundEvents[eventType.id]) {
throw new Error(`eventTypes[number]: id ${eventType.id} is not unique`);
}
foundEvents[eventType.id] = true;
const users =
eventType.users?.map((userWithJustId) => {
return usersStore.find((user) => user.id === userWithJustId.id);
}) || [];
return {
...baseEventType,
...eventType,
workflows: [],
users,
};
});
logger.silly("TestData: Creating EventType", eventTypes);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const eventTypeMock = ({ where }) => {
return new Promise((resolve) => {
const eventType = eventTypesWithUsers.find((e) => e.id === where.id) as unknown as PrismaEventType & {
users: PrismaUser[];
};
resolve(eventType);
});
};
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.eventType.findUnique.mockImplementation(eventTypeMock);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.eventType.findUniqueOrThrow.mockImplementation(eventTypeMock);
}
async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[]) {
logger.silly("TestData: Creating Bookings", bookings);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.booking.findMany.mockImplementation((findManyArg) => {
const where = findManyArg?.where || {};
return new Promise((resolve) => {
resolve(
bookings
// We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed
.filter((booking) => {
/**
* A user is considered busy within a given time period if there
* is a booking they own OR host. This function mocks some of the logic
* for each condition. For details see the following ticket:
* https://github.com/calcom/cal.com/issues/6374
*/
// ~~ FIRST CONDITION ensures that this booking is owned by this user
// and that the status is what we want
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const statusIn = where.OR[0].status?.in || [];
const firstConditionMatches =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
statusIn.includes(booking.status) && booking.userId === where.OR[0].userId;
// We return this booking if either condition is met
return firstConditionMatches;
})
.map((booking) => ({
uid: uuidv4(),
title: "Test Booking Title",
...booking,
eventType: eventTypes.find((eventType) => eventType.id === booking.eventTypeId),
})) as unknown as PrismaBooking[]
);
});
});
}
async function addWebhooks(webhooks: InputWebhook[]) {
prismaMock.webhook.findMany.mockResolvedValue(
webhooks.map((webhook) => {
return {
...webhook,
payloadTemplate: null,
secret: null,
id: uuidv4(),
createdAt: new Date(),
userId: webhook.userId || null,
eventTypeId: webhook.eventTypeId || null,
teamId: webhook.teamId || null,
};
})
);
}
function addUsers(users: InputUser[]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.user.findUniqueOrThrow.mockImplementation((findUniqueArgs) => {
return new Promise((resolve) => {
resolve({
email: `IntegrationTestUser${findUniqueArgs?.where.id}@example.com`,
} as unknown as PrismaUser);
});
});
prismaMock.user.findMany.mockResolvedValue(
users.map((user) => {
return {
...user,
username: `IntegrationTestUser${user.id}`,
email: `IntegrationTestUser${user.id}@example.com`,
};
}) as unknown as PrismaUser[]
);
}
export async function createBookingScenario(data: ScenarioData) {
logger.silly("TestData: Creating Scenario", data);
addUsers(data.users);
const eventType = addEventTypes(data.eventTypes, data.users);
if (data.apps) {
prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
const appMock = ({ where: { slug: whereSlug } }) => {
return new Promise((resolve) => {
if (!data.apps) {
resolve(null);
return;
}
const foundApp = data.apps.find(({ slug }) => slug == whereSlug);
//TODO: Pass just the app name in data.apps and maintain apps in a separate object or load them dyamically
resolve(
({
...foundApp,
...(foundApp?.slug ? TestData.apps[foundApp.slug as keyof typeof TestData.apps] || {} : {}),
enabled: true,
createdAt: new Date(),
updatedAt: new Date(),
categories: [],
} as PrismaApp) || null
);
});
};
// FIXME: How do we know which app to return?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.app.findUnique.mockImplementation(appMock);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prismaMock.app.findFirst.mockImplementation(appMock);
}
data.bookings = data.bookings || [];
allowSuccessfulBookingCreation();
addBookings(data.bookings, data.eventTypes);
// mockBusyCalendarTimes([]);
addWebhooks(data.webhooks || []);
return {
eventType,
};
}
/**
* This fn indents to /ally compute day, month, year for the purpose of testing.
* We are not using DayJS because that's actually being tested by this code.
* - `dateIncrement` adds the increment to current day
* - `monthIncrement` adds the increment to current month
* - `yearIncrement` adds the increment to current year
*/
export const getDate = (
param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {}
) => {
let { dateIncrement, monthIncrement, yearIncrement } = param;
dateIncrement = dateIncrement || 0;
monthIncrement = monthIncrement || 0;
yearIncrement = yearIncrement || 0;
let _date = new Date().getDate() + dateIncrement;
let year = new Date().getFullYear() + yearIncrement;
// Make it start with 1 to match with DayJS requiremet
let _month = new Date().getMonth() + monthIncrement + 1;
// If last day of the month(As _month is plus 1 already it is going to be the 0th day of next month which is the last day of current month)
const lastDayOfMonth = new Date(year, _month, 0).getDate();
const numberOfDaysForNextMonth = +_date - +lastDayOfMonth;
if (numberOfDaysForNextMonth > 0) {
_date = numberOfDaysForNextMonth;
_month = _month + 1;
}
if (_month === 13) {
_month = 1;
year = year + 1;
}
const date = _date < 10 ? "0" + _date : _date;
const month = _month < 10 ? "0" + _month : _month;
return {
date,
month,
year,
dateString: `${year}-${month}-${date}`,
};
};
export function getMockedCredential({
metadataLookupKey,
key,
}: {
metadataLookupKey: string;
key: {
expiry_date?: number;
token_type?: string;
access_token?: string;
refresh_token?: string;
scope: string;
};
}) {
return {
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
appId: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].slug,
key: {
expiry_date: Date.now() + 1000000,
token_type: "Bearer",
access_token: "ACCESS_TOKEN",
refresh_token: "REFRESH_TOKEN",
...key,
},
};
}
export function getGoogleCalendarCredential() {
return getMockedCredential({
metadataLookupKey: "googlecalendar",
key: {
scope:
"https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly",
},
});
}
export function getZoomAppCredential() {
return getMockedCredential({
metadataLookupKey: "zoomvideo",
key: {
scope: "meeting:writed",
},
});
}
export const TestData = {
selectedCalendars: {
google: {
integration: "google_calendar",
externalId: "john@example.com",
},
},
credentials: {
google: getGoogleCalendarCredential(),
},
schedules: {
IstWorkHours: {
id: 1,
name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT",
availability: [
{
userId: null,
eventTypeId: null,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date("1970-01-01T09:30:00.000Z"),
endTime: new Date("1970-01-01T18:00:00.000Z"),
date: null,
},
],
timeZone: Timezones["+5:30"],
},
IstWorkHoursWithDateOverride: (dateString: string) => ({
id: 1,
name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT but with a Date Override for 2PM to 6PM IST(in GST time it is 8:30AM to 12:30PM)",
availability: [
{
userId: null,
eventTypeId: null,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date("1970-01-01T09:30:00.000Z"),
endTime: new Date("1970-01-01T18:00:00.000Z"),
date: null,
},
{
userId: null,
eventTypeId: null,
days: [0, 1, 2, 3, 4, 5, 6],
startTime: new Date(`1970-01-01T14:00:00.000Z`),
endTime: new Date(`1970-01-01T18:00:00.000Z`),
date: dateString,
},
],
timeZone: Timezones["+5:30"],
}),
},
users: {
example: {
name: "Example",
email: "example@example.com",
username: "example",
defaultScheduleId: 1,
timeZone: Timezones["+5:30"],
},
},
apps: {
"google-calendar": {
slug: "google-calendar",
dirName: "whatever",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
keys: {
expiry_date: Infinity,
client_id: "client_id",
client_secret: "client_secret",
redirect_uris: ["http://localhost:3000/auth/callback"],
},
},
"daily-video": {
slug: "daily-video",
dirName: "whatever",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
keys: {
expiry_date: Infinity,
api_key: "",
scale_plan: "false",
client_id: "client_id",
client_secret: "client_secret",
redirect_uris: ["http://localhost:3000/auth/callback"],
},
},
},
};
function allowSuccessfulBookingCreation() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
prismaMock.booking.create.mockImplementation(function (booking) {
return booking.data;
});
}
export class MockError extends Error {
constructor(message: string) {
super(message);
this.name = "MockError";
}
}
export function getOrganizer({
name,
email,
id,
schedules,
credentials,
selectedCalendars,
}: {
name: string;
email: string;
id: number;
schedules: InputUser["schedules"];
credentials?: InputCredential[];
selectedCalendars?: InputSelectedCalendar[];
}) {
return {
...TestData.users.example,
name,
email,
id,
schedules,
credentials,
selectedCalendars,
};
}
export function getScenarioData({
organizer,
eventTypes,
usersApartFromOrganizer = [],
apps = [],
webhooks,
}: // hosts = [],
{
organizer: ReturnType<typeof getOrganizer>;
eventTypes: ScenarioData["eventTypes"];
apps: ScenarioData["apps"];
usersApartFromOrganizer?: ScenarioData["users"];
webhooks?: ScenarioData["webhooks"];
// hosts?: ScenarioData["hosts"];
}) {
const users = [organizer, ...usersApartFromOrganizer];
eventTypes.forEach((eventType) => {
if (
eventType.users?.filter((eventTypeUser) => {
return !users.find((userToCreate) => userToCreate.id === eventTypeUser.id);
}).length
) {
throw new Error(`EventType ${eventType.id} has users that are not present in ScenarioData["users"]`);
}
});
return {
// hosts: [...hosts],
eventTypes: [...eventTypes],
users,
apps: [...apps],
webhooks,
};
}
export function mockEnableEmailFeature() {
prismaMock.feature.findMany.mockResolvedValue([
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
{
slug: "emails",
// It's a kill switch
enabled: false,
},
]);
}
export function mockNoTranslations() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
i18nMock.getTranslation.mockImplementation(() => {
return new Promise((resolve) => {
const identityFn = (key: string) => key;
resolve(identityFn);
});
});
}
export function mockCalendarToHaveNoBusySlots(metadataLookupKey: keyof typeof appStoreMetadata) {
const appStoreLookupKey = metadataLookupKey;
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockResolvedValue({
lib: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
CalendarService: function MockCalendarService() {
return {
createEvent: () => {
return Promise.resolve({
type: "daily_video",
id: "dailyEventName",
password: "dailyvideopass",
url: "http://dailyvideo.example.com",
});
},
getAvailability: (): Promise<EventBusyDate[]> => {
return new Promise((resolve) => {
resolve([]);
});
},
};
},
},
});
}
export function mockSuccessfulVideoMeetingCreation({
metadataLookupKey,
appStoreLookupKey,
}: {
metadataLookupKey: string;
appStoreLookupKey?: string;
}) {
appStoreLookupKey = appStoreLookupKey || metadataLookupKey;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => {
return new Promise((resolve) => {
resolve({
lib: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
VideoApiAdapter: () => ({
createMeeting: () => {
return Promise.resolve({
type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type,
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-${metadataLookupKey}.example.com`,
});
},
}),
},
});
});
});
}
export function mockErrorOnVideoMeetingCreation({
metadataLookupKey,
appStoreLookupKey,
}: {
metadataLookupKey: string;
appStoreLookupKey?: string;
}) {
appStoreLookupKey = appStoreLookupKey || metadataLookupKey;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
appStoreMock.default[appStoreLookupKey].mockImplementation(() => {
return new Promise((resolve) => {
resolve({
lib: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
VideoApiAdapter: () => ({
createMeeting: () => {
throw new MockError("Error creating Video meeting");
},
}),
},
});
});
});
}
export function expectWebhookToHaveBeenCalledWith(
subscriberUrl: string,
data: {
triggerEvent: WebhookTriggerEvents;
payload: { metadata: Record<string, unknown>; responses: Record<string, unknown> };
}
) {
const fetchCalls = fetchMock.mock.calls;
const webhookFetchCall = fetchCalls.find((call) => call[0] === subscriberUrl);
if (!webhookFetchCall) {
throw new Error(`Webhook not called with ${subscriberUrl}`);
}
expect(webhookFetchCall[0]).toBe(subscriberUrl);
const body = webhookFetchCall[1]?.body;
const parsedBody = JSON.parse((body as string) || "{}");
console.log({ payload: parsedBody.payload });
expect(parsedBody.triggerEvent).toBe(data.triggerEvent);
parsedBody.payload.metadata.videoCallUrl = parsedBody.payload.metadata.videoCallUrl
? parsedBody.payload.metadata.videoCallUrl.replace(/\/video\/[a-zA-Z0-9]{22}/, "/video/DYNAMIC_UID")
: parsedBody.payload.metadata.videoCallUrl;
expect(parsedBody.payload.metadata).toContain(data.payload.metadata);
expect(parsedBody.payload.responses).toEqual(data.payload.responses);
}
export function expectWorkflowToBeTriggered() {
// TODO: Implement this.
}
export function expectBookingToBeInDatabase(booking: Partial<Prisma.BookingCreateInput>) {
const createBookingCalledWithArgs = prismaMock.booking.create.mock.calls[0];
expect(createBookingCalledWithArgs[0].data).toEqual(expect.objectContaining(booking));
}
export function getBooker({ name, email }: { name: string; email: string }) {
return {
name,
email,
};
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers<R> {
toHaveEmail(expectedEmail: { htmlToContain?: string; to: string }): R;
}
}
}
expect.extend({
toHaveEmail(
testEmail: ReturnType<Fixtures["emails"]["get"]>[number],
expectedEmail: {
//TODO: Support email HTML parsing to target specific elements
htmlToContain?: string;
to: string;
}
) {
let isHtmlContained = true;
let isToAddressExpected = true;
if (expectedEmail.htmlToContain) {
isHtmlContained = testEmail.html.includes(expectedEmail.htmlToContain);
}
isToAddressExpected = expectedEmail.to === testEmail.to;
return {
pass: isHtmlContained && isToAddressExpected,
message: () => {
if (!isHtmlContained) {
return `Email HTML is not as expected. Expected:"${expectedEmail.htmlToContain}" isn't contained in "${testEmail.html}"`;
}
return `Email To address is not as expected. Expected:${expectedEmail.to} isn't contained in ${testEmail.to}`;
},
};
},
});

View File

@ -89,7 +89,8 @@
"prettier": "^2.8.6",
"tsc-absolute": "^1.0.0",
"typescript": "^4.9.4",
"vitest": "^0.31.1",
"vitest": "^0.34.3",
"vitest-fetch-mock": "^0.2.2",
"vitest-mock-extended": "^1.1.3"
},
"dependencies": {

View File

@ -2,6 +2,7 @@ import type { GetStaticPropsContext } from "next";
export const AppSetupPageMap = {
zapier: import("../../zapier/pages/setup/_getStaticProps"),
make: import("../../make/pages/setup/_getStaticProps"),
};
export const getStaticProps = async (ctx: GetStaticPropsContext) => {

View File

@ -9,6 +9,7 @@ export const AppSetupMap = {
"exchange2016-calendar": dynamic(() => import("../../exchange2016calendar/pages/setup")),
"caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")),
zapier: dynamic(() => import("../../zapier/pages/setup")),
make: dynamic(() => import("../../make/pages/setup")),
closecom: dynamic(() => import("../../closecom/pages/setup")),
sendgrid: dynamic(() => import("../../sendgrid/pages/setup")),
paypal: dynamic(() => import("../../paypal/pages/setup")),

View File

@ -12,6 +12,7 @@ import { appKeysSchema as gtm_zod_ts } from "./gtm/zod";
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/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 metapixel_zod_ts } from "./metapixel/zod";
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
@ -43,6 +44,7 @@ export const appKeysSchemas = {
hubspot: hubspot_zod_ts,
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
make: make_zod_ts,
metapixel: metapixel_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,

View File

@ -29,6 +29,7 @@ import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
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 metapixel_config_json from "./metapixel/config.json";
import mirotalk_config_json from "./mirotalk/config.json";
import n8n_config_json from "./n8n/config.json";
@ -99,6 +100,7 @@ export const appStoreMetadata = {
huddle01video: huddle01video__metadata_ts,
jitsivideo: jitsivideo__metadata_ts,
larkcalendar: larkcalendar__metadata_ts,
make: make_config_json,
metapixel: metapixel_config_json,
mirotalk: mirotalk_config_json,
n8n: n8n_config_json,

View File

@ -12,6 +12,7 @@ import { appDataSchema as gtm_zod_ts } from "./gtm/zod";
import { appDataSchema as hubspot_zod_ts } from "./hubspot/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 metapixel_zod_ts } from "./metapixel/zod";
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
@ -43,6 +44,7 @@ export const appDataSchemas = {
hubspot: hubspot_zod_ts,
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
make: make_zod_ts,
metapixel: metapixel_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,

View File

@ -29,6 +29,7 @@ export const apiHandlers = {
huddle01video: import("./huddle01video/api"),
jitsivideo: import("./jitsivideo/api"),
larkcalendar: import("./larkcalendar/api"),
make: import("./make/api"),
metapixel: import("./metapixel/api"),
mirotalk: import("./mirotalk/api"),
n8n: import("./n8n/api"),

View File

@ -2,7 +2,7 @@
"name": "Basecamp3",
"slug": "basecamp3",
"type": "basecamp3_other_calendar",
"logo": "logo.svg",
"logo": "icon-dark.svg",
"url": "https://basecamp.com/",
"variant": "other",
"categories": ["other"],

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1,26 +1,38 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_117_4)">
<rect width="128" height="128" fill="white"/>
<circle cx="64" cy="64" r="64" fill="#04BF8A"/>
<mask id="path-2-inside-1_117_4" fill="white">
<path d="M32.7099 74.7566C29.6119 75.9781 26.0314 74.4473 25.5846 71.1473C25.1747 68.1192 25.3114 65.0002 26.0042 61.9169C27.1525 56.8057 29.7838 51.9924 33.6065 48.0106C37.4291 44.0289 42.2937 41.0341 47.6612 39.3584C50.9977 38.3167 54.4475 37.8099 57.8599 37.8459C61.1841 37.8809 63.1528 41.2619 62.1562 44.4334V44.4334C61.2866 47.2009 58.433 48.7691 55.5372 48.9409C53.9259 49.0365 52.3131 49.3307 50.7418 49.8213C47.4989 50.8338 44.5597 52.6432 42.2501 55.0489C39.9405 57.4547 38.3507 60.3628 37.6569 63.4509C37.418 64.5141 37.2885 65.5844 37.2675 66.6474C37.2014 69.9769 35.8078 73.5351 32.7099 74.7566V74.7566Z"/>
<g clip-path="url(#clip0_21_40)">
<mask id="mask0_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="128" height="128">
<path d="M128 0H0V128H128V0Z" fill="white"/>
</mask>
<path d="M32.7099 74.7566C29.6119 75.9781 26.0314 74.4473 25.5846 71.1473C25.1747 68.1192 25.3114 65.0002 26.0042 61.9169C27.1525 56.8057 29.7838 51.9924 33.6065 48.0106C37.4291 44.0289 42.2937 41.0341 47.6612 39.3584C50.9977 38.3167 54.4475 37.8099 57.8599 37.8459C61.1841 37.8809 63.1528 41.2619 62.1562 44.4334V44.4334C61.2866 47.2009 58.433 48.7691 55.5372 48.9409C53.9259 49.0365 52.3131 49.3307 50.7418 49.8213C47.4989 50.8338 44.5597 52.6432 42.2501 55.0489C39.9405 57.4547 38.3507 60.3628 37.6569 63.4509C37.418 64.5141 37.2885 65.5844 37.2675 66.6474C37.2014 69.9769 35.8078 73.5351 32.7099 74.7566V74.7566Z" fill="white" stroke="white" stroke-width="2" mask="url(#path-2-inside-1_117_4)"/>
<mask id="path-3-inside-2_117_4" fill="white">
<path d="M52.9036 31.2935C51.7371 28.1744 53.331 24.6215 56.6383 24.2332C59.6732 23.8768 62.7894 24.0688 65.8599 24.8159C70.95 26.0545 75.716 28.7705 79.6295 32.663C83.543 36.5554 86.4513 41.4723 88.0318 46.8685C89.0144 50.2229 89.46 53.6812 89.3637 57.0924C89.2699 60.4155 85.8546 62.3241 82.7012 61.2715V61.2715C79.9496 60.353 78.4321 57.4722 78.3115 54.5738C78.2444 52.9611 77.9788 51.3433 77.5161 49.7636C76.5611 46.5033 74.804 43.5326 72.4395 41.1808C70.0749 38.829 67.1954 37.188 64.12 36.4396C63.0612 36.182 61.9934 36.0336 60.9309 35.9938C57.6031 35.8689 54.0701 34.4126 52.9036 31.2935V31.2935Z"/>
<g mask="url(#mask0_21_40)">
<path d="M64 128C99.3462 128 128 99.3462 128 64C128 28.6538 99.3462 0 64 0C28.6538 0 0 28.6538 0 64C0 99.3462 28.6538 128 64 128Z" fill="#04BF8A"/>
<mask id="mask1_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="24" y="36" width="40" height="41">
<path d="M32.7098 74.7567C29.6118 75.9782 26.0313 74.4474 25.5845 71.1474C25.1746 68.1193 25.3113 65.0003 26.0041 61.917C27.1524 56.8058 29.7837 51.9925 33.6064 48.0107C37.429 44.029 42.2936 41.0342 47.6611 39.3585C50.9976 38.3168 54.4474 37.81 57.8598 37.846C61.184 37.881 63.1527 41.262 62.1561 44.4335C61.2865 47.201 58.4329 48.7692 55.5371 48.941C53.9258 49.0366 52.313 49.3308 50.7417 49.8214C47.4988 50.8339 44.5596 52.6433 42.25 55.049C39.9404 57.4548 38.3506 60.3629 37.6568 63.451C37.4179 64.5142 37.2884 65.5845 37.2674 66.6475C37.2013 69.977 35.8077 73.5352 32.7098 74.7567Z" fill="white" stroke="white" stroke-width="2"/>
</mask>
<path d="M52.9036 31.2935C51.7371 28.1744 53.331 24.6215 56.6383 24.2332C59.6732 23.8768 62.7894 24.0688 65.8599 24.8159C70.95 26.0545 75.716 28.7705 79.6295 32.663C83.543 36.5554 86.4513 41.4723 88.0318 46.8685C89.0144 50.2229 89.46 53.6812 89.3637 57.0924C89.2699 60.4155 85.8546 62.3241 82.7012 61.2715V61.2715C79.9496 60.353 78.4321 57.4722 78.3115 54.5738C78.2444 52.9611 77.9788 51.3433 77.5161 49.7636C76.5611 46.5033 74.804 43.5326 72.4395 41.1808C70.0749 38.829 67.1954 37.188 64.12 36.4396C63.0612 36.182 61.9934 36.0336 60.9309 35.9938C57.6031 35.8689 54.0701 34.4126 52.9036 31.2935V31.2935Z" fill="white" stroke="white" stroke-width="2" mask="url(#path-3-inside-2_117_4)"/>
<mask id="path-4-inside-3_117_4" fill="white">
<path d="M97.7468 52.0457C100.912 51.0099 104.395 52.7501 104.646 56.0708C104.876 59.1178 104.554 62.2233 103.68 65.2601C102.231 70.2944 99.3192 74.9433 95.2674 78.6916C91.2156 82.4399 86.1821 85.1412 80.7248 86.4961C77.3324 87.3383 73.8587 87.6398 70.4544 87.4017C67.1381 87.1698 65.3731 83.6782 66.5559 80.5713V80.5713C67.5879 77.8602 70.5294 76.4638 73.4303 76.4638C75.0444 76.4638 76.6718 76.2656 78.2694 75.869C81.5666 75.0504 84.6078 73.4183 87.0559 71.1536C89.5039 68.8889 91.2632 66.0801 92.1388 63.0385C92.4402 61.9913 92.6328 60.9306 92.7168 59.8706C92.98 56.551 94.5819 53.0816 97.7468 52.0457V52.0457Z"/>
<g mask="url(#mask1_21_40)">
<path d="M32.7098 74.7567C29.6118 75.9782 26.0313 74.4474 25.5845 71.1474C25.1746 68.1193 25.3113 65.0003 26.0041 61.917C27.1524 56.8058 29.7837 51.9925 33.6064 48.0107C37.429 44.029 42.2936 41.0342 47.6611 39.3585C50.9976 38.3168 54.4474 37.81 57.8598 37.846C61.184 37.881 63.1527 41.262 62.1561 44.4335C61.2865 47.201 58.4329 48.7692 55.5371 48.941C53.9258 49.0366 52.313 49.3308 50.7417 49.8214C47.4988 50.8339 44.5596 52.6433 42.25 55.049C39.9404 57.4548 38.3506 60.3629 37.6568 63.451C37.4179 64.5142 37.2884 65.5845 37.2674 66.6475C37.2013 69.977 35.8077 73.5352 32.7098 74.7567Z" fill="white" stroke="white" stroke-width="2"/>
</g>
<mask id="mask2_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="51" y="23" width="40" height="40">
<path d="M52.9037 31.2934C51.7372 28.1743 53.3311 24.6214 56.6384 24.2331C59.6733 23.8767 62.7895 24.0687 65.86 24.8158C70.9501 26.0544 75.7161 28.7704 79.6296 32.6629C83.5431 36.5553 86.4514 41.4722 88.0319 46.8684C89.0145 50.2228 89.4601 53.6811 89.3638 57.0923C89.27 60.4154 85.8547 62.324 82.7013 61.2714C79.9497 60.3529 78.4322 57.4721 78.3116 54.5737C78.2445 52.961 77.9789 51.3432 77.5162 49.7635C76.5612 46.5032 74.8041 43.5325 72.4396 41.1807C70.075 38.8289 67.1955 37.1879 64.1201 36.4395C63.0613 36.1819 61.9935 36.0335 60.931 35.9937C57.6032 35.8688 54.0702 34.4125 52.9037 31.2934Z" fill="white" stroke="white" stroke-width="2"/>
</mask>
<path d="M97.7468 52.0457C100.912 51.0099 104.395 52.7501 104.646 56.0708C104.876 59.1178 104.554 62.2233 103.68 65.2601C102.231 70.2944 99.3192 74.9433 95.2674 78.6916C91.2156 82.4399 86.1821 85.1412 80.7248 86.4961C77.3324 87.3383 73.8587 87.6398 70.4544 87.4017C67.1381 87.1698 65.3731 83.6782 66.5559 80.5713V80.5713C67.5879 77.8602 70.5294 76.4638 73.4303 76.4638C75.0444 76.4638 76.6718 76.2656 78.2694 75.869C81.5666 75.0504 84.6078 73.4183 87.0559 71.1536C89.5039 68.8889 91.2632 66.0801 92.1388 63.0385C92.4402 61.9913 92.6328 60.9306 92.7168 59.8706C92.98 56.551 94.5819 53.0816 97.7468 52.0457V52.0457Z" fill="white" stroke="white" stroke-width="2" mask="url(#path-4-inside-3_117_4)"/>
<mask id="path-5-inside-4_117_4" fill="white">
<path d="M75.895 95.1969C77.127 98.2907 75.6083 101.876 72.3099 102.334C69.2832 102.755 66.1637 102.628 63.0781 101.946C57.963 100.815 53.1408 98.2 49.1462 94.3909C45.1515 90.5817 42.1403 85.7273 40.4464 80.3655C39.3934 77.0326 38.8749 73.5845 38.8994 70.172C38.9232 66.8477 42.2974 64.8675 45.4723 65.8534V65.8534C48.2427 66.7137 49.8205 69.5619 50.0022 72.4571C50.1033 74.0681 50.4029 75.6799 50.8988 77.2495C51.9223 80.489 53.7416 83.422 56.1551 85.7234C58.5687 88.0249 61.4822 89.6048 64.5727 90.2882C65.6367 90.5235 66.7073 90.6493 67.7705 90.6668C71.1001 90.7215 74.663 92.1031 75.895 95.1969V95.1969Z"/>
<g mask="url(#mask2_21_40)">
<path d="M52.9037 31.2934C51.7372 28.1743 53.3311 24.6214 56.6384 24.2331C59.6733 23.8767 62.7895 24.0687 65.86 24.8158C70.9501 26.0544 75.7161 28.7704 79.6296 32.6629C83.5431 36.5553 86.4514 41.4722 88.0319 46.8684C89.0145 50.2228 89.4601 53.6811 89.3638 57.0923C89.27 60.4154 85.8547 62.324 82.7013 61.2714C79.9497 60.3529 78.4322 57.4721 78.3116 54.5737C78.2445 52.961 77.9789 51.3432 77.5162 49.7635C76.5612 46.5032 74.8041 43.5325 72.4396 41.1807C70.075 38.8289 67.1955 37.1879 64.1201 36.4395C63.0613 36.1819 61.9935 36.0335 60.931 35.9937C57.6032 35.8688 54.0702 34.4125 52.9037 31.2934Z" fill="white" stroke="white" stroke-width="2"/>
</g>
<mask id="mask3_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="65" y="50" width="41" height="39">
<path d="M97.747 52.0457C100.912 51.0099 104.395 52.7501 104.646 56.0708C104.876 59.1178 104.554 62.2233 103.68 65.2601C102.231 70.2944 99.3194 74.9433 95.2676 78.6916C91.2158 82.4399 86.1823 85.1412 80.725 86.4961C77.3326 87.3383 73.8589 87.6398 70.4546 87.4017C67.1383 87.1698 65.3733 83.6782 66.5561 80.5713C67.5881 77.8602 70.5296 76.4638 73.4305 76.4638C75.0446 76.4638 76.672 76.2656 78.2696 75.869C81.5668 75.0504 84.608 73.4183 87.0561 71.1536C89.5041 68.8889 91.2634 66.0801 92.139 63.0385C92.4404 61.9913 92.633 60.9306 92.717 59.8706C92.9802 56.551 94.5821 53.0816 97.747 52.0457Z" fill="white" stroke="white" stroke-width="2"/>
</mask>
<path d="M75.895 95.1969C77.127 98.2907 75.6083 101.876 72.3099 102.334C69.2832 102.755 66.1637 102.628 63.0781 101.946C57.963 100.815 53.1408 98.2 49.1462 94.3909C45.1515 90.5817 42.1403 85.7273 40.4464 80.3655C39.3934 77.0326 38.8749 73.5845 38.8994 70.172C38.9232 66.8477 42.2974 64.8675 45.4723 65.8534V65.8534C48.2427 66.7137 49.8205 69.5619 50.0022 72.4571C50.1033 74.0681 50.4029 75.6799 50.8988 77.2495C51.9223 80.489 53.7416 83.422 56.1551 85.7234C58.5687 88.0249 61.4822 89.6048 64.5727 90.2882C65.6367 90.5235 66.7073 90.6493 67.7705 90.6668C71.1001 90.7215 74.663 92.1031 75.895 95.1969V95.1969Z" fill="white" stroke="white" stroke-width="2" mask="url(#path-5-inside-4_117_4)"/>
<g mask="url(#mask3_21_40)">
<path d="M97.747 52.0457C100.912 51.0099 104.395 52.7501 104.646 56.0708C104.876 59.1178 104.554 62.2233 103.68 65.2601C102.231 70.2944 99.3194 74.9433 95.2676 78.6916C91.2158 82.4399 86.1823 85.1412 80.725 86.4961C77.3326 87.3383 73.8589 87.6398 70.4546 87.4017C67.1383 87.1698 65.3733 83.6782 66.5561 80.5713C67.5881 77.8602 70.5296 76.4638 73.4305 76.4638C75.0446 76.4638 76.672 76.2656 78.2696 75.869C81.5668 75.0504 84.608 73.4183 87.0561 71.1536C89.5041 68.8889 91.2634 66.0801 92.139 63.0385C92.4404 61.9913 92.633 60.9306 92.717 59.8706C92.9802 56.551 94.5821 53.0816 97.747 52.0457Z" fill="white" stroke="white" stroke-width="2"/>
</g>
<mask id="mask4_21_40" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="37" y="64" width="41" height="40">
<path d="M75.8949 95.1969C77.1269 98.2907 75.6082 101.876 72.3098 102.334C69.2831 102.755 66.1636 102.628 63.078 101.946C57.9629 100.815 53.1407 98.2 49.1461 94.3909C45.1514 90.5817 42.1402 85.7273 40.4463 80.3655C39.3933 77.0326 38.8748 73.5845 38.8993 70.172C38.9231 66.8477 42.2973 64.8675 45.4722 65.8534C48.2426 66.7137 49.8204 69.5619 50.0021 72.4571C50.1032 74.0681 50.4028 75.6799 50.8987 77.2495C51.9222 80.489 53.7415 83.422 56.155 85.7234C58.5686 88.0249 61.4821 89.6048 64.5726 90.2882C65.6366 90.5235 66.7072 90.6493 67.7704 90.6668C71.1 90.7215 74.6629 92.1031 75.8949 95.1969Z" fill="white" stroke="white" stroke-width="2"/>
</mask>
<g mask="url(#mask4_21_40)">
<path d="M75.8949 95.1969C77.1269 98.2907 75.6082 101.876 72.3098 102.334C69.2831 102.755 66.1636 102.628 63.078 101.946C57.9629 100.815 53.1407 98.2 49.1461 94.3909C45.1514 90.5817 42.1402 85.7273 40.4463 80.3655C39.3933 77.0326 38.8748 73.5845 38.8993 70.172C38.9231 66.8477 42.2973 64.8675 45.4722 65.8534C48.2426 66.7137 49.8204 69.5619 50.0021 72.4571C50.1032 74.0681 50.4028 75.6799 50.8987 77.2495C51.9222 80.489 53.7415 83.422 56.155 85.7234C58.5686 88.0249 61.4821 89.6048 64.5726 90.2882C65.6366 90.5235 66.7072 90.6493 67.7704 90.6668C71.1 90.7215 74.6629 92.1031 75.8949 95.1969Z" fill="white" stroke="white" stroke-width="2"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_117_4">
<clipPath id="clip0_21_40">
<rect width="128" height="128" fill="white"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -97,10 +97,9 @@ export default class GoogleCalendarService implements Calendar {
responseStatus: "accepted",
})) || [];
return new Promise(async (resolve, reject) => {
const [mainHostDestinationCalendar] =
calEventRaw?.destinationCalendar && calEventRaw?.destinationCalendar.length > 0
? calEventRaw.destinationCalendar
: [];
const selectedHostDestinationCalendar = calEventRaw.destinationCalendar?.find(
(cal) => cal.credentialId === credentialId
);
const myGoogleAuth = await this.auth.getToken();
const payload: calendar_v3.Schema$Event = {
summary: calEventRaw.title,
@ -119,8 +118,8 @@ export default class GoogleCalendarService implements Calendar {
id: String(calEventRaw.organizer.id),
responseStatus: "accepted",
organizer: true,
email: mainHostDestinationCalendar?.externalId
? mainHostDestinationCalendar.externalId
email: selectedHostDestinationCalendar?.externalId
? selectedHostDestinationCalendar.externalId
: calEventRaw.organizer.email,
},
...eventAttendees,
@ -144,14 +143,14 @@ export default class GoogleCalendarService implements Calendar {
});
// Find in calEventRaw.destinationCalendar the one with the same credentialId
const selectedCalendar = calEventRaw.destinationCalendar?.find(
(cal) => cal.credentialId === credentialId
)?.externalId;
const selectedCalendar =
calEventRaw.destinationCalendar?.find((cal) => cal.credentialId === credentialId)?.externalId ||
"primary";
calendar.events.insert(
{
auth: myGoogleAuth,
calendarId: selectedCalendar || "primary",
calendarId: selectedCalendar,
requestBody: payload,
conferenceDataVersion: 1,
sendUpdates: "none",

View File

@ -26,6 +26,7 @@ const appStore = {
webexvideo: () => import("./webex"),
giphy: () => import("./giphy"),
zapier: () => import("./zapier"),
make: () => import("./make"),
exchange2013calendar: () => import("./exchange2013calendar"),
exchange2016calendar: () => import("./exchange2016calendar"),
exchangecalendar: () => import("./exchangecalendar"),

View File

@ -0,0 +1,10 @@
---
items:
- 1.jpeg
- 2.jpeg
- 3.jpeg
- 4.jpeg
- 5.jpeg
---
Workflow automation for everyone. Use the Cal.com app in Make to automate your workflows when a booking is created, rescheduled, cancelled or when a meeting has ended. You can also get all your booking with the 'List Bookings' module.<br /><br />**After Installation:** Have you lost your API key? You can always generate a new key on the <a href="/apps/make/setup">**<ins>Make Setup Page</ins>**</a>

View File

@ -0,0 +1,22 @@
# Setting up Make Integration
1. Install the app from the Cal app store and generate an API key. Copy the API key.
2. Go to `/admin/apps/automation` in Cal and set the `invite_link` for Make to `https://www.make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510` to use the app.
3. Create a [Make account](https://www.make.com/en/login), if you don't have one.
4. Go to `Scenarios` in the sidebar and click on **Create a new scenario**.
5. Search for `Cal.com` in the apps list and select from the list of triggers - Booking Created, Booking Deleted, Booking Rescheduled, Meeting Ended
6. To create a **connection** you will need your Cal deployment url and the app API Key generated above. You only need to create a **connection** once, all webhooks can use that connection.
7. Setup the webhook for the desired event in Make.
8. To delete a webhook, go to `Webhooks` in the left sidebar in Make, pick the webhook you want to delete and click **delete**.
## Localhost or Self-hosting
Localhost urls can not be used as the base URL for api endpoints
Possible solution: using [https://ngrok.com/](https://ngrok.com/)
1. Create Account
2. [Download](https://ngrok.com/download) ngrok and start a tunnel to your running localhost
- Use forwarding url as your baseUrl for the URL endpoints
3. Use the ngrok url as your Cal deployment url when creating the **Connection** in Make.

View File

@ -0,0 +1,20 @@
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",
redirect: {
newTab: true,
url: "/apps/make/setup",
},
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};
export default handler;

View File

@ -0,0 +1,5 @@
export { default as add } from "./add";
export { default as listBookings } from "./subscriptions/listBookings";
export { default as deleteSubscription } from "./subscriptions/deleteSubscription";
export { default as addSubscription } from "./subscriptions/addSubscription";
export { default as me } from "./subscriptions/me";

View File

@ -0,0 +1,38 @@
import type { NextApiRequest, NextApiResponse } from "next";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { addSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
}
const validKey = await findValidApiKey(apiKey, "make");
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
const { subscriberUrl, triggerEvent } = req.body;
const createAppSubscription = await addSubscription({
appApiKey: validKey,
triggerEvent: triggerEvent,
subscriberUrl: subscriberUrl,
appId: "make",
});
if (!createAppSubscription) {
return res.status(500).json({ message: "Could not create subscription." });
}
res.status(200).json(createAppSubscription);
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(handler) }),
});

View File

@ -0,0 +1,40 @@
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { deleteSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
const querySchema = z.object({
apiKey: z.string(),
id: z.string(),
});
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { apiKey, id } = querySchema.parse(req.query);
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
}
const validKey = await findValidApiKey(apiKey, "make");
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
const deleteEventSubscription = await deleteSubscription({
appApiKey: validKey,
webhookId: id,
appId: "make",
});
if (!deleteEventSubscription) {
return res.status(500).json({ message: "Could not delete subscription." });
}
res.status(204).json({ message: "Subscription is deleted." });
}
export default defaultHandler({
DELETE: Promise.resolve({ default: defaultResponder(handler) }),
});

View File

@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { listBookings } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
}
const validKey = await findValidApiKey(apiKey, "make");
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
const bookings = await listBookings(validKey);
if (!bookings) {
return res.status(500).json({ message: "Unable to get bookings." });
}
if (bookings.length === 0) {
const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
return res.status(404).json({
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
});
}
res.status(201).json(bookings);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});

View File

@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
}
const validKey = await findValidApiKey(apiKey, "make");
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
if (req.method === "GET") {
try {
const user = await prisma.user.findFirst({
where: {
id: validKey.userId,
},
select: {
username: true,
},
});
res.status(201).json(user);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Unable to get User." });
}
}
}

View File

@ -0,0 +1,18 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Make",
"slug": "make",
"type": "make_automation",
"logo": "icon.svg",
"url": "https://github.com/aar2dee2",
"variant": "automation",
"categories": ["automation"],
"publisher": "aar2dee2",
"email": "support@cal.com",
"description": "From tasks and workflows to apps and systems, build and automate anything in one powerful visual platform.",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"imageSrc": "icon.svg",
"dirName": "make"
}

View File

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

View File

@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/make",
"version": "0.0.0",
"main": "./index.ts",
"description": "Workflow automation for everyone. Use the Cal.com Make app to trigger your workflows when a booking is created, rescheduled, or cancelled, or after a meeting ends.",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*",
"@types/node-schedule": "^2.1.0"
}
}

View File

@ -0,0 +1,20 @@
import type { GetStaticPropsContext } from "next";
import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug";
export interface IMakeSetupProps {
inviteLink: string;
}
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (typeof ctx.params?.slug !== "string") return { notFound: true } as const;
let inviteLink = "";
const appKeys = await getAppKeysFromSlug("make");
if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link;
return {
props: {
inviteLink,
},
};
};

View File

@ -0,0 +1,175 @@
import type { InferGetStaticPropsType } from "next";
import { Trans } from "next-i18next";
import Link from "next/link";
import { useState } from "react";
import { Toaster } from "react-hot-toast";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, Tooltip, showToast } from "@calcom/ui";
import { Clipboard } from "@calcom/ui/components/icon";
import type { getStaticProps } from "./_getStaticProps";
const MAKE = "make";
export default function MakeSetup({ inviteLink }: InferGetStaticPropsType<typeof getStaticProps>) {
const [newApiKeys, setNewApiKeys] = useState<Record<string, string>>({});
const { t } = useLocale();
const utils = trpc.useContext();
const integrations = trpc.viewer.integrations.useQuery({ variant: "automation" });
const oldApiKey = trpc.viewer.apiKeys.findKeyOfType.useQuery({ appId: MAKE });
const teamsList = trpc.viewer.teams.listOwnedTeams.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const teams = teamsList.data?.map((team) => ({ id: team.id, name: team.name }));
const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation({
onSuccess: () => {
utils.viewer.apiKeys.findKeyOfType.invalidate();
},
});
const makeCredentials: { userCredentialIds: number[] } | undefined = integrations.data?.items.find(
(item: { type: string }) => item.type === "make_automation"
);
const [credentialId] = makeCredentials?.userCredentialIds || [false];
const showContent = integrations.data && integrations.isSuccess && credentialId;
async function createApiKey(teamId?: number) {
const event = { note: "Make", expiresAt: null, appId: MAKE, teamId };
const apiKey = await utils.client.viewer.apiKeys.create.mutate(event);
if (oldApiKey.data) {
const oldKey = teamId
? oldApiKey.data.find((key) => key.teamId === teamId)
: oldApiKey.data.find((key) => !key.teamId);
if (oldKey) {
deleteApiKey.mutate({
id: oldKey.id,
});
}
}
return apiKey;
}
async function generateApiKey(teamId?: number) {
const apiKey = await createApiKey(teamId);
setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
}
if (integrations.isLoading) {
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
}
return (
<div className="bg-emphasis flex h-screen">
{showContent ? (
<div className="bg-default m-auto max-w-[43em] overflow-auto rounded pb-10 md:p-10">
<div className="md:flex md:flex-row">
<div className="invisible md:visible">
<img className="h-11" src="/api/app-store/make/icon.svg" alt="Make Logo" />
</div>
<div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5">
<div className="text-default">{t("setting_up_make")}</div>
<>
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>
{!teams ? (
<Button color="secondary" onClick={() => createApiKey()} className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<>
<div className="mt-8 text-sm font-semibold">Your event types:</div>
{!newApiKeys[""] ? (
<Button color="secondary" onClick={() => generateApiKey()} className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<CopyApiKey apiKey={newApiKeys[""]} />
)}
{teams.map((team) => {
return (
<div key={team.name}>
<div className="mt-2 text-sm font-semibold">{team.name}:</div>
{!newApiKeys[team.id] ? (
<Button
color="secondary"
onClick={() => generateApiKey(team.id)}
className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<CopyApiKey apiKey={newApiKeys[team.id]} />
)}
</div>
);
})}
</>
)}
</>
<ol className="mb-5 ml-5 mt-5 list-decimal ltr:mr-5 rtl:ml-5">
<Trans i18nKey="make_setup_instructions">
<li>
Go to
<a href={inviteLink} className="ml-1 mr-1 text-orange-600 underline">
Make Invite Link
</a>
and install the Cal.com app.
</li>
<li>Log into your Make account and create a new Scenario.</li>
<li>Select Cal.com as your Trigger app. Also choose a Trigger event.</li>
<li>Choose your account and then enter your Unique API Key.</li>
<li>Test your Trigger.</li>
<li>You&apos;re set!</li>
</Trans>
</ol>
<Link href="/apps/installed/automation?hl=make" passHref={true} legacyBehavior>
<Button color="secondary">{t("done")}</Button>
</Link>
</div>
</div>
</div>
) : (
<div className="ml-5 mt-5">
<div>{t("install_make_app")}</div>
<div className="mt-3">
<Link href="/apps/make" passHref={true} legacyBehavior>
<Button>{t("go_to_app_store")}</Button>
</Link>
</div>
</div>
)}
<Toaster position="bottom-right" />
</div>
);
}
const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
const { t } = useLocale();
return (
<div>
<div className="my-2 mt-3 flex-wrap sm:flex sm:flex-nowrap">
<code className="bg-subtle h-full w-full whitespace-pre-wrap rounded-md py-[6px] pl-2 pr-2 sm:rounded-r-none sm:pr-5">
{apiKey}
</code>
<Tooltip side="top" content={t("copy_to_clipboard")}>
<Button
onClick={() => {
navigator.clipboard.writeText(apiKey);
showToast(t("api_key_copied"), "success");
}}
type="button"
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
<Clipboard className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
{t("copy")}
</Button>
</Tooltip>
</div>
<div className="text-subtle mb-5 mt-2 text-sm">{t("copy_somewhere_safe")}</div>
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<g id="Make-App-Icon-Circle" transform="translate(3757 -1767)">
<circle id="Ellipse_10" data-name="Ellipse 10" cx="256" cy="256" r="256" transform="translate(-3757 1767)" fill="#6d00cc"/>
<path id="Path_141560" data-name="Path 141560" d="M244.78,14.544a7.187,7.187,0,0,0-7.186,7.192V213.927a7.19,7.19,0,0,0,7.186,7.192h52.063a7.187,7.187,0,0,0,7.186-7.192V21.736a7.183,7.183,0,0,0-7.186-7.192ZM92.066,17.083,5.77,188.795a7.191,7.191,0,0,0,3.192,9.654l46.514,23.379a7.184,7.184,0,0,0,9.654-3.2l86.3-171.711a7.184,7.184,0,0,0-3.2-9.654L101.719,13.886a7.2,7.2,0,0,0-9.654,3.2m72.592.614L127.731,204.876a7.189,7.189,0,0,0,5.632,8.442l51.028,10.306a7.2,7.2,0,0,0,8.481-5.665L229.8,30.786a7.19,7.19,0,0,0-5.637-8.442L173.133,12.038a7.391,7.391,0,0,0-1.427-.144,7.194,7.194,0,0,0-7.048,5.8" transform="translate(-3676.356 1905.425)" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 952 B

View File

@ -0,0 +1,7 @@
import { z } from "zod";
export const appDataSchema = z.object({});
export const appKeysSchema = z.object({
invite_link: z.string().min(1),
});

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -5,6 +5,13 @@ import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
import { test } from "@calcom/web/playwright/lib/fixtures";
import { gotoRoutingLink } from "@calcom/web/playwright/lib/testUtils";
import {
addForm,
saveCurrentForm,
verifySelectOptions,
addOneFieldAndDescriptionAndSaveForm,
} from "./testUtils";
function todo(title: string) {
// eslint-disable-next-line playwright/no-skipped-test, @typescript-eslint/no-empty-function
test.skip(title, () => {});
@ -407,22 +414,6 @@ async function fillSeededForm(page: Page, routingFormId: string) {
await expect(page.locator("text=Custom Page Result")).toBeVisible();
}
export async function addForm(page: Page, { name = "Test Form Name" } = {}) {
await page.goto("/routing-forms/forms");
await page.click('[data-testid="new-routing-form"]');
// Choose to create the Form for the user(which is the first option) and not the team
await page.click('[data-testid="option-0"]');
await page.fill("input[name]", name);
await page.click('[data-testid="add-form"]');
await page.waitForSelector('[data-testid="add-field"]');
const url = page.url();
const formId = new URL(url).pathname.split("/").at(-1);
if (!formId) {
throw new Error("Form ID couldn't be determined from url");
}
return formId;
}
async function addAllTypesOfFieldsAndSaveForm(
formId: string,
page: Page,
@ -480,46 +471,6 @@ async function addAllTypesOfFieldsAndSaveForm(
};
}
export async function addOneFieldAndDescriptionAndSaveForm(
formId: string,
page: Page,
form: { description?: string; field?: { typeIndex: number; label: string } }
) {
await page.goto(`apps/routing-forms/form-edit/${formId}`);
await page.click('[data-testid="add-field"]');
if (form.description) {
await page.fill('[data-testid="description"]', form.description);
}
// Verify all Options of SelectBox
const { optionsInUi: types } = await verifySelectOptions(
{ selector: ".data-testid-field-type", nth: 0 },
["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"],
page
);
const nextFieldIndex = (await page.locator('[data-testid="field"]').count()) - 1;
if (form.field) {
await page.fill(`[data-testid="fields.${nextFieldIndex}.label"]`, form.field.label);
await page
.locator('[data-testid="field"]')
.nth(nextFieldIndex)
.locator(".data-testid-field-type")
.click();
await page
.locator('[data-testid="field"]')
.nth(nextFieldIndex)
.locator('[id*="react-select-"][aria-disabled]')
.nth(form.field.typeIndex)
.click();
}
await saveCurrentForm(page);
return {
types,
};
}
async function selectOption({
page,
selector,
@ -551,26 +502,6 @@ async function verifyFieldOptionsInRule(options: string[], page: Page) {
);
}
async function verifySelectOptions(
selector: { selector: string; nth: number },
expectedOptions: string[],
page: Page
) {
await page.locator(selector.selector).nth(selector.nth).click();
const selectOptions = await page
.locator(selector.selector)
.nth(selector.nth)
.locator('[id*="react-select-"][aria-disabled]')
.allInnerTexts();
const sortedSelectOptions = [...selectOptions].sort();
const sortedExpectedOptions = [...expectedOptions].sort();
expect(sortedSelectOptions).toEqual(sortedExpectedOptions);
return {
optionsInUi: selectOptions,
};
}
async function selectNewRoute(page: Page, { routeSelectNumber = 1 } = {}) {
await selectOption({
selector: {
@ -581,8 +512,3 @@ async function selectNewRoute(page: Page, { routeSelectNumber = 1 } = {}) {
page,
});
}
async function saveCurrentForm(page: Page) {
await page.click('[data-testid="update-form"]');
await page.waitForSelector(".data-testid-toast-success");
}

View File

@ -0,0 +1,83 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
export async function addForm(page: Page, { name = "Test Form Name" } = {}) {
await page.goto("/routing-forms/forms");
await page.click('[data-testid="new-routing-form"]');
// Choose to create the Form for the user(which is the first option) and not the team
await page.click('[data-testid="option-0"]');
await page.fill("input[name]", name);
await page.click('[data-testid="add-form"]');
await page.waitForSelector('[data-testid="add-field"]');
const url = page.url();
const formId = new URL(url).pathname.split("/").at(-1);
if (!formId) {
throw new Error("Form ID couldn't be determined from url");
}
return formId;
}
export async function addOneFieldAndDescriptionAndSaveForm(
formId: string,
page: Page,
form: { description?: string; field?: { typeIndex: number; label: string } }
) {
await page.goto(`apps/routing-forms/form-edit/${formId}`);
await page.click('[data-testid="add-field"]');
if (form.description) {
await page.fill('[data-testid="description"]', form.description);
}
// Verify all Options of SelectBox
const { optionsInUi: types } = await verifySelectOptions(
{ selector: ".data-testid-field-type", nth: 0 },
["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"],
page
);
const nextFieldIndex = (await page.locator('[data-testid="field"]').count()) - 1;
if (form.field) {
await page.fill(`[data-testid="fields.${nextFieldIndex}.label"]`, form.field.label);
await page
.locator('[data-testid="field"]')
.nth(nextFieldIndex)
.locator(".data-testid-field-type")
.click();
await page
.locator('[data-testid="field"]')
.nth(nextFieldIndex)
.locator('[id*="react-select-"][aria-disabled]')
.nth(form.field.typeIndex)
.click();
}
await saveCurrentForm(page);
return {
types,
};
}
export async function saveCurrentForm(page: Page) {
await page.click('[data-testid="update-form"]');
await page.waitForSelector(".data-testid-toast-success");
}
export async function verifySelectOptions(
selector: { selector: string; nth: number },
expectedOptions: string[],
page: Page
) {
await page.locator(selector.selector).nth(selector.nth).click();
const selectOptions = await page
.locator(selector.selector)
.nth(selector.nth)
.locator('[id*="react-select-"][aria-disabled]')
.allInnerTexts();
const sortedSelectOptions = [...selectOptions].sort();
const sortedExpectedOptions = [...expectedOptions].sort();
expect(sortedSelectOptions).toEqual(sortedExpectedOptions);
return {
optionsInUi: selectOptions,
};
}

View File

@ -12,32 +12,50 @@ export async function onFormSubmission(
form: Ensure<SerializableForm<App_RoutingForms_Form> & { user: User }, "fields">,
response: Response
) {
const fieldResponsesByName: Record<string, (typeof response)[keyof typeof response]["value"]> = {};
const fieldResponsesByName: Record<
string,
{
value: Response[keyof Response]["value"];
}
> = {};
for (const [fieldId, fieldResponse] of Object.entries(response)) {
// Use the label lowercased as the key to identify a field.
const key =
form.fields.find((f) => f.id === fieldId)?.identifier ||
(fieldResponse.label as keyof typeof fieldResponsesByName);
fieldResponsesByName[key] = fieldResponse.value;
fieldResponsesByName[key] = {
value: fieldResponse.value,
};
}
const subscriberOptions = {
userId: form.user.id,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
// When team routing forms are implemented, we need to make sure to add the teamId here
teamId: null,
...getWebhookTargetEntity(form),
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) => {
sendGenericWebhookPayload(
webhook.secret,
"FORM_SUBMITTED",
new Date().toISOString(),
sendGenericWebhookPayload({
secretKey: webhook.secret,
triggerEvent: "FORM_SUBMITTED",
createdAt: new Date().toISOString(),
webhook,
fieldResponsesByName
).catch((e) => {
data: {
formId: form.id,
formName: form.name,
teamId: form.teamId,
responses: fieldResponsesByName,
},
rootData: {
// Send responses unwrapped at root level for backwards compatibility
...Object.entries(fieldResponsesByName).reduce((acc, [key, value]) => {
acc[key] = value.value;
return acc;
}, {} as Record<string, Response[keyof Response]["value"]>),
},
}).catch((e) => {
console.error(`Error executing routing form webhook`, webhook, e);
});
});
@ -66,3 +84,10 @@ export const sendResponseEmail = async (
logger.error("Error sending response email", e);
}
};
function getWebhookTargetEntity(form: { teamId?: number | null; user: { id: number } }) {
// If it's a team form, the target must be team webhook
// If it's a user form, the target must be user webhook
const isTeamForm = form.teamId;
return { userId: isTeamForm ? null : form.user.id, teamId: isTeamForm ? form.teamId : null };
}

View File

@ -4,7 +4,7 @@ import { expect } from "@playwright/test";
import {
addForm as addRoutingForm,
addOneFieldAndDescriptionAndSaveForm,
} from "@calcom/app-store/routing-forms/playwright/tests/basic.e2e";
} from "@calcom/app-store/routing-forms/playwright/tests/testUtils";
import { CAL_URL } from "@calcom/lib/constants";
import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
import { test } from "@calcom/web/playwright/lib/fixtures";

View File

@ -1,3 +1,7 @@
---
items:
- 1.jpg
---
Vital App is an app that can can help you combine your health peripherals with your calendar.
#### Supported Actions:

View File

@ -8,7 +8,7 @@ export const metadata = {
installed: true,
category: "automation",
categories: ["automation"],
logo: "icon.svg",
logo: "icon-dark.svg",
label: "Vital",
publisher: "Vital",
slug: "vital-automation",

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 571 B

After

Width:  |  Height:  |  Size: 571 B

View File

@ -1,12 +1,8 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { v4 } from "uuid";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { addSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
@ -23,45 +19,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const { subscriberUrl, triggerEvent } = req.body;
try {
const createSubscription = await prisma.webhook.create({
data: {
id: v4(),
userId: validKey.userId,
teamId: validKey.teamId,
eventTriggers: [triggerEvent],
subscriberUrl,
active: true,
appId: "zapier",
},
});
const createAppSubscription = await addSubscription({
appApiKey: validKey,
triggerEvent: triggerEvent,
subscriberUrl: subscriberUrl,
appId: "zapier",
});
if (triggerEvent === WebhookTriggerEvents.MEETING_ENDED) {
//schedule job for already existing bookings
const where: Prisma.BookingWhereInput = {};
if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
else where.userId = validKey.userId;
const bookings = await prisma.booking.findMany({
where: {
...where,
startTime: {
gte: new Date(),
},
status: BookingStatus.ACCEPTED,
},
});
for (const booking of bookings) {
scheduleTrigger(booking, createSubscription.subscriberUrl, {
id: createSubscription.id,
appId: createSubscription.appId,
});
}
}
res.status(200).json(createSubscription);
} catch (error) {
if (!createAppSubscription) {
return res.status(500).json({ message: "Could not create subscription." });
}
res.status(200).json(createAppSubscription);
}
export default defaultHandler({

View File

@ -1,11 +1,9 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { deleteSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
const querySchema = z.object({
apiKey: z.string(),
@ -24,49 +22,16 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
const webhook = await prisma.webhook.findFirst({
where: {
id,
userId: validKey.userId,
teamId: validKey.teamId,
},
const deleteEventSubscription = await deleteSubscription({
appApiKey: validKey,
webhookId: id,
appId: "zapier",
});
if (!webhook) {
return res.status(401).json({ message: "Not authorized to delete this webhook" });
if (!deleteEventSubscription) {
return res.status(500).json({ message: "Could not delete subscription." });
}
if (webhook?.eventTriggers.includes(WebhookTriggerEvents.MEETING_ENDED)) {
const where: Prisma.BookingWhereInput = {};
if (validKey.teamId) where.eventType = { teamId: validKey.teamId };
else where.userId = validKey.userId;
const bookingsWithScheduledJobs = await prisma.booking.findMany({
where: {
...where,
scheduledJobs: {
isEmpty: false,
},
},
});
for (const booking of bookingsWithScheduledJobs) {
const updatedScheduledJobs = booking.scheduledJobs.filter(
(scheduledJob) => scheduledJob !== `zapier_${webhook.id}`
);
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
scheduledJobs: updatedScheduledJobs,
},
});
}
}
await prisma.webhook.delete({
where: {
id,
},
});
res.status(204).json({ message: "Subscription is deleted." });
}

View File

@ -1,11 +1,8 @@
import type { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getHumanReadableLocationValue } from "@calcom/core/location";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder, getTranslation } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { listBookings } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
@ -20,88 +17,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(401).json({ message: "API key not valid" });
}
try {
const where: Prisma.BookingWhereInput = {};
if (validKey.teamId) {
where.eventType = {
OR: [{ teamId: validKey.teamId }, { parent: { teamId: validKey.teamId } }],
};
} else {
where.userId = validKey.userId;
}
const bookings = await listBookings(validKey);
const bookings = await prisma.booking.findMany({
take: 3,
where,
orderBy: {
id: "desc",
},
select: {
title: true,
description: true,
customInputs: true,
responses: true,
startTime: true,
endTime: true,
location: true,
cancellationReason: true,
status: true,
user: {
select: {
username: true,
name: true,
email: true,
timeZone: true,
locale: true,
},
},
eventType: {
select: {
title: true,
description: true,
requiresConfirmation: true,
price: true,
currency: true,
length: true,
bookingFields: true,
team: true,
},
},
attendees: {
select: {
name: true,
email: true,
timeZone: true,
},
},
},
});
if (bookings.length === 0) {
const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
return res.status(404).json({
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
});
}
const t = await getTranslation(bookings[0].user?.locale ?? "en", "common");
const updatedBookings = bookings.map((booking) => {
return {
...booking,
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
location: getHumanReadableLocationValue(booking.location || "", t),
};
});
res.status(201).json(updatedBookings);
} catch (error) {
console.error(error);
if (!bookings) {
return res.status(500).json({ message: "Unable to get bookings." });
}
if (bookings.length === 0) {
const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
return res.status(404).json({
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
});
}
res.status(201).json(bookings);
}
export default defaultHandler({

View File

@ -1,4 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { stringify } from "querystring";
import { WEBAPP_URL } from "@calcom/lib/constants";
@ -13,7 +14,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!client_id) return res.status(400).json({ message: "zohocrm client id missing." });
const state = encodeOAuthState(req);
const redirectUri = WEBAPP_URL + "/api/integrations/zohocrm/callback";
const url = `https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCRM.modules.ALL,ZohoCRM.users.READ,AaaServer.profile.READ&client_id=${client_id}&response_type=code&access_type=offline&redirect_uri=${redirectUri}&state=${state}`;
const params = {
client_id,
response_type: "code",
redirect_uri: WEBAPP_URL + "/api/integrations/zohocrm/callback",
scope: ["ZohoCRM.modules.ALL", "ZohoCRM.users.READ", "AaaServer.profile.READ"],
access_type: "offline",
state,
prompt: "consent",
};
const query = stringify(params);
const url = `https://accounts.zoho.com/oauth/v2/auth?${query}`;
res.status(200).json({ url });
}

View File

@ -3,7 +3,7 @@
"name": "ZohoCRM",
"slug": "zohocrm",
"type": "zohocrm_other_calendar",
"logo": "icon.png",
"logo": "icon.svg",
"url": "https://github.com/jatinsandilya",
"variant": "other",
"categories": ["crm"],

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -10,6 +10,7 @@ import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvide
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
import { MeetLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { NewCalendarEventType } from "@calcom/types/Calendar";
@ -436,7 +437,14 @@ export default class EventManager {
* This might happen if someone tries to use a location with a missing credential, so we fallback to Cal Video.
* @todo remove location from event types that has missing credentials
* */
if (!videoCredential) videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" };
if (!videoCredential) {
logger.warn(
'Falling back to "daily" video integration for event with location: ' +
event.location +
" because credential is missing for the app"
);
videoCredential = { ...FAKE_DAILY_CREDENTIAL, appName: "FAKE" };
}
return videoCredential;
}

View File

@ -27,9 +27,8 @@ export const TeamInviteEmail = (
})}>
<p style={{ fontSize: "24px", marginBottom: "16px", textAlign: "center" }}>
<>
{props.language("email_no_user_invite_heading", {
{props.language(`email_no_user_invite_heading_${props.isOrg ? "org" : "team"}`, {
appName: APP_NAME,
entity: props.language(props.isOrg ? "organization" : "team").toLowerCase(),
})}
</>
</p>
@ -57,11 +56,10 @@ export const TeamInviteEmail = (
lineHeightStep: "24px",
}}>
<>
{props.language("email_user_invite_subheading", {
{props.language(`email_user_invite_subheading_${props.isOrg ? "org" : "team"}`, {
invitedBy: props.from,
appName: APP_NAME,
teamName: props.teamName,
entity: props.language(props.isOrg ? "organization" : "team").toLowerCase(),
})}
</>
</p>

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