Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0910f65b87 | ||
|
|
343f8ee303 | ||
|
|
de479bb2da | ||
|
|
5dc3065a47 | ||
|
|
36d315343c | ||
|
|
2171a320f5 | ||
|
|
7ee035c3a3 | ||
|
|
1de05ebf3d | ||
|
|
53d7e2ec3f | ||
|
|
f65c7e413f | ||
|
|
b762f60214 | ||
|
|
2498785c49 | ||
|
|
9a6683e01d | ||
|
|
2853288f49 | ||
|
|
d04226ab9a | ||
|
|
cb7ddc455a | ||
|
|
73aa1e8a22 | ||
|
|
a3b5263b76 | ||
|
|
af2c6c0844 | ||
|
|
113195224a | ||
|
|
828092c1d0 | ||
|
|
4b060fc2cd | ||
|
|
16c5b070b6 | ||
|
|
48dde246e9 | ||
|
|
556b382f75 | ||
|
|
85237c4985 | ||
|
|
404bc0e4d6 | ||
|
|
00553e897b | ||
|
|
ed2ce005c9 | ||
|
|
058844b6b8 | ||
|
|
77fa3702db | ||
|
|
9eb2ae470c | ||
|
|
3de7b73f5f | ||
|
|
8e7785ca64 | ||
|
|
608b891536 | ||
|
|
4f26ca1a7b | ||
|
|
c55b36f235 | ||
|
|
bdd3b132d4 | ||
|
|
9a4c20cca4 | ||
|
|
2144fcb23e | ||
|
|
f2c39fc786 | ||
|
|
e83e0a770c | ||
|
|
c818ef3188 | ||
|
|
176564f4ed | ||
|
|
27d59b6413 | ||
|
|
57a65401e7 | ||
|
|
28acbe549a | ||
|
|
3534e3c224 | ||
|
|
e48a6c3dcf | ||
|
|
7d09ccb0d7 | ||
|
|
9029c5b4c4 | ||
|
|
a4c1df3658 | ||
|
|
0b96ef5476 | ||
|
|
ca9e0f5b72 | ||
|
|
a9418b06d6 | ||
|
|
833a326c5b | ||
|
|
b7e2a62e26 | ||
|
|
f687056d60 | ||
|
|
0a39f53a4b | ||
|
|
ea0a64624c | ||
|
|
6a8726f5f8 | ||
|
|
eb97e1660b | ||
|
|
371a0f7245 | ||
|
|
270d4f6e82 | ||
|
|
48e7b616b8 | ||
|
|
8b80ea99c2 | ||
|
|
150c6e0f50 | ||
|
|
4037e87fbd | ||
|
|
a804a29516 | ||
|
|
87b514b91b | ||
|
|
693ca04626 | ||
|
|
67f83c7e7d | ||
|
|
ecfecd832d | ||
|
|
e55302cbc5 | ||
|
|
9514a68039 | ||
|
|
4e3d159401 | ||
|
|
a519941b81 | ||
|
|
f62c58532d | ||
|
|
0981c4cb23 | ||
|
|
9d06f6dd0e | ||
|
|
00df493623 | ||
|
|
2cf2b11a7b | ||
|
|
5c3fd0993e | ||
|
|
811e655720 | ||
|
|
defaafc9be | ||
|
|
e0c6feff1f |
@@ -25,6 +25,7 @@ CALCOM_LICENSE_KEY=
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
UPSTASH_REDIS_REST_URL=
|
||||
UPSTASH_REDIS_REST_TOKEN=
|
||||
INSIGHTS_DATABASE_URL=
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
@@ -106,6 +107,19 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
|
||||
NEXT_PUBLIC_FRESHCHAT_TOKEN=
|
||||
NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||
|
||||
# Google OAuth credentials
|
||||
# To enable Login with Google you need to:
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` above
|
||||
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
|
||||
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
|
||||
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
|
||||
GOOGLE_LOGIN_ENABLED=false
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||
GOOGLE_API_CREDENTIALS=
|
||||
|
||||
# Inbox to send user feedback
|
||||
SEND_FEEDBACK_EMAIL=
|
||||
|
||||
@@ -236,6 +250,12 @@ AUTH_BEARER_TOKEN_VERCEL=
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||
|
||||
# - CALCOM QA ACCOUNT
|
||||
# Used for E2E tests on Cal.com that require 3rd party integrations
|
||||
E2E_TEST_CALCOM_QA_EMAIL="qa@example.com"
|
||||
# Replace with your own password
|
||||
E2E_TEST_CALCOM_QA_PASSWORD="password"
|
||||
|
||||
# - APP CREDENTIAL SYNC ***********************************************************************************
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||
@@ -263,3 +283,9 @@ E2E_TEST_OIDC_USER_EMAIL=
|
||||
E2E_TEST_OIDC_USER_PASSWORD=
|
||||
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# provide a value between 0 and 100 to ensure the percentage of traffic
|
||||
# redirected from the legacy to the future pages
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
with:
|
||||
repo-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextMiddleware } from "next-api-middleware";
|
||||
|
||||
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||
import prisma, { customPrisma } from "@calcom/prisma";
|
||||
import { customPrisma } from "@calcom/prisma";
|
||||
|
||||
const LOCAL_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL;
|
||||
|
||||
@@ -12,41 +12,32 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => {
|
||||
} = req;
|
||||
// If no custom api Id is provided, attach to request the regular cal.com prisma client.
|
||||
if (!key) {
|
||||
req.prisma = prisma;
|
||||
req.prisma = customPrisma();
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// If we have a key, we check if the deployment matching the key, has a databaseUrl value set.
|
||||
const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`)
|
||||
.then((res) => res.json())
|
||||
.then((res) => res.databaseUrl);
|
||||
// If we have a key, we check if the deployment matching the key, has a databaseUrl value set.
|
||||
const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`)
|
||||
.then((res) => res.json())
|
||||
.then((res) => res.databaseUrl);
|
||||
|
||||
if (!databaseUrl) {
|
||||
res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
|
||||
return;
|
||||
}
|
||||
req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } });
|
||||
/* @note:
|
||||
if (!databaseUrl) {
|
||||
res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
|
||||
return;
|
||||
}
|
||||
req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } });
|
||||
/* @note:
|
||||
In order to skip verifyApiKey for customPrisma requests,
|
||||
we pass isAdmin true, and userId 0, if we detect them later,
|
||||
we skip verifyApiKey logic and pass onto next middleware instead.
|
||||
*/
|
||||
req.isAdmin = true;
|
||||
req.isCustomPrisma = true;
|
||||
// We don't need the key from here and on. Prevents unrecognized key errors.
|
||||
delete req.query.key;
|
||||
await next();
|
||||
await req.prisma.$disconnect();
|
||||
// @ts-expect-error testing
|
||||
delete req.prisma;
|
||||
} catch (err) {
|
||||
if (req.prisma) {
|
||||
await req.prisma.$disconnect();
|
||||
// @ts-expect-error testing
|
||||
delete req.prisma;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
req.isAdmin = true;
|
||||
req.isCustomPrisma = true;
|
||||
// We don't need the key from here and on. Prevents unrecognized key errors.
|
||||
delete req.query.key;
|
||||
await next();
|
||||
await req.prisma.$disconnect();
|
||||
// @ts-expect-error testing
|
||||
delete req.prisma;
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ const schemaAvailabilityCreateParams = z
|
||||
startTime: z.date().or(z.string()),
|
||||
endTime: z.date().or(z.string()),
|
||||
days: z.array(z.number()).optional(),
|
||||
date: z.date().or(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -34,6 +35,7 @@ const schemaAvailabilityEditParams = z
|
||||
startTime: z.date().or(z.string()).optional(),
|
||||
endTime: z.date().or(z.string()).optional(),
|
||||
days: z.array(z.number()).optional(),
|
||||
date: z.date().or(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -21,7 +21,16 @@ export const schemaSchedulePublic = z
|
||||
.merge(
|
||||
z.object({
|
||||
availability: z
|
||||
.array(Availability.pick({ id: true, eventTypeId: true, days: true, startTime: true, endTime: true }))
|
||||
.array(
|
||||
Availability.pick({
|
||||
id: true,
|
||||
eventTypeId: true,
|
||||
date: true,
|
||||
days: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
})
|
||||
)
|
||||
.transform((v) =>
|
||||
v.map((item) => ({
|
||||
...item,
|
||||
|
||||
@@ -316,8 +316,13 @@ async function checkPermissions(req: NextApiRequest) {
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
if (!isAdmin && body.teamId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `teamId`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId or teamId */
|
||||
if (isAdmin && (!body.userId || !body.teamId))
|
||||
if (isAdmin && !body.userId && !body.teamId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) {
|
||||
res.status(201).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" });
|
||||
res.status(200).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { deleteUser } from "@calcom/features/users/lib/userDeletionService";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
@@ -41,10 +42,18 @@ export async function deleteHandler(req: NextApiRequest) {
|
||||
// Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user
|
||||
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: query.userId } });
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: query.userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
if (!user) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
|
||||
await prisma.user.delete({ where: { id: user.id } });
|
||||
await deleteUser(user);
|
||||
|
||||
return { message: `User with id: ${user.id} deleted successfully` };
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -148,7 +149,7 @@ describe.skipIf(true)("POST /api/bookings", () => {
|
||||
expect(res._getStatusCode()).toBe(500);
|
||||
expect(JSON.parse(res._getData())).toEqual(
|
||||
expect.objectContaining({
|
||||
message: "No available users found.",
|
||||
message: ErrorCode.NoAvailableUsersFound,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"@calcom/config": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
@@ -49,7 +49,7 @@
|
||||
"storybook-addon-designs": "^6.3.1",
|
||||
"storybook-addon-next": "^1.6.9",
|
||||
"storybook-react-i18next": "^1.1.2",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.1.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { getBucket } from "abTest/utils";
|
||||
import type { NextMiddleware, NextRequest } from "next/server";
|
||||
import { NextResponse, URLPattern } from "next/server";
|
||||
import z from "zod";
|
||||
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
}),
|
||||
enabled,
|
||||
]);
|
||||
|
||||
const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override";
|
||||
const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled";
|
||||
|
||||
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]);
|
||||
|
||||
export const abTestMiddlewareFactory =
|
||||
(next: (req: NextRequest) => Promise<NextResponse<unknown>>): NextMiddleware =>
|
||||
async (req: NextRequest) => {
|
||||
const response = await next(req);
|
||||
|
||||
const { pathname } = req.nextUrl;
|
||||
|
||||
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
|
||||
|
||||
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
|
||||
|
||||
const enabled = route !== null ? route[1] || override : false;
|
||||
|
||||
if (pathname.includes("future") || !enabled) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const safeParsedBucket = override
|
||||
? { success: true as const, data: "future" as const }
|
||||
: bucketSchema.safeParse(req.cookies.get(FUTURE_ROUTES_ENABLED_COOKIE_NAME)?.value);
|
||||
|
||||
if (!safeParsedBucket.success) {
|
||||
// cookie does not exist or it has incorrect value
|
||||
const bucket = getBucket();
|
||||
|
||||
response.cookies.set(FUTURE_ROUTES_ENABLED_COOKIE_NAME, bucket, {
|
||||
expires: Date.now() + 1000 * 60 * 30,
|
||||
httpOnly: true,
|
||||
}); // 30 min in ms
|
||||
|
||||
if (bucket === "legacy") {
|
||||
return response;
|
||||
}
|
||||
|
||||
const url = req.nextUrl.clone();
|
||||
url.pathname = `future${pathname}/`;
|
||||
|
||||
return NextResponse.rewrite(url, response);
|
||||
}
|
||||
|
||||
if (safeParsedBucket.data === "legacy") {
|
||||
return response;
|
||||
}
|
||||
|
||||
const url = req.nextUrl.clone();
|
||||
url.pathname = `future${pathname}/`;
|
||||
|
||||
return NextResponse.rewrite(url, response);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { AB_TEST_BUCKET_PROBABILITY } from "@calcom/lib/constants";
|
||||
|
||||
const cryptoRandom = () => {
|
||||
return crypto.getRandomValues(new Uint8Array(1))[0] / 0xff;
|
||||
};
|
||||
|
||||
export const getBucket = () => {
|
||||
return cryptoRandom() * 100 < AB_TEST_BUCKET_PROBABILITY ? "future" : "legacy";
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { createHydrateClient } from "app/_trpc/createHydrateClient";
|
||||
import superjson from "superjson";
|
||||
|
||||
export const HydrateClient = createHydrateClient({
|
||||
transformer: superjson,
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
|
||||
export const trpc = createTRPCReact<AppRouter>({});
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { DataTransformer } from "@trpc/server";
|
||||
|
||||
export function createHydrateClient(opts: { transformer?: DataTransformer }) {
|
||||
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
|
||||
const { state, children } = props;
|
||||
|
||||
const transformedState: DehydratedState = useMemo(() => {
|
||||
if (opts.transformer) {
|
||||
return opts.transformer.deserialize(state);
|
||||
}
|
||||
return state;
|
||||
}, [state]);
|
||||
|
||||
return <Hydrate state={transformedState}>{children}</Hydrate>;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { HydrateClient } from "app/_trpc/HydrateClient";
|
||||
import { trpc } from "app/_trpc/client";
|
||||
import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
|
||||
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
||||
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
||||
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
||||
|
||||
const ENDPOINTS = [
|
||||
"admin",
|
||||
"apiKeys",
|
||||
"appRoutingForms",
|
||||
"apps",
|
||||
"auth",
|
||||
"availability",
|
||||
"appBasecamp3",
|
||||
"bookings",
|
||||
"deploymentSetup",
|
||||
"eventTypes",
|
||||
"features",
|
||||
"insights",
|
||||
"payments",
|
||||
"public",
|
||||
"saml",
|
||||
"slots",
|
||||
"teams",
|
||||
"organizations",
|
||||
"users",
|
||||
"viewer",
|
||||
"webhook",
|
||||
"workflows",
|
||||
"appsRouter",
|
||||
"googleWorkspace",
|
||||
] as const;
|
||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const resolveEndpoint = (links: any) => {
|
||||
// TODO: Update our trpc routes so they are more clear.
|
||||
// This function parses paths like the following and maps them
|
||||
// to the correct API endpoints.
|
||||
// - viewer.me - 2 segment paths like this are for logged in requests
|
||||
// - viewer.public.i18n - 3 segments paths can be public or authed
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (ctx: any) => {
|
||||
const parts = ctx.op.path.split(".");
|
||||
let endpoint;
|
||||
let path = "";
|
||||
if (parts.length == 2) {
|
||||
endpoint = parts[0] as keyof typeof links;
|
||||
path = parts[1];
|
||||
} else {
|
||||
endpoint = parts[1] as keyof typeof links;
|
||||
path = parts.splice(2, parts.length - 2).join(".");
|
||||
}
|
||||
return links[endpoint]({ ...ctx, op: { ...ctx.op, path } });
|
||||
};
|
||||
};
|
||||
|
||||
export const TrpcProvider: React.FC<{ children: React.ReactNode; dehydratedState?: DehydratedState }> = ({
|
||||
children,
|
||||
dehydratedState,
|
||||
}) => {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: 5000 } },
|
||||
})
|
||||
);
|
||||
const url =
|
||||
typeof window !== "undefined"
|
||||
? "/api/trpc"
|
||||
: process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}/api/trpc`
|
||||
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
|
||||
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
// adds pretty logs to your console in development and logs errors in production
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
|
||||
}),
|
||||
splitLink({
|
||||
// check for context property `skipBatch`
|
||||
condition: (op) => !!op.context.skipBatch,
|
||||
// when condition is true, use normal request
|
||||
true: (runtime) => {
|
||||
const links = Object.fromEntries(
|
||||
ENDPOINTS.map((endpoint) => [
|
||||
endpoint,
|
||||
httpLink({
|
||||
url: `${url}/${endpoint}`,
|
||||
})(runtime),
|
||||
])
|
||||
);
|
||||
return resolveEndpoint(links);
|
||||
},
|
||||
// when condition is false, use batch request
|
||||
false: (runtime) => {
|
||||
const links = Object.fromEntries(
|
||||
ENDPOINTS.map((endpoint) => [
|
||||
endpoint,
|
||||
httpBatchLink({
|
||||
url: `${url}/${endpoint}`,
|
||||
})(runtime),
|
||||
])
|
||||
);
|
||||
return resolveEndpoint(links);
|
||||
},
|
||||
}),
|
||||
],
|
||||
transformer: superjson,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{dehydratedState ? <HydrateClient state={dehydratedState}>{children}</HydrateClient> : children}
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type TFunction } from "i18next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
||||
import { getFixedT } from "@calcom/lib/server/getFixedT";
|
||||
|
||||
import { preparePageMetadata } from "@lib/metadata";
|
||||
|
||||
export const _generateMetadata = async (
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string
|
||||
) => {
|
||||
const h = headers();
|
||||
const canonical = h.get("x-pathname") ?? "";
|
||||
const locale = h.get("x-locale") ?? "en";
|
||||
|
||||
const t = await getFixedT(locale, "common");
|
||||
|
||||
const title = getTitle(t);
|
||||
const description = getDescription(t);
|
||||
|
||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
||||
|
||||
const image =
|
||||
SEO_IMG_OGIMG +
|
||||
constructGenericImage({
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
return preparePageMetadata({
|
||||
title,
|
||||
canonical,
|
||||
image,
|
||||
description,
|
||||
siteName: APP_NAME,
|
||||
metadataBase,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Typescript class based component for custom-error
|
||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
*/
|
||||
import type { NextPage } from "next";
|
||||
import type { ErrorProps } from "next/error";
|
||||
import React from "react";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
import { ErrorPage } from "@components/error/error-page";
|
||||
|
||||
type NextError = Error & { digest?: string };
|
||||
|
||||
// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
|
||||
export type DefaultErrorProps = {
|
||||
error: NextError;
|
||||
reset: () => void; // A function to reset the error boundary
|
||||
};
|
||||
|
||||
type AugmentedError = NextError | HttpError | null;
|
||||
|
||||
type CustomErrorProps = {
|
||||
err?: AugmentedError;
|
||||
statusCode?: number;
|
||||
message?: string;
|
||||
} & Omit<ErrorProps, "err" | "statusCode">;
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[error]"] });
|
||||
|
||||
const CustomError: NextPage<DefaultErrorProps> = (props) => {
|
||||
const { error } = props;
|
||||
let errorObject: CustomErrorProps = {
|
||||
message: error.message,
|
||||
err: error,
|
||||
};
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
const redactedError = redactError(error);
|
||||
errorObject = {
|
||||
statusCode: error.statusCode,
|
||||
title: redactedError.name,
|
||||
message: redactedError.message,
|
||||
err: {
|
||||
...redactedError,
|
||||
...error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
|
||||
log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
|
||||
log.info("errorObject: ", errorObject);
|
||||
|
||||
return (
|
||||
<ErrorPage statusCode={errorObject.statusCode} error={errorObject.err} message={errorObject.message} />
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomError;
|
||||
@@ -0,0 +1,10 @@
|
||||
import EventTypes from "@pages/event-types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("event_types_page_title"),
|
||||
(t) => t("event_types_page_subtitle")
|
||||
);
|
||||
|
||||
export default EventTypes;
|
||||
@@ -0,0 +1,22 @@
|
||||
// pages without layout (e.g., /availability/index.tsx) are supposed to go under (layout) folder
|
||||
import { headers } from "next/headers";
|
||||
import { type ReactElement } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type WrapperWithLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { type NextPage } from "next";
|
||||
|
||||
import CustomError, { type DefaultErrorProps } from "./error";
|
||||
|
||||
export const GlobalError: NextPage<DefaultErrorProps> = (props) => {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<CustomError {...props} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalError;
|
||||
+34
-55
@@ -1,84 +1,63 @@
|
||||
import type { Metadata } from "next";
|
||||
import { headers as nextHeaders, cookies as nextCookies } from "next/headers";
|
||||
import { dir } from "i18next";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import React from "react";
|
||||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
import { prepareRootMetadata } from "@lib/metadata";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
icons: {
|
||||
icon: [
|
||||
{
|
||||
sizes: "32x32",
|
||||
url: "/api/logo?type=favicon-32",
|
||||
},
|
||||
{
|
||||
sizes: "16x16",
|
||||
url: "/api/logo?type=favicon-16",
|
||||
},
|
||||
],
|
||||
apple: {
|
||||
sizes: "180x180",
|
||||
url: "/api/logo?type=apple-touch-icon",
|
||||
export const generateMetadata = () =>
|
||||
prepareRootMetadata({
|
||||
twitterCreator: "@calcom",
|
||||
twitterSite: "@calcom",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
other: [
|
||||
{
|
||||
url: "/safari-pinned-tab.svg",
|
||||
rel: "mask-icon",
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: "/site.webmanifest",
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#f9fafb" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#1C1C1C" },
|
||||
],
|
||||
other: {
|
||||
"msapplication-TileColor": "#000000",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const getInitialProps = async (
|
||||
url: string,
|
||||
headers: ReturnType<typeof nextHeaders>,
|
||||
cookies: ReturnType<typeof nextCookies>
|
||||
) => {
|
||||
const getInitialProps = async (url: string) => {
|
||||
const { pathname, searchParams } = new URL(url);
|
||||
|
||||
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
|
||||
const embedColorScheme = searchParams?.get("ui.color-scheme");
|
||||
|
||||
// @ts-expect-error we cannot access ctx.req in app dir, however headers and cookies are only properties needed to extract the locale
|
||||
const newLocale = await getLocale({ headers, cookies });
|
||||
let direction = "ltr";
|
||||
|
||||
try {
|
||||
const intlLocale = new Intl.Locale(newLocale);
|
||||
// @ts-expect-error INFO: Typescript does not know about the Intl.Locale textInfo attribute
|
||||
direction = intlLocale.textInfo?.direction;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
const newLocale = await getLocale(req);
|
||||
const direction = dir(newLocale);
|
||||
|
||||
return { isEmbed, embedColorScheme, locale: newLocale, direction };
|
||||
};
|
||||
|
||||
const getFallbackProps = () => ({
|
||||
locale: "en",
|
||||
direction: "ltr",
|
||||
isEmbed: false,
|
||||
embedColorScheme: false,
|
||||
});
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const headers = nextHeaders();
|
||||
const cookies = nextCookies();
|
||||
const h = headers();
|
||||
|
||||
const fullUrl = headers.get("x-url") ?? "";
|
||||
const nonce = headers.get("x-csp") ?? "";
|
||||
const fullUrl = h.get("x-url") ?? "";
|
||||
const nonce = h.get("x-csp") ?? "";
|
||||
|
||||
const isSSG = !fullUrl;
|
||||
|
||||
const { locale, direction, isEmbed, embedColorScheme } = isSSG
|
||||
? getFallbackProps()
|
||||
: await getInitialProps(fullUrl);
|
||||
|
||||
const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps(fullUrl, headers, cookies);
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
dir={direction}
|
||||
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}>
|
||||
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}
|
||||
data-nextjs-router="app">
|
||||
<head nonce={nonce}>
|
||||
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
|
||||
// eslint-disable-next-line @next/next/no-sync-scripts
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { Badge, ListItemText, Avatar } from "@calcom/ui";
|
||||
@@ -56,7 +57,7 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
const router = useRouter();
|
||||
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { type DehydratedState } from "@tanstack/react-query";
|
||||
import type { SSRConfig } from "next-i18next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
@@ -10,7 +11,6 @@ import type { ReactNode } from "react";
|
||||
|
||||
import "@calcom/embed-core/src/embed-iframe";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers-app-dir";
|
||||
import AppProviders from "@lib/app-providers-app-dir";
|
||||
@@ -29,13 +29,14 @@ const calFont = localFont({
|
||||
});
|
||||
|
||||
export type PageWrapperProps = Readonly<{
|
||||
getLayout: (page: React.ReactElement) => ReactNode;
|
||||
getLayout: ((page: React.ReactElement) => ReactNode) | null;
|
||||
children: React.ReactElement;
|
||||
requiresLicense: boolean;
|
||||
isThemeSupported: boolean;
|
||||
isBookingPage: boolean;
|
||||
nonce: string | undefined;
|
||||
themeBasis: string | null;
|
||||
dehydratedState?: DehydratedState;
|
||||
isThemeSupported?: boolean;
|
||||
isBookingPage?: boolean;
|
||||
i18n?: SSRConfig;
|
||||
}>;
|
||||
|
||||
@@ -85,4 +86,4 @@ function PageWrapper(props: PageWrapperProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default trpc.withTRPC(PageWrapper);
|
||||
export default PageWrapper;
|
||||
|
||||
@@ -42,6 +42,7 @@ export type AppPageProps = {
|
||||
disableInstall?: boolean;
|
||||
dependencies?: string[];
|
||||
concurrentMeetings: AppType["concurrentMeetings"];
|
||||
paid?: AppType["paid"];
|
||||
};
|
||||
|
||||
export const AppPage = ({
|
||||
@@ -67,6 +68,7 @@ export const AppPage = ({
|
||||
isTemplate,
|
||||
dependencies,
|
||||
concurrentMeetings,
|
||||
paid,
|
||||
}: AppPageProps) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
||||
@@ -163,6 +165,19 @@ export const AppPage = ({
|
||||
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
|
||||
{categories[0]}
|
||||
</Link>{" "}
|
||||
{paid && (
|
||||
<>
|
||||
<Badge className="mr-1">
|
||||
{Intl.NumberFormat(i18n.language, {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(paid.priceInUsd)}
|
||||
/{t("month")}
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
•{" "}
|
||||
<a target="_blank" rel="noreferrer" href={website}>
|
||||
{t("published_by", { author })}
|
||||
@@ -206,6 +221,7 @@ export const AppPage = ({
|
||||
addAppMutationInput={{ type, variant, slug }}
|
||||
multiInstall
|
||||
concurrentMeetings={concurrentMeetings}
|
||||
paid={paid}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -244,6 +260,7 @@ export const AppPage = ({
|
||||
addAppMutationInput={{ type, variant, slug }}
|
||||
credentials={appDbQuery.data?.credentials}
|
||||
concurrentMeetings={concurrentMeetings}
|
||||
paid={paid}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -263,7 +280,7 @@ export const AppPage = ({
|
||||
<SkeletonButton className="mt-6 h-20 grow" />
|
||||
))}
|
||||
|
||||
{price !== 0 && (
|
||||
{price !== 0 && !paid && (
|
||||
<span className="block text-right">
|
||||
{feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar}
|
||||
{feeType === "monthly" && `/${t("month")}`}
|
||||
@@ -273,23 +290,27 @@ export const AppPage = ({
|
||||
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
|
||||
{body}
|
||||
</div>
|
||||
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
||||
<span className="text-default">
|
||||
{teamsPlanRequired ? (
|
||||
t("teams_plan_required")
|
||||
) : price === 0 ? (
|
||||
t("free_to_use_apps")
|
||||
) : (
|
||||
<>
|
||||
{Intl.NumberFormat(i18n.language, {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
useGrouping: false,
|
||||
}).format(price)}
|
||||
{feeType === "monthly" && `/${t("month")}`}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{!paid && (
|
||||
<>
|
||||
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
||||
<span className="text-default">
|
||||
{teamsPlanRequired ? (
|
||||
t("teams_plan_required")
|
||||
) : price === 0 ? (
|
||||
t("free_to_use_apps")
|
||||
) : (
|
||||
<>
|
||||
{Intl.NumberFormat(i18n.language, {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
useGrouping: false,
|
||||
}).format(price)}
|
||||
{feeType === "monthly" && `/${t("month")}`}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
|
||||
<ul className="prose-sm -ml-1 -mr-1 leading-5">
|
||||
|
||||
@@ -26,6 +26,7 @@ export const InstallAppButtonChild = ({
|
||||
multiInstall,
|
||||
credentials,
|
||||
concurrentMeetings,
|
||||
paid,
|
||||
...props
|
||||
}: {
|
||||
userAdminTeams?: UserAdminTeams;
|
||||
@@ -34,6 +35,7 @@ export const InstallAppButtonChild = ({
|
||||
multiInstall?: boolean;
|
||||
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
|
||||
concurrentMeetings?: boolean;
|
||||
paid?: AppFrontendPayload["paid"];
|
||||
} & ButtonProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -46,8 +48,27 @@ export const InstallAppButtonChild = ({
|
||||
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
|
||||
},
|
||||
});
|
||||
const shouldDisableInstallation = !multiInstall ? !!(credentials && credentials.length) : false;
|
||||
|
||||
if (!userAdminTeams?.length || !doesAppSupportTeamInstall(appCategories, concurrentMeetings)) {
|
||||
// Paid apps don't support team installs at the moment
|
||||
// Also, cal.ai(the only paid app at the moment) doesn't support team install either
|
||||
if (paid) {
|
||||
return (
|
||||
<Button
|
||||
data-testid="install-app-button"
|
||||
{...props}
|
||||
disabled={shouldDisableInstallation}
|
||||
color="primary"
|
||||
size="base">
|
||||
{paid.trial ? t("start_paid_trial") : t("install_paid_app")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!userAdminTeams?.length ||
|
||||
!doesAppSupportTeamInstall({ appCategories, concurrentMeetings, isPaid: !!paid })
|
||||
) {
|
||||
return (
|
||||
<Button
|
||||
data-testid="install-app-button"
|
||||
@@ -55,6 +76,7 @@ export const InstallAppButtonChild = ({
|
||||
// @TODO: Overriding color and size prevent us from
|
||||
// having to duplicate InstallAppButton for now.
|
||||
color="primary"
|
||||
disabled={shouldDisableInstallation}
|
||||
size="base">
|
||||
{multiInstall ? t("install_another") : t("install_app")}
|
||||
</Button>
|
||||
|
||||
@@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const getAppDataSetter = (appId: EventTypeAppsList): SetAppData => {
|
||||
const getAppDataSetter = (appId: EventTypeAppsList, credentialId?: number): SetAppData => {
|
||||
return function (key, value) {
|
||||
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
|
||||
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
|
||||
@@ -57,6 +57,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
[appId]: {
|
||||
...appData,
|
||||
[key]: value,
|
||||
credentialId,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -76,7 +77,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
appCards.push(
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
|
||||
key={app.slug}
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
@@ -90,7 +91,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
appCards.push(
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, team.credentialId)}
|
||||
key={app.slug + team?.credentialId}
|
||||
app={{
|
||||
...app,
|
||||
@@ -147,7 +148,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
return (
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
|
||||
key={app.slug}
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
|
||||
@@ -76,6 +76,8 @@ const ChildrenEventTypesList = ({
|
||||
<div>
|
||||
<Label>{t("assign_to")}</Label>
|
||||
<ChildrenEventTypeSelect
|
||||
aria-label="assignment-dropdown"
|
||||
data-testid="assignment-dropdown"
|
||||
onChange={(options) => {
|
||||
onChange &&
|
||||
onChange(
|
||||
|
||||
@@ -14,6 +14,6 @@ type UserAvatarProps = Omit<React.ComponentProps<typeof Avatar>, "alt" | "imageS
|
||||
* It is aware of the user's organization to correctly show the avatar from the correct URL
|
||||
*/
|
||||
export function UserAvatar(props: UserAvatarProps) {
|
||||
const { user, previewSrc, ...rest } = props;
|
||||
return <Avatar {...rest} alt={user.name || ""} imageSrc={previewSrc ?? getUserAvatarUrl(user)} />;
|
||||
const { user, previewSrc = getUserAvatarUrl(user), ...rest } = props;
|
||||
return <Avatar {...rest} alt={user.name || "Nameless User"} imageSrc={previewSrc} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||
import { dir } from "i18next";
|
||||
import type { Session } from "next-auth";
|
||||
import { SessionProvider, useSession } from "next-auth/react";
|
||||
@@ -255,26 +256,28 @@ const AppProviders = (props: PageWrapperProps) => {
|
||||
const isBookingPage = useIsBookingPage();
|
||||
|
||||
const RemainingProviders = (
|
||||
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
|
||||
<SessionProvider>
|
||||
<CustomI18nextProvider i18n={props.i18n}>
|
||||
<TooltipProvider>
|
||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||
<CalcomThemeProvider
|
||||
themeBasis={props.themeBasis}
|
||||
nonce={props.nonce}
|
||||
isThemeSupported={props.isThemeSupported}
|
||||
isBookingPage={props.isBookingPage || isBookingPage}>
|
||||
<FeatureFlagsProvider>
|
||||
<OrgBrandProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</OrgBrandProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</CalcomThemeProvider>
|
||||
</TooltipProvider>
|
||||
</CustomI18nextProvider>
|
||||
</SessionProvider>
|
||||
</EventCollectionProvider>
|
||||
<TrpcProvider dehydratedState={props.dehydratedState}>
|
||||
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
|
||||
<SessionProvider>
|
||||
<CustomI18nextProvider i18n={props.i18n}>
|
||||
<TooltipProvider>
|
||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||
<CalcomThemeProvider
|
||||
themeBasis={props.themeBasis}
|
||||
nonce={props.nonce}
|
||||
isThemeSupported={/* undefined gets treated as true */ props.isThemeSupported ?? true}
|
||||
isBookingPage={props.isBookingPage || isBookingPage}>
|
||||
<FeatureFlagsProvider>
|
||||
<OrgBrandProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</OrgBrandProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</CalcomThemeProvider>
|
||||
</TooltipProvider>
|
||||
</CustomI18nextProvider>
|
||||
</SessionProvider>
|
||||
</EventCollectionProvider>
|
||||
</TrpcProvider>
|
||||
);
|
||||
|
||||
if (isBookingPage) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
export default function useIsBookingPage(): boolean {
|
||||
const pathname = usePathname();
|
||||
const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const userParam = Boolean(searchParams?.get("user"));
|
||||
const teamParam = Boolean(searchParams?.get("team"));
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
export default function useRouterQuery<T extends string>(name: T) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
export function useToggleQuery(name: string) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
|
||||
return {
|
||||
isOn: searchParams?.get(name) === "1",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { truncateOnWord } from "@calcom/lib/text";
|
||||
|
||||
type RootMetadataRecipe = Readonly<{
|
||||
twitterCreator: string;
|
||||
twitterSite: string;
|
||||
robots: {
|
||||
index: boolean;
|
||||
follow: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type PageMetadataRecipe = Readonly<{
|
||||
title: string;
|
||||
canonical: string;
|
||||
image: string;
|
||||
description: string;
|
||||
siteName: string;
|
||||
metadataBase: URL;
|
||||
}>;
|
||||
|
||||
export const prepareRootMetadata = (recipe: RootMetadataRecipe): Metadata => ({
|
||||
icons: {
|
||||
icon: "/favicon.icon",
|
||||
apple: "/api/logo?type=apple-touch-icon",
|
||||
other: [
|
||||
{
|
||||
rel: "icon-mask",
|
||||
url: "/safari-pinned-tab.svg",
|
||||
color: "#000000",
|
||||
},
|
||||
{
|
||||
url: "/api/logo?type=favicon-16",
|
||||
sizes: "16x16",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
url: "/api/logo?type=favicon-32",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: "/site.webmanifest",
|
||||
viewport: "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0",
|
||||
robots: recipe.robots,
|
||||
other: {
|
||||
"application-TileColor": "#ff0000",
|
||||
},
|
||||
themeColor: [
|
||||
{
|
||||
media: "(prefers-color-scheme: light)",
|
||||
color: "#f9fafb",
|
||||
},
|
||||
{
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
color: "#1C1C1C",
|
||||
},
|
||||
],
|
||||
twitter: {
|
||||
site: recipe.twitterSite,
|
||||
creator: recipe.twitterCreator,
|
||||
card: "summary_large_image",
|
||||
},
|
||||
});
|
||||
|
||||
export const preparePageMetadata = (recipe: PageMetadataRecipe): Metadata => ({
|
||||
title: recipe.title,
|
||||
alternates: {
|
||||
canonical: recipe.canonical,
|
||||
},
|
||||
openGraph: {
|
||||
description: truncateOnWord(recipe.description, 158),
|
||||
url: recipe.canonical,
|
||||
type: "website",
|
||||
siteName: recipe.siteName,
|
||||
title: recipe.title,
|
||||
images: [recipe.image],
|
||||
},
|
||||
metadataBase: recipe.metadataBase,
|
||||
});
|
||||
+14
-3
@@ -1,13 +1,16 @@
|
||||
import { get } from "@vercel/edge-config";
|
||||
import { collectEvents } from "next-collect/server";
|
||||
import type { NextMiddleware } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
||||
|
||||
import { csp } from "@lib/csp";
|
||||
|
||||
const middleware: NextMiddleware = async (req) => {
|
||||
import { abTestMiddlewareFactory } from "./abTest/middlewareFactory";
|
||||
|
||||
const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
||||
const url = req.nextUrl;
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
|
||||
@@ -61,6 +64,12 @@ const middleware: NextMiddleware = async (req) => {
|
||||
requestHeaders.set("x-csp-enforce", "true");
|
||||
}
|
||||
|
||||
requestHeaders.set("x-pathname", url.pathname);
|
||||
|
||||
const locale = await getLocale(req);
|
||||
|
||||
requestHeaders.set("x-locale", locale);
|
||||
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
@@ -90,11 +99,13 @@ export const config = {
|
||||
* Paths required by routingForms.handle
|
||||
*/
|
||||
"/apps/routing_forms/:path*",
|
||||
"/event-types",
|
||||
"/future/event-types/",
|
||||
],
|
||||
};
|
||||
|
||||
export default collectEvents({
|
||||
middleware,
|
||||
middleware: abTestMiddlewareFactory(middleware),
|
||||
...nextCollectBasicSettings,
|
||||
cookieName: "__clnds",
|
||||
extend: extendEventData,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.4.10",
|
||||
"version": "3.5.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@hookform/resolvers": "^2.9.7",
|
||||
"@next-auth/prisma-adapter": "^1.0.4",
|
||||
"@next/bundle-analyzer": "^13.1.6",
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
@@ -168,8 +168,9 @@
|
||||
"module-alias": "^2.2.2",
|
||||
"msw": "^0.42.3",
|
||||
"node-html-parser": "^6.1.10",
|
||||
"node-mocks-http": "^1.11.0",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Head from "next/head";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { Copy } from "@calcom/ui/components/icon";
|
||||
@@ -9,7 +9,7 @@ import { Copy } from "@calcom/ui/components/icon";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function Error500() {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
|
||||
@@ -82,7 +82,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
description={markdownStrippedBio}
|
||||
meeting={{
|
||||
title: markdownStrippedBio,
|
||||
profile: { name: `${profile.name}`, image: null },
|
||||
profile: { name: `${profile.name}`, image: user.avatarUrl || null },
|
||||
users: [{ username: `${user.username}`, name: `${user.name}` }],
|
||||
}}
|
||||
nextSeoProps={{
|
||||
@@ -245,7 +245,7 @@ export type UserPageProps = {
|
||||
allowSEOIndexing: boolean;
|
||||
username: string | null;
|
||||
};
|
||||
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[];
|
||||
users: Pick<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl">[];
|
||||
themeBasis: string | null;
|
||||
markdownStrippedBio: string;
|
||||
safeBio: string;
|
||||
@@ -295,6 +295,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
metadata: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
avatarUrl: true,
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
@@ -363,6 +364,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
image: user.avatar,
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor,
|
||||
avatarUrl: user.avatarUrl,
|
||||
darkBrandColor: user.darkBrandColor,
|
||||
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||
username: user.username,
|
||||
@@ -397,6 +399,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
bio: user.bio,
|
||||
avatarUrl: user.avatarUrl,
|
||||
away: user.away,
|
||||
verified: user.verified,
|
||||
})),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const querySchema = z.object({
|
||||
uuid: z.string().transform((objectKey) => objectKey.split(".")[0]),
|
||||
});
|
||||
|
||||
const handleValidationError = (res: NextApiResponse, error: z.ZodError): void => {
|
||||
const errors = error.errors.map((err) => ({
|
||||
path: err.path.join("."),
|
||||
errorCode: `error.validation.${err.code}`,
|
||||
}));
|
||||
|
||||
res.status(400).json({
|
||||
message: "VALIDATION_ERROR",
|
||||
errors,
|
||||
});
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const result = querySchema.safeParse(req.query);
|
||||
if (!result.success) {
|
||||
return handleValidationError(res, result.error);
|
||||
}
|
||||
|
||||
const { uuid: objectKey } = result.data;
|
||||
|
||||
let img;
|
||||
try {
|
||||
const { data } = await prisma.avatar.findUniqueOrThrow({
|
||||
where: {
|
||||
objectKey,
|
||||
},
|
||||
select: {
|
||||
data: true,
|
||||
},
|
||||
});
|
||||
img = data;
|
||||
} catch (e) {
|
||||
// If anything goes wrong or avatar is not found, use default avatar
|
||||
res.writeHead(302, {
|
||||
Location: AVATAR_FALLBACK,
|
||||
});
|
||||
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
|
||||
const imageResp = Buffer.from(decoded, "base64");
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Length": imageResp.length,
|
||||
});
|
||||
|
||||
res.end(imageResp);
|
||||
}
|
||||
@@ -73,7 +73,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
redirectUrl = handler.redirect?.url || getInstalledAppPath(handler);
|
||||
res.json({ url: redirectUrl, newTab: handler.redirect?.newTab });
|
||||
}
|
||||
return res.status(200);
|
||||
if (!res.writableEnded) return res.status(200);
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { buffer } from "micro";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type Stripe from "stripe";
|
||||
|
||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { HttpError as HttpCode } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
// This file is a catch-all for any integration related subscription/paid app.
|
||||
|
||||
const handleSubscriptionUpdate = async (event: Stripe.Event) => {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
if (!subscription.id) throw new HttpCode({ statusCode: 400, message: "Subscription ID not found" });
|
||||
|
||||
const app = await prisma.credential.findFirst({
|
||||
where: {
|
||||
subscriptionId: subscription.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
|
||||
}
|
||||
|
||||
await prisma.credential.update({
|
||||
where: {
|
||||
id: app.id,
|
||||
},
|
||||
data: {
|
||||
paymentStatus: subscription.status,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubscriptionDeleted = async (event: Stripe.Event) => {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
if (!subscription.id) throw new HttpCode({ statusCode: 400, message: "Subscription ID not found" });
|
||||
|
||||
const app = await prisma.credential.findFirst({
|
||||
where: {
|
||||
subscriptionId: subscription.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
|
||||
}
|
||||
|
||||
// should we delete the credential here rather than marking as inactive?
|
||||
await prisma.credential.update({
|
||||
where: {
|
||||
id: app.id,
|
||||
},
|
||||
data: {
|
||||
paymentStatus: "inactive",
|
||||
billingCycleStart: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type WebhookHandler = (event: Stripe.Event) => Promise<void>;
|
||||
|
||||
const webhookHandlers: Record<string, WebhookHandler | undefined> = {
|
||||
"customer.subscription.updated": handleSubscriptionUpdate,
|
||||
"customer.subscription.deleted": handleSubscriptionDeleted,
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (req.method !== "POST") {
|
||||
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
|
||||
}
|
||||
const sig = req.headers["stripe-signature"];
|
||||
if (!sig) {
|
||||
throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" });
|
||||
}
|
||||
|
||||
if (!process.env.STRIPE_WEBHOOK_SECRET) {
|
||||
throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" });
|
||||
}
|
||||
const requestBuffer = await buffer(req);
|
||||
const payload = requestBuffer.toString();
|
||||
|
||||
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
|
||||
|
||||
const handler = webhookHandlers[event.type];
|
||||
if (handler) {
|
||||
await handler(event);
|
||||
} else {
|
||||
/** Not really an error, just letting Stripe know that the webhook was received but unhandled */
|
||||
throw new HttpCode({
|
||||
statusCode: 202,
|
||||
message: `Unhandled Stripe Webhook event type ${event.type}`,
|
||||
});
|
||||
}
|
||||
} catch (_err) {
|
||||
const err = getErrorFromUnknown(_err);
|
||||
console.error(`Webhook Error: ${err.message}`);
|
||||
res.status(err.statusCode ?? 500).send({
|
||||
message: err.message,
|
||||
stack: IS_PRODUCTION ? undefined : err.stack,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Return a response to acknowledge receipt of the event
|
||||
res.json({ received: true });
|
||||
}
|
||||
@@ -79,6 +79,7 @@ function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
|
||||
isTemplate={data.isTemplate}
|
||||
dependencies={data.dependencies}
|
||||
concurrentMeetings={data.concurrentMeetings}
|
||||
paid={data.paid}
|
||||
// tos="https://zoom.us/terms"
|
||||
// privacy="https://zoom.us/privacy"
|
||||
body={
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
|
||||
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function SetupInformation(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const router = useRouter();
|
||||
const slug = searchParams?.get("slug") as string;
|
||||
const { status } = useSession();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
@@ -13,7 +13,7 @@ import { AppCard, SkeletonText } from "@calcom/ui";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
const category = searchParams?.get("category");
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useReducer } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -122,7 +122,7 @@ type ModalState = {
|
||||
};
|
||||
|
||||
export default function InstalledApps() {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t } = useLocale();
|
||||
const category = searchParams?.get("category") as querySchemaType["category"];
|
||||
const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GetStaticPropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { X } from "@calcom/ui/components/icon";
|
||||
@@ -18,7 +18,7 @@ const querySchema = z.object({
|
||||
|
||||
export default function Error() {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { error } = querySchema.parse(searchParams);
|
||||
const isTokenVerificationError = error?.toLowerCase() === "verification";
|
||||
const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
|
||||
|
||||
@@ -4,7 +4,7 @@ import { jwtVerify } from "jose";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken, signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
@@ -17,6 +17,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { WEBAPP_URL, WEBSITE_URL, HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -53,7 +54,7 @@ export default function Login({
|
||||
totpEmail,
|
||||
}: // eslint-disable-next-line @typescript-eslint/ban-types
|
||||
inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const formSchema = z
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Avatar, Button, Select } from "@calcom/ui";
|
||||
@@ -16,7 +16,7 @@ export default function Authorize() {
|
||||
const { status } = useSession();
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
|
||||
const client_id = searchParams?.get("client_id") as string;
|
||||
const state = searchParams?.get("state") as string;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
// To handle the IdP initiated login flow callback
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const code = searchParams?.get("code");
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
@@ -21,7 +22,7 @@ import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
function useSetStep() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
const setStep = (newStep = 1) => {
|
||||
const _searchParams = new URLSearchParams(searchParams ?? undefined);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
@@ -9,6 +9,7 @@ import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomain
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -22,7 +23,7 @@ import { ssrInit } from "@server/lib/ssr";
|
||||
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function Provider(props: SSOProviderPageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { signIn } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
@@ -54,7 +55,7 @@ const querySchema = z.object({
|
||||
});
|
||||
|
||||
export default function Verify() {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const routerQuery = useRouterQuery();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
|
||||
@@ -8,6 +8,7 @@ import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { availabilityAsString } from "@calcom/lib/availability";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -83,7 +84,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
};
|
||||
|
||||
export default function Availability() {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const utils = trpc.useContext();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayout";
|
||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
@@ -131,7 +132,7 @@ const WithQuery = withQuery(trpc.viewer.availability.list as any);
|
||||
|
||||
export default function AvailabilityPage() {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
|
||||
@@ -1,139 +1,20 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter";
|
||||
import { getLayout } from "@calcom/features/troubleshooter/layout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
type User = RouterOutputs["viewer"]["me"];
|
||||
|
||||
export interface IBusySlot {
|
||||
start: string | Date;
|
||||
end: string | Date;
|
||||
title?: string;
|
||||
source?: string | null;
|
||||
}
|
||||
|
||||
const AvailabilityView = ({ user }: { user: User }) => {
|
||||
const { t } = useLocale();
|
||||
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
|
||||
const selectedDate = dayjs(date);
|
||||
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
|
||||
|
||||
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
||||
{
|
||||
username: user.username || "",
|
||||
dateFrom: selectedDate.startOf("day").utc().format(),
|
||||
dateTo: selectedDate.endOf("day").utc().format(),
|
||||
withSource: true,
|
||||
},
|
||||
{
|
||||
enabled: !!user.username,
|
||||
}
|
||||
);
|
||||
|
||||
const overrides =
|
||||
data?.dateOverrides.reduce((acc, override) => {
|
||||
if (
|
||||
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
|
||||
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
|
||||
)
|
||||
return acc;
|
||||
acc.push({ ...override, source: "Date override" });
|
||||
return acc;
|
||||
}, [] as IBusySlot[]) || [];
|
||||
|
||||
return (
|
||||
<div className="bg-default max-w-xl overflow-hidden rounded-md shadow">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
{t("overview_of_day")}{" "}
|
||||
<input
|
||||
type="date"
|
||||
className="inline h-8 border-none bg-inherit p-0"
|
||||
defaultValue={formattedSelectedDate}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) setSelectedDate(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<small className="text-muted block">{t("hover_over_bold_times_tip")}</small>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
if (isLoading)
|
||||
return (
|
||||
<>
|
||||
<SkeletonText className="block h-16 w-full" />
|
||||
<SkeletonText className="block h-16 w-full" />
|
||||
</>
|
||||
);
|
||||
|
||||
if (data && (data.busy.length > 0 || overrides.length > 0))
|
||||
return [...data.busy, ...overrides]
|
||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
||||
.map((slot: IBusySlot) => (
|
||||
<div
|
||||
key={dayjs(slot.start).format("HH:mm")}
|
||||
className="bg-subtle overflow-hidden rounded-md"
|
||||
data-testid="troubleshooter-busy-time">
|
||||
<div className="text-emphasis px-4 py-5 sm:p-6">
|
||||
{t("calendar_shows_busy_between")}{" "}
|
||||
<span className="text-default font-medium" title={dayjs(slot.start).format("HH:mm")}>
|
||||
{dayjs(slot.start).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("and")}{" "}
|
||||
<span className="text-default font-medium" title={dayjs(slot.end).format("HH:mm")}>
|
||||
{dayjs(slot.end).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
||||
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
|
||||
{slot.title && ` - (${slot.title})`}
|
||||
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div className="bg-subtle overflow-hidden rounded-md">
|
||||
<div className="text-emphasis px-4 py-5 sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Troubleshoot() {
|
||||
const { data, isLoading } = trpc.viewer.me.useQuery();
|
||||
function TroubleshooterPage() {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div>
|
||||
<Shell heading={t("troubleshoot")} hideHeadingOnMobile subtitle={t("troubleshoot_description")}>
|
||||
{!isLoading && data && <AvailabilityView user={data} />}
|
||||
</Shell>
|
||||
</div>
|
||||
<>
|
||||
<HeadSeo title={t("troubleshoot")} description={t("troubleshoot_availability")} />
|
||||
<Troubleshooter month={null} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Troubleshoot.PageWrapper = PageWrapper;
|
||||
|
||||
function convertMinsToHrsMins(mins: number) {
|
||||
const h = Math.floor(mins / 60);
|
||||
const m = mins % 60;
|
||||
const hs = h < 10 ? `0${h}` : h;
|
||||
const ms = m < 10 ? `0${m}` : m;
|
||||
return `${hs}:${ms}`;
|
||||
}
|
||||
TroubleshooterPage.getLayout = getLayout;
|
||||
TroubleshooterPage.PageWrapper = PageWrapper;
|
||||
export default TroubleshooterPage;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createEvent } from "ics";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RRule } from "rrule";
|
||||
import { z } from "zod";
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
} from "@calcom/lib/date-fns";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
@@ -98,7 +99,7 @@ export default function Success(props: SuccessProps) {
|
||||
const router = useRouter();
|
||||
const routerQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const {
|
||||
allRemainingBookings,
|
||||
isSuccessBookingPage,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { User } from "@prisma/client";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { FC } from "react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { z } from "zod";
|
||||
@@ -19,6 +21,7 @@ import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsF
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
@@ -208,7 +211,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const orgBranding = useOrgBranding();
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -840,7 +843,7 @@ const Main = ({
|
||||
filters: ReturnType<typeof getTeamsFiltersFromQuery>;
|
||||
}) => {
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
if (!data || status === "loading") {
|
||||
@@ -904,7 +907,7 @@ const Main = ({
|
||||
|
||||
const EventTypesPage = () => {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { open } = useIntercom();
|
||||
const { data: user } = useMeQuery();
|
||||
const [showProfileBanner, setShowProfileBanner] = useState(false);
|
||||
|
||||
@@ -250,10 +250,7 @@ const AppearanceView = ({
|
||||
/>
|
||||
{lightModeError ? (
|
||||
<div className="mt-4">
|
||||
<Alert
|
||||
severity="warning"
|
||||
message="Light Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible."
|
||||
/>
|
||||
<Alert severity="warning" message={t("light_theme_contrast_error")} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -282,10 +279,7 @@ const AppearanceView = ({
|
||||
/>
|
||||
{darkModeError ? (
|
||||
<div className="mt-4">
|
||||
<Alert
|
||||
severity="warning"
|
||||
message="Dark Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible."
|
||||
/>
|
||||
<Alert severity="warning" message={t("dark_theme_contrast_error")} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import OrganizationMemberAvatar from "@calcom/features/ee/organizations/componen
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
@@ -72,37 +72,22 @@ interface DeleteAccountValues {
|
||||
|
||||
type FormValues = {
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
avatar: string;
|
||||
name: string;
|
||||
email: string;
|
||||
bio: string;
|
||||
};
|
||||
|
||||
const checkIfItFallbackImage = (fetchedImgSrc?: string) => {
|
||||
return !fetchedImgSrc || fetchedImgSrc.endsWith(AVATAR_FALLBACK);
|
||||
};
|
||||
|
||||
const ProfileView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { update } = useSession();
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
|
||||
const [fetchedImgSrc, setFetchedImgSrc] = useState<string | undefined>(undefined);
|
||||
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery(undefined, {
|
||||
onSuccess: async (userData) => {
|
||||
try {
|
||||
if (!userData.organization) {
|
||||
const res = await fetch(userData.avatar);
|
||||
if (res.url) setFetchedImgSrc(res.url);
|
||||
} else {
|
||||
setFetchedImgSrc("");
|
||||
}
|
||||
} catch (err) {
|
||||
setFetchedImgSrc("");
|
||||
}
|
||||
},
|
||||
const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, {
|
||||
enabled: !isLoading && !user?.avatarUrl,
|
||||
});
|
||||
|
||||
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await update(res);
|
||||
@@ -234,7 +219,7 @@ const ProfileView = () => {
|
||||
|
||||
const defaultValues = {
|
||||
username: user.username || "",
|
||||
avatar: user.avatar || "",
|
||||
avatar: getUserAvatarUrl(user),
|
||||
name: user.name || "",
|
||||
email: user.email || "",
|
||||
bio: user.bio || "",
|
||||
@@ -251,8 +236,7 @@ const ProfileView = () => {
|
||||
key={JSON.stringify(defaultValues)}
|
||||
defaultValues={defaultValues}
|
||||
isLoading={updateProfileMutation.isLoading}
|
||||
isFallbackImg={checkIfItFallbackImage(fetchedImgSrc)}
|
||||
userAvatar={user.avatar}
|
||||
isFallbackImg={!user.avatarUrl && !avatarData?.avatar}
|
||||
user={user}
|
||||
userOrganization={user.organization}
|
||||
onSubmit={(values) => {
|
||||
@@ -398,7 +382,6 @@ const ProfileForm = ({
|
||||
extraField,
|
||||
isLoading = false,
|
||||
isFallbackImg,
|
||||
userAvatar,
|
||||
user,
|
||||
userOrganization,
|
||||
}: {
|
||||
@@ -407,7 +390,6 @@ const ProfileForm = ({
|
||||
extraField?: React.ReactNode;
|
||||
isLoading: boolean;
|
||||
isFallbackImg: boolean;
|
||||
userAvatar: string;
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
|
||||
}) => {
|
||||
@@ -416,7 +398,7 @@ const ProfileForm = ({
|
||||
|
||||
const profileFormSchema = z.object({
|
||||
username: z.string(),
|
||||
avatar: z.string().nullable(),
|
||||
avatar: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -438,7 +420,6 @@ const ProfileForm = ({
|
||||
} = formMethods;
|
||||
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
|
||||
return (
|
||||
<Form form={formMethods} handleSubmit={onSubmit}>
|
||||
<div className="border-subtle border-x px-4 pb-10 pt-8 sm:px-6">
|
||||
@@ -447,7 +428,7 @@ const ProfileForm = ({
|
||||
control={formMethods.control}
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => {
|
||||
const showRemoveAvatarButton = !isFallbackImg || (value && userAvatar !== value);
|
||||
const showRemoveAvatarButton = value === null ? false : !isFallbackImg;
|
||||
const organization =
|
||||
userOrganization && userOrganization.id
|
||||
? {
|
||||
@@ -474,7 +455,7 @@ const ProfileForm = ({
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||
}}
|
||||
imageSrc={value || undefined}
|
||||
imageSrc={value}
|
||||
triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"}
|
||||
/>
|
||||
|
||||
@@ -482,7 +463,7 @@ const ProfileForm = ({
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
formMethods.setValue("avatar", null, { shouldDirty: true });
|
||||
formMethods.setValue("avatar", "", { shouldDirty: true });
|
||||
}}>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import type { CSSProperties } from "react";
|
||||
import type { SubmitHandler } from "react-hook-form";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
@@ -13,6 +12,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 { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
@@ -37,7 +37,7 @@ type SignupProps = inferSSRProps<typeof getServerSideProps>;
|
||||
const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;
|
||||
|
||||
const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => {
|
||||
const [emailUser, emailDomain] = email.split("@");
|
||||
const [emailUser, emailDomain = ""] = email.split("@");
|
||||
const username =
|
||||
emailDomain === autoAcceptEmailDomain
|
||||
? slugify(emailUser)
|
||||
@@ -53,7 +53,7 @@ function addOrUpdateQueryParam(url: string, key: string, value: string) {
|
||||
}
|
||||
|
||||
export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoAcceptEmail }: SignupProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const telemetry = useTelemetry();
|
||||
const { t, i18n } = useLocale();
|
||||
const flags = useFlagMap();
|
||||
@@ -143,7 +143,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoA
|
||||
methods.clearErrors("apiError");
|
||||
}
|
||||
|
||||
if (methods.getValues().username === undefined && isOrgInviteByLink && orgAutoAcceptEmail) {
|
||||
if (!methods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
|
||||
methods.setValue(
|
||||
"username",
|
||||
getOrgUsernameFromEmail(methods.getValues().email, orgAutoAcceptEmail)
|
||||
|
||||
@@ -40,6 +40,7 @@ test.describe("Availablity tests", () => {
|
||||
const date = json[0].result.data.json.schedule.availability.find((a) => !!a.date);
|
||||
const troubleshooterURL = `/availability/troubleshoot?date=${dayjs(date.date).format("YYYY-MM-DD")}`;
|
||||
await page.goto(troubleshooterURL);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import { SchedulingType } from "@calcom/prisma/client";
|
||||
import type { Schedule, TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import {
|
||||
@@ -342,3 +344,92 @@ test.describe("Booking on different layouts", () => {
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking round robin event", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const teamMatesObj = [{ name: "teammate-1" }];
|
||||
|
||||
const dateRanges: TimeRange = {
|
||||
start: new Date(new Date().setUTCHours(10, 0, 0, 0)), //one hour after default schedule (teammate-1's schedule)
|
||||
end: new Date(new Date().setUTCHours(17, 0, 0, 0)),
|
||||
};
|
||||
|
||||
const schedule: Schedule = [[], [dateRanges], [dateRanges], [dateRanges], [dateRanges], [dateRanges], []];
|
||||
|
||||
const testUser = await users.create(
|
||||
{ username: "test-user", name: "Test User", email: "testuser@example.com", schedule },
|
||||
{
|
||||
hasTeam: true,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
teamEventLength: 120,
|
||||
teammates: teamMatesObj,
|
||||
}
|
||||
);
|
||||
const team = await testUser.getFirstTeam();
|
||||
await page.goto(`/team/${team.team.slug}`);
|
||||
});
|
||||
|
||||
test("Does not book round robin host outside availability with date override", async ({ page, users }) => {
|
||||
const [testUser] = users.get();
|
||||
testUser.apiLogin();
|
||||
|
||||
const team = await testUser.getFirstTeam();
|
||||
|
||||
// Click first event type (round robin)
|
||||
await page.click('[data-testid="event-type-link"]');
|
||||
|
||||
await page.click('[data-testid="incrementMonth"]');
|
||||
|
||||
// books 9AM slots for 120 minutes (test-user is not available at this time, availability starts at 10)
|
||||
await page.locator('[data-testid="time"]').nth(0).click();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.locator('[name="name"]').fill("Test name");
|
||||
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
|
||||
|
||||
await page.click('[data-testid="confirm-book-button"]');
|
||||
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.startsWith("/booking");
|
||||
});
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
const host = await page.locator('[data-testid="booking-host-name"]');
|
||||
const hostName = await host.innerText();
|
||||
|
||||
//expect teammate-1 to be booked, test-user is not available at this time
|
||||
expect(hostName).toBe("teammate-1");
|
||||
|
||||
// make another booking to see if also for the second booking teammate-1 is booked
|
||||
await page.goto(`/team/${team.team.slug}`);
|
||||
|
||||
await page.click('[data-testid="event-type-link"]');
|
||||
|
||||
await page.click('[data-testid="incrementMonth"]');
|
||||
await page.click('[data-testid="incrementMonth"]');
|
||||
|
||||
// Again book a 9AM slot for 120 minutes where test-user is not available
|
||||
await page.locator('[data-testid="time"]').nth(0).click();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.locator('[name="name"]').fill("Test name");
|
||||
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
|
||||
|
||||
await page.click('[data-testid="confirm-book-button"]');
|
||||
|
||||
await page.waitForURL((url) => {
|
||||
return url.pathname.startsWith("/booking");
|
||||
});
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
const hostSecondBooking = await page.locator('[data-testid="booking-host-name"]');
|
||||
const hostNameSecondBooking = await hostSecondBooking.innerText();
|
||||
expect(hostNameSecondBooking).toBe("teammate-1"); // teammate-1 should be booked again
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,16 +5,47 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import { bookTimeSlot, createNewEventType, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Event Types A/B tests", () => {
|
||||
test("should point to the /future/event-types page", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Event Types" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Event Types tests", () => {
|
||||
test.describe("user", () => {
|
||||
testBothFutureAndLegacyRoutes.describe("user", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,34 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
E2E_CLIPBOARD_VALUE?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export type Window = typeof window;
|
||||
// creates the single server fixture
|
||||
export const createClipboardFixture = (page: Page) => {
|
||||
return {
|
||||
reset: async () => {
|
||||
await page.evaluate(() => {
|
||||
delete window.E2E_CLIPBOARD_VALUE;
|
||||
});
|
||||
},
|
||||
get: async () => {
|
||||
return getClipboardValue({ page });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function getClipboardValue({ page }: { page: Page }) {
|
||||
return page.evaluate(() => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
setInterval(() => {
|
||||
if (!window.E2E_CLIPBOARD_VALUE) return;
|
||||
resolve(window.E2E_CLIPBOARD_VALUE);
|
||||
}, 500);
|
||||
setTimeout(() => reject(new Error("Timeout")), 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export function createBookingPageFixture(page: Page) {
|
||||
placeholder?: string
|
||||
) => {
|
||||
await page.getByTestId("add-field").click();
|
||||
await page.locator("#test-field-type > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("test-field-type").click();
|
||||
await page.getByTestId(`select-option-${questionType}`).click();
|
||||
await page.getByLabel("Identifier").dblclick();
|
||||
await page.getByLabel("Identifier").fill(identifier);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { Schedule } from "@calcom/types/schedule";
|
||||
|
||||
import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils";
|
||||
import { TimeZoneEnum } from "./types";
|
||||
@@ -46,6 +47,7 @@ const createTeamEventType = async (
|
||||
schedulingType?: SchedulingType;
|
||||
teamEventTitle?: string;
|
||||
teamEventSlug?: string;
|
||||
teamEventLength?: number;
|
||||
}
|
||||
) => {
|
||||
return await prisma.eventType.create({
|
||||
@@ -65,10 +67,16 @@ const createTeamEventType = async (
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
create: {
|
||||
userId: user.id,
|
||||
isFixed: scenario?.schedulingType === SchedulingType.COLLECTIVE ? true : false,
|
||||
},
|
||||
},
|
||||
schedulingType: scenario?.schedulingType ?? SchedulingType.COLLECTIVE,
|
||||
title: scenario?.teamEventTitle ?? `${teamEventTitle}-team-id-${team.id}`,
|
||||
slug: scenario?.teamEventSlug ?? `${teamEventSlug}-team-id-${team.id}`,
|
||||
length: 30,
|
||||
length: scenario?.teamEventLength ?? 30,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -78,12 +86,14 @@ const createTeamAndAddUser = async (
|
||||
user,
|
||||
isUnpublished,
|
||||
isOrg,
|
||||
isOrgVerified,
|
||||
hasSubteam,
|
||||
organizationId,
|
||||
}: {
|
||||
user: { id: number; username: string | null; role?: MembershipRole };
|
||||
user: { id: number; email: string; username: string | null; role?: MembershipRole };
|
||||
isUnpublished?: boolean;
|
||||
isOrg?: boolean;
|
||||
isOrgVerified?: boolean;
|
||||
hasSubteam?: true;
|
||||
organizationId?: number | null;
|
||||
},
|
||||
@@ -95,7 +105,14 @@ const createTeamAndAddUser = async (
|
||||
};
|
||||
data.metadata = {
|
||||
...(isUnpublished ? { requestedSlug: slug } : {}),
|
||||
...(isOrg ? { isOrganization: true } : {}),
|
||||
...(isOrg
|
||||
? {
|
||||
isOrganization: true,
|
||||
isOrganizationVerified: !!isOrgVerified,
|
||||
orgAutoAcceptEmail: user.email.split("@")[1],
|
||||
isOrganizationConfigured: false,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
data.slug = !isUnpublished ? slug : undefined;
|
||||
if (isOrg && hasSubteam) {
|
||||
@@ -135,7 +152,9 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
||||
schedulingType?: SchedulingType;
|
||||
teamEventTitle?: string;
|
||||
teamEventSlug?: string;
|
||||
teamEventLength?: number;
|
||||
isOrg?: boolean;
|
||||
isOrgVerified?: boolean;
|
||||
hasSubteam?: true;
|
||||
isUnpublished?: true;
|
||||
} = {}
|
||||
@@ -283,9 +302,10 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
||||
if (scenario.hasTeam) {
|
||||
const team = await createTeamAndAddUser(
|
||||
{
|
||||
user: { id: user.id, username: user.username, role: "OWNER" },
|
||||
user: { id: user.id, email: user.email, username: user.username, role: "OWNER" },
|
||||
isUnpublished: scenario.isUnpublished,
|
||||
isOrg: scenario.isOrg,
|
||||
isOrgVerified: scenario.isOrgVerified,
|
||||
hasSubteam: scenario.hasSubteam,
|
||||
organizationId: opts?.organizationId,
|
||||
},
|
||||
@@ -376,6 +396,15 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
||||
await prisma.user.delete({ where: { id } });
|
||||
store.users = store.users.filter((b) => b.id !== id);
|
||||
},
|
||||
set: async (email: string) => {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: { email },
|
||||
include: userIncludes,
|
||||
});
|
||||
const userFixture = createUserFixture(user, store.page);
|
||||
store.users.push(userFixture);
|
||||
return userFixture;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -400,7 +429,11 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||
eventTypes: user.eventTypes,
|
||||
routingForms: user.routingForms,
|
||||
self,
|
||||
apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page),
|
||||
apiLogin: async (password?: string) =>
|
||||
apiLogin({ ...(await self()), password: password || user.username }, store.page),
|
||||
/**
|
||||
* @deprecated use apiLogin instead
|
||||
*/
|
||||
login: async () => login({ ...(await self()), password: user.username }, store.page),
|
||||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
@@ -489,6 +522,7 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||
// ignores adding the worker-index after username
|
||||
useExactUsername?: boolean;
|
||||
roleInOrganization?: MembershipRole;
|
||||
schedule?: Schedule;
|
||||
};
|
||||
|
||||
// creates the actual user in the db.
|
||||
@@ -520,7 +554,7 @@ const createUser = (
|
||||
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
||||
availability: {
|
||||
createMany: {
|
||||
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
|
||||
data: getAvailabilityFromSchedule(opts?.schedule ?? DEFAULT_SCHEDULE),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -641,7 +675,7 @@ export async function apiLogin(
|
||||
|
||||
export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id">, page: Page) {
|
||||
await page.goto(`/event-types/${eventType?.id}?tabName=apps`);
|
||||
await page.locator("div > .ml-auto").first().click();
|
||||
await page.locator("[data-testid='app-switch']").first().click();
|
||||
await page.getByPlaceholder("Price").fill("100");
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type Prisma from "@prisma/client";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import type { Fixtures } from "./lib/fixtures";
|
||||
import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||
@@ -34,6 +38,95 @@ test.describe("Stripe integration", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("when enabling Stripe, credentialId is included", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
await page.goto("/apps/installed");
|
||||
|
||||
await user.getPaymentCredential();
|
||||
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
|
||||
await user.setupEventWithPrice(eventType);
|
||||
|
||||
// Need to wait for the DB to be updated with the metadata
|
||||
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
|
||||
|
||||
// Check event type metadata to see if credentialId is included
|
||||
const eventTypeMetadata = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
id: eventType.id,
|
||||
},
|
||||
select: {
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
|
||||
|
||||
const stripeAppMetadata = metadata?.apps?.stripe;
|
||||
|
||||
expect(stripeAppMetadata).toHaveProperty("credentialId");
|
||||
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
|
||||
});
|
||||
|
||||
test("when enabling Stripe, team credentialId is included", async ({ page, users }) => {
|
||||
const ownerObj = { username: "pro-user", name: "pro-user" };
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(ownerObj, {
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
});
|
||||
await owner.apiLogin();
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
const teamEvent = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto("/apps/stripe");
|
||||
|
||||
/** We start the Stripe flow */
|
||||
await Promise.all([
|
||||
page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"),
|
||||
page.click('[data-testid="install-app-button"]'),
|
||||
page.click('[data-testid="anything else"]'),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForURL("/apps/installed/payment?hl=stripe"),
|
||||
/** We skip filling Stripe forms (testing mode only) */
|
||||
page.click('[id="skip-account-app"]'),
|
||||
]);
|
||||
|
||||
await owner.setupEventWithPrice(teamEvent);
|
||||
|
||||
// Need to wait for the DB to be updated with the metadata
|
||||
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
|
||||
|
||||
// Check event type metadata to see if credentialId is included
|
||||
const eventTypeMetadata = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
id: teamEvent.id,
|
||||
},
|
||||
select: {
|
||||
metadata: true,
|
||||
},
|
||||
});
|
||||
|
||||
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
|
||||
|
||||
const stripeAppMetadata = metadata?.apps?.stripe;
|
||||
|
||||
expect(stripeAppMetadata).toHaveProperty("credentialId");
|
||||
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
|
||||
});
|
||||
|
||||
test("Can book a paid booking", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
|
||||
@@ -174,7 +267,7 @@ test.describe("Stripe integration", () => {
|
||||
await page.getByTestId("price-input-stripe").fill("200");
|
||||
|
||||
// Select currency in dropdown
|
||||
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("stripe-currency-select").click();
|
||||
await page.locator("#react-select-2-input").fill("mexi");
|
||||
await page.locator("#react-select-2-option-81").click();
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import type { API } from "mailhog";
|
||||
import mailhog from "mailhog";
|
||||
|
||||
import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
||||
import { createBookingsFixture } from "../fixtures/bookings";
|
||||
import { createClipboardFixture } from "../fixtures/clipboard";
|
||||
import { createEmbedsFixture } from "../fixtures/embeds";
|
||||
import { createOrgsFixture } from "../fixtures/orgs";
|
||||
import { createPaymentsFixture } from "../fixtures/payments";
|
||||
@@ -28,6 +30,7 @@ export interface Fixtures {
|
||||
emails?: API;
|
||||
routingForms: ReturnType<typeof createRoutingFormsFixture>;
|
||||
bookingPage: ReturnType<typeof createBookingPageFixture>;
|
||||
clipboard: ReturnType<typeof createClipboardFixture>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -85,6 +88,8 @@ export const test = base.extend<Fixtures>({
|
||||
const mailhogAPI = mailhog();
|
||||
await use(mailhogAPI);
|
||||
} else {
|
||||
//FIXME: Ideally we should error out here. If someone is running tests with mailhog disabled, they should be aware of it
|
||||
logger.warn("Mailhog is not enabled - Skipping Emails verification");
|
||||
await use(undefined);
|
||||
}
|
||||
},
|
||||
@@ -92,4 +97,8 @@ export const test = base.extend<Fixtures>({
|
||||
const bookingPage = createBookingPageFixture(page);
|
||||
await use(bookingPage);
|
||||
},
|
||||
clipboard: async ({ page }, use) => {
|
||||
const clipboard = createClipboardFixture(page);
|
||||
await use(clipboard);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { test } from "./fixtures";
|
||||
|
||||
export type RouteVariant = "future" | "legacy";
|
||||
|
||||
const routeVariants = ["future", "legacy"];
|
||||
|
||||
/**
|
||||
* Small wrapper around test.describe().
|
||||
* When using testbothFutureLegacyRoutes.describe() instead of test.describe(), this will run the specified
|
||||
* tests twice. One with the pages route, and one with the new app dir "future" route. It will also add the route variant
|
||||
* name to the test name for easier debugging.
|
||||
* Finally it also adds a parameter routeVariant to your testBothFutureAndLegacyRoutes.describe() callback, which
|
||||
* can be used to do any conditional rendering in the test for a specific route variant (should be as little
|
||||
* as possible).
|
||||
*
|
||||
* See apps/web/playwright/event-types.e2e.ts for an example.
|
||||
*/
|
||||
export const testBothFutureAndLegacyRoutes = {
|
||||
describe: (testName: string, testFn: (routeVariant: RouteVariant) => void) => {
|
||||
routeVariants.forEach((routeVariant) => {
|
||||
test.describe(`${testName} -- ${routeVariant}`, () => {
|
||||
if (routeVariant === "future") {
|
||||
test.beforeEach(({ context }) => {
|
||||
context.addCookies([
|
||||
{ name: "x-calcom-future-routes-override", value: "1", url: "http://localhost:3000" },
|
||||
]);
|
||||
});
|
||||
}
|
||||
testFn(routeVariant as RouteVariant);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Frame, Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { createHash } from "crypto";
|
||||
import EventEmitter from "events";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { createServer } from "http";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import type { API, Messages } from "mailhog";
|
||||
import { totp } from "otplib";
|
||||
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
@@ -278,3 +280,12 @@ export async function createUserWithSeatedEventAndAttendees(
|
||||
});
|
||||
return { user, eventType, booking };
|
||||
}
|
||||
|
||||
export function generateTotpCode(email: string) {
|
||||
const secret = createHash("md5")
|
||||
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
||||
.digest("hex");
|
||||
|
||||
totp.options = { step: 90 };
|
||||
return totp.generate(secret);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ test.describe("2FA Tests", async () => {
|
||||
const user = await users.create();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const userPassword = user.username!;
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
|
||||
// expects the home page for an authorized user
|
||||
await page.goto("/settings/security/two-factor-auth");
|
||||
@@ -94,7 +94,7 @@ test.describe("2FA Tests", async () => {
|
||||
const user = await users.create();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const userPassword = user.username!;
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
|
||||
// expects the home page for an authorized user
|
||||
await page.goto("/settings/security/two-factor-auth");
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { selectFirstAvailableTimeSlotNextMonth, bookTimeSlot } from "./lib/testUtils";
|
||||
import { localize } from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
@@ -69,15 +72,34 @@ test.describe("Managed Event Types tests", () => {
|
||||
await page.goto("/event-types");
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
await page.locator('[class$="control"]').filter({ hasText: "Select..." }).click();
|
||||
await page.getByTestId("assignment-dropdown").click();
|
||||
|
||||
await page.getByTestId(`select-option-${memberUser.id}`).click();
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
});
|
||||
|
||||
await adminUser.logout();
|
||||
await test.step("Managed event type can use Organizer's default app as location", async () => {
|
||||
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
|
||||
|
||||
await page.locator("#location-select").click();
|
||||
const optionText = (await localize("en"))("organizer_default_conferencing_app");
|
||||
await page.locator(`text=${optionText}`).click();
|
||||
await page.locator("[data-testid=update-eventtype]").click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
await gotoBookingPage(page);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Managed event type has locked fields for added member", async () => {
|
||||
await adminUser.logout();
|
||||
|
||||
// Coming back as member user to see if there is a managed event present after assignment
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
@@ -91,3 +113,9 @@ test.describe("Managed Event Types tests", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function gotoBookingPage(page: Page) {
|
||||
const previewLink = await page.getByTestId("preview-button").getAttribute("href");
|
||||
|
||||
await page.goto(previewLink ?? "");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { JSDOM } from "jsdom";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { API, Messages } from "mailhog";
|
||||
|
||||
import { getEmailsReceivedByUser } from "../lib/testUtils";
|
||||
|
||||
export async function expectInvitationEmailToBeReceived(
|
||||
page: Page,
|
||||
emails: API | undefined,
|
||||
userEmail: string,
|
||||
subject: string,
|
||||
returnLink?: string
|
||||
) {
|
||||
if (!emails) return null;
|
||||
// We need to wait for the email to go through, otherwise it will fail
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(5000);
|
||||
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
|
||||
expect(receivedEmails?.total).toBe(1);
|
||||
const [firstReceivedEmail] = (receivedEmails as Messages).items;
|
||||
expect(firstReceivedEmail.subject).toBe(subject);
|
||||
if (!returnLink) return;
|
||||
const dom = new JSDOM(firstReceivedEmail.html);
|
||||
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
|
||||
return anchor?.getAttribute("href");
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { generateTotpCode } from "../lib/testUtils";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
||||
test.afterAll(({ users, emails }) => {
|
||||
users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
function capitalize(text: string) {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test("should be able to create an organization and complete onboarding", async ({
|
||||
page,
|
||||
users,
|
||||
emails,
|
||||
}) => {
|
||||
const orgOwner = await users.create();
|
||||
const orgDomain = `${orgOwner.username}-org`;
|
||||
const orgName = capitalize(`${orgOwner.username}-org`);
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/new");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("Basic info", async () => {
|
||||
// Check required fields
|
||||
await page.locator("button[type=submit]").click();
|
||||
await expect(page.locator(".text-red-700")).toHaveCount(3);
|
||||
|
||||
// Happy path
|
||||
await page.locator("input[name=adminEmail]").fill(`john@${orgDomain}.com`);
|
||||
expect(await page.locator("input[name=name]").inputValue()).toEqual(orgName);
|
||||
expect(await page.locator("input[name=slug]").inputValue()).toEqual(orgDomain);
|
||||
await page.locator("button[type=submit]").click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check admin email about code verification
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
`john@${orgOwner.username}-org.com`,
|
||||
"Verify your email to create an organization"
|
||||
);
|
||||
|
||||
await test.step("Verification", async () => {
|
||||
// Code verification
|
||||
await expect(page.locator("#modal-title")).toBeVisible();
|
||||
await page.locator("input[name='2fa1']").fill(generateTotpCode(`john@${orgDomain}.com`));
|
||||
await page.locator("button:text('Verify')").click();
|
||||
|
||||
// Check admin email about DNS pending action
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
"admin@example.com",
|
||||
"New organization created: pending action"
|
||||
);
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/set-password");
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Admin password", async () => {
|
||||
// Check required fields
|
||||
await page.locator("button[type=submit]").click();
|
||||
await expect(page.locator(".text-red-700")).toHaveCount(3); // 3 password hints
|
||||
|
||||
// Happy path
|
||||
await page.locator("input[name='password']").fill("ADMIN_user2023$");
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/about");
|
||||
});
|
||||
|
||||
await test.step("About the organization", async () => {
|
||||
// Choosing an avatar
|
||||
await page.locator('button:text("Upload")').click();
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
await page.getByText("Choose a file...").click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
|
||||
await page.locator('button:text("Save")').click();
|
||||
|
||||
// About text
|
||||
await page.locator('textarea[name="about"]').fill("This is a testing org");
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/onboard-admins");
|
||||
});
|
||||
|
||||
await test.step("On-board administrators", async () => {
|
||||
// Required field
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Happy path
|
||||
await page.locator('textarea[name="emails"]').fill(`rick@${orgDomain}.com`);
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Check if invited admin received the invitation email
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
`rick@${orgDomain}.com`,
|
||||
`${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
|
||||
);
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/add-teams");
|
||||
});
|
||||
|
||||
await test.step("Create teams", async () => {
|
||||
// Initial state
|
||||
await expect(page.locator('input[name="teams.0.name"]')).toHaveCount(1);
|
||||
await expect(page.locator('button:text("Continue")')).toBeDisabled();
|
||||
|
||||
// Filling one team
|
||||
await page.locator('input[name="teams.0.name"]').fill("Marketing");
|
||||
await expect(page.locator('button:text("Continue")')).toBeEnabled();
|
||||
|
||||
// Adding another team
|
||||
await page.locator('button:text("Add a team")').click();
|
||||
await expect(page.locator('button:text("Continue")')).toBeDisabled();
|
||||
await expect(page.locator('input[name="teams.1.name"]')).toHaveCount(1);
|
||||
await page.locator('input[name="teams.1.name"]').fill("Sales");
|
||||
await expect(page.locator('button:text("Continue")')).toBeEnabled();
|
||||
|
||||
// Finishing the creation wizard
|
||||
await page.locator('button:text("Continue")').click();
|
||||
await page.waitForURL("/event-types");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async ({ users, emails, clipboard }) => {
|
||||
clipboard.reset();
|
||||
await users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
|
||||
const { team: org } = await orgOwner.getOrg();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (external user)", async () => {
|
||||
const invitedUserEmail = `rick@domain-${Date.now()}.com`;
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
// Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(1);
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!inviteLink) return null;
|
||||
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const newPage = await context.newPage();
|
||||
newPage.goto(inviteLink);
|
||||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await expect(newPage.locator(".text-red-700")).toHaveCount(3); // 3 password hints
|
||||
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await newPage.waitForURL("/getting-started?from=signup");
|
||||
await context.close();
|
||||
await newPage.close();
|
||||
|
||||
// Check newly invited member is not pending anymore
|
||||
await page.bringToFront();
|
||||
await page.goto("/settings/organizations/members");
|
||||
page.locator(`[data-testid="login-form"]`);
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
await test.step("To the organization by invite link", async () => {
|
||||
// Get the invite link
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
|
||||
const inviteLink = await clipboard.get();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const inviteLinkPage = await context.newPage();
|
||||
await inviteLinkPage.goto(inviteLink);
|
||||
await inviteLinkPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await expect(inviteLinkPage.locator(".text-red-700")).toHaveCount(4); // email + 3 password hints
|
||||
|
||||
// Happy path
|
||||
await inviteLinkPage.locator("input[name=email]").fill(`rick@domain-${Date.now()}.com`);
|
||||
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await inviteLinkPage.waitForURL("/getting-started");
|
||||
});
|
||||
});
|
||||
|
||||
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrg();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (internal user)", async () => {
|
||||
const invitedUserEmail = `rick@example.com`;
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`
|
||||
);
|
||||
|
||||
// Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -77,7 +77,7 @@ test.describe("Payment app", () => {
|
||||
|
||||
await page.goto(`event-types/${paymentEvent.id}?tabName=apps`);
|
||||
await page.locator("#event-type-form").getByRole("switch").click();
|
||||
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("stripe-currency-select").click();
|
||||
await page.getByTestId("select-option-usd").click();
|
||||
|
||||
await page.getByTestId("price-input-stripe").click();
|
||||
@@ -123,10 +123,10 @@ test.describe("Payment app", () => {
|
||||
await page.getByPlaceholder("Price").click();
|
||||
await page.getByPlaceholder("Price").fill("150");
|
||||
|
||||
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("paypal-currency-select").click();
|
||||
await page.locator("#react-select-2-option-13").click();
|
||||
|
||||
await page.locator(".mb-1 > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("paypal-payment-option-select").click();
|
||||
|
||||
await page.getByText("$MXNCurrencyMexican pesoPayment option").click();
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("UploadAvatar", async () => {
|
||||
test("can upload an image", async ({ page, users }) => {
|
||||
const user = await users.create({});
|
||||
await user.apiLogin();
|
||||
|
||||
await test.step("Can upload an initial picture", async () => {
|
||||
await page.goto("/settings/my-account/profile");
|
||||
|
||||
await page.getByTestId("open-upload-avatar-dialog").click();
|
||||
|
||||
const [fileChooser] = await Promise.all([
|
||||
// It is important to call waitForEvent before click to set up waiting.
|
||||
page.waitForEvent("filechooser"),
|
||||
// Opens the file chooser.
|
||||
page.getByTestId("open-upload-image-filechooser").click(),
|
||||
]);
|
||||
|
||||
await fileChooser.setFiles(`${path.dirname(__filename)}/../fixtures/cal.png`);
|
||||
|
||||
await page.getByTestId("upload-avatar").click();
|
||||
|
||||
await page.locator("input[name='name']").fill(user.email);
|
||||
|
||||
await page.getByText("Update").click();
|
||||
await page.waitForSelector("text=Settings updated successfully");
|
||||
|
||||
const response = await prisma.avatar.findUniqueOrThrow({
|
||||
where: {
|
||||
teamId_userId: {
|
||||
userId: user.id,
|
||||
teamId: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// todo: remove this; ideally the organization-avatar is updated the moment
|
||||
// 'Settings updated succesfully' is saved.
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await expect(await page.getByTestId("organization-avatar").innerHTML()).toContain(response.objectKey);
|
||||
|
||||
const urlResponse = await page.request.get(`/api/avatar/${response.objectKey}.png`, {
|
||||
maxRedirects: 0,
|
||||
});
|
||||
|
||||
await expect(urlResponse?.status()).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { JSDOM } from "jsdom";
|
||||
import type { API, Messages } from "mailhog";
|
||||
|
||||
import { getEmailsReceivedByUser } from "../lib/testUtils";
|
||||
|
||||
export async function expectInvitationEmailToBeReceived(
|
||||
page: Page,
|
||||
emails: API | undefined,
|
||||
userEmail: string,
|
||||
subject: string,
|
||||
returnLink?: string
|
||||
) {
|
||||
if (!emails) return null;
|
||||
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(10000);
|
||||
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
|
||||
expect(receivedEmails?.total).toBe(1);
|
||||
|
||||
const [firstReceivedEmail] = (receivedEmails as Messages).items;
|
||||
|
||||
expect(firstReceivedEmail.subject).toBe(subject);
|
||||
if (!returnLink) return;
|
||||
const dom = new JSDOM(firstReceivedEmail.html);
|
||||
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
|
||||
return anchor?.getAttribute("href");
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { localize } from "../lib/testUtils";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async ({ users, emails, clipboard }) => {
|
||||
clipboard.reset();
|
||||
await users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Team", () => {
|
||||
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
|
||||
const t = await localize("en");
|
||||
const teamOwner = await users.create(undefined, { hasTeam: true });
|
||||
const { team } = await teamOwner.getFirstTeam();
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the team by email (external user)", async () => {
|
||||
const invitedUserEmail = `rick_${Date.now()}@domain-${Date.now()}.com`;
|
||||
await page.locator(`button:text("${t("add")}")`).click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator(`button:text("${t("send_invite")}")`).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${team.name}'s admin invited you to join the team ${team.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
//Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(1);
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!inviteLink) return null;
|
||||
|
||||
// Follow invite link to new window
|
||||
const context = await browser.newContext();
|
||||
const newPage = await context.newPage();
|
||||
await newPage.goto(inviteLink);
|
||||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await expect(newPage.locator('[data-testid="hint-error"]')).toHaveCount(3);
|
||||
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await newPage.waitForURL("/getting-started?from=signup");
|
||||
await newPage.close();
|
||||
await context.close();
|
||||
|
||||
// Check newly invited member is not pending anymore
|
||||
await page.bringToFront();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
await test.step("To the team by invite link", async () => {
|
||||
const user = await users.create({
|
||||
email: `user-invite-${Date.now()}@domain.com`,
|
||||
password: "P4ssw0rd!",
|
||||
});
|
||||
await page.locator(`button:text("${t("add")}")`).click();
|
||||
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
|
||||
const inviteLink = await clipboard.get();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const context = await browser.newContext();
|
||||
const inviteLinkPage = await context.newPage();
|
||||
await inviteLinkPage.goto(inviteLink);
|
||||
await inviteLinkPage.waitForLoadState("domcontentloaded");
|
||||
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await expect(inviteLinkPage.locator('[data-testid="field-error"]')).toHaveCount(2);
|
||||
|
||||
await inviteLinkPage.locator("input[name=email]").fill(user.email);
|
||||
await inviteLinkPage.locator("input[name=password]").fill(user.username || "P4ssw0rd!");
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
|
||||
await inviteLinkPage.waitForURL(`${WEBAPP_URL}/teams**`);
|
||||
});
|
||||
});
|
||||
|
||||
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
|
||||
const t = await localize("en");
|
||||
const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true });
|
||||
const { team } = await teamOwner.getFirstTeam();
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (internal user)", async () => {
|
||||
const invitedUserEmail = `rick@example.com`;
|
||||
await page.locator(`button:text("${t("add")}")`).click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator(`button:text("${t("send_invite")}")`).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -134,7 +134,7 @@ test.describe("Teams - NonOrg", () => {
|
||||
// Anyone of the teammates could be the Host of the booking.
|
||||
const chosenUser = await page.getByTestId("booking-host-name").textContent();
|
||||
expect(chosenUser).not.toBeNull();
|
||||
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
||||
expect(teamMatesObj.concat([{ name: ownerObj.name }]).some(({ name }) => name === chosenUser)).toBe(true);
|
||||
// TODO: Assert whether the user received an email
|
||||
});
|
||||
|
||||
@@ -351,7 +351,7 @@ test.describe("Teams - Org", () => {
|
||||
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
|
||||
await expect(page.locator('[data-testid="404-page"]')).toBeVisible();
|
||||
await expect(page.locator("text=This page could not be found")).toBeVisible();
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
@@ -370,7 +370,7 @@ test.describe("Teams - Org", () => {
|
||||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
|
||||
// All the teammates should be in the booking
|
||||
for (const teammate of teamMatesObj) {
|
||||
for (const teammate of teamMatesObj.concat([{ name: owner.name || "" }])) {
|
||||
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
|
||||
}
|
||||
}
|
||||
@@ -412,7 +412,7 @@ test.describe("Teams - Org", () => {
|
||||
// Anyone of the teammates could be the Host of the booking.
|
||||
const chosenUser = await page.getByTestId("booking-host-name").textContent();
|
||||
expect(chosenUser).not.toBeNull();
|
||||
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true);
|
||||
expect(teamMatesObj.concat([{ name: ownerObj.name }]).some(({ name }) => name === chosenUser)).toBe(true);
|
||||
// TODO: Assert whether the user received an email
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,12 +103,11 @@ test.describe("BOOKING_CREATED", async () => {
|
||||
body.payload.videoCallData = dynamic;
|
||||
body.payload.appsStatus = dynamic;
|
||||
body.payload.metadata.videoCallUrl = dynamic;
|
||||
|
||||
expect(body).toMatchObject({
|
||||
triggerEvent: "BOOKING_CREATED",
|
||||
createdAt: "[redacted/dynamic]",
|
||||
payload: {
|
||||
type: "30 min",
|
||||
type: "30-min",
|
||||
title: "30 min between Nameless and Test Testson",
|
||||
description: "",
|
||||
additionalNotes: "",
|
||||
@@ -236,7 +235,7 @@ test.describe("BOOKING_REJECTED", async () => {
|
||||
triggerEvent: "BOOKING_REJECTED",
|
||||
createdAt: "[redacted/dynamic]",
|
||||
payload: {
|
||||
type: "Opt in",
|
||||
type: "opt-in",
|
||||
title: "Opt in between Nameless and Test Testson",
|
||||
customInputs: {},
|
||||
startTime: "[redacted/dynamic]",
|
||||
@@ -357,7 +356,7 @@ test.describe("BOOKING_REQUESTED", async () => {
|
||||
triggerEvent: "BOOKING_REQUESTED",
|
||||
createdAt: "[redacted/dynamic]",
|
||||
payload: {
|
||||
type: "Opt in",
|
||||
type: "opt-in",
|
||||
title: "Opt in between Nameless and Test Testson",
|
||||
customInputs: {},
|
||||
startTime: "[redacted/dynamic]",
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe("Workflow tests", () => {
|
||||
async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
const [eventType] = user.eventTypes;
|
||||
await user.login();
|
||||
await user.apiLogin();
|
||||
await page.goto(`/workflows`);
|
||||
|
||||
await page.click('[data-testid="create-button"]');
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"next_step": "تخطي الخطوة",
|
||||
"prev_step": "الخطوة السابقة",
|
||||
"install": "تثبيت",
|
||||
"install_paid_app": "اشتراك",
|
||||
"installed": "تم التثبيت",
|
||||
"active_install_one": "{{count}} تثبيت نشط",
|
||||
"active_install_other": "{{count}} تثبيت نشط",
|
||||
@@ -1097,6 +1098,7 @@
|
||||
"developer_documentation": "مستندات المطور",
|
||||
"get_in_touch": "تواصل معنا",
|
||||
"contact_support": "الاتصال بالدعم",
|
||||
"community_support": "الدعم المجتمعي",
|
||||
"feedback": "الملاحظات",
|
||||
"submitted_feedback": "نشكرك على ملاحظاتك!",
|
||||
"feedback_error": "حدث خطأ عند إرسال الملاحظات",
|
||||
|
||||
@@ -268,6 +268,7 @@
|
||||
"set_availability": "Nastavte, jak jste dostupní",
|
||||
"availability_settings": "Nastavení dostupnosti",
|
||||
"continue_without_calendar": "Pokračovat bez kalendáře",
|
||||
"continue_with": "Pokračovat přes {{appName}}",
|
||||
"connect_your_calendar": "Připojit svůj kalendář",
|
||||
"connect_your_video_app": "Propojte své video aplikace",
|
||||
"connect_your_video_app_instructions": "Propojte své video aplikace a používejte je ve svých typech událostí.",
|
||||
@@ -288,6 +289,8 @@
|
||||
"when": "Kdy",
|
||||
"where": "Kde",
|
||||
"add_to_calendar": "Přidat do kalendáře",
|
||||
"add_to_calendar_description": "Vyberte, kam se mají přidávat události, pokud jste zarezervováni.",
|
||||
"add_events_to": "Přidat události do",
|
||||
"add_another_calendar": "Přidat další kalendář",
|
||||
"other": "Ostatní",
|
||||
"email_sign_in_subject": "Váš přihlašovací odkaz pro {{appName}}",
|
||||
@@ -422,6 +425,7 @@
|
||||
"booking_created": "Rezervace vytvořena",
|
||||
"booking_rejected": "Rezervace byla zamítnuta",
|
||||
"booking_requested": "Váš požadavek na rezervaci byl úspěšně odeslán",
|
||||
"booking_payment_initiated": "Platba za rezervaci zahájena",
|
||||
"meeting_ended": "Schůzka skončila",
|
||||
"form_submitted": "Formulář byl odeslán",
|
||||
"booking_paid": "Rezervace uhrazena",
|
||||
@@ -456,6 +460,7 @@
|
||||
"no_event_types_have_been_setup": "Tento uživatel zatím nezaložil žádné typy událostí.",
|
||||
"edit_logo": "Upravit logo",
|
||||
"upload_a_logo": "Nahrát logo",
|
||||
"upload_logo": "Nahrát logo",
|
||||
"remove_logo": "Odstranit logo",
|
||||
"enable": "Povolit",
|
||||
"code": "Kód",
|
||||
@@ -568,6 +573,7 @@
|
||||
"your_team_name": "Název vašeho týmu",
|
||||
"team_updated_successfully": "Tým byl úspěšně aktualizován",
|
||||
"your_team_updated_successfully": "Váš tým byl úspěšně aktualizován.",
|
||||
"your_org_updated_successfully": "Vaše organizace byla aktualizována.",
|
||||
"about": "O aplikaci",
|
||||
"team_description": "Pár vět o vašem týmu. Objeví se na URL stránce vašeho týmu.",
|
||||
"org_description": "Několik vět o vaší organizaci. Zobrazí se na adrese URL vaší organizace.",
|
||||
@@ -599,6 +605,7 @@
|
||||
"hide_book_a_team_member": "Skrýt tlačítko Rezervovat člena týmu",
|
||||
"hide_book_a_team_member_description": "Skrýt tlačítko Rezervovat člena týmu na veřejných stránkách.",
|
||||
"danger_zone": "Nebezpečná zóna",
|
||||
"account_deletion_cannot_be_undone": "Pozor. Odstranění účtu nelze vrátit.",
|
||||
"back": "Zpět",
|
||||
"cancel": "Zrušit",
|
||||
"cancel_all_remaining": "Zrušit vše zbývající",
|
||||
@@ -688,6 +695,7 @@
|
||||
"people": "Lidé",
|
||||
"your_email": "Váš e-mail",
|
||||
"change_avatar": "Změnit avatar",
|
||||
"upload_avatar": "Nahrát avatar",
|
||||
"language": "Jazyk",
|
||||
"timezone": "Časová zóna",
|
||||
"first_day_of_week": "První den v týdnu",
|
||||
@@ -778,6 +786,7 @@
|
||||
"disable_guests": "Zakázat hosty",
|
||||
"disable_guests_description": "Zakázat přidávání dalších hostů během rezervace.",
|
||||
"private_link": "Vygenerovat soukromou adresu URL",
|
||||
"enable_private_url": "Zapnout soukromou adresu URL",
|
||||
"private_link_label": "Soukromý odkaz",
|
||||
"private_link_hint": "Váš soukromý odkaz se po každém použití obnoví",
|
||||
"copy_private_link": "Zkopírovat soukromý odkaz",
|
||||
@@ -840,6 +849,7 @@
|
||||
"next_step": "Přeskočit krok",
|
||||
"prev_step": "Předchozí krok",
|
||||
"install": "Nainstalovat",
|
||||
"install_paid_app": "Odebírat",
|
||||
"installed": "Nainstalováno",
|
||||
"active_install_one": "Aktivní instalace: {{count}}",
|
||||
"active_install_other": "Aktivní instalace: {{count}}",
|
||||
@@ -1088,6 +1098,7 @@
|
||||
"developer_documentation": "Dokumentace vývojáře",
|
||||
"get_in_touch": "Kontaktujte nás",
|
||||
"contact_support": "Kontaktujte podporu",
|
||||
"community_support": "Podpora komunity",
|
||||
"feedback": "Zpětná vazba",
|
||||
"submitted_feedback": "Děkujeme za vaši zpětnou vazbu!",
|
||||
"feedback_error": "Chyba při odesílání zpětné vazby",
|
||||
@@ -1213,6 +1224,7 @@
|
||||
"organizer_name_variable": "Jméno organizátora",
|
||||
"app_upgrade_description": "Pokud chcete použít tuto funkci, musíte provést aktualizaci na účet Pro.",
|
||||
"invalid_number": "Neplatné telefonní číslo",
|
||||
"invalid_url_error_message": "Neplatná adresa URL pro {{label}}. Příklad URL: {{sampleUrl}}",
|
||||
"navigate": "Navigace",
|
||||
"open": "Otevřít",
|
||||
"close": "Zavřít",
|
||||
@@ -1276,6 +1288,7 @@
|
||||
"personal_cal_url": "Moje osobní adresa URL {{appName}}",
|
||||
"bio_hint": "Několik vět o vás. Obsah se zobrazí se na vaší osobní stránce URL.",
|
||||
"user_has_no_bio": "Tento uživatel zatím nepřidal životopis.",
|
||||
"bio": "Bio",
|
||||
"delete_account_modal_title": "Odstranit účet",
|
||||
"confirm_delete_account_modal": "Opravdu chcete odstranit svůj účet {{appName}}?",
|
||||
"delete_my_account": "Odstranit můj účet",
|
||||
@@ -1286,6 +1299,7 @@
|
||||
"select_calendars": "Vyberte kalendáře, u kterých chcete kontrolovat konflikty v zájmu prevence dvojích rezervací.",
|
||||
"check_for_conflicts": "Zkontrolovat konflikty",
|
||||
"view_recordings": "Zobrazit nahrávky",
|
||||
"check_for_recordings": "Zkontrolovat nahrávky",
|
||||
"adding_events_to": "Přidání událostí do:",
|
||||
"follow_system_preferences": "Řídit se předvolbami systému",
|
||||
"custom_brand_colors": "Vlastní barvy značky",
|
||||
@@ -1530,6 +1544,7 @@
|
||||
"problem_registering_domain": "Při registraci subdomény došlo k problému, zkuste to prosím znovu nebo kontaktujte správce",
|
||||
"team_publish": "Zveřejnit tým",
|
||||
"number_text_notifications": "Telefonní číslo (SMS oznámení)",
|
||||
"number_sms_notifications": "Telefonní číslo (SMS oznámení)",
|
||||
"attendee_email_variable": "E-mail účastníka",
|
||||
"attendee_email_info": "E-mail osoby provádějící rezervaci",
|
||||
"kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...",
|
||||
@@ -1594,6 +1609,7 @@
|
||||
"options": "Možnosti",
|
||||
"enter_option": "Zadejte možnost {{index}}",
|
||||
"add_an_option": "Přidat možnost",
|
||||
"location_already_exists": "Toto místo již existuje. Vyberte prosím nové místo",
|
||||
"radio": "Přepínač",
|
||||
"google_meet_warning": "Abyste mohli používat službu Google Meet, musíte jako cílový kalendář nastavit Kalendář Google",
|
||||
"individual": "Jedinec",
|
||||
@@ -1613,6 +1629,7 @@
|
||||
"date_overrides_mark_all_day_unavailable_other": "Zadat nedostupnost ve vybraných datech",
|
||||
"date_overrides_add_btn": "Přidat změnu",
|
||||
"date_overrides_update_btn": "Aktualizovat změnu",
|
||||
"date_successfully_added": "Přidána změna dnů",
|
||||
"event_type_duplicate_copy_text": "{{slug}} – kopie",
|
||||
"set_as_default": "Nastavit jako výchozí",
|
||||
"hide_eventtype_details": "Skrýt podrobnosti o typu události",
|
||||
@@ -1639,6 +1656,7 @@
|
||||
"minimum_round_robin_hosts_count": "Počet hostitelů, kteří se musí zúčastnit",
|
||||
"hosts": "Hostitelé",
|
||||
"upgrade_to_enable_feature": "K povolení této funkce je třeba vytvořit tým. Tým vytvoříte kliknutím.",
|
||||
"orgs_upgrade_to_enable_feature": "Pokud chcete zapnout tuto funkci, musíte upgradovat na náš tarif Enterprise.",
|
||||
"new_attendee": "Nový účastník",
|
||||
"awaiting_approval": "Čeká na schválení",
|
||||
"requires_google_calendar": "Tato aplikace vyžaduje připojení ke Kalendáři Google",
|
||||
@@ -1743,6 +1761,7 @@
|
||||
"show_on_booking_page": "Zobrazit na stránce rezervace",
|
||||
"get_started_zapier_templates": "Začněte používat šablony Zapier",
|
||||
"team_is_unpublished": "Tým {{team}} není zveřejněn",
|
||||
"org_is_unpublished_description": "Tento odkaz organizace není v současné době k dispozici. Kontaktujte prosím vlastníka organizace nebo ho požádejte o jeho zveřejnění.",
|
||||
"team_is_unpublished_description": "Tento odkaz subjektu ({{entity}}) není v současné době k dispozici. Kontaktujte prosím vlastníka subjektu ({{entity}}) nebo ho požádejte o jeho zveřejnění.",
|
||||
"team_member": "Člen týmu",
|
||||
"a_routing_form": "Směrovací formulář",
|
||||
@@ -1877,6 +1896,7 @@
|
||||
"edit_invite_link": "Upravit nastavení odkazu",
|
||||
"invite_link_copied": "Odkaz pozvánky byl zkopírován",
|
||||
"invite_link_deleted": "Odkaz pozvánky byl odstraněn",
|
||||
"api_key_deleted": "Klíč API odstraněn",
|
||||
"invite_link_updated": "Nastavení odkazu pozvánky bylo uloženo",
|
||||
"link_expires_after": "Platnost odkazů vyprší za...",
|
||||
"one_day": "1 den",
|
||||
@@ -2009,7 +2029,13 @@
|
||||
"attendee_last_name_variable": "Příjmení účastníka",
|
||||
"attendee_first_name_info": "Jméno rezervující osoby",
|
||||
"attendee_last_name_info": "Příjmení rezervující osoby",
|
||||
"your_monthly_digest": "Váš měsíční přehled",
|
||||
"member_name": "Jméno člena",
|
||||
"most_popular_events": "Nejoblíbenější události",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Zde je přehled oblíbených událostí vašeho týmu {{teamName}} za posledních 30 dní",
|
||||
"me": "Já",
|
||||
"monthly_digest_email": "E-mail s měsíčním přehledem",
|
||||
"monthly_digest_email_for_teams": "Měsíční e-mail s přehledem pro týmy",
|
||||
"verify_team_tooltip": "Proveďte ověření svého týmu a zapněte odesílání zpráv účastníkům",
|
||||
"member_removed": "Člen odstraněn",
|
||||
"my_availability": "Moje dostupnost",
|
||||
@@ -2039,13 +2065,41 @@
|
||||
"team_no_event_types": "Tento tým nemá žádné typy událostí",
|
||||
"seat_options_doesnt_multiple_durations": "Volba místa nepodporuje více dob trvání",
|
||||
"include_calendar_event": "Zahrnout událost kalendáře",
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Nedávno přidáno",
|
||||
"no_members_found": "Nenalezeni žádní členové",
|
||||
"event_setup_length_error": "Nastavení události: Doba trvání musí být alespoň 1 minuta.",
|
||||
"availability_schedules": "Plány dostupnosti",
|
||||
"unauthorized": "Neautorizováno",
|
||||
"access_cal_account": "{{clientName}} žádá o přístup k vašemu účtu {{appName}}",
|
||||
"select_account_team": "Vyberte účet nebo tým",
|
||||
"allow_client_to": "To umožní klientovi {{clientName}}",
|
||||
"associate_with_cal_account": "Přiřadit vám vaše osobní údaje z klienta {{clientName}}",
|
||||
"see_personal_info": "Zobrazit vaše osobní údaje, včetně všech osobních údajů, které jste zveřejnili",
|
||||
"see_primary_email_address": "Zobrazit vaši primární e-mailovou adresu",
|
||||
"connect_installed_apps": "Připojit se k vašim nainstalovaným aplikacím",
|
||||
"access_event_type": "Číst, upravovat a odstraňovat vaše typy událostí",
|
||||
"access_availability": "Číst, upravovat a odstraňovat vaši dostupnost",
|
||||
"access_bookings": "Číst, upravovat a odstraňovat rezervace",
|
||||
"allow_client_to_do": "Chcete povolit klientovi {{clientName}} provádět uvedené akce?",
|
||||
"oauth_access_information": "Kliknutím na tlačítko Povolit této aplikaci umožníte používat vaše údaje v souladu s podmínkami služby a zásadami ochrany osobních údajů. Přístup k aplikaci {{appName}} můžete odebrat přes App Store.",
|
||||
"allow": "Povolit",
|
||||
"view_only_edit_availability_not_onboarded": "Tento uživatel ještě nedokončil onboarding. Dokud nedokončí onboarding, dostupnost nebude možné nastavit.",
|
||||
"view_only_edit_availability": "Právě máte zobrazenou dostupnost tohoto uživatele. Upravovat lze pouze vlastní dostupnost.",
|
||||
"you_can_override_calendar_in_advanced_tab": "Tuto možnost můžete zrušit pro každou událost zvlášť v pokročilém nastavení jednotlivých typů událostí.",
|
||||
"edit_users_availability": "Upravte dostupnost uživatele: {{username}}",
|
||||
"resend_invitation": "Znovu odeslat pozvánku",
|
||||
"invitation_resent": "Pozvánka byla odeslána znovu.",
|
||||
"add_client": "Přidat klienta",
|
||||
"copy_client_secret_info": "Po zkopírování již nebude možné tajný klíč zobrazit",
|
||||
"add_new_client": "Přidat nového klienta",
|
||||
"this_app_is_not_setup_already": "Tato aplikace ještě nebyla nastavena",
|
||||
"as_csv": "jako CSV",
|
||||
"overlay_my_calendar": "Překryv mého kalendáře",
|
||||
"overlay_my_calendar_toc": "Připojením ke svému kalendáři přijímáte naše zásady ochrany osobních údajů a podmínky používání. Přístup můžete kdykoli odvolat.",
|
||||
"view_overlay_calendar_events": "Zobrazení událostí v kalendáři, aby se zabránilo kolidujícím rezervacím.",
|
||||
"lock_timezone_toggle_on_booking_page": "Uzamčení časového pásma na stránce rezervace",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Uzamčení časového pásma na stránce rezervace (užitečné pro osobní události).",
|
||||
"extensive_whitelabeling": "Vyhrazená podpora zaškolovací a inženýrská podpora",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -717,6 +717,7 @@
|
||||
"next_step": "Spring trin over",
|
||||
"prev_step": "Forrige trin",
|
||||
"install": "Installér",
|
||||
"install_paid_app": "Abonnér",
|
||||
"installed": "Installeret",
|
||||
"active_install_one": "{{count}} aktiv installation",
|
||||
"active_install_other": "{{count}} aktive installationer",
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"next_step": "Schritt überspringen",
|
||||
"prev_step": "Vorheriger Schritt",
|
||||
"install": "Installieren",
|
||||
"install_paid_app": "Abonnieren",
|
||||
"installed": "Installiert",
|
||||
"active_install_one": "{{count}} aktive Installation",
|
||||
"active_install_other": "{{count}} aktive Installationen",
|
||||
@@ -1097,6 +1098,7 @@
|
||||
"developer_documentation": "Entwickler-Dokumentation",
|
||||
"get_in_touch": "Kontakt aufnehmen",
|
||||
"contact_support": "Support kontaktieren",
|
||||
"community_support": "Community Support",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Vielen Dank für Ihr Feedback!",
|
||||
"feedback_error": "Fehler beim Senden des Feedbacks",
|
||||
@@ -2080,14 +2082,24 @@
|
||||
"access_availability": "Lesen, Bearbeiten, Löschen Ihrer Verfügbarkeiten",
|
||||
"access_bookings": "Lesen, Bearbeiten, Löschen Ihrer Termine",
|
||||
"allow_client_to_do": "{{clientName}} zulassen, dies zu tun?",
|
||||
"oauth_access_information": "Indem Sie auf „Erlauben“ klicken, erlauben Sie dieser App, Ihre Informationen gemäß ihrer Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können den Zugriff im {{appName}} App Store aufheben.",
|
||||
"allow": "Zulassen",
|
||||
"view_only_edit_availability_not_onboarded": "Dieser Benutzer hat das Onboarding noch nicht abgeschlossen. Sie können seine Verfügbarkeit erst festlegen, wenn er das Onboarding abgeschlossen hat.",
|
||||
"view_only_edit_availability": "Sie sehen die Verfügbarkeit dieses Benutzers. Sie können nur Ihre eigene Verfügbarkeit bearbeiten.",
|
||||
"you_can_override_calendar_in_advanced_tab": "Sie können dies in den erweiterten Einstellungen pro Termin für jeden Termintyp überschreiben.",
|
||||
"edit_users_availability": "Benutzerverfügbarkeit bearbeiten: {{username}}",
|
||||
"resend_invitation": "Einladung erneut senden",
|
||||
"invitation_resent": "Die Einladung wurde erneut gesendet.",
|
||||
"add_client": "Kunde hinzufügen",
|
||||
"copy_client_secret_info": "Nach dem Kopieren des Geheimnisses können Sie es nicht mehr ansehen",
|
||||
"add_new_client": "Neuen Kunden hinzufügen",
|
||||
"this_app_is_not_setup_already": "Diese App wurde noch nicht eingerichtet",
|
||||
"as_csv": "als CSV",
|
||||
"overlay_my_calendar": "Meinen Kalender überlagern",
|
||||
"overlay_my_calendar_toc": "Durch das Verbinden mit Ihrem Kalender akzeptieren Sie unsere Datenschutzerklärung und Nutzungsbedingungen. Sie können den Zugriff jederzeit widerrufen.",
|
||||
"view_overlay_calendar_events": "Sehen Sie sich Ihre Kalendertermine an, um Buchungskonflikte zu vermeiden.",
|
||||
"lock_timezone_toggle_on_booking_page": "Zeitzone auf der Buchungsseite sperren",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Um die Zeitzone auf der Buchungsseite zu sperren, nützlich für Termine in Person.",
|
||||
"extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -56,6 +56,17 @@
|
||||
"a_refund_failed": "A refund failed",
|
||||
"awaiting_payment_subject": "Awaiting Payment: {{title}} on {{date}}",
|
||||
"meeting_awaiting_payment": "Your meeting is awaiting payment",
|
||||
"dark_theme_contrast_error":"Dark Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible.",
|
||||
"light_theme_contrast_error":"Light Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible.",
|
||||
"payment_not_created_error": "Payment could not be created",
|
||||
"couldnt_charge_card_error": "Could not charge card for Payment",
|
||||
"no_available_users_found_error": "No available users found. Could you try another time slot?",
|
||||
"request_body_end_time_internal_error": "Internal Error. Request body does not contain end time",
|
||||
"create_calendar_event_error": "Unable to create Calendar event in Organizer's calendar",
|
||||
"update_calendar_event_error": "Unable to update Calendar event.",
|
||||
"delete_calendar_event_error": "Unable to delete Calendar event.",
|
||||
"already_signed_up_for_this_booking_error": "You are already signed up for this booking.",
|
||||
"hosts_unavailable_for_booking": "Some of the hosts are unavailable for booking.",
|
||||
"help": "Help",
|
||||
"price": "Price",
|
||||
"paid": "Paid",
|
||||
@@ -606,6 +617,7 @@
|
||||
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
|
||||
"danger_zone": "Danger zone",
|
||||
"account_deletion_cannot_be_undone":"Be Careful. Account deletion cannot be undone.",
|
||||
"team_deletion_cannot_be_undone":"Be Careful. Team deletion cannot be undone",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"cancel_all_remaining": "Cancel all remaining",
|
||||
@@ -849,6 +861,8 @@
|
||||
"next_step": "Skip step",
|
||||
"prev_step": "Prev step",
|
||||
"install": "Install",
|
||||
"install_paid_app": "Subscribe",
|
||||
"start_paid_trial": "Start free Trial",
|
||||
"installed": "Installed",
|
||||
"active_install_one": "{{count}} active install",
|
||||
"active_install_other": "{{count}} active installs",
|
||||
@@ -1097,6 +1111,8 @@
|
||||
"developer_documentation": "Developer Documentation",
|
||||
"get_in_touch": "Get in touch",
|
||||
"contact_support": "Contact Support",
|
||||
"premium_support": "Premium Support",
|
||||
"community_support": "Community Support",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Thank you for your feedback!",
|
||||
"feedback_error": "Error sending feedback",
|
||||
@@ -1361,6 +1377,7 @@
|
||||
"event_name_info": "The event type name",
|
||||
"event_date_info": "The event date",
|
||||
"event_time_info": "The event start time",
|
||||
"event_type_not_found": "EventType not Found",
|
||||
"location_info": "The location of the event",
|
||||
"additional_notes_info": "The additional notes of booking",
|
||||
"attendee_name_info": "The person booking's name",
|
||||
@@ -1401,6 +1418,7 @@
|
||||
"slot_length": "Slot length",
|
||||
"booking_appearance": "Booking Appearance",
|
||||
"appearance_team_description": "Manage settings for your team's booking appearance",
|
||||
"appearance_org_description": "Manage settings for your organization's booking appearance",
|
||||
"only_owner_change": "Only the owner of this team can make changes to the team's booking ",
|
||||
"team_disable_cal_branding_description": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}'",
|
||||
"invited_by_team": "{{teamName}} has invited you to join their team as a {{role}}",
|
||||
@@ -2096,8 +2114,14 @@
|
||||
"overlay_my_calendar":"Overlay my calendar",
|
||||
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
|
||||
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
|
||||
"troubleshooting":"Troubleshooting",
|
||||
"calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts",
|
||||
"availabilty_schedules":"Availability schedules",
|
||||
"manage_calendars":"Manage calendars",
|
||||
"manage_availability_schedules":"Manage availability schedules",
|
||||
"lock_timezone_toggle_on_booking_page": "Lock timezone on booking page",
|
||||
"description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.",
|
||||
"install_calendar":"Install Calendar",
|
||||
"branded_subdomain": "Branded Subdomain",
|
||||
"branded_subdomain_description": "Get your own branded subdomain, such as acme.cal.com",
|
||||
"org_insights": "Organization-wide Insights",
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"next_step": "Saltar paso",
|
||||
"prev_step": "Paso anterior",
|
||||
"install": "Instalar",
|
||||
"install_paid_app": "Suscribirse",
|
||||
"installed": "Instalado",
|
||||
"active_install_one": "{{count}} instalación activa",
|
||||
"active_install_other": "{{count}} instalaciones activas",
|
||||
@@ -1097,6 +1098,7 @@
|
||||
"developer_documentation": "Documentación del desarrollador",
|
||||
"get_in_touch": "Póngase en contacto",
|
||||
"contact_support": "Contactar con Soporte",
|
||||
"community_support": "Soporte comunitario",
|
||||
"feedback": "Comentarios",
|
||||
"submitted_feedback": "¡Gracias por sus comentarios!",
|
||||
"feedback_error": "Error al enviar comentarios",
|
||||
|
||||
@@ -506,6 +506,7 @@
|
||||
"next_step": "Saltatu pausoa",
|
||||
"prev_step": "Aurreko pausoa",
|
||||
"install": "Instalatu",
|
||||
"install_paid_app": "Harpidetu",
|
||||
"installed": "Instalatua",
|
||||
"disconnect": "Deskonektatu",
|
||||
"automation": "Automatizazioa",
|
||||
|
||||
@@ -289,6 +289,8 @@
|
||||
"when": "Quand",
|
||||
"where": "Où",
|
||||
"add_to_calendar": "Ajouter au calendrier",
|
||||
"add_to_calendar_description": "Sélectionnez l'endroit où ajouter des événements lorsque vous êtes réservé.",
|
||||
"add_events_to": "Ajouter les événements à",
|
||||
"add_another_calendar": "Ajouter un autre calendrier",
|
||||
"other": "Autre",
|
||||
"email_sign_in_subject": "Votre lien de connexion pour {{appName}}",
|
||||
@@ -407,7 +409,7 @@
|
||||
"automatically_adjust_theme": "Ajuster automatiquement l'apparence en fonction des préférences de l'invité",
|
||||
"user_dynamic_booking_disabled": "Certains utilisateurs du groupe ont actuellement désactivé les réservations de groupe dynamiques",
|
||||
"allow_dynamic_booking_tooltip": "Les liens de réservation de groupe peuvent être créés dynamiquement en ajoutant plusieurs noms d'utilisateur séparés par un « + ». Exemple : « {{appName}}/bailey+peer ».",
|
||||
"allow_dynamic_booking": "Autoriser les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques",
|
||||
"allow_dynamic_booking": "Autorisez les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques.",
|
||||
"dynamic_booking": "Liens de groupe dynamiques",
|
||||
"allow_seo_indexing": "Autorisez les moteurs de recherche à accéder à votre contenu public.",
|
||||
"seo_indexing": "Autoriser l'indexation SEO",
|
||||
@@ -450,13 +452,14 @@
|
||||
"go_to_billing_portal": "Accéder au portail de facturation",
|
||||
"need_anything_else": "Besoin d'autre chose ?",
|
||||
"further_billing_help": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.",
|
||||
"contact": "Contact",
|
||||
"contact": "Contacter",
|
||||
"our_support_team": "notre équipe d'assistance",
|
||||
"contact_our_support_team": "Contactez notre équipe d'assistance",
|
||||
"uh_oh": "Oups !",
|
||||
"no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de type d'événement.",
|
||||
"edit_logo": "Modifier le logo",
|
||||
"upload_a_logo": "Télécharger un logo",
|
||||
"upload_logo": "Télécharger un logo",
|
||||
"remove_logo": "Supprimer le logo",
|
||||
"enable": "Activer",
|
||||
"code": "Code",
|
||||
@@ -569,6 +572,7 @@
|
||||
"your_team_name": "Nom de votre équipe",
|
||||
"team_updated_successfully": "Équipe mise à jour avec succès",
|
||||
"your_team_updated_successfully": "Votre équipe a été mise à jour avec succès.",
|
||||
"your_org_updated_successfully": "Votre organisation a été mise à jour avec succès.",
|
||||
"about": "À propos",
|
||||
"team_description": "Quelques mots à propos de votre équipe. Ces informations apparaîtront sur la page publique de votre équipe.",
|
||||
"org_description": "Quelques phrases à propos de votre organisation. Elles apparaîtront sur la page de profil public de votre organisation.",
|
||||
@@ -844,6 +848,8 @@
|
||||
"next_step": "Passer l'étape",
|
||||
"prev_step": "Étape précédente",
|
||||
"install": "Installer",
|
||||
"install_paid_app": "S'abonner",
|
||||
"start_paid_trial": "Démarrer l'essai gratuit",
|
||||
"installed": "Installée",
|
||||
"active_install_one": "{{count}} installation active",
|
||||
"active_install_other": "{{count}} installations actives",
|
||||
@@ -1092,6 +1098,8 @@
|
||||
"developer_documentation": "Documentation pour développeurs",
|
||||
"get_in_touch": "Contactez-nous",
|
||||
"contact_support": "Contacter l'assistance",
|
||||
"premium_support": "Assistance Premium",
|
||||
"community_support": "Aide communautaire",
|
||||
"feedback": "Commentaires",
|
||||
"submitted_feedback": "Merci pour vos commentaires !",
|
||||
"feedback_error": "Erreur lors de l'envoi du commentaire",
|
||||
@@ -2024,7 +2032,7 @@
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Voici votre résumé des événements populaires pour votre équipe {{teamName}} au cours des 30 derniers jours",
|
||||
"me": "Moi",
|
||||
"monthly_digest_email": "E-mail de résumé mensuel",
|
||||
"monthly_digest_email_for_teams": "E-mail de résumé mensuel pour les équipes",
|
||||
"monthly_digest_email_for_teams": "E-mail de résumé mensuel pour les équipes.",
|
||||
"verify_team_tooltip": "Vérifiez votre équipe pour activer l'envoi de messages aux participants",
|
||||
"member_removed": "Membre supprimé",
|
||||
"my_availability": "Mes disponibilités",
|
||||
@@ -2065,12 +2073,19 @@
|
||||
"access_bookings": "Lire, modifier, supprimer vos réservations",
|
||||
"allow_client_to_do": "Autoriser {{clientName}} à faire cela ?",
|
||||
"allow": "Autoriser",
|
||||
"you_can_override_calendar_in_advanced_tab": "Vous pouvez modifier ceci pour chaque événement dans les paramètres avancés de chaque type d'événement.",
|
||||
"edit_users_availability": "Modifier la disponibilité de l'utilisateur : {{username}}",
|
||||
"resend_invitation": "Renvoyer l'invitation",
|
||||
"invitation_resent": "L'invitation a été renvoyée.",
|
||||
"add_client": "Ajouter un client",
|
||||
"add_new_client": "Ajouter un nouveau client",
|
||||
"as_csv": "au format CSV",
|
||||
"overlay_my_calendar": "Superposer mon calendrier",
|
||||
"overlay_my_calendar_toc": "En vous connectant à votre calendrier, vous acceptez notre politique de confidentialité et nos conditions d'utilisation. Vous pouvez révoquer cet accès à tout moment.",
|
||||
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
|
||||
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
|
||||
"extensive_whitelabeling": "Marque blanche étendue",
|
||||
"unlimited_teams": "Équipes illimitées",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -268,6 +268,7 @@
|
||||
"set_availability": "ציין את הזמינות שלך",
|
||||
"availability_settings": "הגדרות זמינוּת",
|
||||
"continue_without_calendar": "להמשיך בלי לוח שנה",
|
||||
"continue_with": "להמשיך עם {{appName}}",
|
||||
"connect_your_calendar": "קשר את לוח השנה שלך",
|
||||
"connect_your_video_app": "חיבור אפליקציות הווידאו שלך",
|
||||
"connect_your_video_app_instructions": "חבר/י את אפליקציות הווידאו שלך כדי להשתמש בהן עבור סוגי האירועים שלך.",
|
||||
@@ -288,6 +289,8 @@
|
||||
"when": "מתי",
|
||||
"where": "היכן",
|
||||
"add_to_calendar": "הוספה ללוח השנה",
|
||||
"add_to_calendar_description": "בחר/י להיכן יש להוסיף אירועים כשאת/ה עסוק/ה.",
|
||||
"add_events_to": "הוספת אירועים ל",
|
||||
"add_another_calendar": "להוסיף לוח שנה אחר",
|
||||
"other": "אחר",
|
||||
"email_sign_in_subject": "קישור הכניסה שלך אל {{appName}}",
|
||||
@@ -422,6 +425,7 @@
|
||||
"booking_created": "ההזמנה נוצרה",
|
||||
"booking_rejected": "ההזמנה נדחתה",
|
||||
"booking_requested": "התקבלה בקשת הזמנה",
|
||||
"booking_payment_initiated": "הופעל תשלום על ההזמנה",
|
||||
"meeting_ended": "הפגישה הסתיימה",
|
||||
"form_submitted": "הטופס נשלח",
|
||||
"booking_paid": "בוצע תשלום עבור ההזמנה",
|
||||
@@ -456,6 +460,7 @@
|
||||
"no_event_types_have_been_setup": "משתמש זה עדיין לא הגדיר סוג אירוע.",
|
||||
"edit_logo": "עריכת לוגו",
|
||||
"upload_a_logo": "העלאת לוגו",
|
||||
"upload_logo": "העלאת לוגו",
|
||||
"remove_logo": "הסרת לוגו",
|
||||
"enable": "הפעלה",
|
||||
"code": "קוד",
|
||||
@@ -568,6 +573,7 @@
|
||||
"your_team_name": "שם הצוות שלך",
|
||||
"team_updated_successfully": "עדכון הצוות בוצע בהצלחה",
|
||||
"your_team_updated_successfully": "הצוות שלך עודכן בהצלחה.",
|
||||
"your_org_updated_successfully": "הארגון שלך עודכן בהצלחה.",
|
||||
"about": "אודות",
|
||||
"team_description": "מספר משפטים אודות הצוות. המידע הזה יופיע בדף ה-URL של הצוות.",
|
||||
"org_description": "מספר משפטים אודות הארגון. הם יופיעו בדף עם כתובת ה-URL של הארגון.",
|
||||
@@ -599,6 +605,7 @@
|
||||
"hide_book_a_team_member": "הסתרת הלחצן לשריון זמן של חבר/ת צוות",
|
||||
"hide_book_a_team_member_description": "הסתר/י את הלחצן לשריון זמן של חבר/ת צוות מהדפים הציבוריים שלך.",
|
||||
"danger_zone": "אזור מסוכן",
|
||||
"account_deletion_cannot_be_undone": "יש לנקוט זהירות. מחיקת חשבון היא פעולה בלתי הפיכה.",
|
||||
"back": "הקודם",
|
||||
"cancel": "ביטול",
|
||||
"cancel_all_remaining": "לבטל את כל הנותרים",
|
||||
@@ -688,6 +695,7 @@
|
||||
"people": "אנשים",
|
||||
"your_email": "הדוא\"ל שלך",
|
||||
"change_avatar": "שינוי אווטאר",
|
||||
"upload_avatar": "העלאת אווטאר",
|
||||
"language": "שפה",
|
||||
"timezone": "אזור זמן",
|
||||
"first_day_of_week": "היום הראשון בשבוע",
|
||||
@@ -778,6 +786,7 @@
|
||||
"disable_guests": "השבתת אורחים",
|
||||
"disable_guests_description": "השבת את האפשרות להוסיף אורחים נוספים בעת ביצוע הזמנה.",
|
||||
"private_link": "יצירת קישור פרטי",
|
||||
"enable_private_url": "לאפשר כתובת URL פרטית",
|
||||
"private_link_label": "קישור פרטי",
|
||||
"private_link_hint": "הקישור הפרטי שלך ייווצר מחדש לאחר כל שימוש",
|
||||
"copy_private_link": "העתקת קישור פרטי",
|
||||
@@ -840,6 +849,7 @@
|
||||
"next_step": "לדלג על שלב זה",
|
||||
"prev_step": "לשלב הקודם",
|
||||
"install": "התקנה",
|
||||
"install_paid_app": "הרשמה למינוי",
|
||||
"installed": "מותקן",
|
||||
"active_install_one": "התקנה פעילה {{count}}",
|
||||
"active_install_other": "{{count}} התקנות פעילות",
|
||||
@@ -1088,6 +1098,7 @@
|
||||
"developer_documentation": "מסמכי מפתחים",
|
||||
"get_in_touch": "יצירת קשר",
|
||||
"contact_support": "פנייה לתמיכה",
|
||||
"community_support": "תמיכת קהילה",
|
||||
"feedback": "משוב",
|
||||
"submitted_feedback": "תודה על המשוב!",
|
||||
"feedback_error": "שגיאה בעת שליחת משוב",
|
||||
@@ -1213,6 +1224,7 @@
|
||||
"organizer_name_variable": "שם המארגן/ת",
|
||||
"app_upgrade_description": "כדי להשתמש בתכונה זו, עליך לשדרג לחשבון Pro.",
|
||||
"invalid_number": "מספר טלפון לא תקין",
|
||||
"invalid_url_error_message": "כתובת URL לא חוקית עבור {{label}}. כתובת URL לדוגמה: {{sampleUrl}}",
|
||||
"navigate": "ניווט",
|
||||
"open": "פתח",
|
||||
"close": "סגירה",
|
||||
@@ -1276,6 +1288,7 @@
|
||||
"personal_cal_url": "כתובת ה-URL האישית שלי של {{appName}}",
|
||||
"bio_hint": "מספר משפטים אודותיך. המידע הזה יופיע בדף ה-URL האישי שלך.",
|
||||
"user_has_no_bio": "משתמש זה עדיין לא הוסיף ביוגרפיה.",
|
||||
"bio": "ביוגרפיה",
|
||||
"delete_account_modal_title": "מחיקת החשבון",
|
||||
"confirm_delete_account_modal": "בטוח שברצונך למחוק את חשבון {{appName}} שלך?",
|
||||
"delete_my_account": "מחיקת החשבון שלי",
|
||||
@@ -1286,6 +1299,7 @@
|
||||
"select_calendars": "בחר את לוחות השנה שבהם ברצונך לבדוק אם יש התנגשויות, כדי למנוע כפל הזמנות.",
|
||||
"check_for_conflicts": "בדיקת התנגשויות",
|
||||
"view_recordings": "צפייה בהקלטות",
|
||||
"check_for_recordings": "חיפוש הקלטות",
|
||||
"adding_events_to": "הוספת אירועים ל",
|
||||
"follow_system_preferences": "פעל לפי העדפות המערכת",
|
||||
"custom_brand_colors": "צבעי מותג בהתאמה אישית",
|
||||
@@ -1530,6 +1544,7 @@
|
||||
"problem_registering_domain": "הייתה בעיה ברישום תת-הדומיין; אפשר לנסות שוב או לפנות למנהל/ת מערכת",
|
||||
"team_publish": "פרסום צוות",
|
||||
"number_text_notifications": "מספר טלפון (להודעות טקסט)",
|
||||
"number_sms_notifications": "מספר טלפון (להודעות SMS)",
|
||||
"attendee_email_variable": "כתובת הדוא\"ל של המשתתף",
|
||||
"attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה",
|
||||
"kbar_search_placeholder": "הקלד/י פקודה או חפש/י...",
|
||||
@@ -1594,6 +1609,7 @@
|
||||
"options": "אפשרויות",
|
||||
"enter_option": "הזנת ה-{{index}} של האפשרות",
|
||||
"add_an_option": "הוספת אפשרות",
|
||||
"location_already_exists": "המיקום הזה כבר קיים, יש לבחור מיקום חדש",
|
||||
"radio": "רדיו",
|
||||
"google_meet_warning": "כדי להשתמש ב-Google Meet, יש להגדיר את Google Calendar כלוח השנה של המארח",
|
||||
"individual": "משתמש בודד",
|
||||
@@ -1613,6 +1629,7 @@
|
||||
"date_overrides_mark_all_day_unavailable_other": "סימון אי-זמינות בתאריכים מסוימים",
|
||||
"date_overrides_add_btn": "הוספת מעקף",
|
||||
"date_overrides_update_btn": "עדכון מעקף",
|
||||
"date_successfully_added": "עקיפת תאריך נוספה בהצלחה",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-עותק",
|
||||
"set_as_default": "להגדיר כברירת מחדל",
|
||||
"hide_eventtype_details": "הסתרת פרטי סוג האירוע",
|
||||
@@ -1639,6 +1656,7 @@
|
||||
"minimum_round_robin_hosts_count": "מספר המארחים שחייבים להשתתף",
|
||||
"hosts": "מארחים",
|
||||
"upgrade_to_enable_feature": "אתה צריך לייצר צוות כדי להפעיל את היכולת. לחץ ליצירת צוות.",
|
||||
"orgs_upgrade_to_enable_feature": "כדי לאפשר שימוש בתכונה הזו, יש לשדרג לתוכנית שלנו לארגונים.",
|
||||
"new_attendee": "משתתף/ת חדש/ה",
|
||||
"awaiting_approval": "בהמתנה לאישור",
|
||||
"requires_google_calendar": "האפליקציה הזו מחייבת חיבור ל-Google Calendar",
|
||||
@@ -1743,6 +1761,7 @@
|
||||
"show_on_booking_page": "להציג בדף ההזמנות",
|
||||
"get_started_zapier_templates": "התחל עם תבניות Zapier",
|
||||
"team_is_unpublished": "צוות {{team}} אינו מפורסם",
|
||||
"org_is_unpublished_description": "הקישור לארגון הזה אינו זמין כעת. יש ליצור קשר עם הבעלים של הארגון או לבקש מהם לפרסם אותו.",
|
||||
"team_is_unpublished_description": "קישור ה-{{entity}} הזה אינו זמין כעת. יש ליצור קשר עם הבעלים של ה-{{entity}} או לבקש מהם לפרסם אותו.",
|
||||
"team_member": "חבר צוות",
|
||||
"a_routing_form": "טופס ניתוב",
|
||||
@@ -1877,6 +1896,7 @@
|
||||
"edit_invite_link": "עריכת הגדרות הקישור",
|
||||
"invite_link_copied": "קישור ההזמנה הועתק",
|
||||
"invite_link_deleted": "קישור ההזמנה נמחק",
|
||||
"api_key_deleted": "מפתח API נמחק",
|
||||
"invite_link_updated": "הגדרות קישור ההזמנה נשמרו",
|
||||
"link_expires_after": "הקישורים מוגדרים לפוג לאחר...",
|
||||
"one_day": "יום אחד",
|
||||
@@ -2009,7 +2029,13 @@
|
||||
"attendee_last_name_variable": "שם המשפחה של המשתתף",
|
||||
"attendee_first_name_info": "השם הפרטי של האדם שביצע את ההזמנה",
|
||||
"attendee_last_name_info": "שם המשפחה של האדם שביצע את ההזמנה",
|
||||
"your_monthly_digest": "הסיכום החודשי שלך",
|
||||
"member_name": "שם החבר/ה",
|
||||
"most_popular_events": "האירועים הפופולריים ביותר",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "הנה הסיכום של האירועים הפופולריים של הצוות שלך, {{teamName}}, ל-30 הימים האחרונים",
|
||||
"me": "אני",
|
||||
"monthly_digest_email": "אימייל עם סיכום חודשי",
|
||||
"monthly_digest_email_for_teams": "אימייל עם סיכום חודשי עבור צוותים",
|
||||
"verify_team_tooltip": "אמת/י את הצוות שלך כדי לאפשר שליחת הודעות למשתתפים",
|
||||
"member_removed": "החבר הוסר",
|
||||
"my_availability": "הזמינות שלי",
|
||||
@@ -2039,13 +2065,41 @@
|
||||
"team_no_event_types": "אין לצוות זה אף סוג של אירוע",
|
||||
"seat_options_doesnt_multiple_durations": "האפשרויות של הושבה במקומות לא תומכות במשכי זמן שונים",
|
||||
"include_calendar_event": "כלילת אירוע מלוח השנה",
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "נוספו לאחרונה",
|
||||
"no_members_found": "לא נמצא אף חבר",
|
||||
"event_setup_length_error": "הגדרת אירוע: משך הזמן חייב להיות לפחות דקה אחת.",
|
||||
"availability_schedules": "לוחות זמנים לזמינוּת",
|
||||
"unauthorized": "אין הרשאה",
|
||||
"access_cal_account": "{{clientName}} רוצה לקבל גישה לחשבון {{appName}} שלך",
|
||||
"select_account_team": "בחירת חשבון או צוות",
|
||||
"allow_client_to": "הדבר יאפשר ל-{{clientName}}:",
|
||||
"associate_with_cal_account": "לשייך בינך לבין הפרטים האישיים שלך מ-{{clientName}}",
|
||||
"see_personal_info": "לראות את הפרטים האישיים שלך, כולל פרטים אישיים שהגדרת כגלויים לכולם",
|
||||
"see_primary_email_address": "לראות את כתובת הדוא\"ל הראשית שלך",
|
||||
"connect_installed_apps": "להתחבר לאפליקציות המותקנות שלך",
|
||||
"access_event_type": "לקרוא, לערוך ולמחוק את סוגי האירועים שלך",
|
||||
"access_availability": "לקרוא, לערוך ולמחוק את הזמינות שלך",
|
||||
"access_bookings": "לקרוא, לערוך ולמחוק את ההזמנות שלך",
|
||||
"allow_client_to_do": "האם לאפשר ל-{{clientName}} לעשות זאת?",
|
||||
"oauth_access_information": "לחיצה על 'אפשר' מהווה מתן הרשאה מצידך ליישום זה להשתמש במידע שלך בהתאם לתנאי השירות ולמדיניות הפרטיות שלו. ניתן לשלול את הגישה בחנות האפליקציות של {{appName}}.",
|
||||
"allow": "אפשר",
|
||||
"view_only_edit_availability_not_onboarded": "משתמש זה לא השלים תהליך הטמעה. לא תהיה לך אפשרות להגדיר את הזמינות שלו עד שהוא יעשה זאת.",
|
||||
"view_only_edit_availability": "את/ה צופה בזמינות של משתמש זה. יש לך אפשרות לערוך רק את פרטי הזמינות שלך.",
|
||||
"you_can_override_calendar_in_advanced_tab": "ניתן לעקוף זאת על בסיס כל אירוע לגופו בהגדרות המתקדמות בכל סוג אירוע.",
|
||||
"edit_users_availability": "עריכת הזמינות של משתמש: {{username}}",
|
||||
"resend_invitation": "שליחת ההזמנה מחדש",
|
||||
"invitation_resent": "ההזמנה נשלחה מחדש.",
|
||||
"add_client": "הוספת לקוח",
|
||||
"copy_client_secret_info": "לאחר העתקת הסוד, כבר לא תהיה לך אפשרות לראות אותו",
|
||||
"add_new_client": "הוספת לקוח חדש",
|
||||
"this_app_is_not_setup_already": "האפליקציה הזו עדיין לא הוגדרה",
|
||||
"as_csv": "כ-CSV",
|
||||
"overlay_my_calendar": "הצג את לוח השנה שלי בשכבת-על",
|
||||
"overlay_my_calendar_toc": "על ידי חיבור אל לוח השנה שלך, את/ה מקבל/ת את מדיניות הפרטיות ואת תנאי השימוש שלנו. אפשר לשלול את הגישה בכל שלב.",
|
||||
"view_overlay_calendar_events": "ראה/י את האירועים שלך בלוח השנה כדי למנוע התנגשות בהזמנות.",
|
||||
"lock_timezone_toggle_on_booking_page": "נעילת אזור הזמן בדף ההזמנות",
|
||||
"description_lock_timezone_toggle_on_booking_page": "כדי לנעול את אזור הזמן בדף ההזמנות – שימושי לאירועים אישיים.",
|
||||
"extensive_whitelabeling": "תהליך הטמעה והנדסת תמיכה אישי",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"next_step": "Salta passo",
|
||||
"prev_step": "Passo precedente",
|
||||
"install": "Installa",
|
||||
"install_paid_app": "Abbonati",
|
||||
"installed": "Installato",
|
||||
"active_install_one": "{{count}} installazione attiva",
|
||||
"active_install_other": "{{count}} installazioni attive",
|
||||
@@ -1097,6 +1098,7 @@
|
||||
"developer_documentation": "Documentazione sviluppatore",
|
||||
"get_in_touch": "Contattaci",
|
||||
"contact_support": "Contatta il supporto",
|
||||
"community_support": "Supporto della community",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Grazie per il tuo feedback!",
|
||||
"feedback_error": "Errore durante l'invio del feedback",
|
||||
|
||||
@@ -268,6 +268,7 @@
|
||||
"set_availability": "利用可否の設定",
|
||||
"availability_settings": "利用できる設定",
|
||||
"continue_without_calendar": "カレンダーなしで続行",
|
||||
"continue_with": "{{appName}} で続行",
|
||||
"connect_your_calendar": "カレンダーに接続",
|
||||
"connect_your_video_app": "ビデオアプリを接続",
|
||||
"connect_your_video_app_instructions": "ご利用のイベントの種類で動画アプリを使用するには、ビデオアプリを接続してください。",
|
||||
@@ -288,6 +289,8 @@
|
||||
"when": "日時",
|
||||
"where": "参加方法",
|
||||
"add_to_calendar": "カレンダーに追加",
|
||||
"add_to_calendar_description": "予約時にイベントを追加する場所を選びます。",
|
||||
"add_events_to": "イベントの追加先",
|
||||
"add_another_calendar": "別のカレンダーを追加",
|
||||
"other": "その他",
|
||||
"email_sign_in_subject": "{{appName}} のサインインリンク",
|
||||
@@ -422,6 +425,7 @@
|
||||
"booking_created": "予約を作成しました",
|
||||
"booking_rejected": "予約が拒否されました",
|
||||
"booking_requested": "予約がリクエストされました",
|
||||
"booking_payment_initiated": "予約に関するお支払いを開始しました",
|
||||
"meeting_ended": "ミーティングが終了しました",
|
||||
"form_submitted": "フォームが送信されました",
|
||||
"booking_paid": "予約の支払いが済みました",
|
||||
@@ -456,6 +460,7 @@
|
||||
"no_event_types_have_been_setup": "このユーザーはまだイベントタイプを設定していません。",
|
||||
"edit_logo": "ロゴを編集",
|
||||
"upload_a_logo": "ロゴをアップロード",
|
||||
"upload_logo": "ロゴをアップロード",
|
||||
"remove_logo": "ロゴを削除する",
|
||||
"enable": "有効にする",
|
||||
"code": "コード",
|
||||
@@ -568,6 +573,7 @@
|
||||
"your_team_name": "チーム名",
|
||||
"team_updated_successfully": "チームが正常に更新されました",
|
||||
"your_team_updated_successfully": "チームが正常に更新されました。",
|
||||
"your_org_updated_successfully": "組織は正常に更新されました。",
|
||||
"about": "このアプリについて",
|
||||
"team_description": "あなたのチームについての簡単な説明文です。あなたのチームの URL ページに表示されます。",
|
||||
"org_description": "あなたの組織についての簡単な説明文です。あなたの組織の URL ページに表示されます。",
|
||||
@@ -599,6 +605,7 @@
|
||||
"hide_book_a_team_member": "「チームメンバーを予約する」ボタンを非表示にする",
|
||||
"hide_book_a_team_member_description": "公開ページから「チームメンバーを予約する」ボタンを非表示にします。",
|
||||
"danger_zone": "危険ゾーン",
|
||||
"account_deletion_cannot_be_undone": "ご注意ください。アカウントの削除は元に戻せません。",
|
||||
"back": "戻る",
|
||||
"cancel": "キャンセル",
|
||||
"cancel_all_remaining": "残りをすべてキャンセル",
|
||||
@@ -688,6 +695,7 @@
|
||||
"people": "ユーザー",
|
||||
"your_email": "あなたのメールアドレス",
|
||||
"change_avatar": "アバターを変更",
|
||||
"upload_avatar": "アバターをアップロード",
|
||||
"language": "言語",
|
||||
"timezone": "タイムゾーン",
|
||||
"first_day_of_week": "週の最初の日",
|
||||
@@ -778,6 +786,7 @@
|
||||
"disable_guests": "ゲストを無効化",
|
||||
"disable_guests_description": "予約中のゲストの追加を無効にします。",
|
||||
"private_link": "プライベートリンクを生成",
|
||||
"enable_private_url": "プライベート URL を有効にする",
|
||||
"private_link_label": "プライベートリンク",
|
||||
"private_link_hint": "プライベートリンクは使用するたびに再生成されます",
|
||||
"copy_private_link": "プライベートリンクをコピー",
|
||||
@@ -840,6 +849,7 @@
|
||||
"next_step": "手順をスキップ",
|
||||
"prev_step": "前の手順",
|
||||
"install": "インストール",
|
||||
"install_paid_app": "サブスクライブ",
|
||||
"installed": "インストール済み",
|
||||
"active_install_one": "{{count}} 件のアクティブなインストール",
|
||||
"active_install_other": "{{count}} 件のアクティブなインストール",
|
||||
@@ -1088,6 +1098,7 @@
|
||||
"developer_documentation": "開発者向けドキュメント",
|
||||
"get_in_touch": "お問い合わせ",
|
||||
"contact_support": "サポートに連絡",
|
||||
"community_support": "コミュニティサポート",
|
||||
"feedback": "フィードバック",
|
||||
"submitted_feedback": "フィードバックをありがとうございます!",
|
||||
"feedback_error": "フィードバックの送信エラー",
|
||||
@@ -1213,6 +1224,7 @@
|
||||
"organizer_name_variable": "主催者名",
|
||||
"app_upgrade_description": "この機能を利用するには、Pro アカウントへのアップグレードが必要です。",
|
||||
"invalid_number": "電話番号が無効です",
|
||||
"invalid_url_error_message": "{{label}} の無効な URL です。サンプル URL:{{sampleUrl}}",
|
||||
"navigate": "ナビゲート",
|
||||
"open": "開く",
|
||||
"close": "閉じる",
|
||||
@@ -1276,6 +1288,7 @@
|
||||
"personal_cal_url": "私の個人 {{appName}} URL",
|
||||
"bio_hint": "あなたに関する簡潔な説明。これはあなたの個人 URL ページに表示されます。",
|
||||
"user_has_no_bio": "このユーザーはまだ経歴を追加していません。",
|
||||
"bio": "経歴",
|
||||
"delete_account_modal_title": "アカウントを削除する",
|
||||
"confirm_delete_account_modal": "{{appName}} アカウントを削除してもよろしいですか?",
|
||||
"delete_my_account": "アカウントを削除する",
|
||||
@@ -1286,6 +1299,7 @@
|
||||
"select_calendars": "ダブルブッキングを防ぐために、スケジュールの重なりをチェックするカレンダーを選択してください。",
|
||||
"check_for_conflicts": "スケジュールの重なりをチェック",
|
||||
"view_recordings": "録音を表示",
|
||||
"check_for_recordings": "レコーディングを確認",
|
||||
"adding_events_to": "イベントの追加先",
|
||||
"follow_system_preferences": "システム環境設定に従う",
|
||||
"custom_brand_colors": "カスタムブランドカラー",
|
||||
@@ -1530,6 +1544,7 @@
|
||||
"problem_registering_domain": "サブドメインの登録時に問題が発生しました。もう一度お試しいただくか、管理者までお問い合せください",
|
||||
"team_publish": "チームを公開",
|
||||
"number_text_notifications": "電話番号 (テキスト通知)",
|
||||
"number_sms_notifications": "電話番号(SMS 通知)",
|
||||
"attendee_email_variable": "出席者のメールアドレス",
|
||||
"attendee_email_info": "予約者のメールアドレス",
|
||||
"kbar_search_placeholder": "コマンドを入力するか、検索してください...",
|
||||
@@ -1594,6 +1609,7 @@
|
||||
"options": "オプション",
|
||||
"enter_option": "オプション {{index}} を入力してください",
|
||||
"add_an_option": "オプションを追加",
|
||||
"location_already_exists": "この場所はすでに存在します。新しい場所を選んでください",
|
||||
"radio": "ラジオボタン",
|
||||
"google_meet_warning": "Google Meet を使用するには、目的のカレンダーを Google カレンダーに設定する必要があります",
|
||||
"individual": "個人",
|
||||
@@ -1613,6 +1629,7 @@
|
||||
"date_overrides_mark_all_day_unavailable_other": "選択した日付を参加不可としてマーク",
|
||||
"date_overrides_add_btn": "上書きを追加",
|
||||
"date_overrides_update_btn": "上書きを更新",
|
||||
"date_successfully_added": "日付の上書きが正常に追加されました",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
||||
"set_as_default": "デフォルトとして設定",
|
||||
"hide_eventtype_details": "イベントの種類の詳細を非表示にする",
|
||||
@@ -1639,6 +1656,7 @@
|
||||
"minimum_round_robin_hosts_count": "出席が必要なホストの数",
|
||||
"hosts": "ホスト",
|
||||
"upgrade_to_enable_feature": "この機能を有効にするには、チームを作成する必要があります。クリックしてチームを作成してください。",
|
||||
"orgs_upgrade_to_enable_feature": "この機能を有効にするには Enterprise プランにアップグレードする必要があります。",
|
||||
"new_attendee": "新規参加者",
|
||||
"awaiting_approval": "承認を待っています",
|
||||
"requires_google_calendar": "このアプリは Google カレンダーとの接続が必要です",
|
||||
@@ -1743,6 +1761,7 @@
|
||||
"show_on_booking_page": "予約ページに表示",
|
||||
"get_started_zapier_templates": "Zapier テンプレートの使用を開始する",
|
||||
"team_is_unpublished": "{{team}} は公開されていません",
|
||||
"org_is_unpublished_description": "この組織のリンクは現在利用できません。組織の所有者に連絡するか、リンクを公開するよう依頼してください。",
|
||||
"team_is_unpublished_description": "この {{entity}} のリンクは現在利用できません。{{entity}} の所有者に問い合わせるか、リンクを公開するように依頼してください。",
|
||||
"team_member": "チームメンバー",
|
||||
"a_routing_form": "ルーティングフォーム",
|
||||
@@ -1877,6 +1896,7 @@
|
||||
"edit_invite_link": "リンクの設定を編集する",
|
||||
"invite_link_copied": "招待リンクをコピーしました",
|
||||
"invite_link_deleted": "招待リンクを削除しました",
|
||||
"api_key_deleted": "API キーを削除しました",
|
||||
"invite_link_updated": "招待リンクの設定を保存しました",
|
||||
"link_expires_after": "リンクの期限切れまで...",
|
||||
"one_day": "1 日",
|
||||
@@ -2009,7 +2029,13 @@
|
||||
"attendee_last_name_variable": "出席者の姓",
|
||||
"attendee_first_name_info": "予約者の名",
|
||||
"attendee_last_name_info": "予約者の姓",
|
||||
"your_monthly_digest": "月 1 回のダイジェスト",
|
||||
"member_name": "メンバーの名前",
|
||||
"most_popular_events": "最も人気のイベント",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "こちらはこの 30 日間、チーム {{teamName}} で最も人気があったイベントのサマリーです",
|
||||
"me": "私",
|
||||
"monthly_digest_email": "月 1 回のダイジェストのメール",
|
||||
"monthly_digest_email_for_teams": "チームのための月 1 回のダイジェストのメール",
|
||||
"verify_team_tooltip": "出席者へのメッセージ送信ができるようにするには、チームを確認してください",
|
||||
"member_removed": "メンバーが削除されました",
|
||||
"my_availability": "私の空き状況",
|
||||
@@ -2039,13 +2065,41 @@
|
||||
"team_no_event_types": "このチームにはイベントタイプはありません",
|
||||
"seat_options_doesnt_multiple_durations": "座席オプションは複数の期間をサポートしていません",
|
||||
"include_calendar_event": "カレンダーのイベントを含める",
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "最近追加されました",
|
||||
"no_members_found": "メンバーが見つかりません",
|
||||
"event_setup_length_error": "イベント設定:時間は 1 分以上でなくてはいけません。",
|
||||
"availability_schedules": "空き状況一覧",
|
||||
"unauthorized": "権限がありません",
|
||||
"access_cal_account": "{{clientName}}があなたの {{appName}} アカウントへのアクセスを求めています",
|
||||
"select_account_team": "アカウントまたはチームを選択",
|
||||
"allow_client_to": "これにより {{clientName}} は",
|
||||
"associate_with_cal_account": "あなたと {{clientName}} からのあなたの個人情報を関連づけられます",
|
||||
"see_personal_info": "あなたの個人情報(あなたがこれまでに公開したあなたの個人情報など)を表示",
|
||||
"see_primary_email_address": "プライマリメールアドレスを表示",
|
||||
"connect_installed_apps": "インストールしたアプリに接続",
|
||||
"access_event_type": "イベントタイプを読み取り、編集し、削除する",
|
||||
"access_availability": "空き状況を読み取り、編集し、削除する",
|
||||
"access_bookings": "予約を読み取り、編集し、削除する",
|
||||
"allow_client_to_do": "{{clientName}} にこれの実行を許可しますか?",
|
||||
"oauth_access_information": "「許可」をクリックすると、このアプリのサービス利用規約とプライバシーポリシーに従って、アプリにあなたの個人情報の使用を許可することになります。{{appName}} の App Store でアクセスを削除できます。",
|
||||
"allow": "許可",
|
||||
"view_only_edit_availability_not_onboarded": "このユーザーはオンボーディングを完了していません。オンボーディングを完了するまで、ユーザーの空き状況は設定できません。",
|
||||
"view_only_edit_availability": "このユーザーの空き状況を表示しています。編集できるのは自分の空き状況だけです。",
|
||||
"you_can_override_calendar_in_advanced_tab": "各イベントタイプの詳細設定で、イベントごとにこれを上書きすることができます。",
|
||||
"edit_users_availability": "ユーザーの空き状況を編集:{{username}}",
|
||||
"resend_invitation": "招待を再送",
|
||||
"invitation_resent": "招待は再送されました。",
|
||||
"add_client": "顧客を追加",
|
||||
"copy_client_secret_info": "このシークレットをコピーすると、もう表示できなくなります",
|
||||
"add_new_client": "新しい顧客を追加",
|
||||
"this_app_is_not_setup_already": "このアプリはまだ設定されていません",
|
||||
"as_csv": "CSV として",
|
||||
"overlay_my_calendar": "カレンダーを重ね合わせる",
|
||||
"overlay_my_calendar_toc": "カレンダーに接続することで、弊社のプライバシーポリシーと利用規約に同意することになります。アクセスはいつでも取り消せます。",
|
||||
"view_overlay_calendar_events": "カレンダーのイベントを表示して予約が重ならないようにします。",
|
||||
"lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定する",
|
||||
"description_lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定するためのもので、対面のイベントに役立ちます。",
|
||||
"extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"next_step": "건너뛰기",
|
||||
"prev_step": "이전 단계",
|
||||
"install": "설치",
|
||||
"install_paid_app": "구독",
|
||||
"installed": "설치됨",
|
||||
"active_install_one": "{{count}}개 활성 설치",
|
||||
"active_install_other": "{{count}}개 활성 설치",
|
||||
@@ -1097,6 +1098,7 @@
|
||||
"developer_documentation": "개발자 문서",
|
||||
"get_in_touch": "연락하기",
|
||||
"contact_support": "지원 문의",
|
||||
"community_support": "커뮤니티 지원",
|
||||
"feedback": "피드백",
|
||||
"submitted_feedback": "피드백을 주셔서 감사합니다!",
|
||||
"feedback_error": "피드백을 보내는 중 오류 발생",
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"next_step": "Stap overslaan",
|
||||
"prev_step": "Vorige stap",
|
||||
"install": "Installeren",
|
||||
"install_paid_app": "Abonneren",
|
||||
"installed": "Geinstalleerd",
|
||||
"active_install_one": "{{count}} actieve installatie",
|
||||
"active_install_other": "{{count}} actieve installaties",
|
||||
@@ -1097,6 +1098,7 @@
|
||||
"developer_documentation": "Ontwikkelaarsdocumentatie",
|
||||
"get_in_touch": "Neem contact op",
|
||||
"contact_support": "Neem contact op met de ondersteuning",
|
||||
"community_support": "Ondersteuning door de community",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Bedankt voor uw feedback!",
|
||||
"feedback_error": "Fout bij verzenden van feedback",
|
||||
|
||||
@@ -706,6 +706,7 @@
|
||||
"next_step": "Hopp over trinn",
|
||||
"prev_step": "Forrige trinn",
|
||||
"install": "Installer",
|
||||
"install_paid_app": "Abonner",
|
||||
"installed": "Installert",
|
||||
"active_install_one": "{{count}} aktiv installasjon",
|
||||
"active_install_other": "{{count}} aktive installasjoner",
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"next_step": "Pomiń krok",
|
||||
"prev_step": "Poprzedni krok",
|
||||
"install": "Zainstaluj",
|
||||
"install_paid_app": "Subskrybuj",
|
||||
"installed": "Zainstalowane",
|
||||
"active_install_one": "Aktywne instalacje: {{count}}",
|
||||
"active_install_other": "Aktywne instalacje: {{count}}",
|
||||
@@ -1097,6 +1098,7 @@
|
||||
"developer_documentation": "Dokumentacja dla programistów",
|
||||
"get_in_touch": "Kontakt",
|
||||
"contact_support": "Skontaktuj się z pomocą techniczną",
|
||||
"community_support": "Wsparcie społeczności",
|
||||
"feedback": "Opinia",
|
||||
"submitted_feedback": "Dziękujemy za opinię!",
|
||||
"feedback_error": "Podczas wysyłania opinii wystąpił błąd",
|
||||
|
||||
@@ -289,6 +289,8 @@
|
||||
"when": "Quando",
|
||||
"where": "Onde",
|
||||
"add_to_calendar": "Adicionar ao calendário",
|
||||
"add_to_calendar_description": "Selecione onde adicionar eventos quando você já tiver uma reserva.",
|
||||
"add_events_to": "Adicionar eventos a",
|
||||
"add_another_calendar": "Adicionar outro calendário",
|
||||
"other": "Outras",
|
||||
"email_sign_in_subject": "Seu link de login para {{appName}}",
|
||||
@@ -423,6 +425,7 @@
|
||||
"booking_created": "Reserva Criada",
|
||||
"booking_rejected": "Reserva recusada",
|
||||
"booking_requested": "Reserva solicitada",
|
||||
"booking_payment_initiated": "Pagamento da reserva iniciado",
|
||||
"meeting_ended": "Reunião encerrada",
|
||||
"form_submitted": "Formulário enviado",
|
||||
"booking_paid": "Reserva paga",
|
||||
@@ -457,6 +460,7 @@
|
||||
"no_event_types_have_been_setup": "Este usuário ainda não definiu nenhum tipo de evento.",
|
||||
"edit_logo": "Editar logotipo",
|
||||
"upload_a_logo": "Envie um logotipo",
|
||||
"upload_logo": "Carregar logotipo",
|
||||
"remove_logo": "Remover logotipo",
|
||||
"enable": "Ativado",
|
||||
"code": "Código",
|
||||
@@ -569,6 +573,7 @@
|
||||
"your_team_name": "Nome do seu time",
|
||||
"team_updated_successfully": "Time atualizado com sucesso",
|
||||
"your_team_updated_successfully": "Seu time foi atualizado com sucesso.",
|
||||
"your_org_updated_successfully": "Sua organização foi atualizada com sucesso.",
|
||||
"about": "Sobre",
|
||||
"team_description": "Fale um pouco sobre seu time. Isso aparecerá na página pública do seu time.",
|
||||
"org_description": "Uma breve descrição sobre sua organização. Isso aparecerá na página principal da sua organização.",
|
||||
@@ -600,6 +605,7 @@
|
||||
"hide_book_a_team_member": "Ocultar botão de agendar um membro da equipe",
|
||||
"hide_book_a_team_member_description": "Oculte o botão de agendar um membro da equipe em suas páginas públicas.",
|
||||
"danger_zone": "Zona de perigo",
|
||||
"account_deletion_cannot_be_undone": "Cuidado. A exclusão da conta não pode ser desfeita.",
|
||||
"back": "Voltar",
|
||||
"cancel": "Cancelar",
|
||||
"cancel_all_remaining": "Cancelar todos os restantes",
|
||||
@@ -689,6 +695,7 @@
|
||||
"people": "Pessoas",
|
||||
"your_email": "Seu Email",
|
||||
"change_avatar": "Alterar Avatar",
|
||||
"upload_avatar": "Enviar avatar",
|
||||
"language": "Idioma",
|
||||
"timezone": "Fuso Horário",
|
||||
"first_day_of_week": "Primeiro Dia da Semana",
|
||||
@@ -779,6 +786,7 @@
|
||||
"disable_guests": "Desativar participantes",
|
||||
"disable_guests_description": "Desativar a adição de participantes adicionais durante a reserva.",
|
||||
"private_link": "Gerar link privado",
|
||||
"enable_private_url": "Ativar URL privada",
|
||||
"private_link_label": "Link privado",
|
||||
"private_link_hint": "Seu link privado será reaproveitado após cada uso",
|
||||
"copy_private_link": "Copiar link privado",
|
||||
@@ -841,6 +849,7 @@
|
||||
"next_step": "Ignorar passo",
|
||||
"prev_step": "Passo anterior",
|
||||
"install": "Instalar",
|
||||
"install_paid_app": "Assinar",
|
||||
"installed": "Instalado",
|
||||
"active_install_one": "{{count}} instalação ativa",
|
||||
"active_install_other": "{{count}} instalações ativas",
|
||||
@@ -935,7 +944,7 @@
|
||||
"create_events_on": "Criar eventos em",
|
||||
"enterprise_license": "Este não é um recurso corporativo",
|
||||
"enterprise_license_description": "Para ativar este recurso, obtenha uma chave de desenvolvimento no console {{consoleUrl}} e adicione ao seu .env como CALCOM_LICENSE_KEY. Caso sua equipe já tenha uma licença, entre em contato com {{supportMail}} para obter ajuda.",
|
||||
"enterprise_license_development": "Você pdoe testar este recurso no modo de desenvolvimento. Para uso em produção, solicite que um administrador acesse <2>/auth/setup</2> e insira uma chave de licença.",
|
||||
"enterprise_license_development": "Você pode testar este recurso no modo de desenvolvimento. Para uso em produção, solicite que um administrador acesse <2>/auth/setup</2> e insira uma chave de licença.",
|
||||
"missing_license": "Falta a licença",
|
||||
"signup_requires": "Licença comercial obrigatória",
|
||||
"signup_requires_description": "Atualmente a {{companyName}} não oferece nenhuma versão gratuita de código aberto da página de inscrição. Para obter acesso completo aos componentes da inscrição, você precisa adquirir uma licença comercial. Para uso pessoal, recomendamos o Prisma Data Platform ou outras interfaces em Postgres para criação de contas.",
|
||||
@@ -1089,6 +1098,7 @@
|
||||
"developer_documentation": "Documentação do desenvolvedor",
|
||||
"get_in_touch": "Entrar em contato",
|
||||
"contact_support": "Fale com o suporte",
|
||||
"community_support": "Suporte da comunidade",
|
||||
"feedback": "Comentário",
|
||||
"submitted_feedback": "Agradecemos o seu comentário!",
|
||||
"feedback_error": "Erro ao enviar comentário",
|
||||
@@ -1214,6 +1224,7 @@
|
||||
"organizer_name_variable": "Nome do organizador",
|
||||
"app_upgrade_description": "Para usar este recurso, atualize para uma conta Pro.",
|
||||
"invalid_number": "Número de telefone inválido",
|
||||
"invalid_url_error_message": "URL inválida para {{label}}. URL de amostra: {{sampleUrl}}",
|
||||
"navigate": "Navegar",
|
||||
"open": "Abrir",
|
||||
"close": "Fechar",
|
||||
@@ -1277,6 +1288,7 @@
|
||||
"personal_cal_url": "Meu URL pessoal da {{appName}}",
|
||||
"bio_hint": "Escreva algumas frases sobre você. Isso aparecerá no URL da sua página pessoal.",
|
||||
"user_has_no_bio": "Este usuário ainda não adicionou uma biografia.",
|
||||
"bio": "Biografia",
|
||||
"delete_account_modal_title": "Excluir conta",
|
||||
"confirm_delete_account_modal": "Tem certeza de que deseja excluir sua conta {{appName}}?",
|
||||
"delete_my_account": "Excluir minha conta",
|
||||
@@ -1287,6 +1299,7 @@
|
||||
"select_calendars": "Selecionar em quais calendários você deseja verificar se há conflitos para evitar dupla reserva.",
|
||||
"check_for_conflicts": "Verificar se há conflitos",
|
||||
"view_recordings": "Ver gravações",
|
||||
"check_for_recordings": "Verificar gravações",
|
||||
"adding_events_to": "Adicionando eventos a",
|
||||
"follow_system_preferences": "Seguir preferências do sistema",
|
||||
"custom_brand_colors": "Cores personalizadas da marca",
|
||||
@@ -1531,6 +1544,7 @@
|
||||
"problem_registering_domain": "Houve um problema ao registrar o subdomínio, tente novamente ou fale com o administrador",
|
||||
"team_publish": "Publicar equipe",
|
||||
"number_text_notifications": "Número de telefone (notificações via texto)",
|
||||
"number_sms_notifications": "Número de telefone (notificações SMS)",
|
||||
"attendee_email_variable": "E-mail do participante",
|
||||
"attendee_email_info": "O e-mail da pessoa que fez a reserva",
|
||||
"kbar_search_placeholder": "Digite um comando ou procure...",
|
||||
@@ -1595,6 +1609,7 @@
|
||||
"options": "Opções",
|
||||
"enter_option": "insira a opção {{index}}",
|
||||
"add_an_option": "Adicionar uma opção",
|
||||
"location_already_exists": "Este local já existe. Escolha um novo local",
|
||||
"radio": "Rádio",
|
||||
"google_meet_warning": "Para usar o Google Meet, é preciso definir seu calendário de destino para o Google Calendar",
|
||||
"individual": "Individual",
|
||||
@@ -1614,6 +1629,7 @@
|
||||
"date_overrides_mark_all_day_unavailable_other": "Marcar indisponível nas datas selecionadas",
|
||||
"date_overrides_add_btn": "Adicionar substituição",
|
||||
"date_overrides_update_btn": "Atualizar substituição",
|
||||
"date_successfully_added": "Substituição de data adicionada com sucesso",
|
||||
"event_type_duplicate_copy_text": "Cópia de {{slug}}",
|
||||
"set_as_default": "Definir como padrão",
|
||||
"hide_eventtype_details": "Ocultar detalhes do tipo de evento",
|
||||
@@ -1640,6 +1656,7 @@
|
||||
"minimum_round_robin_hosts_count": "Número de hosts necessários para participar",
|
||||
"hosts": "Hosts",
|
||||
"upgrade_to_enable_feature": "É preciso criar uma equipe para ativar este recurso. Clique para criar uma.",
|
||||
"orgs_upgrade_to_enable_feature": "Você precisa atualizar para nosso plano empresarial para ativar esse recurso.",
|
||||
"new_attendee": "Novo participante",
|
||||
"awaiting_approval": "Aguardando aprovação",
|
||||
"requires_google_calendar": "Este aplicativo requer uma conexão com o Google Calendar",
|
||||
@@ -1744,6 +1761,7 @@
|
||||
"show_on_booking_page": "Mostrar na página de reservas",
|
||||
"get_started_zapier_templates": "Comece agora com modelos do Zapier",
|
||||
"team_is_unpublished": "Publicação de {{team}} cancelada",
|
||||
"org_is_unpublished_description": "Este link da organização não está disponível no momento. Entre em contato com o proprietário da organização ou peça que seja publicado.",
|
||||
"team_is_unpublished_description": "O link de {{entity}} não está disponível por enquanto. Fale com o proprietário de {{entity}} ou peça que seja publicado.",
|
||||
"team_member": "Membro da equipe",
|
||||
"a_routing_form": "Formulário de roteamento",
|
||||
@@ -1878,6 +1896,7 @@
|
||||
"edit_invite_link": "Editar configurações do link",
|
||||
"invite_link_copied": "Link de convite copiado",
|
||||
"invite_link_deleted": "Link de convite excluído",
|
||||
"api_key_deleted": "Chave da API excluída",
|
||||
"invite_link_updated": "Configurações do link de convite salvas",
|
||||
"link_expires_after": "Link configurado para expirar após...",
|
||||
"one_day": "1 dia",
|
||||
@@ -2010,7 +2029,13 @@
|
||||
"attendee_last_name_variable": "Sobrenome do participante",
|
||||
"attendee_first_name_info": "Nome da pessoa que fez a reserva",
|
||||
"attendee_last_name_info": "O sobrenome da pessoa que fez a reserva",
|
||||
"your_monthly_digest": "Seu Resumo Mensal",
|
||||
"member_name": "Nome do Membro",
|
||||
"most_popular_events": "Eventos Mais Populares",
|
||||
"summary_of_events_for_your_team_for_the_last_30_days": "Aqui está seu resumo dos eventos populares da sua equipe {{teamName}} nos últimos 30 dias",
|
||||
"me": "Eu",
|
||||
"monthly_digest_email": "E-mail de Resumo Mensal",
|
||||
"monthly_digest_email_for_teams": "E-mail de resumo mensal para as equipes",
|
||||
"verify_team_tooltip": "Verifique seu time para habilitar o envio de mensagens aos participantes",
|
||||
"member_removed": "Membro removido",
|
||||
"my_availability": "Minha disponibilidade",
|
||||
@@ -2040,13 +2065,41 @@
|
||||
"team_no_event_types": "Sua equipe não tem tipos de evento",
|
||||
"seat_options_doesnt_multiple_durations": "A opção de assento não é compatível com múltiplas durações",
|
||||
"include_calendar_event": "Incluir evento do calendário",
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Adicionou recentemente",
|
||||
"no_members_found": "Nenhum membro encontrado",
|
||||
"event_setup_length_error": "Configuração do evento: deve ter pelo menos 1 minuto de duração.",
|
||||
"availability_schedules": "Agendamentos de disponibilidade",
|
||||
"unauthorized": "Não autorizado",
|
||||
"access_cal_account": "{{clientName}} gostaria de acessar a sua conta {{appName}}",
|
||||
"select_account_team": "Selecionar conta ou equipe",
|
||||
"allow_client_to": "Isso permitirá ao {{clientName}}",
|
||||
"associate_with_cal_account": "Associar você com suas informações pessoais de {{clientName}}",
|
||||
"see_personal_info": "Veja suas informações pessoais, incluindo quaisquer informações pessoais disponibilizadas publicamente",
|
||||
"see_primary_email_address": "Veja seu endereço de e-mail principal",
|
||||
"connect_installed_apps": "Conecte-se aos seus aplicativos instalados",
|
||||
"access_event_type": "Ler, editar, excluir os seus tipos de eventos",
|
||||
"access_availability": "Ler, editar, excluir sua disponibilidade",
|
||||
"access_bookings": "Ler, editar, excluir suas reservas",
|
||||
"allow_client_to_do": "Permitir que {{clientName}} faça isto?",
|
||||
"oauth_access_information": "Ao clicar em Permitir, você autoriza que este aplicativo use suas informações, de acordo com os termos de serviço e política de privacidade. Você pode remover o acesso na App Store do {{appName}}.",
|
||||
"allow": "Permitir",
|
||||
"view_only_edit_availability_not_onboarded": "Este usuário não concluiu a integração. Não será possível definir a disponibilidade antes de concluir a integração.",
|
||||
"view_only_edit_availability": "Você está vendo a disponibilidade deste usuário. Você só pode editar sua disponibilidade.",
|
||||
"you_can_override_calendar_in_advanced_tab": "Você pode substituir isso por evento nas configurações Avançadas em cada tipo de evento.",
|
||||
"edit_users_availability": "Editar disponibilidade do usuário: {{username}}",
|
||||
"resend_invitation": "Reenviar convite",
|
||||
"invitation_resent": "O convite foi reenviado.",
|
||||
"add_client": "Adicionar cliente",
|
||||
"copy_client_secret_info": "Depois de copiar o segredo, você não poderá mais visualizá-lo",
|
||||
"add_new_client": "Adicionar novo cliente",
|
||||
"this_app_is_not_setup_already": "Este app ainda não foi configurado",
|
||||
"as_csv": "como CSV",
|
||||
"overlay_my_calendar": "Sobrepor meu calendário",
|
||||
"overlay_my_calendar_toc": "Ao conectar-se à seu Calendário, você aceita nossa política de privacidade e termos de uso. Você pode revogar o acesso a qualquer momento.",
|
||||
"view_overlay_calendar_events": "Veja seus eventos do calendário para evitar conflito nos agendamentos.",
|
||||
"lock_timezone_toggle_on_booking_page": "Bloquear fuso horário na página de reserva",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Bloquear o fuso horário na página de reservas, útil para eventos presenciais.",
|
||||
"extensive_whitelabeling": "Marca própria abrangente",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -849,6 +849,7 @@
|
||||
"next_step": "Ignorar passo",
|
||||
"prev_step": "Passo anterior",
|
||||
"install": "Instalar",
|
||||
"install_paid_app": "Subscrever",
|
||||
"installed": "Instalado",
|
||||
"active_install_one": "{{count}} instalação activa",
|
||||
"active_install_other": "{{count}} instalações activas",
|
||||
@@ -1097,6 +1098,7 @@
|
||||
"developer_documentation": "Documentação para programadores",
|
||||
"get_in_touch": "Entre em contacto",
|
||||
"contact_support": "Contacte o suporte",
|
||||
"community_support": "Apoio da Comunidade",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Obrigado pelo seu feedback!",
|
||||
"feedback_error": "Erro ao enviar feedback",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user