Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce6454915 | ||
|
|
93cb21f55a | ||
|
|
0aca769d06 | ||
|
|
c0bda6257e | ||
|
|
ae22246f05 | ||
|
|
e2931d9fa2 | ||
|
|
e6a8a31b53 | ||
|
|
ee86c751f7 | ||
|
|
073b212c05 | ||
|
|
cd8e2124c4 | ||
|
|
9dd36bd20e | ||
|
|
b1404356ff | ||
|
|
f467bb7957 | ||
|
|
597b14cf4c | ||
|
|
2bdd6df755 | ||
|
|
ad958f10bd | ||
|
|
e5eb7c3906 | ||
|
|
667d42d5da | ||
|
|
a23a9ad15a | ||
|
|
4482ffc66b | ||
|
|
493efabd2e | ||
|
|
a0355e276d | ||
|
|
b5c993e153 | ||
|
|
da3ee3f1fe | ||
|
|
d9fc595444 | ||
|
|
06d13e0f6e | ||
|
|
7dc3266133 | ||
|
|
2c96444058 | ||
|
|
39c48e7f79 | ||
|
|
b6e1989f17 | ||
|
|
45b5b2a376 | ||
|
|
05726ec94c | ||
|
|
6340ff43d8 | ||
|
|
e64b26c7e1 | ||
|
|
c829a7de82 | ||
|
|
0205bfb066 | ||
|
|
5fd7d63388 | ||
|
|
e1ea2cb532 | ||
|
|
88d151aa35 | ||
|
|
4e7c93a322 | ||
|
|
281c7628c1 | ||
|
|
1b24bc14cd | ||
|
|
be3d2fca05 | ||
|
|
327e11e7c7 | ||
|
|
bd999345f4 | ||
|
|
081776b4f9 | ||
|
|
8c328ce5d7 | ||
|
|
0fa2a6aea3 | ||
|
|
440251f9c3 | ||
|
|
7f6847f7c7 | ||
|
|
9f1fa83a97 | ||
|
|
f00b112792 | ||
|
|
5339a489f5 | ||
|
|
f7df204e0b | ||
|
|
4975aba5e0 | ||
|
|
29d61ad67b |
@@ -160,9 +160,6 @@ CLOSECOM_API_KEY=
|
||||
# Sendgrid internal sync service
|
||||
SENDGRID_SYNC_API_KEY=
|
||||
|
||||
# Sentry
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
||||
|
||||
# Change your Brand
|
||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
# the repo. Unless a later match takes precedence,
|
||||
# @global-owner1 and @global-owner2 will be requested for
|
||||
# review when someone opens a pull request.
|
||||
* @calcom/reviewers
|
||||
# ⬇️ Removed so we can manually trigger checks
|
||||
# * @calcom/reviewers
|
||||
|
||||
.github/ @calcom/core
|
||||
.husky/ @calcom/core
|
||||
|
||||
@@ -20,4 +20,4 @@ jobs:
|
||||
days-before-pr-stale: 14
|
||||
days-before-pr-close: 7
|
||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued.
|
||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||
|
||||
@@ -2,7 +2,6 @@ name: PR Update
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [review_requested]
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
|
||||
@@ -68,5 +68,3 @@ public/embed
|
||||
|
||||
# Copied app-store images
|
||||
public/app-store
|
||||
# Sentry
|
||||
.sentryclirc
|
||||
|
||||
@@ -238,7 +238,7 @@ const Component = ({
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="prose-sm prose prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
|
||||
<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>
|
||||
|
||||
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||
<>
|
||||
{heading && (
|
||||
<div className="flex flex-col gap-6 rounded-md border p-7">
|
||||
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
|
||||
<ShellSubHeading
|
||||
title={t("calendar")}
|
||||
subtitle={t("installed_app_calendar_description")}
|
||||
|
||||
@@ -258,7 +258,12 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
};
|
||||
|
||||
const title = booking.title;
|
||||
const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||
// To be used after we run query on legacy bookings
|
||||
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||
|
||||
const showRecordingsButtons =
|
||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useRouter } from "next/router";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button, TextArea } from "@calcom/ui";
|
||||
@@ -34,7 +33,6 @@ export default function CancelBooking(props: Props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||
useTheme(props.theme);
|
||||
|
||||
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
||||
if (node !== null) {
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
|
||||
@@ -105,8 +104,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
|
||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||
|
||||
/*
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (top !== window) {
|
||||
//page_view will be collected automatically by _middleware.ts
|
||||
telemetry.event(
|
||||
@@ -114,7 +114,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
||||
);
|
||||
}
|
||||
}, [telemetry]);
|
||||
}, [telemetry]); */
|
||||
const embedUiConfig = useEmbedUiConfig();
|
||||
// get dynamic user list here
|
||||
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||
|
||||
@@ -39,7 +39,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
|
||||
@@ -252,13 +252,13 @@ const BookingPage = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (top !== window) {
|
||||
/* if (top !== window) {
|
||||
//page_view will be collected automatically by _middleware.ts
|
||||
telemetry.event(
|
||||
telemetryEventTypes.embedView,
|
||||
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
||||
);
|
||||
}
|
||||
} */
|
||||
reserveSlot();
|
||||
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
|
||||
return () => {
|
||||
@@ -547,7 +547,7 @@ const BookingPage = ({
|
||||
<div
|
||||
className={classNames(
|
||||
"main",
|
||||
isBackgroundTransparent ? "" : "bg-default",
|
||||
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
|
||||
"border-booker sm:border-booker-width rounded-md"
|
||||
)}>
|
||||
<div className="sm:flex">
|
||||
|
||||
@@ -75,8 +75,8 @@ export function CalendarSwitch(props: {
|
||||
/>
|
||||
{props.defaultSelected && (
|
||||
<Badge variant="gray">
|
||||
<div className="flex">
|
||||
<ArrowLeft className="text-default mr-1" /> {t("adding_events_to")}
|
||||
<div className="flex items-center">
|
||||
<ArrowLeft className="text-default mr-1 h-4 w-4" /> {t("adding_events_to")}
|
||||
</div>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -79,7 +79,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
|
||||
const ActionButtons = () => {
|
||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||
<div className="rlt:space-x-reverse ms-2 me-2 mt-px flex flex-row space-x-2 rtl:space-x-reverse">
|
||||
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setOpenDialogSaveUsername(true)}
|
||||
@@ -123,7 +123,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
autoCapitalize="none"
|
||||
autoCorrect="none"
|
||||
className={classNames(
|
||||
"mb-0 mt-0 rounded-md ltr:rounded-l-none rtl:rounded-r-none",
|
||||
"mb-0 mt-0 rounded-md rounded-l-none",
|
||||
markAsError
|
||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
||||
: ""
|
||||
|
||||
@@ -27,7 +27,8 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children
|
||||
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
|
||||
Component: NextAppProps["Component"] & {
|
||||
requiresLicense?: boolean;
|
||||
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||
isThemeSupported?: boolean;
|
||||
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
|
||||
};
|
||||
|
||||
@@ -61,45 +62,66 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
||||
return <I18nextAdapter {...passedProps} />;
|
||||
};
|
||||
|
||||
const enum ThemeSupport {
|
||||
// e.g. Login Page
|
||||
None = "none",
|
||||
// Entire App except Booking Pages
|
||||
App = "systemOnly",
|
||||
// Booking Pages(including Routing Forms)
|
||||
Booking = "userConfigured",
|
||||
}
|
||||
|
||||
const CalcomThemeProvider = (
|
||||
props: PropsWithChildren<
|
||||
WithNonceProps & { isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean) }
|
||||
WithNonceProps & {
|
||||
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||
isThemeSupported?: boolean;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
|
||||
// undefined or true means we use system theme
|
||||
const router = useRouter();
|
||||
const isThemeSupported = (() => {
|
||||
if (typeof props.isThemeSupported === "function") {
|
||||
return props.isThemeSupported({ router: router });
|
||||
const isBookingPage = (() => {
|
||||
if (typeof props.isBookingPage === "function") {
|
||||
return props.isBookingPage({ router: router });
|
||||
}
|
||||
if (typeof props.isThemeSupported === "undefined") {
|
||||
return true;
|
||||
}
|
||||
return props.isThemeSupported;
|
||||
|
||||
return props.isBookingPage;
|
||||
})();
|
||||
|
||||
const forcedTheme = !isThemeSupported ? "light" : undefined;
|
||||
const themeSupport = isBookingPage
|
||||
? ThemeSupport.Booking
|
||||
: // if isThemeSupported is explicitly false, we don't use theme there
|
||||
props.isThemeSupported === false
|
||||
? ThemeSupport.None
|
||||
: ThemeSupport.App;
|
||||
|
||||
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
||||
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
||||
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
||||
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
||||
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
||||
const isEmbedMode = typeof embedNamespace === "string";
|
||||
// If embedNamespace is not defined, we use the default storageKey -> The default storage key changs based on if we force light mode or not
|
||||
// This is done to ensure that the default theme is light when we force light mode and as soon as you navigate to a page that is dark we dont need a hard refresh to change
|
||||
|
||||
const storageKey = isEmbedMode
|
||||
? `embed-theme-${embedNamespace}`
|
||||
: !isThemeSupported
|
||||
? "cal-light"
|
||||
: "theme";
|
||||
: themeSupport === ThemeSupport.App
|
||||
? "app-theme"
|
||||
: themeSupport === ThemeSupport.Booking
|
||||
? "booking-theme"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
nonce={props.nonce}
|
||||
enableColorScheme={false}
|
||||
enableSystem={isThemeSupported}
|
||||
enableSystem={themeSupport !== ThemeSupport.None}
|
||||
forcedTheme={forcedTheme}
|
||||
storageKey={storageKey}
|
||||
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
|
||||
// This is how login to dashboard soft navigation changes theme from light to dark
|
||||
key={storageKey}
|
||||
attribute="class">
|
||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||
@@ -134,7 +156,8 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
<TooltipProvider>
|
||||
<CalcomThemeProvider
|
||||
nonce={props.pageProps.nonce}
|
||||
isThemeSupported={props.Component.isThemeSupported}>
|
||||
isThemeSupported={props.Component.isThemeSupported}
|
||||
isBookingPage={props.Component.isBookingPage}>
|
||||
<FeatureFlagsProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</FeatureFlagsProvider>
|
||||
|
||||
+1
-4
@@ -13,9 +13,6 @@ function getCspPolicy(nonce: string) {
|
||||
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
||||
// Maybe see how @next-safe/middleware does it if it's supported.
|
||||
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
||||
const SENTRY_ENDPOINT = process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
? new URL(process.env.NEXT_PUBLIC_SENTRY_DSN, "http://base_url").origin
|
||||
: "";
|
||||
|
||||
// We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com
|
||||
// FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others
|
||||
@@ -36,7 +33,7 @@ function getCspPolicy(nonce: string) {
|
||||
} app.cal.com;
|
||||
font-src 'self';
|
||||
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
|
||||
connect-src 'self' ${SENTRY_ENDPOINT}
|
||||
connect-src 'self'
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+1
-19
@@ -1,6 +1,5 @@
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
const os = require("os");
|
||||
|
||||
const { withAxiom } = require("next-axiom");
|
||||
@@ -318,21 +317,4 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const sentryWebpackPluginOptions = {
|
||||
silent: true, // Suppresses all logs
|
||||
};
|
||||
|
||||
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
nextConfig.sentry = {
|
||||
hideSourceMaps: true,
|
||||
// Prevents Sentry from running on this Edge function, where Sentry doesn't work yet (build whould crash the api route).
|
||||
excludeServerRoutes: [/\/api\/social\/og\/image\/?/],
|
||||
};
|
||||
}
|
||||
|
||||
// Sentry should be the last thing to export to catch everything right
|
||||
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
|
||||
: moduleExports;
|
||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.7.18",
|
||||
"version": "2.8.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -57,7 +57,6 @@
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
"@radix-ui/react-toggle-group": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"@sentry/nextjs": "^7.20.0",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tanstack/react-query": "^4.3.9",
|
||||
|
||||
@@ -3,7 +3,6 @@ import classNames from "classnames";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import {
|
||||
@@ -24,7 +23,6 @@ import defaultEvents, {
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -93,14 +91,15 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
const query = { ...router.query };
|
||||
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
if (top !== window) {
|
||||
//page_view will be collected automatically by _middleware.ts
|
||||
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
|
||||
}
|
||||
}, [telemetry, router.asPath]);
|
||||
}, [telemetry, router.asPath]); */
|
||||
const isEventListEmpty = eventTypes.length === 0;
|
||||
return (
|
||||
<>
|
||||
@@ -196,6 +195,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
);
|
||||
}
|
||||
|
||||
User.isBookingPage = true;
|
||||
|
||||
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
return (
|
||||
await prisma.eventType.findMany({
|
||||
|
||||
@@ -54,6 +54,8 @@ export default function Type(props: AvailabilityPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
Type.isBookingPage = true;
|
||||
|
||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
// load server side dependencies
|
||||
|
||||
@@ -69,6 +69,8 @@ export default function Book(props: BookPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
Book.isBookingPage = true;
|
||||
|
||||
const querySchema = z.object({
|
||||
bookingUid: z.string().optional(),
|
||||
count: z.coerce.number().optional(),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* Typescript class based component for custom-error
|
||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
*/
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import type { NextPage, NextPageContext } from "next";
|
||||
import type { ErrorProps } from "next/error";
|
||||
import NextError from "next/error";
|
||||
@@ -50,7 +49,6 @@ const CustomError: NextPage<CustomErrorProps> = (props) => {
|
||||
*/
|
||||
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
|
||||
const { res, err, asPath } = ctx;
|
||||
await Sentry.captureUnderscoreErrorException(ctx);
|
||||
const errorInitialProps = (await NextError.getInitialProps({
|
||||
res,
|
||||
err,
|
||||
|
||||
@@ -15,7 +15,7 @@ type AppPageType = {
|
||||
getServerSideProps: AppGetServerSideProps;
|
||||
// A component than can accept any properties
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
default: ((props: any) => JSX.Element) & Pick<AppProps["Component"], "isThemeSupported" | "getLayout">;
|
||||
default: ((props: any) => JSX.Element) & Pick<AppProps["Component"], "isBookingPage" | "getLayout">;
|
||||
};
|
||||
|
||||
type Found = {
|
||||
@@ -70,17 +70,17 @@ const AppPage: AppPageType["default"] = function AppPage(props) {
|
||||
return <route.Component {...componentProps} />;
|
||||
};
|
||||
|
||||
AppPage.isThemeSupported = ({ router }) => {
|
||||
AppPage.isBookingPage = ({ router }) => {
|
||||
const route = getRoute(router.query.slug as string, router.query.pages as string[]);
|
||||
if (route.notFound) {
|
||||
return false;
|
||||
}
|
||||
const isThemeSupported = route.Component.isThemeSupported;
|
||||
if (typeof isThemeSupported === "function") {
|
||||
return isThemeSupported({ router });
|
||||
const isBookingPage = route.Component.isBookingPage;
|
||||
if (typeof isBookingPage === "function") {
|
||||
return isBookingPage({ router });
|
||||
}
|
||||
|
||||
return !!isThemeSupported;
|
||||
return !!isBookingPage;
|
||||
};
|
||||
|
||||
AppPage.getLayout = (page, router) => {
|
||||
|
||||
@@ -41,7 +41,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -177,7 +176,7 @@ export default function Success(props: SuccessProps) {
|
||||
// - Event Type has require confirmation option enabled always
|
||||
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
||||
// - It's a paid event and payment is pending.
|
||||
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
|
||||
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
|
||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||
|
||||
const isCancelled =
|
||||
@@ -186,13 +185,13 @@ export default function Success(props: SuccessProps) {
|
||||
(!!seatReferenceUid &&
|
||||
!bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid));
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
// const telemetry = useTelemetry();
|
||||
/* useEffect(() => {
|
||||
if (top !== window) {
|
||||
//page_view will be collected automatically by _middleware.ts
|
||||
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/booking"));
|
||||
}
|
||||
}, [telemetry]);
|
||||
}, [telemetry]); */
|
||||
|
||||
useEffect(() => {
|
||||
const users = eventType.users;
|
||||
@@ -269,7 +268,9 @@ export default function Success(props: SuccessProps) {
|
||||
return t("emailed_you_and_attendees" + titleSuffix);
|
||||
}
|
||||
|
||||
useTheme(isSuccessBookingPage ? props.profile.theme : "light");
|
||||
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
|
||||
// As Booking Page it has to support configured theme, but as booking detail page it should not do any change. Let Shell.tsx handle it.
|
||||
useTheme(isSuccessBookingPage ? props.profile.theme : undefined);
|
||||
useBrandColors({
|
||||
brandColor: props.profile.brandColor,
|
||||
darkBrandColor: props.profile.darkBrandColor,
|
||||
@@ -706,6 +707,8 @@ export default function Success(props: SuccessProps) {
|
||||
);
|
||||
}
|
||||
|
||||
Success.isBookingPage = true;
|
||||
|
||||
type RecurringBookingsProps = {
|
||||
eventType: SuccessProps["eventType"];
|
||||
recurringBookings: SuccessProps["recurringBookings"];
|
||||
|
||||
@@ -24,6 +24,8 @@ export default function Type(props: DynamicAvailabilityPageProps) {
|
||||
return <AvailabilityPage {...props} />;
|
||||
}
|
||||
|
||||
Type.isBookingPage = true;
|
||||
|
||||
const querySchema = z.object({
|
||||
link: z.string().optional().default(""),
|
||||
slug: z.string().optional().default(""),
|
||||
|
||||
@@ -19,6 +19,8 @@ export default function Book(props: HashLinkPageProps) {
|
||||
return <BookingPage {...props} />;
|
||||
}
|
||||
|
||||
Book.isBookingPage = true;
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const ssr = await ssrInit(context);
|
||||
const link = asStringOrThrow(context.query.link as string);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@calcom/features/ee/users/pages/users-edit-view";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@calcom/features/ee/users/pages/users-add-view";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@calcom/features/ee/users/pages/users-listing-view";
|
||||
@@ -95,7 +95,17 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-default mx-2 px-4 py-8 shadow sm:rounded-lg sm:px-10">
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(signUp)} className="bg-default space-y-6">
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (methods.formState?.errors?.apiError) {
|
||||
methods.clearErrors("apiError");
|
||||
}
|
||||
methods.handleSubmit(signUp)(event);
|
||||
}}
|
||||
className="bg-default space-y-6">
|
||||
{errors.apiError && <Alert severity="error" message={errors.apiError?.message} />}
|
||||
<div className="space-y-2">
|
||||
<TextField
|
||||
|
||||
@@ -203,4 +203,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
};
|
||||
|
||||
TeamPage.isBookingPage = true;
|
||||
|
||||
export default TeamPage;
|
||||
|
||||
@@ -24,6 +24,8 @@ export default function TeamType(props: AvailabilityTeamPageProps) {
|
||||
return <AvailabilityPage {...props} />;
|
||||
}
|
||||
|
||||
TeamType.isBookingPage = true;
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const slugParam = asStringOrNull(context.query.slug);
|
||||
const typeParam = asStringOrNull(context.query.type);
|
||||
|
||||
@@ -23,6 +23,7 @@ export type TeamBookingPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export default function TeamBookingPage(props: TeamBookingPageProps) {
|
||||
return <BookingPage {...props} />;
|
||||
}
|
||||
TeamBookingPage.isBookingPage = true;
|
||||
|
||||
const querySchema = z.object({
|
||||
rescheduleUid: z.string().optional(),
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "اسم المستخدم الحالي",
|
||||
"example_1": "مثال 1",
|
||||
"example_2": "مثال 2",
|
||||
"additional_input_label": "تسمية الإدخال الاضافي",
|
||||
"company_size": "حجم الشركة",
|
||||
"what_help_needed": "ما الذي تحتاج إلى مساعدة بشأنه؟",
|
||||
"variable_format": "تنسيق المتغير",
|
||||
"webhook_subscriber_url_reserved": "رابط مشترك Webhook معرّف بالفعل",
|
||||
"custom_input_as_variable_info": "تجاهل جميع الأحرف الخاصة من تسمية الإدخال الإضافي (استخدم الأحرف والأرقام فقط)، واستخدم الحروف الكبيرة لجميع الأحرف واستبدل المساحات البيضاء بالشرط.",
|
||||
"using_additional_inputs_as_variables": "كيف أستخدم مدخلات إضافية كمتغيرات؟",
|
||||
"download_desktop_app": "تحميل تطبيق سطح المكتب",
|
||||
"set_ping_link": "تعيين رابط Ping",
|
||||
"rate_limit_exceeded": "تم تجاوز حد السعر",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "إضافة حد",
|
||||
"team_name_required": "اسم الفريق مطلوب",
|
||||
"show_attendees": "مشاركة معلومات الحضور مع الضيوف",
|
||||
"how_additional_inputs_as_variables": "كيفية استخدام مدخلات إضافية كمتغيرات",
|
||||
"format": "التنسيق",
|
||||
"uppercase_for_letters": "استخدم الحروف الكبيرة لجميع الأحرف",
|
||||
"replace_whitespaces_underscores": "استبدل المساحات البيضاء بالشرطات السفلية",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "هل تحتاج إلى أي شيء آخر؟",
|
||||
"billing_help_description": "إذا كنت تحتاج إلى أي مساعدة إضافية بخصوص الفوترة، فإن فريق الدعم لدينا في انتظارك لتقديم المساعدة.",
|
||||
"billing_help_cta": "الاتصال بالدعم",
|
||||
"ignore_special_characters": "تجاهل الأحرف الخاصة في تسمية الإدخال الإضافية. استخدم فقط الأحرف والأرقام",
|
||||
"retry": "إعادة المحاولة",
|
||||
"fetching_calendars_error": "حدثت مشكلة أثناء جلب التقويمات الخاصة بك. يرجى <1>إعادة المحاولة</1> أو الاتصال بدعم العملاء.",
|
||||
"calendar_connection_fail": "فشل الاتصال بالتقويم",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "رمز التحقق",
|
||||
"can_you_try_again": "هلّا جربت المحاولة ثانيةً في وقت مختلف؟",
|
||||
"verify": "التحقق",
|
||||
"timezone_variable": "المنطقة الزمنية",
|
||||
"invalid_event_name_variables": "ثمة متغيّر غير غير صالح في اسم فعاليتك",
|
||||
"select_all": "اختيار الكل",
|
||||
"default_conferencing_bulk_title": "التحديث الجماعي لمجموعة أنواع الفعاليات الحالية",
|
||||
"invalid_event_name_variables": "ثمة متغيّر غير غير صالح في اسم فعاليتك",
|
||||
"default_conferencing_bulk_description": "تحديث المواقع لأنواع الفعاليات المحددة"
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Současné uživatelské jméno",
|
||||
"example_1": "Příklad 1",
|
||||
"example_2": "Příklad 2",
|
||||
"additional_input_label": "Další vstupní štítek",
|
||||
"company_size": "Velikost společnosti",
|
||||
"what_help_needed": "S čím potřebujete pomoci?",
|
||||
"variable_format": "Formát proměnné",
|
||||
"webhook_subscriber_url_reserved": "Adresa URL odběratele webhooku je již definována",
|
||||
"custom_input_as_variable_info": "Ignorujte všechny speciální znaky doplňkového vstupního štítku (používejte pouze písmena a číslice), používejte velká písmena a bílé znaky nahraďte podtržítky.",
|
||||
"using_additional_inputs_as_variables": "Jak používat doplňkové vstupy jako proměnné?",
|
||||
"download_desktop_app": "Stáhněte si aplikaci pro stolní počítače",
|
||||
"set_ping_link": "Nastavení odkazu Ping",
|
||||
"rate_limit_exceeded": "Byl překročen limit rychlosti",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Přidat limit",
|
||||
"team_name_required": "Je vyžadován název týmu",
|
||||
"show_attendees": "Sdílet informace o účastnících",
|
||||
"how_additional_inputs_as_variables": "Jak používat doplňkové vstupy jako proměnné",
|
||||
"format": "Formát",
|
||||
"uppercase_for_letters": "Použít velká písmena pro všechna písmena",
|
||||
"replace_whitespaces_underscores": "Nahradit mezery podtržítky",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Potřebujete ještě něco?",
|
||||
"billing_help_description": "Pokud potřebujete jakoukoliv pomoc s fakturací, náš tým zákaznické podpory je tu pro Vás.",
|
||||
"billing_help_cta": "Kontaktovat podporu",
|
||||
"ignore_special_characters": "Ignorovat speciální znaky ve štítku Dodatečný vstup. Použít pouze písmena a čísla",
|
||||
"retry": "Opakovat",
|
||||
"fetching_calendars_error": "Při ukládání vašich údajů došlo k problému. Zkuste to znovu, <1>případně</1> se obraťte na zákaznickou podporu.",
|
||||
"calendar_connection_fail": "Připojení kalendáře se nezdařilo",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Ověřovací kód",
|
||||
"can_you_try_again": "Můžete to zkusit znovu a použít jiný čas?",
|
||||
"verify": "Ověřit",
|
||||
"timezone_variable": "Časová zóna",
|
||||
"invalid_event_name_variables": "V názvu události je neplatná proměnná",
|
||||
"select_all": "Vybrat vše",
|
||||
"default_conferencing_bulk_title": "Hromadná aktualizace stávajících typů událostí",
|
||||
"invalid_event_name_variables": "V názvu události je neplatná proměnná",
|
||||
"default_conferencing_bulk_description": "Aktualizujte místa pro vybrané typy událostí"
|
||||
}
|
||||
|
||||
@@ -1122,13 +1122,11 @@
|
||||
"current_username": "Nuværende brugernavn",
|
||||
"example_1": "Eksempel 1",
|
||||
"example_2": "Eksempel 2",
|
||||
"additional_input_label": "Ekstra Input Label",
|
||||
"company_size": "Firma størrelse",
|
||||
"what_help_needed": "Hvad har du brug for hjælp til?",
|
||||
"variable_format": "Variabelt format",
|
||||
"webhook_subscriber_url_reserved": "Webhook abonnent url er allerede defineret",
|
||||
"custom_input_as_variable_info": "Ignorér alle specialtegn i den ekstra input label (brug kun bogstaver og tal), brug store bogstaver for alle bogstaver og erstat blanke tegn med understregninger.",
|
||||
"using_additional_inputs_as_variables": "Hvordan bruger jeg Ekstra Inputs som variabler?",
|
||||
"download_desktop_app": "Download desktop app",
|
||||
"set_ping_link": "Angiv Ping link",
|
||||
"rate_limit_exceeded": "Satsgrænsen er overskredet",
|
||||
@@ -1338,7 +1336,6 @@
|
||||
"add_limit": "Tilføj Begrænsning",
|
||||
"team_name_required": "Teamnavn påkrævet",
|
||||
"show_attendees": "Del deltagerinformation mellem gæster",
|
||||
"how_additional_inputs_as_variables": "Sådan bruger du Ekstra Inputs som Variabler",
|
||||
"format": "Formatér",
|
||||
"uppercase_for_letters": "Brug store bogstaver for alle bogstaver",
|
||||
"replace_whitespaces_underscores": "Erstat blanke tegn med understregninger",
|
||||
@@ -1353,7 +1350,6 @@
|
||||
"billing_help_title": "Brug for noget andet?",
|
||||
"billing_help_description": "Hvis du har brug for yderligere hjælp til fakturering, er vores supportteam her for at hjælpe.",
|
||||
"billing_help_cta": "Kontakt support",
|
||||
"ignore_special_characters": "Ignorér specialtegn i din Ekstra Input Label. Brug kun bogstaver og tal",
|
||||
"retry": "Prøv igen",
|
||||
"fetching_calendars_error": "Der opstod et problem med at hente dine kalendere. Venligst <1>prøv igen</1> eller kontakt kundeservice.",
|
||||
"calendar_connection_fail": "Forbindelse til kalender mislykkedes",
|
||||
@@ -1619,5 +1615,6 @@
|
||||
"no_responses_yet": "Ingen svar endnu",
|
||||
"this_will_be_the_placeholder": "Dette vil være pladsholderen",
|
||||
"verification_code": "Bekræftelseskode",
|
||||
"verify": "Bekræft"
|
||||
"verify": "Bekræft",
|
||||
"timezone_variable": "Tidszone"
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Aktueller Benutzername",
|
||||
"example_1": "Beispiel 1",
|
||||
"example_2": "Beispiel 2",
|
||||
"additional_input_label": "Zusätzliches Eingabefeld",
|
||||
"company_size": "Unternehmensgröße",
|
||||
"what_help_needed": "Wobei benötigen Sie Hilfe?",
|
||||
"variable_format": "Variablenformat",
|
||||
"webhook_subscriber_url_reserved": "Webhook Abonnenten-URL ist bereits definiert",
|
||||
"custom_input_as_variable_info": "Ignorieren Sie alle Sonderzeichen der zusätzlichen Eingabebezeichnung (verwenden Sie nur Buchstaben und Zahlen), schreiben Sie alle Buchstaben groß und ersetzen Sie Leerzeichen durch Unterstriche.",
|
||||
"using_additional_inputs_as_variables": "Wie kann ich zusätzliche Eingaben als Variablen verwenden?",
|
||||
"download_desktop_app": "Desktop-App herunterladen",
|
||||
"set_ping_link": "Ping-Link festlegen",
|
||||
"rate_limit_exceeded": "Anmeldeversuchsgrenze überschritten",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Begrenzung hinzufügen",
|
||||
"team_name_required": "Teamname erforderlich",
|
||||
"show_attendees": "Teilnehmerinformationen zwischen Gästen teilen",
|
||||
"how_additional_inputs_as_variables": "Wie man zusätzliche Eingaben als Variablen verwendet",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Großbuchstaben für alle Buchstaben verwenden",
|
||||
"replace_whitespaces_underscores": "Leerzeichen durch Unterstriche ersetzen",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Brauchen Sie etwas anderes?",
|
||||
"billing_help_description": "Wenn Sie weitere Hilfe bei der Rechnungsstellung benötigen, hilft Ihnen unser Support-Team gerne weiter.",
|
||||
"billing_help_cta": "Support kontaktieren",
|
||||
"ignore_special_characters": "Ignorieren Sie Sonderzeichen in Ihrer zusätzlichen Eingabebezeichnung. Verwenden Sie nur Buchstaben und Zahlen",
|
||||
"retry": "Erneut versuchen",
|
||||
"fetching_calendars_error": "Beim Abrufen Ihrer Kalender ist ein Problem aufgetreten. Bitte <1>versuchen Sie es erneut</1> oder wenden Sie sich an den Support.",
|
||||
"calendar_connection_fail": "Kalenderverbindung fehlgeschlagen",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Bestätigungscode",
|
||||
"can_you_try_again": "Können Sie es zu einem anderen Zeitpunkt erneut versuchen?",
|
||||
"verify": "Verifizieren",
|
||||
"timezone_variable": "Zeitzone",
|
||||
"invalid_event_name_variables": "Es gibt eine ungültige Variable in Ihrem Terminnamen",
|
||||
"select_all": "Alle auswählen",
|
||||
"default_conferencing_bulk_title": "Massenaktualisierung vorhandener Termintypen",
|
||||
"invalid_event_name_variables": "Es gibt eine ungültige Variable in Ihrem Terminnamen",
|
||||
"default_conferencing_bulk_description": "Orte für die ausgewählten Termintypen aktualisieren"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"identity_provider": "Identity provider",
|
||||
"trial_days_left": "You have $t(day, {\"count\": {{days}} }) left on your PRO trial",
|
||||
"day_one": "{{count}} day",
|
||||
"day_other": "{{count}} days",
|
||||
@@ -1149,13 +1150,13 @@
|
||||
"current_username": "Current username",
|
||||
"example_1": "Example 1",
|
||||
"example_2": "Example 2",
|
||||
"additional_input_label": "Additional Input Label",
|
||||
"booking_question_identifier": "Booking Question Identifier",
|
||||
"company_size": "Company size",
|
||||
"what_help_needed": "What do you need help with?",
|
||||
"variable_format": "Variable format",
|
||||
"webhook_subscriber_url_reserved": "Webhook subscriber url is already defined",
|
||||
"custom_input_as_variable_info": "Ignore all special characters of the additional input label (use only letters and numbers), use uppercase for all letters and replace whitespaces with underscores.",
|
||||
"using_additional_inputs_as_variables": "How do I use Additional Inputs as variables?",
|
||||
"using_booking_questions_as_variables": "How do I use booking questions as variables?",
|
||||
"download_desktop_app": "Download desktop app",
|
||||
"set_ping_link": "Set Ping link",
|
||||
"rate_limit_exceeded": "Rate limit exceeded",
|
||||
@@ -1318,6 +1319,15 @@
|
||||
"exchange_authentication_standard": "Basic authentication",
|
||||
"exchange_authentication_ntlm": "NTLM authentication",
|
||||
"exchange_compression": "GZip compression",
|
||||
"exchange_version": "Exchange Version",
|
||||
"exchange_version_2007_SP1": "2007 SP1",
|
||||
"exchange_version_2010": "2010",
|
||||
"exchange_version_2010_SP1": "2010 SP1",
|
||||
"exchange_version_2010_SP2": "2010 SP2",
|
||||
"exchange_version_2013": "2013",
|
||||
"exchange_version_2013_SP1": "2013 SP1",
|
||||
"exchange_version_2015": "2015",
|
||||
"exchange_version_2016": "2016",
|
||||
"routing_forms_description": "Create forms to direct attendees to the correct destinations",
|
||||
"routing_forms_send_email_owner": "Send Email to Owner",
|
||||
"routing_forms_send_email_owner_description": "Sends an email to the owner when the form is submitted",
|
||||
@@ -1372,7 +1382,7 @@
|
||||
"add_limit": "Add Limit",
|
||||
"team_name_required": "Team name required",
|
||||
"show_attendees": "Share attendee information between guests",
|
||||
"how_additional_inputs_as_variables": "How to use Additional Inputs as Variables",
|
||||
"how_booking_questions_as_variables": "How to use booking questions as variables?",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Use uppercase for all letters",
|
||||
"replace_whitespaces_underscores": "Replace whitespaces with underscores",
|
||||
@@ -1387,7 +1397,7 @@
|
||||
"billing_help_title": "Need anything else?",
|
||||
"billing_help_description": "If you need any further help with billing, our support team are here to help.",
|
||||
"billing_help_cta": "Contact support",
|
||||
"ignore_special_characters": "Ignore special characters in your Additional Input label. Use only letters and numbers",
|
||||
"ignore_special_characters_booking_questions": "Ignore special characters in your booking question identifier. Use only letters and numbers",
|
||||
"retry": "Retry",
|
||||
"fetching_calendars_error": "There was a problem fetching your calendars. Please <1>try again</1> or reach out to customer support.",
|
||||
"calendar_connection_fail": "Calendar connection failed",
|
||||
@@ -1695,9 +1705,17 @@
|
||||
"verification_code": "Verification code",
|
||||
"can_you_try_again": "Can you try again with a different time?",
|
||||
"verify": "Verify",
|
||||
"timezone_variable": "Timezone",
|
||||
"timezone_info": "The timezone of the person receiving",
|
||||
"event_end_time_variable": "Event end time",
|
||||
"event_end_time_info": "The event end time",
|
||||
"cancel_url_variable": "Cancel URL",
|
||||
"cancel_url_info": "The URL to cancel the booking",
|
||||
"reschedule_url_variable": "Reschedule URL",
|
||||
"reschedule_url_info": "The URL to reschedule the booking",
|
||||
"invalid_event_name_variables": "There is an invalid variable in your event name",
|
||||
"select_all": "Select All",
|
||||
"default_conferencing_bulk_title": "Bulk update existing event types",
|
||||
"invalid_event_name_variables": "There is an invalid variable in your event name",
|
||||
"members_default_schedule": "Member's default schedule",
|
||||
"set_by_admin": "Set by team admin",
|
||||
"members_default_location": "Member's default location",
|
||||
@@ -1759,6 +1777,7 @@
|
||||
"collect_no_show_fee": "Collect no-show fee",
|
||||
"no_show_fee_charged": "No-show fee charged",
|
||||
"insights": "Insights",
|
||||
"testing_workflow_info_message": "When testing this workflow, be aware that Emails and SMS can only be scheduled at least 1 hour in advance",
|
||||
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
|
||||
"acknowledge_booking_no_show_fee": "I acknowledge that if I do not attend this event that a {{amount, currency}} no show fee will be applied to my card.",
|
||||
"card_details": "Card details",
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Nombre de usuario actual",
|
||||
"example_1": "Ejemplo 1",
|
||||
"example_2": "Ejemplo 2",
|
||||
"additional_input_label": "Etiqueta de entrada adicional",
|
||||
"company_size": "Tamaño de la empresa",
|
||||
"what_help_needed": "¿Con qué necesitas ayuda?",
|
||||
"variable_format": "Formato variable",
|
||||
"webhook_subscriber_url_reserved": "La URL del suscriptor de Webhook ya está definida",
|
||||
"custom_input_as_variable_info": "Ignore todos los caracteres especiales de la etiqueta de entrada adicional (utilice solo letras y números), utilice mayúsculas para todas las letras y sustituya los espacios en blanco por guiones bajos.",
|
||||
"using_additional_inputs_as_variables": "¿Cómo usar entradas adicionales como variables?",
|
||||
"download_desktop_app": "Descargar la aplicación de escritorio",
|
||||
"set_ping_link": "Establecer enlace Ping",
|
||||
"rate_limit_exceeded": "Límite de tasa excedido",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Agregue un límite",
|
||||
"team_name_required": "Nombre de equipo requerido",
|
||||
"show_attendees": "Comparta información de los asistentes entre los invitados",
|
||||
"how_additional_inputs_as_variables": "Cómo usar entradas adicionales como variables",
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Use mayúsculas para todas las letras",
|
||||
"replace_whitespaces_underscores": "Reemplace espacios en blanco con guiones bajos",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "¿Necesitas algo más?",
|
||||
"billing_help_description": "Si necesitas más ayuda con la facturación, nuestro equipo de soporte está a tu disposición.",
|
||||
"billing_help_cta": "Contacte con soporte",
|
||||
"ignore_special_characters": "Ignore caracteres especiales en su etiqueta de entrada adicional. Use solo letras y números",
|
||||
"retry": "Reintentar",
|
||||
"fetching_calendars_error": "Se produjo un problema al recuperar sus calendarios. <1>Inténtelo de nuevo</1> o contacte con el servicio de atención al cliente.",
|
||||
"calendar_connection_fail": "Error de conexión con el calendario",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Código de verificación",
|
||||
"can_you_try_again": "¿Puede intentarlo de nuevo con una hora diferente?",
|
||||
"verify": "Verificar",
|
||||
"timezone_variable": "Zona Horaria",
|
||||
"invalid_event_name_variables": "Hay una variable no válida en el nombre de su evento",
|
||||
"select_all": "Seleccionar todo",
|
||||
"default_conferencing_bulk_title": "Actualización masiva de tipos de eventos existentes",
|
||||
"invalid_event_name_variables": "Hay una variable no válida en el nombre de su evento",
|
||||
"default_conferencing_bulk_description": "Actualizar las ubicaciones para los tipos de eventos seleccionados"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"identity_provider": "Fournisseur d'identité",
|
||||
"trial_days_left": "Il vous reste $t(day, {\"count\": {{days}} }) pour votre essai Pro",
|
||||
"day_one": "{{count}} jour",
|
||||
"day_other": "{{count}} jours",
|
||||
@@ -820,7 +821,7 @@
|
||||
"number_apps_one": "{{count}} Application",
|
||||
"number_apps_other": "{{count}} applications",
|
||||
"trending_apps": "Applications populaires",
|
||||
"most_popular": "Le plus populaire",
|
||||
"most_popular": "Les plus populaires",
|
||||
"explore_apps": "Applications {{category}}",
|
||||
"installed_apps": "Apps installées",
|
||||
"free_to_use_apps": "Gratuit",
|
||||
@@ -1149,13 +1150,11 @@
|
||||
"current_username": "Nom d'utilisateur actuel",
|
||||
"example_1": "Exemple 1",
|
||||
"example_2": "Exemple 2",
|
||||
"additional_input_label": "Libellé du champ supplémentaire",
|
||||
"company_size": "Taille de l'entreprise",
|
||||
"what_help_needed": "Avez-vous besoin d'aide ?",
|
||||
"variable_format": "Format des variables",
|
||||
"webhook_subscriber_url_reserved": "Le lien de l'abonné webhook est déjà défini",
|
||||
"custom_input_as_variable_info": "Ignorer tous les caractères spéciaux du libellé de champ supplémentaire (utiliser uniquement des lettres et des chiffres), utiliser des majuscules pour toutes les lettres et remplacer les espaces par des tirets bas.",
|
||||
"using_additional_inputs_as_variables": "Comment utiliser des champs supplémentaires en tant que variables ?",
|
||||
"download_desktop_app": "Télécharger l'application de bureau",
|
||||
"set_ping_link": "Définir le lien Ping",
|
||||
"rate_limit_exceeded": "Limite dépassée",
|
||||
@@ -1318,6 +1317,15 @@
|
||||
"exchange_authentication_standard": "Authentification de base",
|
||||
"exchange_authentication_ntlm": "Authentification NTLM",
|
||||
"exchange_compression": "Compression GZip",
|
||||
"exchange_version": "Version d'Exchange",
|
||||
"exchange_version_2007_SP1": "2007 SP1",
|
||||
"exchange_version_2010": "2010",
|
||||
"exchange_version_2010_SP1": "2010 SP1",
|
||||
"exchange_version_2010_SP2": "2010 SP2",
|
||||
"exchange_version_2013": "2013",
|
||||
"exchange_version_2013_SP1": "2013 SP1",
|
||||
"exchange_version_2015": "2015",
|
||||
"exchange_version_2016": "2016",
|
||||
"routing_forms_description": "Créez des formulaires pour acheminer les participants vers les bonnes destinations.",
|
||||
"routing_forms_send_email_owner": "Envoyer un e-mail au propriétaire",
|
||||
"routing_forms_send_email_owner_description": "Envoie un e-mail au propriétaire lorsque le formulaire est soumis",
|
||||
@@ -1372,7 +1380,6 @@
|
||||
"add_limit": "Ajouter une limite",
|
||||
"team_name_required": "Nom d'équipe requis",
|
||||
"show_attendees": "Partagez les informations des participants entre les invités",
|
||||
"how_additional_inputs_as_variables": "Comment utiliser des champs supplémentaires en tant que variables",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
|
||||
"replace_whitespaces_underscores": "Remplacez les espaces par des tirets bas.",
|
||||
@@ -1387,7 +1394,6 @@
|
||||
"billing_help_title": "Besoin d'autre chose ?",
|
||||
"billing_help_description": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.",
|
||||
"billing_help_cta": "Contacter l'assistance",
|
||||
"ignore_special_characters": "Ignorez les caractères spéciaux dans le libellé du champ supplémentaire, utilisez uniquement les lettres et les chiffres.",
|
||||
"retry": "Réessayer",
|
||||
"fetching_calendars_error": "Un problème est survenu lors de la récupération de vos calendriers. Veuillez <1>réessayer</1> ou contacter le service client.",
|
||||
"calendar_connection_fail": "Échec de la connexion au calendrier",
|
||||
@@ -1695,9 +1701,10 @@
|
||||
"verification_code": "Code de vérification",
|
||||
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
|
||||
"verify": "Vérifier",
|
||||
"timezone_variable": "Fuseau horaire",
|
||||
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
|
||||
"select_all": "Tout sélectionner",
|
||||
"default_conferencing_bulk_title": "Mise à jour multiple des types d'événements existants",
|
||||
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
|
||||
"members_default_schedule": "Planning par défaut du membre",
|
||||
"set_by_admin": "Défini par l'administrateur de l'équipe",
|
||||
"members_default_location": "Lieu par défaut du membre",
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "שם משתמש נוכחי",
|
||||
"example_1": "דוגמה 1",
|
||||
"example_2": "דוגמה 2",
|
||||
"additional_input_label": "תווית קלט נוספת",
|
||||
"company_size": "גודל החברה",
|
||||
"what_help_needed": "במה דרושה לך עזרה?",
|
||||
"variable_format": "הפורמט של המשתנה",
|
||||
"webhook_subscriber_url_reserved": "כתובת ה-URL של מנוי Webhook כבר מוגדרת",
|
||||
"custom_input_as_variable_info": "התעלמות מכל התווים המיוחדים של תווית הקלט הנוספת (שימוש באותיות ובספרות בלבד), שימוש באותיות רישיות עבור כל האותיות והחלפת רווחים במקפים תחתונים.",
|
||||
"using_additional_inputs_as_variables": "איך ניתן להשתמש בסוגי קלט נוספים כמשתנים?",
|
||||
"download_desktop_app": "הורדת האפליקציה למחשבים שולחניים",
|
||||
"set_ping_link": "הגדרת קישור Ping",
|
||||
"rate_limit_exceeded": "חריגה מהגבלת קצב",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "הוספת הגבלה",
|
||||
"team_name_required": "נדרש שם צוות",
|
||||
"show_attendees": "שיתוף האורחים בפרטי המשתתפים",
|
||||
"how_additional_inputs_as_variables": "איך ניתן להשתמש בסוגי קלט נוספים כמשתנים",
|
||||
"format": "פורמט",
|
||||
"uppercase_for_letters": "שימוש באותיות רישיות עבור כל האותיות",
|
||||
"replace_whitespaces_underscores": "החלפת רווחים במקפים תחתונים",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "צריך/ה משהו נוסף?",
|
||||
"billing_help_description": "אם דרוש לך סיוע נוסף בענייני חיוב, צוות התמיכה שלנו כאן כדי לעזור.",
|
||||
"billing_help_cta": "פנייה לתמיכה",
|
||||
"ignore_special_characters": "להתעלם מתווים מיוחדים בתווית הקלט הנוספת. להשתמש באותיות ובספרות בלבד",
|
||||
"retry": "ניסיון נוסף",
|
||||
"fetching_calendars_error": "אירעה בעיה בטעינת לוחות השנה שלך. <1>נסה/י שוב</1> או פנה/י למחלקת תמיכת הלקוחות.",
|
||||
"calendar_connection_fail": "החיבור ללוח השנה נכשל",
|
||||
@@ -1657,9 +1653,10 @@
|
||||
"verification_code": "קוד אימות",
|
||||
"can_you_try_again": "אתה יכול לנסות שוב עם שעה אחרת?",
|
||||
"verify": "אמת",
|
||||
"timezone_variable": "אזור זמן",
|
||||
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך",
|
||||
"select_all": "בחר הכל",
|
||||
"default_conferencing_bulk_title": "בצע עדכון אצווה של סוגי הארועים הקיימים",
|
||||
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך",
|
||||
"requires_at_least_one_schedule": "אתה חייב שיהיה לך לפחות לוח זמנים אחד",
|
||||
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו",
|
||||
"looking_for_more_analytics": "מחפש עוד מידע אנליטי?",
|
||||
|
||||
@@ -217,5 +217,6 @@
|
||||
"additional_notes_variable": "Egyéb jegyzetek",
|
||||
"organizer_name_info": "Neved",
|
||||
"theme_light": "Világos",
|
||||
"theme_dark": "Sötét"
|
||||
"theme_dark": "Sötét",
|
||||
"timezone_variable": "Időzóna"
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hidden_team_owner_message": "Per utilizzare i team ti occorre un account Pro. Sarai nascosto finché non cambierai piano.",
|
||||
"link_expires": "p.s. Scade tra {{expiresIn}} ore.",
|
||||
"upgrade_to_per_seat": "Passa al piano Per-posto",
|
||||
"seat_options_doesnt_support_confirmation": "L'opzione Posti non supporta la richiesta di conferma",
|
||||
"seat_options_doesnt_support_confirmation": "L'opzione di prenotazione dei posti non è disponibile per le prenotazioni che richiedono conferma",
|
||||
"team_upgrade_seats_details": "Per i {{memberCount}} membri del tuo team, {{unpaidCount}} posti non sono pagati. A € {{seatPrice}}/m per posto, il costo totale stimato della tua adesione è di € {{totalCost}}/m.",
|
||||
"team_upgrade_banner_description": "Grazie per aver provato il nostro piano team. Abbiamo notato che è necessario effettuare l'upgrade del tuo team \"{{teamName}}\".",
|
||||
"team_upgrade_banner_action": "Effettua l'upgrade",
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Nome utente corrente",
|
||||
"example_1": "Esempio 1",
|
||||
"example_2": "Esempio 2",
|
||||
"additional_input_label": "Etichetta del campo di input aggiuntivo",
|
||||
"company_size": "Dimensione dell'azienda",
|
||||
"what_help_needed": "Per cosa ti serve assistenza?",
|
||||
"variable_format": "Formato variabile",
|
||||
"webhook_subscriber_url_reserved": "URL dell'abbonato Webhook già definito",
|
||||
"custom_input_as_variable_info": "Ignora tutti i caratteri speciali dell'etichetta del campo di input aggiuntivo (usa solo lettere e numeri), usa solo lettere maiuscole e sostituisci gli spazi con il trattino basso.",
|
||||
"using_additional_inputs_as_variables": "Come usare i campi di input aggiuntivi come variabili?",
|
||||
"download_desktop_app": "Scarica app desktop",
|
||||
"set_ping_link": "Imposta link di Ping",
|
||||
"rate_limit_exceeded": "Limite superato",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Aggiungi limite",
|
||||
"team_name_required": "Nome del team richiesto",
|
||||
"show_attendees": "Condividi le informazioni dei partecipanti tra gli ospiti",
|
||||
"how_additional_inputs_as_variables": "Come usare input aggiuntivi come variabili",
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Usa tutte lettere maiuscole",
|
||||
"replace_whitespaces_underscores": "Sostituisci spazi con sottolineature",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Hai bisogno di altro?",
|
||||
"billing_help_description": "Se hai bisogno di ulteriore aiuto per la fatturazione, il nostro team di supporto è qui per aiutarti.",
|
||||
"billing_help_cta": "Contatta l'assistenza",
|
||||
"ignore_special_characters": "Ignora i caratteri speciali nelle etichette degli input aggiuntivi. Usa solo lettere e numeri",
|
||||
"retry": "Riprova",
|
||||
"fetching_calendars_error": "Si è verificato un problema durante il recupero dei calendari. <1>Riprova</1> o contatta l'assistenza clienti.",
|
||||
"calendar_connection_fail": "Impossibile connettersi al calendario",
|
||||
@@ -1593,9 +1589,9 @@
|
||||
"create_a_one_one_template": "Crea un modello di incontro uno a uno per un tipo di evento e distribuiscilo a più membri.",
|
||||
"collective_or_roundrobin": "Di gruppo o round robin",
|
||||
"book_your_team_members": "Usa eventi di gruppo per riunire tutti i membri del team o eventi round robin per fare partecipare a rotazione le persone giuste.",
|
||||
"event_no_longer_attending_subject": "Non partecipa più a {{title}} del {{date}}",
|
||||
"event_no_longer_attending_subject": "Non partecipa più a {{title}} con il seguente orario: {{date}}",
|
||||
"no_longer_attending": "Non partecipi più a questo evento",
|
||||
"attendee_no_longer_attending_subject": "Un partecipante non partecipa più a {{title}} del {{date}}",
|
||||
"attendee_no_longer_attending_subject": "Un partecipante non partecipa più a {{title}} con il seguente orario {{date}}",
|
||||
"attendee_no_longer_attending": "Un partecipante non partecipa più al tuo evento",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} ha cancellato. Di conseguenza, si è liberato un posto in questa fascia oraria",
|
||||
"create_event_on": "Crea evento in data",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Codice di verifica",
|
||||
"can_you_try_again": "Riprovare specificando un orario differente.",
|
||||
"verify": "Verifica",
|
||||
"timezone_variable": "Timezone",
|
||||
"invalid_event_name_variables": "Nel nome dell'evento è presente una variabile non valida",
|
||||
"select_all": "Seleziona tutto",
|
||||
"default_conferencing_bulk_title": "Aggiorna in blocco i tipi di eventi esistenti",
|
||||
"invalid_event_name_variables": "Nel nome dell'evento è presente una variabile non valida",
|
||||
"default_conferencing_bulk_description": "Aggiorna le posizioni per i tipi di eventi selezionati"
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hidden_team_owner_message": "チームをご利用いただくためには、Pro アカウントが必要です。アップグレードするまでは非表示となります。",
|
||||
"link_expires": "追記: 有効期限は {{expiresIn}} 時間後に切れます。",
|
||||
"upgrade_to_per_seat": "座席ごとにアップグレードする",
|
||||
"seat_options_doesnt_support_confirmation": "座席オプションは確認要件をサポートしていません",
|
||||
"seat_options_doesnt_support_confirmation": "予約に確認が必要な場合、座席予約はできません",
|
||||
"team_upgrade_seats_details": "あなたのチームに所属する {{memberCount}} 人のメンバーのうち、{{unpaidCount}} 名分のライセンスが未払いとなっています。ライセンスごとの料金は、${{seatPrice}} / 月ですので、あなたのメンバーシップの合計金額は、${{totalCost}} / 月になると予想されます。",
|
||||
"team_upgrade_banner_description": "新しいチームプランをお試しいただき、ありがとうございます。あなたのチーム「{{teamName}}」には、アップグレードが必要なようです。",
|
||||
"team_upgrade_banner_action": "アップグレードはこちら",
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "現在のユーザー名",
|
||||
"example_1": "例 1",
|
||||
"example_2": "例 2",
|
||||
"additional_input_label": "追加の入力ラベル",
|
||||
"company_size": "会社の規模",
|
||||
"what_help_needed": "何かお困りですか?",
|
||||
"variable_format": "変数の形式",
|
||||
"webhook_subscriber_url_reserved": "ウェブフック購読者 URL は既に定義されています",
|
||||
"custom_input_as_variable_info": "追加の入力ラベルに含まれている特殊文字はすべて無視し (アルファベットと数字のみを使用)、アルファベットをすべて大文字に、そして空白をアンダースコアへと変換します。",
|
||||
"using_additional_inputs_as_variables": "追加の入力内容を変数として使用するにはどうすればよいですか?",
|
||||
"download_desktop_app": "デスクトップアプリをダウンロード",
|
||||
"set_ping_link": "Ping リンクを設定",
|
||||
"rate_limit_exceeded": "試行回数の上限を超えました",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "上限を追加する",
|
||||
"team_name_required": "チーム名が必要です",
|
||||
"show_attendees": "ゲスト間で出席者情報を共有する",
|
||||
"how_additional_inputs_as_variables": "変数として追加の入力を使用する方法は?",
|
||||
"format": "書式",
|
||||
"uppercase_for_letters": "すべての文字に大文字を使用する",
|
||||
"replace_whitespaces_underscores": "空白をアンダーバー「 _ 」に置き換える",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "他にもお手伝いできることはありませんか?",
|
||||
"billing_help_description": "請求に関してサポートが必要な場合には、サポートチームがお手伝いさせていただきます。",
|
||||
"billing_help_cta": "サポートに問い合わせる",
|
||||
"ignore_special_characters": "追加の入力ラベルに含まれている特殊文字はすべて無視し、アルファベットと数字のみを使用してください",
|
||||
"retry": "再試行",
|
||||
"fetching_calendars_error": "カレンダーの取得中にエラーが発生しました。<1>もう一度お試しいただく</1>か、カスタマーサポートまでお問い合わせください。",
|
||||
"calendar_connection_fail": "カレンダーの接続に失敗しました",
|
||||
@@ -1590,7 +1586,7 @@
|
||||
"recommended_next_steps": "推奨される次のステップ",
|
||||
"create_a_managed_event": "管理されたイベントの種類を作成",
|
||||
"meetings_are_better_with_the_right": "ミーティングは適切なチームメンバーの参加によってより良いものになります。今すぐ彼らを招待しましょう。",
|
||||
"create_a_one_one_template": "イベントタイプに One-One One テンプレートを作成して複数のメンバーに配布します。",
|
||||
"create_a_one_one_template": "イベントタイプにマンツーマンテンプレートを作成して複数のメンバーに配布します。",
|
||||
"collective_or_roundrobin": "共有またはラウンドロビン",
|
||||
"book_your_team_members": "共有イベントでチームメンバーを一緒に予約するか、ラウンドロビンで持ち回りにして適切なユーザーを確保します。",
|
||||
"event_no_longer_attending_subject": "{{date}} の {{title}} には出席しなくなりました",
|
||||
@@ -1608,7 +1604,7 @@
|
||||
"booking_questions_title": "予約の質問",
|
||||
"booking_questions_description": "予約ページで尋ねる質問をカスタマイズする",
|
||||
"add_a_booking_question": "質問を追加",
|
||||
"duplicate_email": "メールアドレスが重複しています",
|
||||
"duplicate_email": "メールが重複しています",
|
||||
"booking_with_payment_cancelled": "このイベントの支払いはもうできません",
|
||||
"booking_with_payment_cancelled_already_paid": "この予約に関するお支払いの払い戻しについては、現在処理中です。",
|
||||
"booking_with_payment_cancelled_refunded": "この予約に関するお支払いは、返金されています。",
|
||||
@@ -1647,10 +1643,11 @@
|
||||
"app_is_connected": "{{dependencyName}} が接続されました",
|
||||
"requires_app": "{{dependencyName}} が必要です",
|
||||
"verification_code": "確認コード",
|
||||
"can_you_try_again": "別のタイミングでもう1度お試しください",
|
||||
"can_you_try_again": "別の時間帯でもう1度お試しください",
|
||||
"verify": "確認する",
|
||||
"timezone_variable": "タイムゾーン",
|
||||
"invalid_event_name_variables": "イベント名に無効な変数があります",
|
||||
"select_all": "すべてを選択",
|
||||
"default_conferencing_bulk_title": "既存のイベントタイプを一括更新する",
|
||||
"invalid_event_name_variables": "イベント名に無効な変数があります",
|
||||
"default_conferencing_bulk_description": "選択したイベントタイプの場所を更新します"
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
"done": "완료",
|
||||
"all_done": "다 되었습니다!",
|
||||
"all_apps": "모두",
|
||||
"available_apps": "사용 가능한 앱",
|
||||
"check_email_reset_password": "이메일을 확인하세요. 비밀번호 재설정을 위한 링크를 보냈습니다.",
|
||||
"finish": "끝",
|
||||
"few_sentences_about_yourself": "자신에 대해 몇 마디를 써보세요. 개인 URL 페이지에 보이는 내용입니다.",
|
||||
@@ -737,6 +738,8 @@
|
||||
"minimum_booking_notice": "최소 고지",
|
||||
"slot_interval": "시간 슬롯 간격",
|
||||
"slot_interval_default": "이벤트 길이 사용(기본값)",
|
||||
"delete_event_type": "이벤트 타입을 삭제하시겠습니까?",
|
||||
"confirm_delete_event_type": "네, 삭제합니다",
|
||||
"delete_account": "계정 삭제",
|
||||
"confirm_delete_account": "예, 계정을 삭제합니다",
|
||||
"delete_account_confirmation_message": "{{appName}} 계정을 삭제하시겠습니까? 계정 링크를 공유한 사람은 더 이상 이 링크를 사용하여 예약할 수 없으며 저장한 모든 기본 설정이 손실됩니다.",
|
||||
@@ -1127,13 +1130,11 @@
|
||||
"current_username": "현재 사용자 이름",
|
||||
"example_1": "예 1",
|
||||
"example_2": "예 2",
|
||||
"additional_input_label": "추가 입력 레이블",
|
||||
"company_size": "회사 규모",
|
||||
"what_help_needed": "어떻게 도와 드릴까요?",
|
||||
"variable_format": "변수 형식",
|
||||
"webhook_subscriber_url_reserved": "웹훅 구독자 URL이 이미 정의되었습니다",
|
||||
"custom_input_as_variable_info": "추가 입력 레이블의 모든 특수 문자는 무시하고(문자와 숫자만 사용) 모든 문자에 대문자를 사용하며 공백은 밑줄로 대체합니다.",
|
||||
"using_additional_inputs_as_variables": "추가 입력을 변수로 사용하려면 어떻게 하나요?",
|
||||
"download_desktop_app": "데스크탑 앱 다운로드",
|
||||
"set_ping_link": "Ping 링크 설정",
|
||||
"rate_limit_exceeded": "요금 한도 초과됨",
|
||||
@@ -1257,6 +1258,7 @@
|
||||
"download_responses": "다운로드 응답",
|
||||
"download_responses_description": "양식에 대한 모든 응답을 CSV 형식으로 다운로드합니다.",
|
||||
"download": "다운로드",
|
||||
"recording_from_your_recent_call": "Cal.com에서 최근 통화 내용 녹음 파일을 다운로드 할 준비가 되었습니다",
|
||||
"create_your_first_form": "첫 양식 작성",
|
||||
"create_your_first_form_description": "라우팅 양식을 사용하면 적격성 질문을 올리고 올바른 사람 또는 이벤트 유형으로 라우팅할 수 있습니다.",
|
||||
"create_your_first_webhook": "첫 웹훅 생성",
|
||||
@@ -1347,7 +1349,6 @@
|
||||
"add_limit": "제한 추가",
|
||||
"team_name_required": "팀 이름 필수",
|
||||
"show_attendees": "게스트 간에 참석자 정보 공유",
|
||||
"how_additional_inputs_as_variables": "추가 입력을 변수로 사용하는 방법",
|
||||
"format": "형식",
|
||||
"uppercase_for_letters": "모든 문자에 대문자 사용",
|
||||
"replace_whitespaces_underscores": "공백을 밑줄로 바꾸기",
|
||||
@@ -1362,7 +1363,6 @@
|
||||
"billing_help_title": "또 필요한 것이 있나요?",
|
||||
"billing_help_description": "청구와 관련하여 추가적인 도움이 필요한 경우 지원 팀이 도와드리겠습니다.",
|
||||
"billing_help_cta": "지원 문의",
|
||||
"ignore_special_characters": "추가 입력 레이블의 특수 문자는 무시하십시오. 문자와 숫자만 사용하십시오",
|
||||
"retry": "재시도",
|
||||
"fetching_calendars_error": "캘린더를 가져오는 중 문제가 발생했습니다. <1>다시 시도</1>하거나 고객 지원에 문의하십시오.",
|
||||
"calendar_connection_fail": "캘린더 연결 실패",
|
||||
@@ -1445,11 +1445,14 @@
|
||||
"fixed_round_robin": "고정 라운드 로빈",
|
||||
"add_one_fixed_attendee": "한 명의 고정 참석자를 추가하고 여러 참석자를 라운드 로빈 방식으로 순환합니다.",
|
||||
"calcom_is_better_with_team": "Cal.com이 팀 기능으로 더욱 좋아졌습니다",
|
||||
"the_calcom_team": "Cal.com 팀",
|
||||
"add_your_team_members": "이벤트 타입에 팀원을 추가하세요. 집단 일정 관리를 사용해 모든 사람을 포함하거나 라운드 로빈 일정 관리로 가장 적합한 인원을 찾으세요.",
|
||||
"booking_limit_reached": "이 이벤트 타입에 대한 예약 한도에 도달했습니다",
|
||||
"duration_limit_reached": "이 이벤트 타입에 대한 기간 한도에 도달했습니다",
|
||||
"admin_has_disabled": "관리자가 {{appName}} 앱을 비활성화했습니다",
|
||||
"disabled_app_affects_event_type": "관리자가 이벤트 타입 {{eventType}}에 영향을 미치는 {{appName}} 앱을 비활성화했습니다",
|
||||
"email_subject_slug_replacement": "팀 관리자가 당신의 이벤트 /{{slug}}를 변경하였습니다",
|
||||
"email_body_slug_replacement_info": "링크는 계속 유효하지만 일부 설정이 변경될 수 있습니다. 이벤트 타입에서 이를 검토할 수 있습니다.",
|
||||
"disable_payment_app": "관리자가 이벤트 타입 {{title}}에 영향을 미치는 {{appName}} 앱을 비활성화했습니다. 참석자는 여전히 이러한 타입의 이벤트를 예약할 수 있지만 결제하라는 메시지는 표시되지 않습니다. 관리자가 결제 수단을 다시 활성화할 때까지 이를 방지하기 위해 이벤트 타입을 숨길 수 있습니다.",
|
||||
"payment_disabled_still_able_to_book": "참석자는 여전히 이러한 타입의 이벤트를 예약할 수 있지만 결제하라는 메시지는 표시되지 않습니다. 관리자가 결제 수단을 다시 활성화할 때까지 이를 방지하기 위해 이벤트 타입을 숨길 수 있습니다.",
|
||||
"app_disabled_with_event_type": "관리자가 이벤트 타입 {{title}}에 영향을 미치는 {{appName}} 앱을 비활성화했습니다.",
|
||||
@@ -1512,6 +1515,7 @@
|
||||
"create_your_first_team_webhook_description": "이 팀 이벤트 유형에 대한 첫 번째 웹훅 만들기",
|
||||
"create_webhook_team_event_type": "이 팀 이벤트 유형에 대한 웹훅 만들기",
|
||||
"disable_success_page": "예약 성공 페이지 비활성화(리디렉션 URL이 있는 경우에만 작동)",
|
||||
"invalid_admin_password": "관리자이지만 비밀번호 길이가 15자 이상이 아니거나, 이중 인증이 설정되지 않았습니다",
|
||||
"change_password_admin": "관리자 액세스를 위해 비밀번호 변경",
|
||||
"username_already_taken": "이미 사용 중인 사용자 이름입니다",
|
||||
"assignment": "할당",
|
||||
@@ -1660,21 +1664,35 @@
|
||||
"verification_code": "인증 코드",
|
||||
"can_you_try_again": "다른 시간으로 다시 해보시겠어요?",
|
||||
"verify": "인증",
|
||||
"timezone_variable": "시간대",
|
||||
"invalid_event_name_variables": "이벤트 이름에 유효하지 않은 변수가 있습니다",
|
||||
"select_all": "모두 선택",
|
||||
"default_conferencing_bulk_title": "기존 이벤트 유형 일괄 업데이트",
|
||||
"invalid_event_name_variables": "이벤트 이름에 유효하지 않은 변수가 있습니다",
|
||||
"requires_at_least_one_schedule": "적어도 한 개 이상의 스케줄이 필요합니다",
|
||||
"default_conferencing_bulk_description": "선택한 이벤트 유형의 위치 업데이트",
|
||||
"locked_by_admin": "관리자에 의해 잠금 됨",
|
||||
"review_event_type": "이벤트 타입 검토",
|
||||
"looking_for_more_analytics": "추가적인 분석을 원하시나요?",
|
||||
"looking_for_more_insights": "더 많은 인사이트를 찾고계신가요?",
|
||||
"add_filter": "필터 추가",
|
||||
"select_user": "사용자 선택",
|
||||
"select_event_type": "이벤트 타입 선택",
|
||||
"select_date_range": "날짜 범위 선택",
|
||||
"no_event_types_found": "이벤트 타입을 찾을 수 없습니다",
|
||||
"events_created": "생성된 이벤트",
|
||||
"events_completed": "완료된 이벤트",
|
||||
"events_cancelled": "취소된 이벤트",
|
||||
"events_rescheduled": "일정이 변경된 이벤트",
|
||||
"analytics_for_organisation": "인사이트",
|
||||
"clear_filters": "필터 삭제",
|
||||
"no_show_fee": "No-show 수수료",
|
||||
"submit_payment_information": "결제 정보 제출",
|
||||
"no_show_fee_charged_text_body": "No-show 수수료가 부과되었습니다",
|
||||
"error_charging_card": "No-show 수수료 청구 중 문제가 발생했습니다. 나중에 다시 시도해주세요.",
|
||||
"collect_no_show_fee": "No-show 수수료를 청구하세요",
|
||||
"no_show_fee_charged": "No-show 수수료가 부과됩니다",
|
||||
"insights": "인사이트",
|
||||
"insights_no_data_found_for_filter": "선택한 필터 또는 날짜에 대한 데이터를 찾을 수 없습니다.",
|
||||
"complete_your_booking": "예약을 완료하세요",
|
||||
"email_invite_team": "{{email}} 이 초대되었습니다."
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Huidige gebruikersnaam",
|
||||
"example_1": "Voorbeeld 1",
|
||||
"example_2": "Voorbeeld 2",
|
||||
"additional_input_label": "Aanvullend invoerlabel",
|
||||
"company_size": "Bedrijfsgrootte",
|
||||
"what_help_needed": "Waar heeft u hulp bij nodig?",
|
||||
"variable_format": "Variabele indeling",
|
||||
"webhook_subscriber_url_reserved": "De abonnee-URL voor Webhook is al gedefinieerd",
|
||||
"custom_input_as_variable_info": "Negeer alle speciale tekens van het label voor aanvullende invoer (gebruik alleen letters en cijfers) en gebruik hoofdletters voor alle letters en vervang spaties door onderstrepingstekens.",
|
||||
"using_additional_inputs_as_variables": "Hoe gebruik ik aanvullende invoer als variabelen?",
|
||||
"download_desktop_app": "Desktop-app downloaden",
|
||||
"set_ping_link": "Ping-link instellen",
|
||||
"rate_limit_exceeded": "Tariefgrenswaarde overschreden",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Voeg een limiet toe",
|
||||
"team_name_required": "Teamnaam verplicht",
|
||||
"show_attendees": "Deel de deelnemersinformatie met de gasten",
|
||||
"how_additional_inputs_as_variables": "Extra invoeren als variabelen gebruiken",
|
||||
"format": "Formatteren",
|
||||
"uppercase_for_letters": "Gebruik hoofdletters voor alle letters",
|
||||
"replace_whitespaces_underscores": "Vervang spaties door onderstrepingstekens",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Nog iets nodig?",
|
||||
"billing_help_description": "Als u verder nog hulp nodig heeft bij de facturering, is ons ondersteuningsteam er om u te helpen.",
|
||||
"billing_help_cta": "Contact opnemen met de ondersteuning",
|
||||
"ignore_special_characters": "Speciale tekens negeren in uw label Extra invoer. Alleen letters en cijfers gebruiken",
|
||||
"retry": "Opnieuw proberen",
|
||||
"fetching_calendars_error": "Er is een probleem opgetreden bij het ophalen van uw agenda's. <1>Probeer het opnieuw</1> of neem contact op met de klantenservice.",
|
||||
"calendar_connection_fail": "Agendaverbinding mislukt",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Verificatiecode",
|
||||
"can_you_try_again": "Kunt u het opnieuw proberen met een andere tijd?",
|
||||
"verify": "Verifiëren",
|
||||
"timezone_variable": "Tijdzone",
|
||||
"invalid_event_name_variables": "Uw gebeurtenisnaam bevat een ongeldige variabele",
|
||||
"select_all": "Alles selecteren",
|
||||
"default_conferencing_bulk_title": "Bestaande gebeurtenistypen in bulk bijwerken",
|
||||
"invalid_event_name_variables": "Uw gebeurtenisnaam bevat een ongeldige variabele",
|
||||
"default_conferencing_bulk_description": "Werk de locaties voor de geselecteerde gebeurtenistypen bij"
|
||||
}
|
||||
|
||||
@@ -1101,13 +1101,11 @@
|
||||
"current_username": "Nåværende brukernavn",
|
||||
"example_1": "Eksempel 1",
|
||||
"example_2": "Eksempel 2",
|
||||
"additional_input_label": "Etikett for tilleggs-input",
|
||||
"company_size": "Bedriftsstørrelse",
|
||||
"what_help_needed": "Hva trenger du hjelp med?",
|
||||
"variable_format": "Variabel format",
|
||||
"webhook_subscriber_url_reserved": "Webhook-abonnent-url er allerede definert",
|
||||
"custom_input_as_variable_info": "Ignorer alle spesialtegn i tilleggs-input-etiketten (bruk bare bokstaver og tall), bruk store bokstaver for alle bokstaver og erstatt mellomrom med understrek.",
|
||||
"using_additional_inputs_as_variables": "Hvordan bruker jeg Tilleggsinput som variabler?",
|
||||
"download_desktop_app": "Last ned skrivebordsappen",
|
||||
"set_ping_link": "Sett Ping-lenke",
|
||||
"rate_limit_exceeded": "Sats-grensen er overskredet",
|
||||
@@ -1312,7 +1310,6 @@
|
||||
"add_limit": "Legg til Grense",
|
||||
"team_name_required": "Team-navn er påkrevd",
|
||||
"show_attendees": "Del deltakerinformasjon mellom gjester",
|
||||
"how_additional_inputs_as_variables": "Hvordan bruke Tilleggs-input som Variabler",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Bruk store bokstaver for alle bokstaver",
|
||||
"replace_whitespaces_underscores": "Erstatt mellomrom med understrek",
|
||||
@@ -1327,7 +1324,6 @@
|
||||
"billing_help_title": "Trenger du noe annet?",
|
||||
"billing_help_description": "Hvis du trenger ytterligere hjelp med betaling, er supportteamet vårt her for å hjelpe.",
|
||||
"billing_help_cta": "Kontakt kundeservice",
|
||||
"ignore_special_characters": "Ignorer spesialtegn i Tilleggsinput-etiketten. Bruk kun bokstaver og tall",
|
||||
"retry": "Prøv igjen",
|
||||
"fetching_calendars_error": "Det oppsto et problem med å hente kalenderne dine. Vennligst <1>prøv igjen</1> eller ta kontakt med kundeservice.",
|
||||
"calendar_connection_fail": "Kalender-tilkobling mislyktes",
|
||||
@@ -1466,5 +1462,6 @@
|
||||
"sso_configuration": "Enkel Pålogging",
|
||||
"email_no_user_cta": "Opprett brukeren din",
|
||||
"change_default_conferencing_app": "Sett som standard",
|
||||
"booking_confirmation_failed": "Booking-bekreftelse feilet"
|
||||
"booking_confirmation_failed": "Booking-bekreftelse feilet",
|
||||
"timezone_variable": "Tidssone"
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Bieżąca nazwa użytkownika",
|
||||
"example_1": "Przykład 1",
|
||||
"example_2": "Przykład 2",
|
||||
"additional_input_label": "Dodatkowa etykieta wejściowa",
|
||||
"company_size": "Wielkość firmy",
|
||||
"what_help_needed": "W czym potrzebujesz pomocy?",
|
||||
"variable_format": "Format zmiennej",
|
||||
"webhook_subscriber_url_reserved": "Adres URL subskrybenta Webhook jest już zdefiniowany.",
|
||||
"custom_input_as_variable_info": "Ignoruj wszystkie znaki specjalne etykiety dodatkowych danych wejściowych (używaj tylko liter i cyfr), użyj wielkich liter w przypadku wszystkich liter i zastąp spacje znakami podkreślenia.",
|
||||
"using_additional_inputs_as_variables": "Jak wykorzystać dodatkowe dane wejściowe jako zmienne?",
|
||||
"download_desktop_app": "Pobierz aplikację komputerową",
|
||||
"set_ping_link": "Ustaw link do aplikacji Ping",
|
||||
"rate_limit_exceeded": "Przekroczono limit prób",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Dodaj limit",
|
||||
"team_name_required": "Wymagana nazwa zespołu",
|
||||
"show_attendees": "Udostępniaj gościom informacje o uczestnikach",
|
||||
"how_additional_inputs_as_variables": "Jak wykorzystać dodatkowe dane wejściowe jako zmienne?",
|
||||
"format": "Formatowanie",
|
||||
"uppercase_for_letters": "Użyj wielkich liter w przypadku wszystkich liter",
|
||||
"replace_whitespaces_underscores": "Zastąp spacje podkreśleniami",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Potrzebujesz czegoś innego?",
|
||||
"billing_help_description": "Jeśli potrzebujesz dodatkowej pomocy w rozliczaniu, nasz zespół pomocy technicznej chętnie jej udzieli.",
|
||||
"billing_help_cta": "Skontaktuj się z pomocą techniczną",
|
||||
"ignore_special_characters": "Ignoruj znaki specjalne w dodatkowej etykiecie wejściowej. Używaj tylko liter i cyfr",
|
||||
"retry": "Spróbuj ponownie",
|
||||
"fetching_calendars_error": "Podczas pobierania kalendarzy wystąpił problem. <1>Spróbuj ponownie</1> lub skontaktuj się z pomocą techniczną.",
|
||||
"calendar_connection_fail": "Połączenie z kalendarzem nie powiodło się",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Kod weryfikacyjny",
|
||||
"can_you_try_again": "Możesz spróbować ponownie w innym terminie?",
|
||||
"verify": "Zweryfikuj",
|
||||
"timezone_variable": "Strefa Czasowa",
|
||||
"invalid_event_name_variables": "W Twojej nazwie wydarzenia występuje nieprawidłowa zmienna.",
|
||||
"select_all": "Zaznacz wszystko",
|
||||
"default_conferencing_bulk_title": "Zbiorcza aktualizacja istniejących typów wydarzeń",
|
||||
"invalid_event_name_variables": "W Twojej nazwie wydarzenia występuje nieprawidłowa zmienna.",
|
||||
"default_conferencing_bulk_description": "Aktualizuj lokalizacje wybranych typów wydarzeń"
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Nome de usuário atual",
|
||||
"example_1": "Exemplo 1",
|
||||
"example_2": "Exemplo 2",
|
||||
"additional_input_label": "Etiqueta de entrada adicional",
|
||||
"company_size": "Tamanho da empresa",
|
||||
"what_help_needed": "Em que você precisa de ajuda?",
|
||||
"variable_format": "Formato variável",
|
||||
"webhook_subscriber_url_reserved": "URL de assinante de webhook já está definida",
|
||||
"custom_input_as_variable_info": "Ignore todos os caracteres especiais da etiqueta de entrada adicional (use apenas letras e números), use todas as letras maiúsculas e substitua espaços em branco por sublinhados.",
|
||||
"using_additional_inputs_as_variables": "Como usar entradas adicionais como variáveis?",
|
||||
"download_desktop_app": "Baixar app para desktop",
|
||||
"set_ping_link": "Definir link do Ping",
|
||||
"rate_limit_exceeded": "Limite excedido",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Adicione limite",
|
||||
"team_name_required": "Nome da equipe obrigatório",
|
||||
"show_attendees": "Compartilhe informações do participante entre convidados",
|
||||
"how_additional_inputs_as_variables": "Como usar Dados Adicionais como variáveis",
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Use letras maiúsculas para todas as letras",
|
||||
"replace_whitespaces_underscores": "Substituir espaços em branco com sublinhados",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Precisa de algo mais?",
|
||||
"billing_help_description": "Se precisar de mais ajuda com o faturamento, o nosso time de suporte está aqui para ajudar.",
|
||||
"billing_help_cta": "Falar com o suporte",
|
||||
"ignore_special_characters": "Ignore caracteres especiais no seu rótulo adicional de entrada. Use apenas letras e números",
|
||||
"retry": "Repetir",
|
||||
"fetching_calendars_error": "Houve um problema ao buscar suas agendas. <1>Tente novamente</1> ou entre em contato com o suporte ao cliente.",
|
||||
"calendar_connection_fail": "Falha na conexão com a agenda",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Código de verificação",
|
||||
"can_you_try_again": "Poderia tentar novamente em um horário diferente?",
|
||||
"verify": "Verificar",
|
||||
"timezone_variable": "Fuso Horário",
|
||||
"invalid_event_name_variables": "Há uma variável inválida no seu nome do evento",
|
||||
"select_all": "Selecionar tudo",
|
||||
"default_conferencing_bulk_title": "Atualize em massa os tipos de evento existentes",
|
||||
"invalid_event_name_variables": "Há uma variável inválida no seu nome do evento",
|
||||
"default_conferencing_bulk_description": "Atualize as localizações para os tipos de evento selecionados"
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Nome de utilizador atual",
|
||||
"example_1": "Exemplo 1",
|
||||
"example_2": "Exemplo 2",
|
||||
"additional_input_label": "Etiqueta de campo adicional",
|
||||
"company_size": "Tamanho da empresa",
|
||||
"what_help_needed": "Em que precisa de ajuda?",
|
||||
"variable_format": "Formato da variável",
|
||||
"webhook_subscriber_url_reserved": "O endereço webhook de subscrição já está definido",
|
||||
"custom_input_as_variable_info": "Ignora todos os caracteres especiais da etiqueta de entrada adicional (utiliza apenas letras e números), utiliza maiúsculas para todas as letras e substitui espaços em branco por carateres de sublinhado.",
|
||||
"using_additional_inputs_as_variables": "Como utilizar entradas adicionais como variáveis?",
|
||||
"download_desktop_app": "Descarregar a aplicação para o computador",
|
||||
"set_ping_link": "Definir ligação do Ping",
|
||||
"rate_limit_exceeded": "Limite de transações excedido",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Adicionar limite",
|
||||
"team_name_required": "É necessário um nome para a equipa",
|
||||
"show_attendees": "Partilhar informações dos participantes entre os convidados",
|
||||
"how_additional_inputs_as_variables": "Como utilizar as Entradas adicionais como Variáveis",
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Utilize letras maiúsculas para todas as letras",
|
||||
"replace_whitespaces_underscores": "Substituir espaços em branco com carateres de sublinhado",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Precisa de outra coisa?",
|
||||
"billing_help_description": "Se precisar de mais ajuda com a faturação, a nossa equipa de apoio está aqui para ajudar.",
|
||||
"billing_help_cta": "Contactar o apoio",
|
||||
"ignore_special_characters": "Ignorar caracteres especiais na sua etiqueta de Campo adicional. Utilizar apenas letras e números",
|
||||
"retry": "Repetir",
|
||||
"fetching_calendars_error": "Ocorreu um problema ao obter os seus calendários. Por favor, <1>tente novamente</1> ou contacte o apoio ao cliente.",
|
||||
"calendar_connection_fail": "A ligação ao calendário falhou",
|
||||
@@ -1590,14 +1586,14 @@
|
||||
"recommended_next_steps": "Próximos passos recomendados",
|
||||
"create_a_managed_event": "Criar um tipo de evento gerido",
|
||||
"meetings_are_better_with_the_right": "Os eventos são melhores com os elementos certos da equipa. Convide-os agora.",
|
||||
"create_a_one_one_template": "Crie um modelo um-para-um para um tipo de evento e distribua-o a vários membros.",
|
||||
"create_a_one_one_template": "Crie um modelo de um-para-um para um tipo de evento e distribua-o a vários membros.",
|
||||
"collective_or_roundrobin": "Coletiva ou rotativa",
|
||||
"book_your_team_members": "Associe os elementos da sua equipa a eventos coletivos ou rode pela equipa com utilização de uma distribuição equilibrada para obter a pessoa certa.",
|
||||
"event_no_longer_attending_subject": "Já não irá participar em {{title}} a {{date}}",
|
||||
"event_no_longer_attending_subject": "Já não irá participar em {{title}} em {{date}}",
|
||||
"no_longer_attending": "Você já não vai estar presente neste evento",
|
||||
"attendee_no_longer_attending_subject": "Um participante não irá estar presente em {{title}} a {{date}}",
|
||||
"attendee_no_longer_attending_subject": "Um participante não irá estar presente em {{title}} em {{date}}",
|
||||
"attendee_no_longer_attending": "Um participante já não irá participar no seu evento",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} cancelou. Isto significa que existe uma nova vaga para este horário",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} cancelou. Isto significa que existe uma nova disponibilidade para este horário",
|
||||
"create_event_on": "Criar evento em",
|
||||
"default_app_link_title": "Definir uma ligação predefinida de aplicação",
|
||||
"default_app_link_description": "Ao definir uma ligação predefinida da aplicação, permite que todos os tipos de eventos recém-criados utilizem a ligação de aplicação que definiu.",
|
||||
@@ -1617,7 +1613,7 @@
|
||||
"form_builder_field_already_exists": "Já existe um campo com este nome",
|
||||
"form_builder_field_add_subtitle": "Personalize as questões colocadas na página da reserva",
|
||||
"get_started_zapier_templates": "Comece a utilizar os modelos Zapier",
|
||||
"team_is_unpublished": "{{team}} não está publicado",
|
||||
"team_is_unpublished": "A equipa {{team}} ainda não está publicada",
|
||||
"team_is_unpublished_description": "Esta ligação de equipa não está disponível neste momento. Por favor, entre em contacto com o responsável pela equipa ou solicite-lhes a respetiva publicação.",
|
||||
"team_member": "Membro da equipa",
|
||||
"a_routing_form": "Um formulário de encaminhamento",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Código de verificação",
|
||||
"can_you_try_again": "Pode tentar novamente noutra altura?",
|
||||
"verify": "Verificar",
|
||||
"timezone_variable": "Fuso Horário",
|
||||
"invalid_event_name_variables": "Existe uma variável inválida no nome do seu evento",
|
||||
"select_all": "Selecionar tudo",
|
||||
"default_conferencing_bulk_title": "Atualizar os tipos de evento existentes em conjunto",
|
||||
"invalid_event_name_variables": "Existe uma variável inválida no nome do seu evento",
|
||||
"default_conferencing_bulk_description": "Atualizar os locais para os tipos de evento selecionados"
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hidden_team_owner_message": "Aveți nevoie de un cont Pro pentru a utiliza echipe. Sunteți ascuns până când faceți upgrade.",
|
||||
"link_expires": "p.s. Expiră în {{expiresIn}} ore.",
|
||||
"upgrade_to_per_seat": "Faceți upgrade la nivelul Per licență",
|
||||
"seat_options_doesnt_support_confirmation": "Opțiunea pentru locuri nu acceptă cerința de confirmare",
|
||||
"seat_options_doesnt_support_confirmation": "Opțiunea pentru locuri nu este disponibilă dacă rezervarea solicită de confirmare",
|
||||
"team_upgrade_seats_details": "Printre cei {{memberCount}} (de) membri din echipa dvs., {{unpaidCount}} (de) locuri nu sunt plătite. La {{seatPrice}} USD/lună/licență, costul total estimat al abonamentului dvs. este de {{totalCost}} USD/lună.",
|
||||
"team_upgrade_banner_description": "Vă mulțumim că ați încercat noul nostru plan pentru echipe. Am observat că planul echipei dvs. „{{teamName}}” necesită un upgrade.",
|
||||
"team_upgrade_banner_action": "Realizați upgrade aici",
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Nume de utilizator actual",
|
||||
"example_1": "Exemplul 1",
|
||||
"example_2": "Exemplul 2",
|
||||
"additional_input_label": "Etichetă intrare suplimentară",
|
||||
"company_size": "Mărime companie",
|
||||
"what_help_needed": "Pentru ce anume aveți nevoie de ajutor?",
|
||||
"variable_format": "Format variabile",
|
||||
"webhook_subscriber_url_reserved": "URL-ul abonatului Webhook este deja definit",
|
||||
"custom_input_as_variable_info": "Ignorați toate caracterele speciale de pe eticheta de introducere suplimentară (utilizați numai litere și cifre), utilizați majuscule pentru toate literele și înlocuiți spațiile goale cu caractere de subliniere.",
|
||||
"using_additional_inputs_as_variables": "Cum se folosesc intrările suplimentare ca variabile?",
|
||||
"download_desktop_app": "Descărcați aplicația pentru desktop",
|
||||
"set_ping_link": "Setare link Ping",
|
||||
"rate_limit_exceeded": "Limită rată depășită",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Adăugați o limită",
|
||||
"team_name_required": "Este necesar numele echipei",
|
||||
"show_attendees": "Distribuiți informațiile despre participanți între invitați",
|
||||
"how_additional_inputs_as_variables": "Cum se utilizează intrările suplimentare ca variabile",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Utilizați majuscule pentru toate literele",
|
||||
"replace_whitespaces_underscores": "Înlocuiți spațiile cu caractere de subliniere",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Aveți nevoie de altceva?",
|
||||
"billing_help_description": "Dacă aveți nevoie de ajutor suplimentar pentru facturare, echipa noastră de asistență este aici pentru a vă ajuta.",
|
||||
"billing_help_cta": "Contactați serviciul de asistență",
|
||||
"ignore_special_characters": "Ignorați caracterele speciale pe eticheta pentru intrări suplimentare. Utilizați doar litere și cifre",
|
||||
"retry": "Reîncercați",
|
||||
"fetching_calendars_error": "A survenit o problemă legată de recuperarea calendarelor dvs. <1>Încercați din nou</1> sau contactați serviciul de asistență pentru clienți.",
|
||||
"calendar_connection_fail": "Conexiune calendar nereușită",
|
||||
@@ -1590,15 +1586,15 @@
|
||||
"recommended_next_steps": "Următorii pași recomandați",
|
||||
"create_a_managed_event": "Creați un tip de eveniment gestionat",
|
||||
"meetings_are_better_with_the_right": "Ședințele sunt mai agreabile dacă participă membrii potriviți ai echipei. Invitați-i acum.",
|
||||
"create_a_one_one_template": "Creează un șablon unu-unu pentru un tip de eveniment și distribuie-l mai multor membri.",
|
||||
"create_a_one_one_template": "Creați un șablon individualizat pentru un tip de eveniment și distribuiți-l mai multor membri.",
|
||||
"collective_or_roundrobin": "Alocare colectivă sau prin rotație",
|
||||
"book_your_team_members": "Efectuați rezervări comune pentru membrii echipei. Alegeți evenimente colective sau creați un ciclu pentru a găsi persoana potrivită în urma alocării prin rotație.",
|
||||
"event_no_longer_attending_subject": "Nu mai participă la {{title}} de pe {{date}}",
|
||||
"event_no_longer_attending_subject": "Nu mai participă la {{title}} de la {{date}}",
|
||||
"no_longer_attending": "Nu mai participi la acest eveniment",
|
||||
"attendee_no_longer_attending_subject": "Un participant nu mai ia parte la {{title}} de pe {{date}}",
|
||||
"attendee_no_longer_attending_subject": "Un participant nu mai ia parte la {{title}} de la {{date}}",
|
||||
"attendee_no_longer_attending": "Un participant nu mai ia parte la evenimentul tău",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} a anulat. Acest lucru înseamnă că s-a eliberat un loc pentru acest interval de timp",
|
||||
"create_event_on": "Crează eveniment pe",
|
||||
"create_event_on": "Creați eveniment pe",
|
||||
"default_app_link_title": "Setați un link implicit pentru aplicații",
|
||||
"default_app_link_description": "Setarea unui link implicit pentru aplicații permite tuturor tipurilor de evenimente nou-create să utilizeze linkul pentru aplicații pe care l-ați setat.",
|
||||
"change_default_conferencing_app": "Setează ca implicit",
|
||||
@@ -1652,14 +1648,15 @@
|
||||
"this_meeting_has_not_started_yet": "Această întâlnire nu a început încă",
|
||||
"this_app_requires_connected_account": "{{appName}} necesită un cont {{dependencyName}} conectat",
|
||||
"connect_app": "Conectează {{dependencyName}}",
|
||||
"app_is_connected": "{{dependencyName}} este conectat",
|
||||
"app_is_connected": "{{dependencyName}} este conectată",
|
||||
"requires_app": "Necesită {{dependencyName}}",
|
||||
"verification_code": "Cod de verificare",
|
||||
"can_you_try_again": "Poți încerca din nou cu o oră diferită?",
|
||||
"verify": "Verifică",
|
||||
"timezone_variable": "Timezone",
|
||||
"invalid_event_name_variables": "Numele evenimentului include o variabilă nevalidă",
|
||||
"select_all": "Selectează tot",
|
||||
"default_conferencing_bulk_title": "Actualizează în masă tipurile de evenimente existente",
|
||||
"invalid_event_name_variables": "Numele evenimentului include o variabilă nevalidă",
|
||||
"default_conferencing_bulk_description": "Actualizează locațiile pentru tipurile de evenimente selectate",
|
||||
"looking_for_more_analytics": "Cauți mai multe analize?",
|
||||
"add_filter": "Adaugă filtru",
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Текущее имя пользователя",
|
||||
"example_1": "Пример 1",
|
||||
"example_2": "Пример 2",
|
||||
"additional_input_label": "Дополнительная метка ввода",
|
||||
"company_size": "Размер компании",
|
||||
"what_help_needed": "Какая помощь вам нужна?",
|
||||
"variable_format": "Формат переменной",
|
||||
"webhook_subscriber_url_reserved": "URL вебхука для подписчика уже определен",
|
||||
"custom_input_as_variable_info": "Игнорировать все специальные символы в дополнительной метке ввода (использовать только буквы и цифры), использовать верхний регистр для всех букв и заменить пробелы подчеркиванием.",
|
||||
"using_additional_inputs_as_variables": "Как использовать ответы на дополнительные вопросы в качестве переменных?",
|
||||
"download_desktop_app": "Скачать приложение для компьютера",
|
||||
"set_ping_link": "Ссылка на Ping",
|
||||
"rate_limit_exceeded": "Превышен лимит",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Добавить ограничение",
|
||||
"team_name_required": "Требуется название команды",
|
||||
"show_attendees": "Предоставлять гостям информацию об участниках",
|
||||
"how_additional_inputs_as_variables": "Как использовать дополнительные данные в качестве переменных",
|
||||
"format": "Формат",
|
||||
"uppercase_for_letters": "Использовать только прописные буквы",
|
||||
"replace_whitespaces_underscores": "Заменить пробелы подчеркиванием",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Нужно что-нибудь еще?",
|
||||
"billing_help_description": "Если вам нужна дальнейшая помощь с оплатой счета, наша служба поддержки готова помочь.",
|
||||
"billing_help_cta": "Обратиться в службу поддержки",
|
||||
"ignore_special_characters": "Не используйте специальные символы в метке дополнительного поля ввода. Используйте только буквы и цифры",
|
||||
"retry": "Повторить",
|
||||
"fetching_calendars_error": "Ошибка при загрузке календарей. <1>Попробуйте еще раз</1> или обратитесь в службу поддержки.",
|
||||
"calendar_connection_fail": "Не удалось подключиться к календарю",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Код подтверждения",
|
||||
"can_you_try_again": "Попробуйте пожалуйста позже",
|
||||
"verify": "Подтвердить",
|
||||
"timezone_variable": "Часовой пояс",
|
||||
"invalid_event_name_variables": "В названии события недопустимая переменная",
|
||||
"select_all": "Выбрать все",
|
||||
"default_conferencing_bulk_title": "Массовое обновление существующих типов событий",
|
||||
"invalid_event_name_variables": "В названии события недопустимая переменная",
|
||||
"default_conferencing_bulk_description": "Обновить местоположение для выбранных типов событий"
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Trenutno korisničko ime",
|
||||
"example_1": "Primer 1",
|
||||
"example_2": "Primer 2",
|
||||
"additional_input_label": "Dodatna oznaka ulaza",
|
||||
"company_size": "Veličina kompanije",
|
||||
"what_help_needed": "U vezi sa čim vam je potrebna pomoć?",
|
||||
"variable_format": "Format promenljive",
|
||||
"webhook_subscriber_url_reserved": "Url adresa Webhook pretplatnika je već definisana",
|
||||
"custom_input_as_variable_info": "Zanemarite sve specijalne znakove za dodatne oznake unosa (koristite samo slova i brojeve), koristite samo velika slova i zamenite razmake donjim crtama.",
|
||||
"using_additional_inputs_as_variables": "Kako da koristite dodatne unose kao promenljive?",
|
||||
"download_desktop_app": "Preuzmite aplikaciju za stoni računar",
|
||||
"set_ping_link": "Podesi Ping vezu",
|
||||
"rate_limit_exceeded": "Premašeno ograničenje stope",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Dodajte ograničenje",
|
||||
"team_name_required": "Ime tima je potrebno",
|
||||
"show_attendees": "Podelite informacije o učesnicima između gostiju",
|
||||
"how_additional_inputs_as_variables": "Kako da koristite dodatne unose kao promenljive",
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Koristite velika slova za sva slova",
|
||||
"replace_whitespaces_underscores": "Zamenite prazna mesta donjom crtom",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Treba li vam još nešto?",
|
||||
"billing_help_description": "Ako vam treba pomoć sa naplatom, naš tim podrške je tu da pomogne.",
|
||||
"billing_help_cta": "Obratite se podršci",
|
||||
"ignore_special_characters": "Ignorisati specijalne znakove u vašim oznakama Dodatnih unosa. Koristiti samo slova i brojeve",
|
||||
"retry": "Pokušajte ponovo",
|
||||
"fetching_calendars_error": "Došlo je do problema prilikom dobavljanja vaših kalendara. <1>Pokušajte ponovo</1> ili se obratite korisničkoj podršci.",
|
||||
"calendar_connection_fail": "Povezivanje sa kalendarom nije uspelo",
|
||||
@@ -1652,14 +1648,15 @@
|
||||
"this_meeting_has_not_started_yet": "Ovaj sastanak još nije počeo",
|
||||
"this_app_requires_connected_account": "{{appName}} zahteva povezani {{dependencyName}} nalog",
|
||||
"connect_app": "Povežite {{dependencyName}}",
|
||||
"app_is_connected": "{{dependencyName}} je povezan",
|
||||
"app_is_connected": "{{dependencyName}} je povezana",
|
||||
"requires_app": "Zahteva {{dependencyName}}",
|
||||
"verification_code": "Verifikacioni kôd",
|
||||
"can_you_try_again": "Možete li da pokušate ponovo kasnije?",
|
||||
"can_you_try_again": "Možete li da pokušate sa drugim terminom?",
|
||||
"verify": "Verifikuj",
|
||||
"timezone_variable": "Vremenska zona",
|
||||
"invalid_event_name_variables": "U nazivu vašeg događaja se nalazi nevažeća promenljiva",
|
||||
"select_all": "Izaberite sve",
|
||||
"default_conferencing_bulk_title": "Masovno ažuriranje postojećih vrsti događaja",
|
||||
"invalid_event_name_variables": "U nazivu vašeg događaja se nalazi nevažeća promenljiva",
|
||||
"requires_at_least_one_schedule": "Potrebno je da imate barem jedan raspored",
|
||||
"default_conferencing_bulk_description": "Ažuriranje lokacija za izabrane tipove događaja",
|
||||
"looking_for_more_analytics": "Tražite još analitike?",
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Nuvarande användarnamn",
|
||||
"example_1": "Exempel 1",
|
||||
"example_2": "Exempel 2",
|
||||
"additional_input_label": "Ytterligare indata-etikett",
|
||||
"company_size": "Företagets storlek",
|
||||
"what_help_needed": "Vad behöver du hjälp med?",
|
||||
"variable_format": "Variabelformat",
|
||||
"webhook_subscriber_url_reserved": "Webhook-prenumerantens url är redan definierad",
|
||||
"custom_input_as_variable_info": "Ignorera alla specialtecken i etiketten för ytterligare indata (använd endast bokstäver och siffror), använd versaler för alla bokstäver och ersätt blanktecken med understreck.",
|
||||
"using_additional_inputs_as_variables": "Hur använder man ytterligare indata som variabler?",
|
||||
"download_desktop_app": "Ladda ned skrivbordsappen",
|
||||
"set_ping_link": "Ange Ping-länk",
|
||||
"rate_limit_exceeded": "Betygsgränsen överskriden",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Lägg till gräns",
|
||||
"team_name_required": "Teamnamn krävs",
|
||||
"show_attendees": "Dela deltagarinformation mellan gäster",
|
||||
"how_additional_inputs_as_variables": "Hur man använder ytterligare ingångar som variabler",
|
||||
"format": "Formatera",
|
||||
"uppercase_for_letters": "Använd versaler för alla bokstäver",
|
||||
"replace_whitespaces_underscores": "Ersätt blanktecken med understreck",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Behöver du något annat?",
|
||||
"billing_help_description": "Om du behöver mer hjälp med fakturering finns vårt supportteam här för att hjälpa till.",
|
||||
"billing_help_cta": "Kontakta support",
|
||||
"ignore_special_characters": "Ignorera specialtecken för etiketten Ytterligare indata. Använd endast bokstäver och siffror",
|
||||
"retry": "Försök igen",
|
||||
"fetching_calendars_error": "Det gick inte att hämta dina kalendrar. <1>Försök igen</1> eller kontakta kundtjänst.",
|
||||
"calendar_connection_fail": "Det gick inte att ansluta kalender",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Verifieringskod",
|
||||
"can_you_try_again": "Kan du försöka igen med en annan tid?",
|
||||
"verify": "Verifiera",
|
||||
"timezone_variable": "Tidszon",
|
||||
"invalid_event_name_variables": "Det finns en ogiltig variabel i ditt händelsenamn",
|
||||
"select_all": "Välj alla",
|
||||
"default_conferencing_bulk_title": "Uppdatera befintliga händelsetyper i bulk",
|
||||
"invalid_event_name_variables": "Det finns en ogiltig variabel i ditt händelsenamn",
|
||||
"default_conferencing_bulk_description": "Uppdatera platserna för de valda händelsetyperna"
|
||||
}
|
||||
|
||||
@@ -110,9 +110,9 @@
|
||||
"verification_code": "சரிபார்ப்பு குறியீடு",
|
||||
"can_you_try_again": "வேறு நேரத்தில் மீண்டும் முயற்சிக்க முடியுமா?",
|
||||
"verify": "சரிபார்க்கவும்",
|
||||
"invalid_event_name_variables": "உங்கள் நிகழ்வின் பெயரில் பிழை உள்ளது",
|
||||
"select_all": "அனைத்தையும் தேர்வுசெய்",
|
||||
"default_conferencing_bulk_title": "தற்போதுள்ள நிகழ்வு வகைகளை மொத்தமாகப் புதுப்பிக்கவும்",
|
||||
"invalid_event_name_variables": "உங்கள் நிகழ்வின் பெயரில் பிழை உள்ளது",
|
||||
"default_conferencing_bulk_description": "தேர்ந்தெடுக்கப்பட்ட நிகழ்வு வகைகளுக்கான இருப்பிடங்களைப் புதுப்பிக்கவும்",
|
||||
"looking_for_more_analytics": "மேலும் பகுப்பாய்வுகளைத் தேடுகிறீர்களா?",
|
||||
"add_filter": "வடிகட்டிச் சேர்க்கவும்",
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Mevcut kullanıcı adı",
|
||||
"example_1": "Örnek 1",
|
||||
"example_2": "Örnek 2",
|
||||
"additional_input_label": "Ek Veri Giriş Etiketi",
|
||||
"company_size": "Şirket büyüklüğü",
|
||||
"what_help_needed": "Hangi konuda yardıma ihtiyacınız var?",
|
||||
"variable_format": "Değişken biçimi",
|
||||
"webhook_subscriber_url_reserved": "Web kancası abone URL'si zaten tanımlı",
|
||||
"custom_input_as_variable_info": "Ek giriş etiketindeki tüm özel karakterleri yok sayın (yalnızca harfler ve rakamlar kullanın), tüm harfler için büyük harfleri kullanın ve boşlukları alt çizgilerle değiştirin.",
|
||||
"using_additional_inputs_as_variables": "Değişken olarak ek girdiler nasıl kullanılır?",
|
||||
"download_desktop_app": "Masaüstü uygulamasını indirin",
|
||||
"set_ping_link": "Ping bağlantısını ayarla",
|
||||
"rate_limit_exceeded": "Deneme sayısı aşıldı",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Kısıtlama Ekleyin",
|
||||
"team_name_required": "Ekip adı gereklidir",
|
||||
"show_attendees": "Katılımcı bilgilerini misafirler arasında paylaşın",
|
||||
"how_additional_inputs_as_variables": "Ek Girdiler Değişken Olarak Nasıl Kullanılır",
|
||||
"format": "Biçim",
|
||||
"uppercase_for_letters": "Tüm harfleri büyük harf olarak kullanın",
|
||||
"replace_whitespaces_underscores": "Boşlukları alt çizgi ile değiştir",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Başka bir şeye ihtiyacınız var mı?",
|
||||
"billing_help_description": "Faturalandırma konusunda daha fazla yardıma ihtiyacınız olursa destek ekibimiz size yardımcı olmaya hazır.",
|
||||
"billing_help_cta": "Destek ekibimize ulaşın",
|
||||
"ignore_special_characters": "Ek Girdi etiketinizdeki özel karakterleri yok sayın. Yalnızca harf ve sayı kullanın",
|
||||
"retry": "Yeniden dene",
|
||||
"fetching_calendars_error": "Takvimleriniz alınırken bir sorun oluştu. Lütfen <1>tekrar deneyin</1> veya müşteri destek ekibiyle iletişme geçin.",
|
||||
"calendar_connection_fail": "Takvim bağlantısı yapılamadı",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Doğrulama kodu",
|
||||
"can_you_try_again": "Başka bir zaman tekrar deneyebilir misiniz?",
|
||||
"verify": "Doğrula",
|
||||
"timezone_variable": "Saat dilimi",
|
||||
"invalid_event_name_variables": "Etkinlik adınızda geçersiz bir değişken var",
|
||||
"select_all": "Tümünü Seç",
|
||||
"default_conferencing_bulk_title": "Mevcut etkinlik türlerini toplu olarak güncelleyin",
|
||||
"invalid_event_name_variables": "Etkinlik adınızda geçersiz bir değişken var",
|
||||
"default_conferencing_bulk_description": "Seçilen etkinlik türleri için konumları güncelleyin"
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"hidden_team_owner_message": "Щоб користуватися функціями команди, потрібен обліковий запис версії Pro. До переходу на цю версію вас буде приховано.",
|
||||
"link_expires": "P.S. Термін дії – ще {{expiresIn}} год.",
|
||||
"upgrade_to_per_seat": "Перейдіть на версію з оплатою за місце",
|
||||
"seat_options_doesnt_support_confirmation": "Для параметра «Місця» підтвердження не потрібно",
|
||||
"seat_options_doesnt_support_confirmation": "Для бронювань із місцями підтвердження не надається",
|
||||
"team_upgrade_seats_details": "У вас стільки неоплачених місць для учасників команди: {{unpaidCount}} із {{memberCount}}. Одне місце коштує {{seatPrice}} $ за місяць, тому загальна вартість вашого членства складатиме приблизно {{totalCost}} $ за місяць.",
|
||||
"team_upgrade_banner_description": "Дякуємо, що спробували наш новий план для команд! Ми помітили, що вашу команду «{{teamName}}» потрібно перевести на план вищого рівня.",
|
||||
"team_upgrade_banner_action": "Перейдіть на інший план",
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Поточне ім’я користувача",
|
||||
"example_1": "Приклад 1",
|
||||
"example_2": "Приклад 2",
|
||||
"additional_input_label": "Додатковий введений підпис",
|
||||
"company_size": "Розмір компанії",
|
||||
"what_help_needed": "З чим саме потрібна допомога?",
|
||||
"variable_format": "Формат змінної",
|
||||
"webhook_subscriber_url_reserved": "URL-адресу для абонента у вебгуку вже визначено",
|
||||
"custom_input_as_variable_info": "Усі спеціальні символи в додатковому введеному підписі пропускаються (використовуються тільки літери та цифри), використовуються лише великі літери, пробіли замінюються на підкреслення.",
|
||||
"using_additional_inputs_as_variables": "Як використовувати додаткові поля введення як змінні?",
|
||||
"download_desktop_app": "Завантажити додаток для ПК",
|
||||
"set_ping_link": "Установити Ping-посилання",
|
||||
"rate_limit_exceeded": "Перевищено ліміт кількості запитів",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Додати ліміт",
|
||||
"team_name_required": "Назва команди обов’язкова",
|
||||
"show_attendees": "Поширювати інформацію про учасників серед гостей",
|
||||
"how_additional_inputs_as_variables": "Як використовувати додаткові поля введення як змінні",
|
||||
"format": "Формат",
|
||||
"uppercase_for_letters": "Використовувати всі великі літери",
|
||||
"replace_whitespaces_underscores": "Заміняти пробіли підкресленнями",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Потрібно щось інше?",
|
||||
"billing_help_description": "Якщо вам потрібна додаткова допомога з виставленням рахунків, наша служба підтримки готова її надати.",
|
||||
"billing_help_cta": "Звернутися в службу підтримки",
|
||||
"ignore_special_characters": "Пропускати спеціальні символи в підписі додаткового поля. Використовувати лише літери та цифри.",
|
||||
"retry": "Повторити",
|
||||
"fetching_calendars_error": "Під час отримання календарів виникла проблема. <1>Повторіть спробу</1> або зверніться в службу підтримки.",
|
||||
"calendar_connection_fail": "Не вдалося підключитися до календаря",
|
||||
@@ -1590,7 +1586,7 @@
|
||||
"recommended_next_steps": "Рекомендовані наступні кроки",
|
||||
"create_a_managed_event": "Створіть подію керованого типу",
|
||||
"meetings_are_better_with_the_right": "Краще проводити наради з потрібними учасниками команди. Запросіть їх зараз.",
|
||||
"create_a_one_one_template": "Створіть єдиний шаблон для типу заходу та розповсюдьте його між учасниками.",
|
||||
"create_a_one_one_template": "Створіть шаблон «один на один» для типу заходу та розповсюдьте його між учасниками.",
|
||||
"collective_or_roundrobin": "Колективна або циклічна",
|
||||
"book_your_team_members": "Забронюйте час усіх учасників команди одночасно за допомогою колективних подій або виберіть потрібну людину за циклічним принципом.",
|
||||
"event_no_longer_attending_subject": "Більше не відвідує захід «{{title}}» о {{date}}",
|
||||
@@ -1598,7 +1594,7 @@
|
||||
"attendee_no_longer_attending_subject": "Учасник більше не відвідує захід «{{title}}» о {{date}}",
|
||||
"attendee_no_longer_attending": "Учасник більше не відвідує ваш захід",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} скасував(-ла) участь. Це означає, що на цей час з’явилося вільне місце.",
|
||||
"create_event_on": "Створити захід",
|
||||
"create_event_on": "Створити захід в календарі",
|
||||
"default_app_link_title": "Установити посилання на додаток за замовчуванням",
|
||||
"default_app_link_description": "Посилання на додаток за замовчуванням використовуватиметься для всіх новостворених типів подій.",
|
||||
"change_default_conferencing_app": "Установити за замовчуванням",
|
||||
@@ -1617,7 +1613,7 @@
|
||||
"form_builder_field_already_exists": "Поле з такою назвою вже існує",
|
||||
"form_builder_field_add_subtitle": "Налаштуйте запитання, які ставитимуться на сторінці бронювання",
|
||||
"get_started_zapier_templates": "Розпочніть роботу із шаблонами Zapier",
|
||||
"team_is_unpublished": "Публікацію такого елемента скасовано: {{team}}",
|
||||
"team_is_unpublished": "Публікацію такої команди скасовано: {{team}}",
|
||||
"team_is_unpublished_description": "Посилання на команду зараз недоступне. Зверніться до власника команди або попросіть його опублікувати це посилання.",
|
||||
"team_member": "Учасник команди",
|
||||
"a_routing_form": "Форма переспрямування",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Код перевірки",
|
||||
"can_you_try_again": "Можете спробувати, указавши інший час?",
|
||||
"verify": "Перевірити",
|
||||
"timezone_variable": "Часовий пояс",
|
||||
"invalid_event_name_variables": "Назва вашого заходу містить неприпустиму змінну",
|
||||
"select_all": "Вибрати всі",
|
||||
"default_conferencing_bulk_title": "Масове оновлення існуючих типів заходів",
|
||||
"invalid_event_name_variables": "Назва вашого заходу містить неприпустиму змінну",
|
||||
"default_conferencing_bulk_description": "Оновіть розташування для вибраних типів заходів"
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "Tên người dùng hiện tại",
|
||||
"example_1": "Ví dụ 1",
|
||||
"example_2": "Ví dụ 2",
|
||||
"additional_input_label": "Nhãn thông tin đầu vào bổ sung",
|
||||
"company_size": "Quy mô công ty",
|
||||
"what_help_needed": "Bạn cần giúp điều gì?",
|
||||
"variable_format": "Định dạng biến số",
|
||||
"webhook_subscriber_url_reserved": "Url ghi danh Webhook đã được lập định",
|
||||
"custom_input_as_variable_info": "Bỏ qua tất cả các ký tự đặc biệt của nhãn thông tin đầu vào bổ sung (chỉ dùng chữ cái và con số), dùng chữ in hoa cho tất cả các chữ cái và thay thế khoảng trắng bằng dấu gạch dưới.",
|
||||
"using_additional_inputs_as_variables": "Cách sử dụng thông tin đầu vào bổ sung ở vai trò biến số?",
|
||||
"download_desktop_app": "Tải ứng dụng trên máy tính bàn",
|
||||
"set_ping_link": "Đặt liên kết Ping",
|
||||
"rate_limit_exceeded": "Đã vượt quá giới hạn đăng nhập",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "Thêm giới hạn",
|
||||
"team_name_required": "Cần có tên nhóm",
|
||||
"show_attendees": "Chia sẻ thông tin người tham gia cho khách",
|
||||
"how_additional_inputs_as_variables": "Cách sử dụng Đầu vào bổ sung làm Tham số",
|
||||
"format": "Định dạng",
|
||||
"uppercase_for_letters": "In hoa hết tất cả các chữ cái",
|
||||
"replace_whitespaces_underscores": "Thay thế khoảng trắng bằng dấu gạch dưới",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "Cần gì nữa không?",
|
||||
"billing_help_description": "Nếu bạn cần thêm bất kỳ trợ giúp nào về thanh toán, nhóm hỗ trợ của chúng tôi luôn sẵn sàng trợ giúp.",
|
||||
"billing_help_cta": "Liên hệ với bộ phận hỗ trợ",
|
||||
"ignore_special_characters": "Bỏ qua những kí tự đặc biệt trong Nhãn đầu vào bổ sung. Chỉ dùng chữ cái và số",
|
||||
"retry": "Thử lại",
|
||||
"fetching_calendars_error": "Có vấn đề khi tìm dữ liệu của bạn. Vui lòng <1>thử lại</1> hoặc liên hệ với bộ phận hỗ trợ khách hàng.",
|
||||
"calendar_connection_fail": "Kết nối lịch bất thành",
|
||||
@@ -1649,8 +1645,9 @@
|
||||
"verification_code": "Mã xác minh",
|
||||
"can_you_try_again": "Bạn có thể thử lại lúc khác được chứ?",
|
||||
"verify": "Xác minh",
|
||||
"timezone_variable": "Múi giờ",
|
||||
"invalid_event_name_variables": "Có biến số không hợp lệ trong tên sự kiện của bạn",
|
||||
"select_all": "Chọn tất cả",
|
||||
"default_conferencing_bulk_title": "Cập nhật hàng loại những loại sự kiện hiện có",
|
||||
"invalid_event_name_variables": "Có biến số không hợp lệ trong tên sự kiện của bạn",
|
||||
"default_conferencing_bulk_description": "Cập nhật địa điểm cho những loại sự kiện đã chọn"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"identity_provider": "身份提供程序",
|
||||
"trial_days_left": "您的 PRO 试用还有 $t(day, {\"count\": {{days}} }) 到期",
|
||||
"day_one": "{{count}} 天",
|
||||
"day_other": "{{count}} 天",
|
||||
@@ -1126,13 +1127,11 @@
|
||||
"current_username": "当前用户名",
|
||||
"example_1": "示例 1",
|
||||
"example_2": "示例 2",
|
||||
"additional_input_label": "附加输入标签",
|
||||
"company_size": "公司规模",
|
||||
"what_help_needed": "您需要什么帮助?",
|
||||
"variable_format": "变量格式",
|
||||
"webhook_subscriber_url_reserved": "Webhook 订阅者 url 已定义",
|
||||
"custom_input_as_variable_info": "忽略附加输入标签的所有特殊字符(仅使用字母和数字),所有字母均使用大写,并将空格替换为下划线。",
|
||||
"using_additional_inputs_as_variables": "如何将附加输入用作变量?",
|
||||
"download_desktop_app": "下载桌面应用",
|
||||
"set_ping_link": "设置 Ping 链接",
|
||||
"rate_limit_exceeded": "已超过速率限制",
|
||||
@@ -1345,7 +1344,6 @@
|
||||
"add_limit": "添加限制",
|
||||
"team_name_required": "需要团队名称",
|
||||
"show_attendees": "在访客之间共享参与者信息",
|
||||
"how_additional_inputs_as_variables": "如何将附加输入用作变量",
|
||||
"format": "格式",
|
||||
"uppercase_for_letters": "所有字母均使用大写",
|
||||
"replace_whitespaces_underscores": "将空格替换为下划线",
|
||||
@@ -1360,7 +1358,6 @@
|
||||
"billing_help_title": "还有其他需要?",
|
||||
"billing_help_description": "如果您在支付帐单时需要任何进一步的帮助,我们的支持团队随时为您提供。",
|
||||
"billing_help_cta": "联系支持",
|
||||
"ignore_special_characters": "忽略附加输入标签中的特殊字符。仅使用字母和数字",
|
||||
"retry": "重试",
|
||||
"fetching_calendars_error": "获取您的日历时出现问题。请<1>重试</1>或联系客户支持。",
|
||||
"calendar_connection_fail": "日历连接失败",
|
||||
@@ -1599,7 +1596,7 @@
|
||||
"attendee_no_longer_attending_subject": "一名参与者不再参加于 {{date}} 举行的 {{title}} ",
|
||||
"attendee_no_longer_attending": "一名参与者不再参加您的活动",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} 已取消。这意味着该时段有一个位置空出来",
|
||||
"create_event_on": "活动创建时间",
|
||||
"create_event_on": "创建活动于",
|
||||
"default_app_link_title": "设置默认应用链接",
|
||||
"default_app_link_description": "设置默认应用链接可以让所有新创建的活动类型使用您设置的应用链接。",
|
||||
"change_default_conferencing_app": "设置为默认",
|
||||
@@ -1650,8 +1647,9 @@
|
||||
"verification_code": "验证码",
|
||||
"can_you_try_again": "可否用不同的时间再次尝试?",
|
||||
"verify": "验证",
|
||||
"timezone_variable": "时区",
|
||||
"invalid_event_name_variables": "活动名称中存在无效变量",
|
||||
"select_all": "全选",
|
||||
"default_conferencing_bulk_title": "批量更新现有事件类型",
|
||||
"invalid_event_name_variables": "活动名称中存在无效变量",
|
||||
"default_conferencing_bulk_description": "更新选定活动类型的位置"
|
||||
}
|
||||
|
||||
@@ -1126,13 +1126,11 @@
|
||||
"current_username": "目前使用者名稱",
|
||||
"example_1": "範例 1",
|
||||
"example_2": "範例 2",
|
||||
"additional_input_label": "額外的輸入標籤",
|
||||
"company_size": "公司規模",
|
||||
"what_help_needed": "您需要什麼協助?",
|
||||
"variable_format": "變數格式",
|
||||
"webhook_subscriber_url_reserved": "已定義 Webhook 訂閱者網址",
|
||||
"custom_input_as_variable_info": "忽略額外輸入標籤的所有特殊字元 (只使用字母和數字),所有字母一律使用大寫,並以底線取代空格。",
|
||||
"using_additional_inputs_as_variables": "如何將額外輸入內容作為變數使用?",
|
||||
"download_desktop_app": "下載桌面版應用程式",
|
||||
"set_ping_link": "設定 Ping 連結",
|
||||
"rate_limit_exceeded": "已超過限速",
|
||||
@@ -1345,7 +1343,6 @@
|
||||
"add_limit": "新增限制",
|
||||
"team_name_required": "需提供團隊名稱",
|
||||
"show_attendees": "和賓客分享與會者資訊",
|
||||
"how_additional_inputs_as_variables": "如何將額外輸入內容作為變數使用",
|
||||
"format": "格式",
|
||||
"uppercase_for_letters": "一律使用大寫字母",
|
||||
"replace_whitespaces_underscores": "以底線取代空格",
|
||||
@@ -1360,7 +1357,6 @@
|
||||
"billing_help_title": "還需要什麼嗎?",
|
||||
"billing_help_description": "若您需要關於付費的進一步協助,我們的支援團隊會隨時幫助您。",
|
||||
"billing_help_cta": "聯絡支援",
|
||||
"ignore_special_characters": "忽略額外輸入標籤中的所有特殊字元。只使用字母和數字",
|
||||
"retry": "重試",
|
||||
"fetching_calendars_error": "擷取行事曆時發生問題。請<1>再試一次</1>,或聯絡客服支援團隊。",
|
||||
"calendar_connection_fail": "行事曆連線失敗",
|
||||
@@ -1647,10 +1643,11 @@
|
||||
"app_is_connected": "已連結 {{dependencyName}}",
|
||||
"requires_app": "需要 {{dependencyName}}",
|
||||
"verification_code": "驗證碼",
|
||||
"can_you_try_again": "您可以使用不同的時間再試一次嗎?",
|
||||
"can_you_try_again": "您可以再試試其他時間嗎?",
|
||||
"verify": "驗證",
|
||||
"timezone_variable": "時區",
|
||||
"invalid_event_name_variables": "您的活動名稱包含無效的變數",
|
||||
"select_all": "全部選擇",
|
||||
"default_conferencing_bulk_title": "大量更新現有活動類型",
|
||||
"invalid_event_name_variables": "您的活動名稱包含無效的變數",
|
||||
"default_conferencing_bulk_description": "更新所選活動類型的地點"
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
defaults.url=https://sentry.io/
|
||||
defaults.org=calcom
|
||||
defaults.project=cal
|
||||
@@ -1,16 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever the server handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -205,7 +205,7 @@ html.todesktop nav a svg{
|
||||
https://github.com/tailwindlabs/tailwindcss/pull/5732
|
||||
*/
|
||||
@layer utilities {
|
||||
@variants responsive {
|
||||
@layer responsive {
|
||||
/* Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import checkSession from "../../_utils/auth";
|
||||
import { ExchangeAuthentication } from "../enums";
|
||||
import { ExchangeAuthentication, ExchangeVersion } from "../enums";
|
||||
import { CalendarService } from "../lib";
|
||||
|
||||
const formSchema = z
|
||||
@@ -17,6 +17,7 @@ const formSchema = z
|
||||
username: z.string().email(),
|
||||
password: z.string(),
|
||||
authenticationMethod: z.number().default(ExchangeAuthentication.STANDARD),
|
||||
exchangeVersion: z.number().default(ExchangeVersion.Exchange2016),
|
||||
useCompression: z.boolean().default(false),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -2,3 +2,13 @@ export enum ExchangeAuthentication {
|
||||
STANDARD = 0,
|
||||
NTLM = 1,
|
||||
}
|
||||
export enum ExchangeVersion {
|
||||
Exchange2007_SP1 = 0,
|
||||
Exchange2010 = 1,
|
||||
Exchange2010_SP1 = 2,
|
||||
Exchange2010_SP2 = 3,
|
||||
Exchange2013 = 4,
|
||||
Exchange2013_SP1 = 5,
|
||||
Exchange2015 = 6,
|
||||
Exchange2016 = 7,
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ export default class ExchangeCalendarService implements Calendar {
|
||||
}
|
||||
|
||||
private getExchangeService(): ExchangeService {
|
||||
const service: ExchangeService = new ExchangeService();
|
||||
const service: ExchangeService = new ExchangeService(this.payload.exchangeVersion);
|
||||
service.Credentials = new WebCredentials(this.payload.username, this.payload.password);
|
||||
service.Url = new Uri(this.payload.url);
|
||||
if (this.payload.authenticationMethod === ExchangeAuthentication.NTLM) {
|
||||
|
||||
@@ -8,13 +8,14 @@ import z from "zod";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, EmailField, Form, PasswordField, SelectField, Switch, TextField } from "@calcom/ui";
|
||||
|
||||
import { ExchangeAuthentication } from "../../enums";
|
||||
import { ExchangeAuthentication, ExchangeVersion } from "../../enums";
|
||||
|
||||
interface IFormData {
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
authenticationMethod: ExchangeAuthentication;
|
||||
exchangeVersion: ExchangeVersion;
|
||||
useCompression: boolean;
|
||||
}
|
||||
|
||||
@@ -24,6 +25,7 @@ const schema = z
|
||||
username: z.string().email(),
|
||||
password: z.string(),
|
||||
authenticationMethod: z.number().default(ExchangeAuthentication.STANDARD),
|
||||
exchangeVersion: z.number().default(ExchangeVersion.Exchange2016),
|
||||
useCompression: z.boolean().default(false),
|
||||
})
|
||||
.strict();
|
||||
@@ -33,13 +35,27 @@ export default function ExchangeSetup() {
|
||||
const router = useRouter();
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const form = useForm<IFormData>({
|
||||
defaultValues: { authenticationMethod: ExchangeAuthentication.STANDARD },
|
||||
defaultValues: {
|
||||
authenticationMethod: ExchangeAuthentication.STANDARD,
|
||||
exchangeVersion: ExchangeVersion.Exchange2016,
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
const authenticationMethods = [
|
||||
{ value: ExchangeAuthentication.STANDARD, label: t("exchange_authentication_standard") },
|
||||
{ value: ExchangeAuthentication.NTLM, label: t("exchange_authentication_ntlm") },
|
||||
];
|
||||
const exchangeVersions = [
|
||||
{ value: ExchangeVersion.Exchange2007_SP1, label: t("exchange_version_2007_SP1") },
|
||||
{ value: ExchangeVersion.Exchange2010, label: t("exchange_version_2010") },
|
||||
{ value: ExchangeVersion.Exchange2010_SP1, label: t("exchange_version_2010_SP1") },
|
||||
{ value: ExchangeVersion.Exchange2010_SP2, label: t("exchange_version_2010_SP2") },
|
||||
{ value: ExchangeVersion.Exchange2013, label: t("exchange_version_2013") },
|
||||
{ value: ExchangeVersion.Exchange2013_SP1, label: t("exchange_version_2013_SP1") },
|
||||
{ value: ExchangeVersion.Exchange2015, label: t("exchange_version_2015") },
|
||||
{ value: ExchangeVersion.Exchange2016, label: t("exchange_version_2016") },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-emphasis flex h-screen">
|
||||
@@ -111,6 +127,21 @@ export default function ExchangeSetup() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="exchangeVersion"
|
||||
control={form.control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<SelectField
|
||||
label={t("exchange_version")}
|
||||
options={exchangeVersions}
|
||||
defaultValue={exchangeVersions[7]}
|
||||
onChange={async (version) => {
|
||||
onChange(version?.value);
|
||||
form.setValue("exchangeVersion", version!.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Switch
|
||||
label={t("exchange_compression")}
|
||||
name="useCompression"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui";
|
||||
import { Alert, Button, Dialog, DialogClose, DialogContent, DialogFooter, Input } from "@calcom/ui";
|
||||
|
||||
interface ISearchDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -87,15 +87,15 @@ export const SearchDialog = (props: ISearchDialog) => {
|
||||
const renderTab = (Icon: any, text: string, mode: Mode) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex cursor-pointer items-center border-b-2 p-2 text-sm",
|
||||
selectedMode === mode ? "border-black" : "text-subtle border-transparent"
|
||||
"flex cursor-pointer items-center border-b-2 p-2 text-sm ",
|
||||
selectedMode === mode ? "text-default border-emphasis" : "text-subtle border-transparent"
|
||||
)}
|
||||
onClick={() => {
|
||||
setKeyword("");
|
||||
setGifImage("");
|
||||
setSelectedMode(mode);
|
||||
}}>
|
||||
<Icon color={selectedMode === mode ? "black" : "grey"} className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
<Icon className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
@@ -117,7 +117,7 @@ export const SearchDialog = (props: ISearchDialog) => {
|
||||
{t("add_gif_to_confirmation")}
|
||||
</h3>
|
||||
<p className="text-subtle mb-3 text-sm font-light">{t("find_gif_spice_confirmation")}</p>
|
||||
<div className="flex items-center border-b border-solid">
|
||||
<div className="border-emphasis flex items-center border-b border-solid">
|
||||
{renderTab(SearchIcon, t("search_giphy"), MODE_SEARCH)}
|
||||
{renderTab(LinkIcon, t("add_link_from_giphy"), MODE_URL)}
|
||||
</div>
|
||||
@@ -125,9 +125,8 @@ export const SearchDialog = (props: ISearchDialog) => {
|
||||
className="flex w-full justify-center space-x-2 space-y-2 rtl:space-x-reverse"
|
||||
onSubmit={handleFormSubmit}>
|
||||
<div className="relative block w-full pt-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
className="border-default block w-full rounded-sm shadow-sm sm:text-sm"
|
||||
placeholder={
|
||||
selectedMode === MODE_SEARCH
|
||||
? t("search_giphy")
|
||||
|
||||
@@ -276,8 +276,13 @@ export function FormActionsProvider({ appUrl, children }: { appUrl: string; chil
|
||||
}
|
||||
return { previousValue };
|
||||
},
|
||||
onSettled: () => {
|
||||
onSettled: (routingForm) => {
|
||||
utils.viewer.appRoutingForms.forms.invalidate();
|
||||
if (routingForm) {
|
||||
utils.viewer.appRoutingForms.formQuery.invalidate({
|
||||
id: routingForm.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (err, value, context) => {
|
||||
if (context?.previousValue) {
|
||||
|
||||
@@ -23,6 +23,7 @@ export type CommonProps<
|
||||
placeholder?: string;
|
||||
readOnly?: boolean;
|
||||
className?: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
value: TVal;
|
||||
setValue: (value: TVal) => void;
|
||||
|
||||
@@ -332,7 +332,6 @@ export default function FormEditPage({
|
||||
/>
|
||||
);
|
||||
}
|
||||
FormEditPage.isThemeSupported = true;
|
||||
|
||||
FormEditPage.getLayout = (page: React.ReactElement) => {
|
||||
return (
|
||||
|
||||
@@ -252,8 +252,6 @@ export default function RoutingForms({
|
||||
);
|
||||
}
|
||||
|
||||
RoutingForms.isThemeSupported = true;
|
||||
|
||||
RoutingForms.getLayout = (page: React.ReactElement) => {
|
||||
return <Shell withoutMain={true}>{page}</Shell>;
|
||||
};
|
||||
|
||||
@@ -197,8 +197,6 @@ export default function ReporterWrapper({
|
||||
);
|
||||
}
|
||||
|
||||
ReporterWrapper.isThemeSupported = true;
|
||||
|
||||
ReporterWrapper.getLayout = (page: React.ReactElement) => {
|
||||
return (
|
||||
<Shell backPath="/apps/routing-forms/forms" withoutMain={true}>
|
||||
|
||||
@@ -523,7 +523,6 @@ export default function RouteBuilder({
|
||||
/>
|
||||
);
|
||||
}
|
||||
RouteBuilder.isThemeSupported = true;
|
||||
|
||||
RouteBuilder.getLayout = (page: React.ReactElement) => {
|
||||
return (
|
||||
|
||||
@@ -26,8 +26,6 @@ export default function Router({ form, message }: inferSSRProps<typeof getServer
|
||||
);
|
||||
}
|
||||
|
||||
Router.isThemeSupported = true;
|
||||
|
||||
const querySchema = z
|
||||
.object({
|
||||
form: z.string(),
|
||||
|
||||
@@ -165,7 +165,7 @@ export default function RoutingLink(props: inferSSRProps<typeof getServerSidePro
|
||||
return <RoutingForm {...props} />;
|
||||
}
|
||||
|
||||
RoutingLink.isThemeSupported = true;
|
||||
RoutingLink.isBookingPage = true;
|
||||
|
||||
export const getServerSideProps = async function getServerSideProps(
|
||||
context: AppGetServerSidePropsContext,
|
||||
|
||||
@@ -66,7 +66,7 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(t("unexpected_error_try_again"), "error");
|
||||
// @TODO: notify sentry
|
||||
// @TODO: notify
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
|
||||
@@ -149,7 +149,6 @@ module.exports = {
|
||||
plugins: [
|
||||
require("@tailwindcss/forms"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("@tailwindcss/line-clamp"),
|
||||
require("tailwind-scrollbar"),
|
||||
require("tailwindcss-radix")(),
|
||||
plugin(({ addVariant }) => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Cre
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
||||
let coldStart = true;
|
||||
|
||||
export const getCalendarCredentials = (credentials: Array<CredentialPayload>) => {
|
||||
const calendarCredentials = getApps(credentials)
|
||||
@@ -183,48 +184,71 @@ export const getCachedResults = async (
|
||||
*/
|
||||
const getNextCache = async (username: string, month: string): Promise<EventBusyDate[][]> => {
|
||||
let localCache: EventBusyDate[][] = [];
|
||||
const { NODE_ENV } = process.env;
|
||||
const cacheDir = `${NODE_ENV === "development" ? NODE_ENV : process.env.BUILD_ID}`;
|
||||
const baseUrl = `${WEBAPP_URL}/_next/data/${cacheDir}/en`;
|
||||
const url = `${baseUrl}/${username}/calendar-cache/${month}.json?user=${username}&month=${month}`;
|
||||
try {
|
||||
const { NODE_ENV } = process.env;
|
||||
const cacheDir = `${NODE_ENV === "development" ? NODE_ENV : process.env.BUILD_ID}`;
|
||||
const baseUrl = `${WEBAPP_URL}/_next/data/${cacheDir}/en`;
|
||||
console.log(`${baseUrl}/${username}/calendar-cache/${month}.json?user=${username}&month=${month}`);
|
||||
localCache = await fetch(
|
||||
`${baseUrl}/${username}/calendar-cache/${month}.json?user=${username}&month=${month}`
|
||||
)
|
||||
localCache = await fetch(url)
|
||||
.then((r) => r.json())
|
||||
.then((json) => json?.pageProps?.results);
|
||||
// No need to wait for this, the purpose is to force re-validation every second as indicated
|
||||
// in page getStaticProps.
|
||||
fetch(`${baseUrl}/${username}/calendar-cache/${month}`).catch(console.log);
|
||||
} catch (e) {
|
||||
log.warn(e);
|
||||
log.warn(url, e);
|
||||
}
|
||||
return localCache;
|
||||
};
|
||||
/**
|
||||
* Get months between given dates
|
||||
* @returns ["2023-04", "2024-05"]
|
||||
*/
|
||||
const getMonths = (dateFrom: string, dateTo: string): string[] => {
|
||||
const months: string[] = [dayjs(dateFrom).format("YYYY-MM")];
|
||||
for (
|
||||
let i = 1;
|
||||
dayjs(dateFrom).add(i, "month").isBefore(dateTo) ||
|
||||
dayjs(dateFrom).add(i, "month").isSame(dateTo, "month");
|
||||
i++
|
||||
) {
|
||||
months.push(dayjs(dateFrom).add(i, "month").format("YYYY-MM"));
|
||||
}
|
||||
return months;
|
||||
};
|
||||
|
||||
const createCalendarCachePage = (username: string, month: string): void => {
|
||||
// No need to wait for this, the purpose is to force re-validation every second as indicated
|
||||
// in page getStaticProps.
|
||||
fetch(`${WEBAPP_URL}/${username}/calendar-cache/${month}`).catch(console.log);
|
||||
};
|
||||
export const getBusyCalendarTimes = async (
|
||||
username: string,
|
||||
withCredentials: CredentialPayload[],
|
||||
dateFrom: string,
|
||||
dateTo: string
|
||||
dateTo: string,
|
||||
selectedCalendars: SelectedCalendar[]
|
||||
) => {
|
||||
let results: EventBusyDate[][] = [];
|
||||
if (dayjs(dateFrom).isSame(dayjs(dateTo), "month")) {
|
||||
results = await getNextCache(username, dayjs(dateFrom).format("YYYY-MM"));
|
||||
} else {
|
||||
// if dateFrom and dateTo is from different months get cache by each month
|
||||
const months: string[] = [dayjs(dateFrom).format("YYYY-MM")];
|
||||
for (
|
||||
let i = 1;
|
||||
dayjs(dateFrom).add(i, "month").isBefore(dateTo) ||
|
||||
dayjs(dateFrom).add(i, "month").isSame(dateTo, "month");
|
||||
i++
|
||||
) {
|
||||
months.push(dayjs(dateFrom).add(i, "month").format("YYYY-MM"));
|
||||
const months = getMonths(dateFrom, dateTo);
|
||||
try {
|
||||
if (coldStart) {
|
||||
results = await getCachedResults(withCredentials, dateFrom, dateTo, selectedCalendars);
|
||||
logger.info("Generating calendar cache in background");
|
||||
// on cold start the calendar cache page generated in the background
|
||||
Promise.all(months.map((month) => createCalendarCachePage(username, month)));
|
||||
} else {
|
||||
if (months.length === 1) {
|
||||
results = await getNextCache(username, dayjs(dateFrom).format("YYYY-MM"));
|
||||
} else {
|
||||
// if dateFrom and dateTo is from different months get cache by each month
|
||||
const data: EventBusyDate[][][] = await Promise.all(
|
||||
months.map((month) => getNextCache(username, month))
|
||||
);
|
||||
results = data.flat(1);
|
||||
}
|
||||
}
|
||||
const data: EventBusyDate[][][] = await Promise.all(months.map((month) => getNextCache(username, month)));
|
||||
results = data.flat(1);
|
||||
} catch (e) {
|
||||
logger.warn(e);
|
||||
}
|
||||
coldStart = false;
|
||||
return results.reduce((acc, availability) => acc.concat(availability), []);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||
import type { EventBusyDetails } from "@calcom/types/Calendar";
|
||||
|
||||
export async function getBusyTimes(params: {
|
||||
@@ -17,6 +18,7 @@ export async function getBusyTimes(params: {
|
||||
beforeEventBuffer?: number;
|
||||
afterEventBuffer?: number;
|
||||
endTime: string;
|
||||
selectedCalendars: SelectedCalendar[];
|
||||
}) {
|
||||
const {
|
||||
credentials,
|
||||
@@ -27,6 +29,7 @@ export async function getBusyTimes(params: {
|
||||
endTime,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
selectedCalendars,
|
||||
} = params;
|
||||
logger.silly(
|
||||
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
|
||||
@@ -119,7 +122,20 @@ export async function getBusyTimes(params: {
|
||||
performance.mark("prismaBookingGetEnd");
|
||||
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
|
||||
if (credentials?.length > 0) {
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(username, credentials, startTime, endTime);
|
||||
const startConnectedCalendarsGet = performance.now();
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(
|
||||
username,
|
||||
credentials,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedCalendars
|
||||
);
|
||||
const endConnectedCalendarsGet = performance.now();
|
||||
logger.debug(
|
||||
`Connected Calendars get took ${
|
||||
endConnectedCalendarsGet - startConnectedCalendarsGet
|
||||
} ms for user ${username}`
|
||||
);
|
||||
busyTimes.push(
|
||||
...calendarBusyTimes.map((value) => ({
|
||||
...value,
|
||||
|
||||
@@ -149,6 +149,7 @@ export async function getUserAvailability(
|
||||
username: `${user.username}`,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
selectedCalendars: user.selectedCalendars,
|
||||
});
|
||||
|
||||
let bufferedBusyTimes: EventBusyDetails[] = busyTimes.map((a) => ({
|
||||
|
||||
@@ -104,7 +104,7 @@ const IntegrationContainer = ({
|
||||
} else if (app.keys) {
|
||||
showKeyModal(true);
|
||||
} else {
|
||||
enableAppMutation.mutate({ slug: app.slug, enabled: app.enabled });
|
||||
enableAppMutation.mutate({ slug: app.slug, enabled: !app.enabled });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -117,7 +117,7 @@ const IntegrationContainer = ({
|
||||
title={t("disable_app")}
|
||||
variety="danger"
|
||||
onConfirm={() => {
|
||||
enableAppMutation.mutate({ slug: app.slug, enabled: app.enabled });
|
||||
enableAppMutation.mutate({ slug: app.slug, enabled: !app.enabled });
|
||||
}}>
|
||||
{t("disable_app_description")}
|
||||
</ConfirmationDialogContent>
|
||||
|
||||
@@ -1,18 +1,81 @@
|
||||
import { LRUCache } from "lru-cache";
|
||||
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import { getServerSession as getServerSessionInner } from "next-auth/next";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
import { AUTH_OPTIONS } from "./next-auth-options";
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
/**
|
||||
* Stores the session in memory using the stringified token as the key.
|
||||
*
|
||||
*/
|
||||
const CACHE = new LRUCache<string, Session>({ max: 1000 });
|
||||
|
||||
/**
|
||||
* This is a slimmed down version of the `getServerSession` function from
|
||||
* `next-auth`.
|
||||
*
|
||||
* Instead of requiring the entire options object for NextAuth, we create
|
||||
* a compatible session using information from the incoming token.
|
||||
*
|
||||
* The downside to this is that we won't refresh sessions if the users
|
||||
* token has expired (30 days). This should be fine as we call `/auth/session`
|
||||
* frequently enough on the client-side to keep the session alive.
|
||||
*/
|
||||
export async function getServerSession(options: {
|
||||
req: NextApiRequest | GetServerSidePropsContext["req"];
|
||||
res: NextApiResponse | GetServerSidePropsContext["res"];
|
||||
res?: NextApiResponse | GetServerSidePropsContext["res"];
|
||||
authOptions?: AuthOptions;
|
||||
}) {
|
||||
const { req, res, authOptions = AUTH_OPTIONS } = options;
|
||||
const { req, authOptions: { secret } = {} } = options;
|
||||
|
||||
const session = await getServerSessionInner(req, res, authOptions);
|
||||
const token = await getToken({
|
||||
req,
|
||||
secret,
|
||||
});
|
||||
|
||||
// that these are equal are ensured in `[...nextauth]`'s callback
|
||||
return session as Session | null;
|
||||
if (!token || !token.email || !token.sub) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cachedSession = CACHE.get(JSON.stringify(token));
|
||||
|
||||
if (cachedSession) {
|
||||
return cachedSession;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: token.email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
|
||||
const session: Session = {
|
||||
hasValidLicense,
|
||||
expires: new Date(typeof token.exp === "number" ? token.exp * 1000 : Date.now()).toISOString(),
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
emailVerified: user.emailVerified,
|
||||
email_verified: user.emailVerified !== null,
|
||||
role: user.role,
|
||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
impersonatedByUID: token.impersonatedByUID ?? undefined,
|
||||
belongsToActiveTeam: token.belongsToActiveTeam,
|
||||
},
|
||||
};
|
||||
|
||||
CACHE.set(JSON.stringify(token), session);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
import type { UserPermissionRole, Membership, Team } from "@prisma/client";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
import { SignJWT } from "jose";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import type { Provider } from "next-auth/providers";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import type { TransportOptions } from "nodemailer";
|
||||
import nodemailer from "nodemailer";
|
||||
import { authenticator } from "otplib";
|
||||
import path from "path";
|
||||
|
||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
import { isENVDev } from "@calcom/lib/env";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import rateLimit from "@calcom/lib/rateLimit";
|
||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
@@ -40,26 +31,10 @@ const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } =
|
||||
const GOOGLE_LOGIN_ENABLED = process.env.GOOGLE_LOGIN_ENABLED === "true";
|
||||
const IS_GOOGLE_LOGIN_ENABLED = !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET && GOOGLE_LOGIN_ENABLED);
|
||||
|
||||
const transporter = nodemailer.createTransport<TransportOptions>({
|
||||
...(serverConfig.transport as TransportOptions),
|
||||
} as TransportOptions);
|
||||
|
||||
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
|
||||
|
||||
const signJwt = async (payload: { email: string }) => {
|
||||
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
return new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setSubject(payload.email)
|
||||
.setIssuedAt()
|
||||
.setIssuer(WEBSITE_URL)
|
||||
.setAudience(`${WEBSITE_URL}/auth/login`)
|
||||
.setExpirationTime("2m")
|
||||
.sign(secret);
|
||||
};
|
||||
|
||||
const loginWithTotp = async (user: { email: string }) =>
|
||||
`/auth/login?totp=${await signJwt({ email: user.email })}`;
|
||||
`/auth/login?totp=${await (await import("./signJwt")).default({ email: user.email })}`;
|
||||
|
||||
type UserTeams = {
|
||||
teams: (Membership & {
|
||||
@@ -67,13 +42,16 @@ type UserTeams = {
|
||||
})[];
|
||||
};
|
||||
|
||||
const checkIfUserBelongsToActiveTeam = <T extends UserTeams>(user: T): boolean =>
|
||||
user.teams.filter((m: { team: { metadata: unknown } }) => {
|
||||
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||
export const checkIfUserBelongsToActiveTeam = <T extends UserTeams>(user: T) =>
|
||||
user.teams.some((m: { team: { metadata: unknown } }) => {
|
||||
if (!IS_TEAM_BILLING_ENABLED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||
return false;
|
||||
}).length > 0;
|
||||
|
||||
return metadata.success && metadata.data?.subscriptionId;
|
||||
});
|
||||
|
||||
const providers: Provider[] = [
|
||||
CredentialsProvider({
|
||||
@@ -165,7 +143,7 @@ const providers: Provider[] = [
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const isValidToken = authenticator.check(credentials.totpCode, secret);
|
||||
const isValidToken = (await import("otplib")).authenticator.check(credentials.totpCode, secret);
|
||||
if (!isValidToken) {
|
||||
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
||||
}
|
||||
@@ -266,7 +244,7 @@ if (isSAMLLoginEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { oauthController } = await jackson();
|
||||
const { oauthController } = await (await import("@calcom/features/ee/sso/lib/jackson")).default();
|
||||
|
||||
// Fetch access token
|
||||
const { access_token } = await oauthController.token({
|
||||
@@ -303,37 +281,14 @@ if (isSAMLLoginEnabled) {
|
||||
);
|
||||
}
|
||||
|
||||
if (true) {
|
||||
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
|
||||
providers.push(
|
||||
EmailProvider({
|
||||
type: "email",
|
||||
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
|
||||
// Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify.
|
||||
sendVerificationRequest: ({ identifier, url }) => {
|
||||
const originalUrl = new URL(url);
|
||||
const webappUrl = new URL(WEBAPP_URL);
|
||||
if (originalUrl.origin !== webappUrl.origin) {
|
||||
url = url.replace(originalUrl.origin, webappUrl.origin);
|
||||
}
|
||||
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const emailTemplate = Handlebars.compile(emailFile);
|
||||
transporter.sendMail({
|
||||
from: `${process.env.EMAIL_FROM}` || APP_NAME,
|
||||
to: identifier,
|
||||
subject: "Your sign-in link for " + APP_NAME,
|
||||
html: emailTemplate({
|
||||
base_url: WEBAPP_URL,
|
||||
signin_url: url,
|
||||
email: identifier,
|
||||
}),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
providers.push(
|
||||
EmailProvider({
|
||||
type: "email",
|
||||
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
|
||||
// Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify.
|
||||
sendVerificationRequest: async (props) => (await import("./sendVerificationRequest")).default(props),
|
||||
})
|
||||
);
|
||||
|
||||
function isNumber(n: string) {
|
||||
return !isNaN(parseFloat(n)) && !isNaN(+n);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
import type { SendVerificationRequestParams } from "next-auth/providers/email";
|
||||
import type { TransportOptions } from "nodemailer";
|
||||
import nodemailer from "nodemailer";
|
||||
import path from "path";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||
|
||||
const transporter = nodemailer.createTransport<TransportOptions>({
|
||||
...(serverConfig.transport as TransportOptions),
|
||||
} as TransportOptions);
|
||||
|
||||
const sendVerificationRequest = async ({ identifier, url }: SendVerificationRequestParams) => {
|
||||
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
|
||||
const originalUrl = new URL(url);
|
||||
const webappUrl = new URL(WEBAPP_URL);
|
||||
if (originalUrl.origin !== webappUrl.origin) {
|
||||
url = url.replace(originalUrl.origin, webappUrl.origin);
|
||||
}
|
||||
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const emailTemplate = Handlebars.compile(emailFile);
|
||||
// async transporter
|
||||
transporter.sendMail({
|
||||
from: `${process.env.EMAIL_FROM}` || APP_NAME,
|
||||
to: identifier,
|
||||
subject: "Your sign-in link for " + APP_NAME,
|
||||
html: emailTemplate({
|
||||
base_url: WEBAPP_URL,
|
||||
signin_url: url,
|
||||
email: identifier,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default sendVerificationRequest;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { SignJWT } from "jose";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
|
||||
const signJwt = async (payload: { email: string }) => {
|
||||
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
return new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setSubject(payload.email)
|
||||
.setIssuedAt()
|
||||
.setIssuer(WEBSITE_URL)
|
||||
.setAudience(`${WEBSITE_URL}/auth/login`)
|
||||
.setExpirationTime("2m")
|
||||
.sign(secret);
|
||||
};
|
||||
|
||||
export default signJwt;
|
||||
@@ -14,6 +14,7 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"jose": "^4.13.1",
|
||||
"lru-cache": "^9.0.3",
|
||||
"next-auth": "^4.20.1",
|
||||
"nodemailer": "^6.7.8",
|
||||
"otplib": "^12.0.1"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WebhookTriggerEvents, WorkflowReminder, Prisma } from "@prisma/client";
|
||||
import type { Prisma, WebhookTriggerEvents, WorkflowReminder } from "@prisma/client";
|
||||
import { BookingStatus, MembershipRole, WorkflowMethods } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
@@ -58,6 +58,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
||||
paid: true,
|
||||
eventType: {
|
||||
select: {
|
||||
slug: true,
|
||||
owner: true,
|
||||
teamId: true,
|
||||
recurringEvent: true,
|
||||
@@ -334,7 +335,11 @@ async function handler(req: CustomRequest) {
|
||||
await sendCancelledReminders({
|
||||
workflows: bookingToDelete.eventType?.workflows,
|
||||
smsReminderNumber: bookingToDelete.smsReminderNumber,
|
||||
evt,
|
||||
evt: {
|
||||
...evt,
|
||||
...{ eventType: { slug: bookingToDelete.eventType.slug } },
|
||||
},
|
||||
hideBranding: !!bookingToDelete.eventType.owner?.hideBranding,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PrismaClient, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
|
||||
import type { Prisma, PrismaClient, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
|
||||
import { BookingStatus, WebhookTriggerEvents } from "@prisma/client";
|
||||
|
||||
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||
@@ -10,6 +10,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[handleConfirmation] book:user"] });
|
||||
@@ -66,11 +67,24 @@ export async function handleConfirmation(args: {
|
||||
let updatedBookings: {
|
||||
scheduledJobs: string[];
|
||||
id: number;
|
||||
description: string | null;
|
||||
location: string | null;
|
||||
attendees: {
|
||||
name: string;
|
||||
email: string;
|
||||
}[];
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
uid: string;
|
||||
smsReminderNumber: string | null;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
customInputs: Prisma.JsonValue;
|
||||
eventType: {
|
||||
bookingFields: Prisma.JsonValue | null;
|
||||
slug: string;
|
||||
owner: {
|
||||
hideBranding?: boolean | null;
|
||||
} | null;
|
||||
workflows: (WorkflowsOnEventTypes & {
|
||||
workflow: Workflow & {
|
||||
steps: WorkflowStep[];
|
||||
@@ -104,6 +118,13 @@ export async function handleConfirmation(args: {
|
||||
select: {
|
||||
eventType: {
|
||||
select: {
|
||||
slug: true,
|
||||
bookingFields: true,
|
||||
owner: {
|
||||
select: {
|
||||
hideBranding: true,
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
include: {
|
||||
workflow: {
|
||||
@@ -115,15 +136,21 @@ export async function handleConfirmation(args: {
|
||||
},
|
||||
},
|
||||
},
|
||||
description: true,
|
||||
attendees: true,
|
||||
location: true,
|
||||
uid: true,
|
||||
startTime: true,
|
||||
metadata: true,
|
||||
endTime: true,
|
||||
smsReminderNumber: true,
|
||||
customInputs: true,
|
||||
id: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const updatedBookingsResult = await Promise.all(updateBookingsPromise);
|
||||
updatedBookings = updatedBookings.concat(updatedBookingsResult);
|
||||
} else {
|
||||
@@ -142,6 +169,13 @@ export async function handleConfirmation(args: {
|
||||
select: {
|
||||
eventType: {
|
||||
select: {
|
||||
slug: true,
|
||||
bookingFields: true,
|
||||
owner: {
|
||||
select: {
|
||||
hideBranding: true,
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
include: {
|
||||
workflow: {
|
||||
@@ -155,8 +189,13 @@ export async function handleConfirmation(args: {
|
||||
},
|
||||
uid: true,
|
||||
startTime: true,
|
||||
metadata: true,
|
||||
endTime: true,
|
||||
smsReminderNumber: true,
|
||||
description: true,
|
||||
attendees: true,
|
||||
location: true,
|
||||
customInputs: true,
|
||||
id: true,
|
||||
scheduledJobs: true,
|
||||
},
|
||||
@@ -172,14 +211,21 @@ export async function handleConfirmation(args: {
|
||||
evtOfBooking.startTime = updatedBookings[index].startTime.toISOString();
|
||||
evtOfBooking.endTime = updatedBookings[index].endTime.toISOString();
|
||||
evtOfBooking.uid = updatedBookings[index].uid;
|
||||
const eventTypeSlug = updatedBookings[index].eventType?.slug || "";
|
||||
|
||||
const isFirstBooking = index === 0;
|
||||
const videoCallUrl =
|
||||
bookingMetadataSchema.parse(updatedBookings[index].metadata || {})?.videoCallUrl || "";
|
||||
|
||||
await scheduleWorkflowReminders({
|
||||
workflows: updatedBookings[index]?.eventType?.workflows || [],
|
||||
smsReminderNumber: updatedBookings[index].smsReminderNumber,
|
||||
calendarEvent: evtOfBooking,
|
||||
calendarEvent: {
|
||||
...evtOfBooking,
|
||||
...{ metadata: { videoCallUrl }, eventType: { slug: eventTypeSlug } },
|
||||
},
|
||||
isFirstRecurringEvent: isFirstBooking,
|
||||
hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
customInputs: true,
|
||||
disableGuests: true,
|
||||
users: userSelect,
|
||||
slug: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -227,6 +228,11 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
seatsShowAttendees: true,
|
||||
bookingLimits: true,
|
||||
durationLimits: true,
|
||||
owner: {
|
||||
select: {
|
||||
hideBranding: true,
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
include: {
|
||||
workflow: {
|
||||
@@ -800,6 +806,7 @@ async function handler(
|
||||
id: organizerUser.id,
|
||||
name: organizerUser.name || "Nameless",
|
||||
email: organizerUser.email || "Email-less",
|
||||
username: organizerUser.username || undefined,
|
||||
timeZone: organizerUser.timeZone,
|
||||
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
|
||||
timeFormat: organizerUser.timeFormat === 24 ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR,
|
||||
@@ -1441,7 +1448,7 @@ async function handler(
|
||||
await scheduleWorkflowReminders({
|
||||
workflows: eventType.workflows,
|
||||
smsReminderNumber: smsReminderNumber || null,
|
||||
calendarEvent: { ...evt, responses, ...{ metadata } },
|
||||
calendarEvent: { ...evt, ...{ metadata, eventType: { slug: eventType.slug } } },
|
||||
requiresConfirmation: evt.requiresConfirmation || false,
|
||||
isRescheduleEvent: !!rescheduleUid,
|
||||
isFirstRecurringEvent: true,
|
||||
@@ -2058,10 +2065,14 @@ async function handler(
|
||||
await scheduleWorkflowReminders({
|
||||
workflows: eventType.workflows,
|
||||
smsReminderNumber: smsReminderNumber || null,
|
||||
calendarEvent: { ...evt, responses, ...{ metadata: metadataFromEvent } },
|
||||
calendarEvent: {
|
||||
...evt,
|
||||
...{ metadata: metadataFromEvent, eventType: { slug: eventType.slug } },
|
||||
},
|
||||
requiresConfirmation: evt.requiresConfirmation || false,
|
||||
isRescheduleEvent: !!rescheduleUid,
|
||||
isFirstRecurringEvent: true,
|
||||
hideBranding: !!eventType.owner?.hideBranding,
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("Error while scheduling workflow reminders", error);
|
||||
|
||||
@@ -1,37 +1,20 @@
|
||||
import type { Payment } from "@prisma/client";
|
||||
import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
||||
import type { StripeCardElementChangeEvent, StripeElementLocale } from "@stripe/stripe-js";
|
||||
import { useElements, useStripe, PaymentElement, Elements } from "@stripe/react-stripe-js";
|
||||
import type stripejs from "@stripe/stripe-js";
|
||||
import type { StripeElementLocale } from "@stripe/stripe-js";
|
||||
import { useRouter } from "next/router";
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
||||
import type { StripePaymentData, StripeSetupIntentData } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Checkbox } from "@calcom/ui";
|
||||
|
||||
import type { EventType } from ".prisma/client";
|
||||
|
||||
const CARD_OPTIONS: stripejs.StripeCardElementOptions = {
|
||||
iconStyle: "solid" as const,
|
||||
classes: {
|
||||
base: "block p-2 w-full border-solid border-2 border-default rounded-md dark:bg-black dark:text-inverted dark:border-black focus-within:ring-black focus-within:border-black text-sm",
|
||||
},
|
||||
style: {
|
||||
base: {
|
||||
color: "#666",
|
||||
iconColor: "#666",
|
||||
fontFamily: "ui-sans-serif, system-ui",
|
||||
fontSmoothing: "antialiased",
|
||||
fontSize: "16px",
|
||||
"::placeholder": {
|
||||
color: "#888888",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
payment: Omit<Payment, "id" | "fee" | "success" | "refunded" | "externalId" | "data"> & {
|
||||
data: StripePaymentData | StripeSetupIntentData;
|
||||
@@ -49,7 +32,7 @@ type States =
|
||||
| { status: "error"; error: Error }
|
||||
| { status: "ok" };
|
||||
|
||||
export default function PaymentComponent(props: Props) {
|
||||
const PaymentForm = (props: Props) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const [state, setState] = useState<States>({ status: "idle" });
|
||||
@@ -62,20 +45,10 @@ export default function PaymentComponent(props: Props) {
|
||||
elements?.update({ locale: i18n.language as StripeElementLocale });
|
||||
}, [elements, i18n.language]);
|
||||
|
||||
const handleChange = async (event: StripeCardElementChangeEvent) => {
|
||||
// Listen for changes in the CardElement
|
||||
// and display any errors as the customer types their card details
|
||||
setState({ status: "idle" });
|
||||
if (event.error)
|
||||
setState({ status: "error", error: new Error(event.error?.message || t("missing_card_fields")) });
|
||||
};
|
||||
|
||||
const handleSubmit = async (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!stripe || !elements || !router.isReady) return;
|
||||
const card = elements.getElement(CardElement);
|
||||
if (!card) return;
|
||||
setState({ status: "processing" });
|
||||
|
||||
let payload;
|
||||
@@ -85,16 +58,18 @@ export default function PaymentComponent(props: Props) {
|
||||
};
|
||||
if (paymentOption === "HOLD" && "setupIntent" in props.payment.data) {
|
||||
const setupIntentData = props.payment.data as unknown as StripeSetupIntentData;
|
||||
payload = await stripe.confirmCardSetup(setupIntentData.setupIntent.client_secret!, {
|
||||
payment_method: {
|
||||
card,
|
||||
payload = await stripe.confirmSetup({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `${CAL_URL}/booking/${props.bookingUid}`,
|
||||
},
|
||||
});
|
||||
} else if (paymentOption === "ON_BOOKING") {
|
||||
const paymentData = props.payment.data as unknown as StripePaymentData;
|
||||
payload = await stripe.confirmCardPayment(paymentData.client_secret!, {
|
||||
payment_method: {
|
||||
card,
|
||||
payload = await stripe.confirmPayment({
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: `${CAL_URL}/booking/${props.bookingUid}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -120,15 +95,12 @@ export default function PaymentComponent(props: Props) {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form id="payment-form" className="bg-subtle mt-4 rounded-md p-6" onSubmit={handleSubmit}>
|
||||
<p className="font-semibold">{t("card_details")}</p>
|
||||
<CardElement
|
||||
className="my-5 bg-white p-2"
|
||||
id="card-element"
|
||||
options={CARD_OPTIONS}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div>
|
||||
<PaymentElement onChange={() => setState({ status: "idle" })} />
|
||||
</div>
|
||||
{paymentOption === "HOLD" && (
|
||||
<div className="bg-info mt-2 mb-5 rounded-md p-3">
|
||||
<Checkbox
|
||||
@@ -149,12 +121,11 @@ export default function PaymentComponent(props: Props) {
|
||||
<span id="button-text">{t("cancel")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={!holdAcknowledged || ["processing", "error"].includes(state.status)}
|
||||
loading={state.status === "processing"}
|
||||
id="submit"
|
||||
className="border-subtle border">
|
||||
color="secondary">
|
||||
<span id="button-text">
|
||||
{state.status === "processing" ? (
|
||||
<div className="spinner" id="spinner" />
|
||||
@@ -173,4 +144,47 @@ export default function PaymentComponent(props: Props) {
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const ELEMENT_STYLES: stripejs.Appearance = {
|
||||
theme: "none",
|
||||
};
|
||||
|
||||
const ELEMENT_STYLES_DARK: stripejs.Appearance = {
|
||||
theme: "night",
|
||||
variables: {
|
||||
colorText: "#d6d6d6",
|
||||
fontWeightNormal: "600",
|
||||
borderRadius: "6px",
|
||||
colorBackground: "#101010",
|
||||
colorPrimary: "#d6d6d6",
|
||||
},
|
||||
};
|
||||
|
||||
export default function PaymentComponent(props: Props) {
|
||||
const stripePromise = getStripe((props.payment.data as StripePaymentData).stripe_publishable_key);
|
||||
const paymentOption = props.payment.paymentOption;
|
||||
const [darkMode, setDarkMode] = useState<boolean>(false);
|
||||
let clientSecret: string | null;
|
||||
|
||||
useEffect(() => {
|
||||
setDarkMode(window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
}, []);
|
||||
|
||||
if (paymentOption === "HOLD" && "setupIntent" in props.payment.data) {
|
||||
clientSecret = props.payment.data.setupIntent.client_secret;
|
||||
} else if (!("setupIntent" in props.payment.data)) {
|
||||
clientSecret = props.payment.data.client_secret;
|
||||
}
|
||||
|
||||
return (
|
||||
<Elements
|
||||
stripe={stripePromise}
|
||||
options={{
|
||||
clientSecret: clientSecret!,
|
||||
appearance: darkMode ? ELEMENT_STYLES_DARK : ELEMENT_STYLES,
|
||||
}}>
|
||||
<PaymentForm {...props} />
|
||||
</Elements>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Elements } from "@stripe/react-stripe-js";
|
||||
import classNames from "classnames";
|
||||
import Head from "next/head";
|
||||
import type { FC } from "react";
|
||||
@@ -6,8 +5,6 @@ import { useEffect, useState } from "react";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
|
||||
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
||||
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
||||
import type { StripePaymentData } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
@@ -65,9 +62,9 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<main className="mx-auto max-w-3xl py-24">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto scroll-auto">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<div className="inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
@@ -126,17 +123,14 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
||||
<div className="text-default mt-4 text-center dark:text-gray-300">{t("paid")}</div>
|
||||
)}
|
||||
{props.payment.appId === "stripe" && !props.payment.success && (
|
||||
<Elements
|
||||
stripe={getStripe((props.payment.data as StripePaymentData).stripe_publishable_key)}>
|
||||
<PaymentComponent
|
||||
payment={props.payment}
|
||||
eventType={props.eventType}
|
||||
user={props.user}
|
||||
location={props.booking.location}
|
||||
bookingId={props.booking.id}
|
||||
bookingUid={props.booking.uid}
|
||||
/>
|
||||
</Elements>
|
||||
<PaymentComponent
|
||||
payment={props.payment}
|
||||
eventType={props.eventType}
|
||||
user={props.user}
|
||||
location={props.booking.location}
|
||||
bookingId={props.booking.id}
|
||||
bookingUid={props.booking.uid}
|
||||
/>
|
||||
)}
|
||||
{props.payment.refunded && (
|
||||
<div className="text-default mt-4 text-center dark:text-gray-300">{t("refunded")}</div>
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import { noop } from "lodash";
|
||||
import { useRouter } from "next/router";
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { nameOfDay } from "@calcom/lib/weekday";
|
||||
import { Button, EmailField, Form, Label, Select, TextField, TimezoneSelect } from "@calcom/ui";
|
||||
|
||||
import type { UserAdminRouterOutputs } from "../server/trpc-router";
|
||||
|
||||
type User = UserAdminRouterOutputs["get"]["user"];
|
||||
|
||||
type Option<T extends string | number = string> = {
|
||||
value: T;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type OptionValues = {
|
||||
locale: Option;
|
||||
timeFormat: Option<number>;
|
||||
timeZone: string;
|
||||
weekStart: Option;
|
||||
role: Option;
|
||||
identityProvider: Option;
|
||||
};
|
||||
|
||||
type FormValues = Pick<User, "avatar" | "name" | "username" | "email" | "bio"> & OptionValues;
|
||||
|
||||
export const UserForm = ({
|
||||
defaultValues,
|
||||
localeProp = "en",
|
||||
onSubmit = noop,
|
||||
submitLabel = "save",
|
||||
}: {
|
||||
defaultValues?: Pick<User, keyof FormValues>;
|
||||
localeProp?: string;
|
||||
onSubmit: (data: FormValues) => void;
|
||||
submitLabel?: string;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
const localeOptions = useMemo(() => {
|
||||
return (router.locales || []).map((locale) => ({
|
||||
value: locale,
|
||||
label: new Intl.DisplayNames(locale, { type: "language" }).of(locale) || "",
|
||||
}));
|
||||
}, [router.locales]);
|
||||
|
||||
const timeFormatOptions = [
|
||||
{ value: 12, label: t("12_hour") },
|
||||
{ value: 24, label: t("24_hour") },
|
||||
];
|
||||
|
||||
const weekStartOptions = [
|
||||
{ value: "Sunday", label: nameOfDay(localeProp, 0) },
|
||||
{ value: "Monday", label: nameOfDay(localeProp, 1) },
|
||||
{ value: "Tuesday", label: nameOfDay(localeProp, 2) },
|
||||
{ value: "Wednesday", label: nameOfDay(localeProp, 3) },
|
||||
{ value: "Thursday", label: nameOfDay(localeProp, 4) },
|
||||
{ value: "Friday", label: nameOfDay(localeProp, 5) },
|
||||
{ value: "Saturday", label: nameOfDay(localeProp, 6) },
|
||||
];
|
||||
|
||||
const userRoleOptions = [
|
||||
{ value: "USER", label: t("user") },
|
||||
{ value: "ADMIN", label: t("admin") },
|
||||
];
|
||||
|
||||
const identityProviderOptions = [
|
||||
{ value: "CAL", label: "CAL" },
|
||||
{ value: "GOOGLE", label: "GOOGLE" },
|
||||
{ value: "SAML", label: "SAML" },
|
||||
];
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
avatar: defaultValues?.avatar,
|
||||
name: defaultValues?.name,
|
||||
username: defaultValues?.username,
|
||||
email: defaultValues?.email,
|
||||
bio: defaultValues?.bio,
|
||||
locale: {
|
||||
value: defaultValues?.locale || localeOptions[0].value,
|
||||
label: localeOptions.find((option) => option.value === localeProp)?.label || "",
|
||||
},
|
||||
timeFormat: {
|
||||
value: defaultValues?.timeFormat || 12,
|
||||
label: timeFormatOptions.find((option) => option.value === defaultValues?.timeFormat)?.label || "12",
|
||||
},
|
||||
timeZone: defaultValues?.timeZone || "",
|
||||
weekStart: {
|
||||
value: defaultValues?.weekStart || weekStartOptions[0].value,
|
||||
label: nameOfDay(localeProp, defaultValues?.weekStart === "Sunday" ? 0 : 1),
|
||||
},
|
||||
role: {
|
||||
value: defaultValues?.role || userRoleOptions[0].value,
|
||||
label:
|
||||
userRoleOptions.find((option) => option.value === defaultValues?.role)?.label ||
|
||||
userRoleOptions[0].label,
|
||||
},
|
||||
identityProvider: {
|
||||
value: defaultValues?.identityProvider || identityProviderOptions[0].value,
|
||||
label:
|
||||
identityProviderOptions.find((option) => option.value === defaultValues?.role)?.label ||
|
||||
identityProviderOptions[0].label,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form form={form} className="space-y-4" handleSubmit={onSubmit}>
|
||||
{/* TODO: Enable Avatar uploader in a follow up */}
|
||||
{/* <div className="flex items-center">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
|
||||
<div className="ml-4">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="avatar-upload"
|
||||
buttonMsg="Change avatar"
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
form.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||
}}
|
||||
imageSrc={value || undefined}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div> */}
|
||||
<Controller
|
||||
name="role"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div>
|
||||
<Label className="text-default font-medium" htmlFor="role">
|
||||
{t("role")}
|
||||
</Label>
|
||||
<Select<{ label: string; value: string }>
|
||||
value={value}
|
||||
options={userRoleOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="identityProvider"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div>
|
||||
<Label className="text-default font-medium" htmlFor="identityProvider">
|
||||
{t("identity_provider")}
|
||||
</Label>
|
||||
<Select<{ label: string; value: string }>
|
||||
value={value}
|
||||
options={identityProviderOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TextField label="Name" placeholder="example" required {...form.register("name")} />
|
||||
<TextField label="Username" placeholder="example" required {...form.register("username")} />
|
||||
<EmailField label="Email" placeholder="user@example.com" required {...form.register("email")} />
|
||||
<TextField label="About" {...form.register("bio")} />
|
||||
<Controller
|
||||
name="locale"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<Label className="text-default">
|
||||
<>{t("language")}</>
|
||||
</Label>
|
||||
<Select<{ label: string; value: string }>
|
||||
className="capitalize"
|
||||
options={localeOptions}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
control={form.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-default mt-8">
|
||||
<>{t("timezone")}</>
|
||||
</Label>
|
||||
<TimezoneSelect
|
||||
id="timezone"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
if (event) form.setValue("timeZone", event.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="timeFormat"
|
||||
control={form.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-default mt-8">
|
||||
<>{t("time_format")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={timeFormatOptions}
|
||||
onChange={(event) => {
|
||||
if (event) form.setValue("timeFormat", { ...event });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="weekStart"
|
||||
control={form.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-default mt-8">
|
||||
<>{t("start_of_week")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={weekStartOptions}
|
||||
onChange={(event) => {
|
||||
if (event) form.setValue("weekStart", { ...event });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<Button type="submit" color="primary">
|
||||
{t(submitLabel)}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge, ConfirmationDialogContent, Dialog, DropdownActions, showToast, Table } from "@calcom/ui";
|
||||
import { Edit, Trash } from "@calcom/ui/components/icon";
|
||||
|
||||
import { withLicenseRequired } from "../../common/components/v2/LicenseRequired";
|
||||
|
||||
const { Body, Cell, ColumnTitle, Header, Row } = Table;
|
||||
|
||||
function UsersTableBare() {
|
||||
const utils = trpc.useContext();
|
||||
const [data] = trpc.viewer.users.list.useSuspenseQuery();
|
||||
const mutation = trpc.viewer.users.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast("User has been deleted", "success");
|
||||
await utils.viewer.users.list.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err.message);
|
||||
showToast("There has been an error deleting this user.", "error");
|
||||
},
|
||||
onSettled: () => {
|
||||
setUserToDelete(null);
|
||||
},
|
||||
});
|
||||
const [userToDelete, setUserToDelete] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<Header>
|
||||
<ColumnTitle widthClassNames="w-auto">User</ColumnTitle>
|
||||
<ColumnTitle>Timezone</ColumnTitle>
|
||||
<ColumnTitle>Role</ColumnTitle>
|
||||
<ColumnTitle widthClassNames="w-auto">
|
||||
<span className="sr-only">Edit</span>
|
||||
</ColumnTitle>
|
||||
</Header>
|
||||
|
||||
<Body>
|
||||
{data.map((user) => (
|
||||
<Row key={user.email}>
|
||||
<Cell widthClassNames="w-auto">
|
||||
{/*
|
||||
Using flexbox in a table will mess with the overflow, that's why I used position absolute
|
||||
to position the image.
|
||||
*/}
|
||||
<div className="min-h-10 relative pl-12">
|
||||
<img
|
||||
className="absolute left-0 top-1/2 h-10 w-10 -translate-y-1/2 rounded-full"
|
||||
src={user.avatar}
|
||||
alt={`Avatar of ${user.name}`}
|
||||
/>
|
||||
|
||||
<div className="text-subtle ml-4 font-medium">
|
||||
<span className="text-default">{user.name}</span>
|
||||
<span className="ml-3">/{user.username}</span>
|
||||
<br />
|
||||
<span className="break-all">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell>{user.timeZone}</Cell>
|
||||
<Cell>
|
||||
<Badge className="capitalize" variant={user.role === "ADMIN" ? "red" : "gray"}>
|
||||
{user.role.toLowerCase()}
|
||||
</Badge>
|
||||
</Cell>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<div className="flex w-full justify-end">
|
||||
<DropdownActions
|
||||
actions={[
|
||||
{
|
||||
id: "edit",
|
||||
label: "Edit",
|
||||
href: `/settings/admin/users/${user.id}/edit`,
|
||||
icon: Edit,
|
||||
},
|
||||
// {
|
||||
// id: "reset-password",
|
||||
// label: "Reset Password",
|
||||
// href: `/deployments/${router.query.deploymentId}/users/${user.id}/edit`,
|
||||
// icon: FiLock,
|
||||
// },
|
||||
{
|
||||
id: "delete",
|
||||
label: "Delete",
|
||||
color: "destructive",
|
||||
onClick: () => setUserToDelete(user.id),
|
||||
icon: Trash,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</Table>
|
||||
<DeleteUserDialog
|
||||
user={userToDelete}
|
||||
onClose={() => setUserToDelete(null)}
|
||||
onConfirm={() => {
|
||||
if (!userToDelete) return;
|
||||
mutation.mutate({ userId: userToDelete });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DeleteUserDialog = ({
|
||||
user,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: {
|
||||
user: number | null;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function -- noop
|
||||
<Dialog name="delete-user" open={!!user} onOpenChange={(open) => (open ? () => {} : onClose())}>
|
||||
<ConfirmationDialogContent
|
||||
title="Delete User"
|
||||
confirmBtnText="Delete"
|
||||
cancelBtnText="Cancel"
|
||||
variety="danger"
|
||||
onConfirm={onConfirm}>
|
||||
<p>Are you sure you want to delete this user?</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const UsersTable = withLicenseRequired(UsersTableBare);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { getParserWithGeneric } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Meta, showToast } from "@calcom/ui";
|
||||
|
||||
import { getLayout } from "../../../settings/layouts/SettingsLayout";
|
||||
import LicenseRequired from "../../common/components/v2/LicenseRequired";
|
||||
import { UserForm } from "../components/UserForm";
|
||||
import { userBodySchema } from "../schemas/userBodySchema";
|
||||
|
||||
const UsersAddView = () => {
|
||||
const router = useRouter();
|
||||
const utils = trpc.useContext();
|
||||
const mutation = trpc.viewer.users.add.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast("User added successfully", "success");
|
||||
await utils.viewer.users.list.invalidate();
|
||||
router.replace(router.asPath.replace("/add", ""));
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err.message);
|
||||
showToast("There has been an error adding this user.", "error");
|
||||
},
|
||||
});
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta title="Add new user" description="Here you can add a new user." />
|
||||
<UserForm
|
||||
submitLabel="Add user"
|
||||
onSubmit={async (values) => {
|
||||
const parser = getParserWithGeneric(userBodySchema);
|
||||
const parsedValues = parser(values);
|
||||
mutation.mutate(parsedValues);
|
||||
}}
|
||||
/>
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
UsersAddView.getLayout = getLayout;
|
||||
|
||||
export default UsersAddView;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { z } from "zod";
|
||||
|
||||
import NoSSR from "@calcom/core/components/NoSSR";
|
||||
import { getParserWithGeneric } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Meta, showToast } from "@calcom/ui";
|
||||
|
||||
import { getLayout } from "../../../settings/layouts/SettingsLayout";
|
||||
import LicenseRequired from "../../common/components/v2/LicenseRequired";
|
||||
import { UserForm } from "../components/UserForm";
|
||||
import { userBodySchema } from "../schemas/userBodySchema";
|
||||
|
||||
const userIdSchema = z.object({ id: z.coerce.number() });
|
||||
|
||||
const UsersEditPage = () => {
|
||||
const router = useRouter();
|
||||
const input = userIdSchema.safeParse(router.query);
|
||||
|
||||
if (!input.success) return <div>Invalid input</div>;
|
||||
|
||||
return <UsersEditView userId={input.data.id} />;
|
||||
};
|
||||
|
||||
const UsersEditView = ({ userId }: { userId: number }) => {
|
||||
const router = useRouter();
|
||||
const [data] = trpc.viewer.users.get.useSuspenseQuery({ userId });
|
||||
const { user } = data;
|
||||
const utils = trpc.useContext();
|
||||
const mutation = trpc.viewer.users.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.users.list.invalidate();
|
||||
await utils.viewer.users.get.invalidate();
|
||||
showToast("User updated successfully", "success");
|
||||
router.replace(`${router.asPath.split("/users/")[0]}/users`);
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err.message);
|
||||
showToast("There has been an error updating this user.", "error");
|
||||
},
|
||||
});
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta title={`Editing user: ${user.username}`} description="Here you can edit a current user." />
|
||||
<NoSSR>
|
||||
<UserForm
|
||||
key={JSON.stringify(user)}
|
||||
onSubmit={(values) => {
|
||||
const parser = getParserWithGeneric(userBodySchema);
|
||||
const parsedValues = parser(values);
|
||||
const data: Partial<typeof parsedValues & { userId: number }> = {
|
||||
...parsedValues,
|
||||
userId: user.id,
|
||||
};
|
||||
// TODO: Add support for avatar in the API
|
||||
delete data.avatar;
|
||||
// Don't send username if it's the same as the current one
|
||||
if (user.username === data.username) delete data.username;
|
||||
mutation.mutate(data);
|
||||
}}
|
||||
defaultValues={user}
|
||||
/>
|
||||
</NoSSR>
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
UsersEditPage.getLayout = getLayout;
|
||||
|
||||
export default UsersEditPage;
|
||||
@@ -0,0 +1,31 @@
|
||||
import NoSSR from "@calcom/core/components/NoSSR";
|
||||
import { Button, Meta } from "@calcom/ui";
|
||||
|
||||
import { getLayout } from "../../../settings/layouts/SettingsLayout";
|
||||
import LicenseRequired from "../../common/components/v2/LicenseRequired";
|
||||
import { UsersTable } from "../components/UsersTable";
|
||||
|
||||
const DeploymentUsersListPage = () => {
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta
|
||||
title="Users"
|
||||
description="A list of all the users in your account including their name, title, email and role."
|
||||
CTA={
|
||||
<div className="mt-4 space-x-5 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
{/* TODO: Add import users functionality */}
|
||||
{/* <Button disabled>Import users</Button> */}
|
||||
<Button href="/settings/admin/users/add">Add user</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<NoSSR>
|
||||
<UsersTable />
|
||||
</NoSSR>
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
DeploymentUsersListPage.getLayout = getLayout;
|
||||
|
||||
export default DeploymentUsersListPage;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { optionToValueSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export const userBodySchema = z
|
||||
.object({
|
||||
locale: optionToValueSchema(z.string()),
|
||||
role: optionToValueSchema(z.enum(["USER", "ADMIN"])),
|
||||
weekStart: optionToValueSchema(z.string()),
|
||||
timeFormat: optionToValueSchema(z.number()),
|
||||
})
|
||||
.passthrough();
|
||||
@@ -0,0 +1,110 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
|
||||
import { _UserModel as User } from "@calcom/prisma/zod";
|
||||
import type { inferRouterOutputs } from "@calcom/trpc";
|
||||
import { TRPCError } from "@calcom/trpc";
|
||||
import { authedAdminProcedure, middleware, router } from "@calcom/trpc/server/trpc";
|
||||
|
||||
export type UserAdminRouter = typeof userAdminRouter;
|
||||
export type UserAdminRouterOutputs = inferRouterOutputs<UserAdminRouter>;
|
||||
|
||||
const userIdSchema = z.object({ userId: z.coerce.number() });
|
||||
|
||||
const userBodySchema = User.pick({
|
||||
name: true,
|
||||
email: true,
|
||||
username: true,
|
||||
bio: true,
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
theme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
timeFormat: true,
|
||||
// brandColor: true,
|
||||
// darkBrandColor: true,
|
||||
allowDynamicBooking: true,
|
||||
// away: true,
|
||||
role: true,
|
||||
// @note: disallowing avatar changes via API for now. We can add it later if needed. User should upload image via UI.
|
||||
// avatar: true,
|
||||
});
|
||||
|
||||
/** This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url */
|
||||
export function getAvatarUrlFromUser(user: {
|
||||
avatar: string | null;
|
||||
username: string | null;
|
||||
email: string;
|
||||
}) {
|
||||
if (!user.avatar || !user.username) return defaultAvatarSrc({ email: user.email });
|
||||
return `${WEBAPP_URL}/${user.username}/avatar.png`;
|
||||
}
|
||||
|
||||
/** @see https://www.prisma.io/docs/concepts/components/prisma-client/excluding-fields#excluding-the-password-field */
|
||||
function exclude<UserType, Key extends keyof UserType>(user: UserType, keys: Key[]): Omit<UserType, Key> {
|
||||
for (const key of keys) {
|
||||
delete user[key];
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/** Reusable logic that checks for admin permissions and if the requested user exists */
|
||||
const authedAdminWithUserMiddleware = middleware(async ({ ctx, next, rawInput }) => {
|
||||
const { prisma } = ctx;
|
||||
const parsed = userIdSchema.safeParse(rawInput);
|
||||
if (!parsed.success) throw new TRPCError({ code: "BAD_REQUEST", message: "User id is required" });
|
||||
const { userId: id } = parsed.data;
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
if (!user) throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||
return next({
|
||||
ctx: {
|
||||
user: ctx.user,
|
||||
requestedUser:
|
||||
/** Don't leak the password */
|
||||
exclude(user, ["password"]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const authedAdminProcedureWithRequestedUser = authedAdminProcedure.use(authedAdminWithUserMiddleware);
|
||||
|
||||
export const userAdminRouter = router({
|
||||
get: authedAdminProcedureWithRequestedUser.input(userIdSchema).query(async ({ ctx }) => {
|
||||
const { requestedUser } = ctx;
|
||||
return { user: requestedUser };
|
||||
}),
|
||||
list: authedAdminProcedure.query(async ({ ctx }) => {
|
||||
const { prisma } = ctx;
|
||||
// TODO: Add search, pagination, etc.
|
||||
const users = await prisma.user.findMany();
|
||||
return users.map((user) => ({
|
||||
/** Don't leak the password */
|
||||
...exclude(user, ["password"]),
|
||||
/**
|
||||
* FIXME: This should be either a prisma extension or middleware
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/middleware
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/result
|
||||
**/
|
||||
avatar: getAvatarUrlFromUser(user),
|
||||
}));
|
||||
}),
|
||||
add: authedAdminProcedure.input(userBodySchema).mutation(async ({ ctx, input }) => {
|
||||
const { prisma } = ctx;
|
||||
const user = await prisma.user.create({ data: input });
|
||||
return { user, message: `User with id: ${user.id} added successfully` };
|
||||
}),
|
||||
update: authedAdminProcedureWithRequestedUser
|
||||
.input(userBodySchema.partial())
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { prisma, requestedUser } = ctx;
|
||||
const user = await prisma.user.update({ where: { id: requestedUser.id }, data: input });
|
||||
return { user, message: `User with id: ${user.id} updated successfully` };
|
||||
}),
|
||||
delete: authedAdminProcedureWithRequestedUser.input(userIdSchema).mutation(async ({ ctx }) => {
|
||||
const { prisma, requestedUser } = ctx;
|
||||
await prisma.user.delete({ where: { id: requestedUser.id } });
|
||||
return { message: `User with id: ${requestedUser.id} deleted successfully` };
|
||||
}),
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import sgMail from "@sendgrid/mail";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -164,51 +165,63 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
let emailContent = {
|
||||
emailSubject: reminder.workflowStep.emailSubject || "",
|
||||
emailBody: {
|
||||
text: reminder.workflowStep.reminderBody || "",
|
||||
html: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
|
||||
},
|
||||
emailBody: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
|
||||
};
|
||||
|
||||
switch (reminder.workflowStep.template) {
|
||||
case WorkflowTemplates.REMINDER:
|
||||
emailContent = emailReminderTemplate(
|
||||
reminder.booking.startTime.toISOString() || "",
|
||||
reminder.booking.endTime.toISOString() || "",
|
||||
reminder.booking.eventType?.title || "",
|
||||
timeZone || "",
|
||||
attendeeName || "",
|
||||
name || ""
|
||||
);
|
||||
break;
|
||||
case WorkflowTemplates.CUSTOM:
|
||||
const variables: VariablesType = {
|
||||
eventName: reminder.booking?.eventType?.title || "",
|
||||
organizerName: reminder.booking.user?.name || "",
|
||||
attendeeName: reminder.booking.attendees[0].name,
|
||||
attendeeEmail: reminder.booking.attendees[0].email,
|
||||
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
|
||||
eventTime: dayjs(reminder.booking.startTime).tz(timeZone),
|
||||
timeZone: timeZone,
|
||||
location: reminder.booking.location || "",
|
||||
additionalNotes: reminder.booking.description,
|
||||
responses: reminder.booking.responses,
|
||||
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
|
||||
};
|
||||
const emailSubject = await customTemplate(
|
||||
reminder.workflowStep.emailSubject || "",
|
||||
variables,
|
||||
locale || ""
|
||||
);
|
||||
emailContent.emailSubject = emailSubject.text;
|
||||
emailContent.emailBody = await customTemplate(
|
||||
reminder.workflowStep.reminderBody || "",
|
||||
variables,
|
||||
locale || ""
|
||||
);
|
||||
break;
|
||||
let emailBodyEmpty = false;
|
||||
|
||||
if (reminder.workflowStep.reminderBody) {
|
||||
const { responses } = getCalEventResponses({
|
||||
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
|
||||
booking: reminder.booking,
|
||||
});
|
||||
|
||||
const variables: VariablesType = {
|
||||
eventName: reminder.booking.eventType?.title || "",
|
||||
organizerName: reminder.booking.user?.name || "",
|
||||
attendeeName: reminder.booking.attendees[0].name,
|
||||
attendeeEmail: reminder.booking.attendees[0].email,
|
||||
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
|
||||
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
|
||||
timeZone: timeZone,
|
||||
location: reminder.booking.location || "",
|
||||
additionalNotes: reminder.booking.description,
|
||||
responses: responses,
|
||||
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
|
||||
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
|
||||
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
|
||||
};
|
||||
const emailSubject = customTemplate(
|
||||
reminder.workflowStep.emailSubject || "",
|
||||
variables,
|
||||
locale || "",
|
||||
!!reminder.booking.user?.hideBranding
|
||||
).text;
|
||||
emailContent.emailSubject = emailSubject;
|
||||
emailContent.emailBody = customTemplate(
|
||||
reminder.workflowStep.reminderBody || "",
|
||||
variables,
|
||||
locale || "",
|
||||
!!reminder.booking.user?.hideBranding
|
||||
).html;
|
||||
|
||||
emailBodyEmpty =
|
||||
customTemplate(reminder.workflowStep.reminderBody || "", variables, locale || "").text.length === 0;
|
||||
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
|
||||
emailContent = emailReminderTemplate(
|
||||
false,
|
||||
reminder.workflowStep.action,
|
||||
reminder.booking.startTime.toISOString() || "",
|
||||
reminder.booking.endTime.toISOString() || "",
|
||||
reminder.booking.eventType?.title || "",
|
||||
timeZone || "",
|
||||
attendeeName || "",
|
||||
name || "",
|
||||
!!reminder.booking.user?.hideBranding
|
||||
);
|
||||
}
|
||||
if (emailContent.emailSubject.length > 0 && emailContent.emailBody.text.length > 0 && sendTo) {
|
||||
|
||||
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
|
||||
const batchIdResponse = await client.request({
|
||||
url: "/v3/mail/batch",
|
||||
method: "POST",
|
||||
@@ -224,8 +237,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
name: reminder.workflowStep.sender || "Cal.com",
|
||||
},
|
||||
subject: emailContent.emailSubject,
|
||||
text: emailContent.emailBody.text,
|
||||
html: emailContent.emailBody.html,
|
||||
html: emailContent.emailBody,
|
||||
batchId: batchId,
|
||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||
replyTo: reminder.booking.user?.email || senderEmail,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user