Compare commits
+1
-1
@@ -30,4 +30,4 @@ packages/features/ee/workflows @CarinaWolli @calcom/core
|
||||
packages/features/tips @PeerRich @calcom/core
|
||||
packages/ui @calcom/ui
|
||||
packages/config @calcom/core
|
||||
packages/app-store/ee/routing-forms @hariombalhara @calcom/core
|
||||
packages/app-store/routing-forms @hariombalhara @calcom/core
|
||||
|
||||
@@ -77,9 +77,6 @@ lint-results
|
||||
#Storybook
|
||||
apps/storybook/build-storybook.log
|
||||
|
||||
# Infisical
|
||||
.infisical.json
|
||||
|
||||
# Snaplet
|
||||
.snaplet/snapshots
|
||||
.snaplet/structure.d.ts
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"workspaceId": "63ea8121b6e2b0543ba79616",
|
||||
"defaultEnvironment": "dev",
|
||||
"gitBranchToEnvironmentMapping": {
|
||||
"main": "staging",
|
||||
"production": "prod"
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -4,7 +4,7 @@ Contributions are what make the open source community such an amazing place to l
|
||||
|
||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
|
||||
|
||||
## Priorities
|
||||
## Priorities
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -57,7 +57,6 @@ Contributions are what make the open source community such an amazing place to l
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
The development branch is `main`. This is the branch that all pull
|
||||
|
||||
@@ -350,17 +350,16 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
||||
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<!-- CONTRIBUTORS -->
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
<a href="https://github.com/calcom/cal.com/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=calcom/cal.com" />
|
||||
</a>
|
||||
|
||||
|
||||
<!-- TRANSLATIONS -->
|
||||
|
||||
### Translations
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
|
||||
return (
|
||||
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
|
||||
<div className="flex gap-x-3 p-4">
|
||||
<div className="flex gap-x-3 px-5 py-4">
|
||||
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
||||
<div className="flex grow flex-col gap-y-1 truncate">
|
||||
<div className="flex items-center gap-x-2">
|
||||
|
||||
@@ -669,8 +669,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
embedUrl: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const router = useRouter();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const dialogContentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const s = (href: string) => {
|
||||
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
|
||||
const [a, b] = href.split("=");
|
||||
@@ -809,7 +812,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<DialogContent size="xl" className="p-0.5" type="creation">
|
||||
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
|
||||
<div className="flex">
|
||||
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
||||
@@ -960,6 +963,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div>Button Color</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
popoverAlign="start"
|
||||
container={dialogContentRef?.current ?? undefined}
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
setPreviewState((previewState) => {
|
||||
@@ -979,6 +984,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div>Text Color</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
popoverAlign="start"
|
||||
container={dialogContentRef?.current ?? undefined}
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
setPreviewState((previewState) => {
|
||||
@@ -1060,6 +1067,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
<div className="mb-2 pt-2">{palette.title}</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
popoverAlign="start"
|
||||
container={dialogContentRef?.current ?? undefined}
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
addToPalette({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
FiShield,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
/* These app slugs all require Google Cal to be installed */
|
||||
|
||||
const Component = ({
|
||||
name,
|
||||
type,
|
||||
@@ -45,6 +47,7 @@ const Component = ({
|
||||
isProOnly,
|
||||
images,
|
||||
isTemplate,
|
||||
dependencies,
|
||||
}: Parameters<typeof App>[0]) => {
|
||||
const { t } = useLocale();
|
||||
const hasImages = images && images.length > 0;
|
||||
@@ -76,6 +79,15 @@ const Component = ({
|
||||
}
|
||||
);
|
||||
|
||||
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
|
||||
enabled: !!dependencies,
|
||||
});
|
||||
|
||||
const disableInstall =
|
||||
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
|
||||
|
||||
// const disableInstall = requiresGCal && !gCalInstalled.data;
|
||||
|
||||
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
|
||||
// Such apps, can only be installed once.
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
|
||||
@@ -137,6 +149,7 @@ const Component = ({
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
disableInstall={disableInstall}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
@@ -176,6 +189,7 @@ const Component = ({
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
disableInstall={disableInstall}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
@@ -203,6 +217,16 @@ const Component = ({
|
||||
) : (
|
||||
<SkeletonButton className="h-10 w-24" />
|
||||
)}
|
||||
|
||||
{dependencies &&
|
||||
(!dependencyData.isLoading ? (
|
||||
<div className="mt-6">
|
||||
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
|
||||
</div>
|
||||
) : (
|
||||
<SkeletonButton className="mt-6 h-20 grow" />
|
||||
))}
|
||||
|
||||
{price !== 0 && (
|
||||
<span className="block text-right">
|
||||
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
|
||||
@@ -332,6 +356,8 @@ export default function App(props: {
|
||||
isProOnly: AppType["isProOnly"];
|
||||
images?: string[];
|
||||
isTemplate?: boolean;
|
||||
disableInstall?: boolean;
|
||||
dependencies?: string[];
|
||||
}) {
|
||||
return (
|
||||
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
||||
|
||||
@@ -88,7 +88,7 @@ function CalendarSwitch(props: {
|
||||
}
|
||||
);
|
||||
return (
|
||||
<div className="flex py-1">
|
||||
<div className="flex flex-col py-1 sm:flex-row">
|
||||
<Switch
|
||||
key={props.externalId}
|
||||
name="enabled"
|
||||
@@ -99,7 +99,7 @@ function CalendarSwitch(props: {
|
||||
}}
|
||||
/>
|
||||
{!!props.destination && (
|
||||
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
|
||||
<span className="ml-8 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800 sm:ml-4">
|
||||
<FiArrowLeft className="h-4 w-4" />
|
||||
{t("adding_events_to")}
|
||||
</span>
|
||||
@@ -184,8 +184,8 @@ function ConnectedCalendarsList(props: Props) {
|
||||
<div className="border-t border-gray-200">
|
||||
{!fromOnboarding && (
|
||||
<>
|
||||
<p className="px-4 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-2 p-4">
|
||||
<p className="px-5 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-2 px-5 py-4">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
@@ -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 border-gray-200 p-7">
|
||||
<div className="flex flex-col gap-6 rounded-md border p-7">
|
||||
<ShellSubHeading
|
||||
title={t("calendar")}
|
||||
subtitle={t("installed_app_calendar_description")}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
||||
return (
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
) : (
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
|
||||
@@ -64,13 +64,13 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
return (
|
||||
<div ref={slotPickerRef}>
|
||||
{!!date ? (
|
||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
||||
<div className="mb-6 flex items-center text-left text-base">
|
||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
|
||||
<div className="mb-4 flex items-center text-left text-base">
|
||||
<div className="mr-4">
|
||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||
</span>
|
||||
<span className="text-bookinglight font-medium">
|
||||
<span className="text-bookinglight">
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||
</span>
|
||||
</div>
|
||||
@@ -85,7 +85,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={ref} className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||
<div
|
||||
ref={ref}
|
||||
className="scroll-bar scrollbar-track-w-20 relative -mb-4 flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||
{slots.length > 0 &&
|
||||
slots.map((slot) => {
|
||||
type BookingURL = {
|
||||
|
||||
@@ -46,14 +46,16 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
const { profile, eventType, isBookingPage = false, children } = props;
|
||||
const { date: bookingDate } = useRouterQuery("date");
|
||||
const { t } = useLocale();
|
||||
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
|
||||
const { duration, setQuery: setDuration } = useRouterQuery("duration");
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
eventType.metadata?.multipleDuration &&
|
||||
!eventType.metadata?.multipleDuration?.includes(Number(duration))
|
||||
!duration ||
|
||||
isNaN(Number(duration)) ||
|
||||
(eventType.metadata?.multipleDuration &&
|
||||
!eventType.metadata?.multipleDuration.includes(Number(duration)))
|
||||
) {
|
||||
setDuration(eventType.length.toString());
|
||||
setDuration(eventType.length);
|
||||
}
|
||||
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
|
||||
|
||||
@@ -81,20 +83,20 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
size="sm"
|
||||
truncateAfter={3}
|
||||
/>
|
||||
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||
{profile.name}
|
||||
</h2>
|
||||
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold text-gray-900">
|
||||
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
|
||||
{eventType?.description && (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex",
|
||||
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div className="scroll-bar scrollbar-track-w-20 max-h-[200px] max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -164,7 +164,7 @@ export const SlotPicker = ({
|
||||
<DatePicker
|
||||
isLoading={isLoading}
|
||||
className={classNames(
|
||||
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-5 lg:min-w-[455px]",
|
||||
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
|
||||
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
|
||||
)}
|
||||
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
|
||||
|
||||
@@ -168,7 +168,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
<div
|
||||
className={classNames(
|
||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
||||
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||
"min-w-full md:w-[280px] md:min-w-[280px]",
|
||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||
)}>
|
||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
@@ -15,6 +15,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
|
||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
@@ -38,6 +39,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button, Form, Tooltip } from "@calcom/ui";
|
||||
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -213,6 +215,20 @@ const BookingPage = ({
|
||||
duration = Number(queryDuration);
|
||||
}
|
||||
|
||||
// This is a workaround for forcing the same time format for both server side rendering and client side rendering
|
||||
// At initial render, we use the default time format which is 12H
|
||||
const [withDefaultTimeFormat, setWithDefaultTimeFormat] = useState(true);
|
||||
const parseDateFunc = useCallback(
|
||||
(date: string | null | Dayjs) => {
|
||||
return parseDate(date, i18n, withDefaultTimeFormat);
|
||||
},
|
||||
[withDefaultTimeFormat]
|
||||
);
|
||||
// After intial render on client side, we let parseDateFunc to use the time format from the localStorage
|
||||
useEffect(() => {
|
||||
setWithDefaultTimeFormat(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (top !== window) {
|
||||
//page_view will be collected automatically by _middleware.ts
|
||||
@@ -370,15 +386,26 @@ const BookingPage = ({
|
||||
// Calculate the booking date(s)
|
||||
let recurringStrings: string[] = [],
|
||||
recurringDates: Date[] = [];
|
||||
const parseRecurringDatesFunc = useCallback(
|
||||
(date: string | null | Dayjs, recurringEvent: RecurringEvent, recurringCount: number) => {
|
||||
return parseRecurringDates(
|
||||
{
|
||||
startDate: date,
|
||||
timeZone: timeZone(),
|
||||
recurringEvent: recurringEvent,
|
||||
recurringCount: recurringCount,
|
||||
withDefaultTimeFormat: withDefaultTimeFormat,
|
||||
},
|
||||
i18n
|
||||
);
|
||||
},
|
||||
[withDefaultTimeFormat, date, eventType.recurringEvent, recurringEventCount]
|
||||
);
|
||||
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
||||
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||
{
|
||||
startDate: date,
|
||||
timeZone: timeZone(),
|
||||
recurringEvent: eventType.recurringEvent,
|
||||
recurringCount: parseInt(recurringEventCount.toString()),
|
||||
},
|
||||
i18n
|
||||
[recurringStrings, recurringDates] = parseRecurringDatesFunc(
|
||||
date,
|
||||
eventType.recurringEvent,
|
||||
parseInt(recurringEventCount.toString())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -507,7 +534,7 @@ const BookingPage = ({
|
||||
<div className="text-sm font-medium">
|
||||
{isClientTimezoneAvailable &&
|
||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||
`${parseDate(date, i18n)}`}
|
||||
`${parseDateFunc(date)}`}
|
||||
{isClientTimezoneAvailable &&
|
||||
!rescheduleUid &&
|
||||
eventType.recurringEvent?.freq &&
|
||||
@@ -537,7 +564,7 @@ const BookingPage = ({
|
||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{isClientTimezoneAvailable &&
|
||||
typeof booking.startTime === "string" &&
|
||||
parseDate(dayjs(booking.startTime), i18n)}
|
||||
parseDateFunc(dayjs(booking.startTime))}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -615,17 +642,28 @@ export default BookingPage;
|
||||
function ErrorMessage({ error }: { error: unknown }) {
|
||||
const { t } = useLocale();
|
||||
const { query: { rescheduleUid } = {} } = useRouter();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ltr:ml-3 rtl:mr-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
<p className="text-sm text-blue-700">
|
||||
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
|
||||
{error instanceof HttpError || error instanceof Error ? t(error.message) : "Unknown error"}
|
||||
{error instanceof HttpError || error instanceof Error ? (
|
||||
<>
|
||||
{t("can_you_try_again")}{" "}
|
||||
<span className="cursor-pointer underline" onClick={() => router.back()}>
|
||||
{t("go_back")}
|
||||
</span>
|
||||
.
|
||||
</> /* t(error.message) */
|
||||
) : (
|
||||
"Unknown error"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,8 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
||||
type="text"
|
||||
placeholder={placeHolder_}
|
||||
{...register("customEventName", {
|
||||
validate: (value) => validateCustomEventName(value, t("invalid_event_name_variables")),
|
||||
validate: (value) =>
|
||||
validateCustomEventName(value, t("invalid_event_name_variables"), event.bookingFields),
|
||||
})}
|
||||
className="mb-0"
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import DestinationCalendarSelector from "@calcom/features/calendars/DestinationC
|
||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||
@@ -36,11 +37,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name + " input";
|
||||
});
|
||||
|
||||
const eventNameObject: EventNameObjectType = {
|
||||
attendeeName: t("scheduler"),
|
||||
eventType: eventType.title,
|
||||
eventName: eventType.eventName,
|
||||
host: eventType.users[0]?.name || "Nameless",
|
||||
bookingFields: bookingFields,
|
||||
t,
|
||||
};
|
||||
|
||||
@@ -308,7 +317,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<CustomEventTypeModal
|
||||
close={closeEventNameTip}
|
||||
setValue={setEventName}
|
||||
defaultValue={eventType.eventName || ""}
|
||||
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
|
||||
placeHolder={eventNamePlaceholder}
|
||||
event={eventNameObject}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { Key } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||
@@ -11,8 +13,17 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||
import findDurationType from "@calcom/lib/findDurationType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { PeriodType } from "@calcom/prisma/client";
|
||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputField,
|
||||
Label,
|
||||
Select,
|
||||
SettingsToggle,
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||
|
||||
const MinimumBookingNoticeInput = React.forwardRef<
|
||||
@@ -260,7 +271,34 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
formMethods.setValue("bookingLimits", {});
|
||||
}
|
||||
}}>
|
||||
<BookingLimits />
|
||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
<hr />
|
||||
<Controller
|
||||
name="durationLimits"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<SettingsToggle
|
||||
title={t("limit_total_booking_duration")}
|
||||
description={t("limit_total_booking_duration_description")}
|
||||
checked={Object.keys(value ?? {}).length > 0}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("durationLimits", {
|
||||
PER_DAY: 60,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("durationLimits", {});
|
||||
}
|
||||
}}>
|
||||
<IntervalLimitsManager
|
||||
propertyName="durationLimits"
|
||||
defaultLimit={60}
|
||||
step={15}
|
||||
textFieldSuffix={t("minutes")}
|
||||
/>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
@@ -348,124 +386,158 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
);
|
||||
};
|
||||
|
||||
const validationOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"];
|
||||
type BookingLimitsKey = keyof BookingLimit;
|
||||
const BookingLimits = () => {
|
||||
type IntervalLimitsKey = keyof IntervalLimit;
|
||||
|
||||
const intervalOrderKeys = ["PER_DAY", "PER_WEEK", "PER_MONTH", "PER_YEAR"] as const;
|
||||
|
||||
const INTERVAL_LIMIT_OPTIONS = intervalOrderKeys.map((key) => ({
|
||||
value: key as keyof IntervalLimit,
|
||||
label: `Per ${key.split("_")[1].toLocaleLowerCase()}`,
|
||||
}));
|
||||
|
||||
type IntervalLimitItemProps = {
|
||||
key: Key;
|
||||
limitKey: IntervalLimitsKey;
|
||||
step: number;
|
||||
value: number;
|
||||
textFieldSuffix?: string;
|
||||
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
||||
hasDeleteButton?: boolean;
|
||||
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
||||
onLimitChange: (intervalLimitsKey: IntervalLimitsKey, limit: number) => void;
|
||||
onIntervalSelect: (interval: SingleValue<{ value: keyof IntervalLimit; label: string }>) => void;
|
||||
};
|
||||
|
||||
const IntervalLimitItem = ({
|
||||
limitKey,
|
||||
step,
|
||||
value,
|
||||
textFieldSuffix,
|
||||
selectOptions,
|
||||
hasDeleteButton,
|
||||
onDelete,
|
||||
onLimitChange,
|
||||
onIntervalSelect,
|
||||
}: IntervalLimitItemProps) => {
|
||||
return (
|
||||
<div className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse" key={limitKey}>
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
containerClassName={`${textFieldSuffix ? "w-36" : "w-16"} -mb-1`}
|
||||
placeholder={`${value}`}
|
||||
min={step}
|
||||
step={step}
|
||||
defaultValue={value}
|
||||
addOnSuffix={textFieldSuffix}
|
||||
onChange={(e) => onLimitChange(limitKey, parseInt(e.target.value))}
|
||||
/>
|
||||
<Select
|
||||
options={selectOptions}
|
||||
isSearchable={false}
|
||||
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
||||
onChange={onIntervalSelect}
|
||||
/>
|
||||
{hasDeleteButton && (
|
||||
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> = {
|
||||
propertyName: K;
|
||||
defaultLimit: number;
|
||||
step: number;
|
||||
textFieldSuffix?: string;
|
||||
};
|
||||
|
||||
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
propertyName,
|
||||
defaultLimit,
|
||||
step,
|
||||
textFieldSuffix,
|
||||
}: IntervalLimitsManagerProps<K>) => {
|
||||
const { watch, setValue, control } = useFormContext<FormValues>();
|
||||
const watchBookingLimits = watch("bookingLimits");
|
||||
const watchIntervalLimits = watch(propertyName);
|
||||
const { t } = useLocale();
|
||||
|
||||
const [animateRef] = useAutoAnimate<HTMLUListElement>();
|
||||
|
||||
const BOOKING_LIMIT_OPTIONS: {
|
||||
value: keyof BookingLimit;
|
||||
label: string;
|
||||
}[] = [
|
||||
{
|
||||
value: "PER_DAY",
|
||||
label: "Per Day",
|
||||
},
|
||||
{
|
||||
value: "PER_WEEK",
|
||||
label: "Per Week",
|
||||
},
|
||||
{
|
||||
value: "PER_MONTH",
|
||||
label: "Per Month",
|
||||
},
|
||||
{
|
||||
value: "PER_YEAR",
|
||||
label: "Per Year",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="bookingLimits"
|
||||
name={propertyName}
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const currentBookingLimits = value;
|
||||
const currentIntervalLimits = value;
|
||||
|
||||
const addLimit = () => {
|
||||
if (!currentIntervalLimits || !watchIntervalLimits) return;
|
||||
const currentKeys = Object.keys(watchIntervalLimits);
|
||||
|
||||
const [rest] = Object.values(INTERVAL_LIMIT_OPTIONS).filter(
|
||||
(option) => !currentKeys.includes(option.value)
|
||||
);
|
||||
if (!rest || !currentKeys.length) return;
|
||||
//currentDurationLimits is always defined so can be casted
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(propertyName, {
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ul ref={animateRef}>
|
||||
{currentBookingLimits &&
|
||||
watchBookingLimits &&
|
||||
Object.entries(currentBookingLimits)
|
||||
.sort(([limitkeyA], [limitKeyB]) => {
|
||||
{currentIntervalLimits &&
|
||||
watchIntervalLimits &&
|
||||
Object.entries(currentIntervalLimits)
|
||||
.sort(([limitKeyA], [limitKeyB]) => {
|
||||
return (
|
||||
validationOrderKeys.indexOf(limitkeyA as BookingLimitsKey) -
|
||||
validationOrderKeys.indexOf(limitKeyB as BookingLimitsKey)
|
||||
intervalOrderKeys.indexOf(limitKeyA as IntervalLimitsKey) -
|
||||
intervalOrderKeys.indexOf(limitKeyB as IntervalLimitsKey)
|
||||
);
|
||||
})
|
||||
.map(([key, bookingAmount]) => {
|
||||
const bookingLimitKey = key as BookingLimitsKey;
|
||||
.map(([key, value]) => {
|
||||
const limitKey = key as IntervalLimitsKey;
|
||||
return (
|
||||
<div
|
||||
className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse"
|
||||
key={bookingLimitKey}>
|
||||
<Input
|
||||
id={`${bookingLimitKey}-limit`}
|
||||
type="number"
|
||||
className="mb-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
|
||||
placeholder="1"
|
||||
min={1}
|
||||
defaultValue={bookingAmount}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setValue(`bookingLimits.${bookingLimitKey}`, parseInt(val));
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
options={BOOKING_LIMIT_OPTIONS.filter(
|
||||
(option) => !Object.keys(currentBookingLimits).includes(option.value)
|
||||
)}
|
||||
isSearchable={false}
|
||||
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
|
||||
onChange={(val) => {
|
||||
const current = currentBookingLimits;
|
||||
const currentValue = watchBookingLimits[bookingLimitKey];
|
||||
<IntervalLimitItem
|
||||
key={key}
|
||||
limitKey={limitKey}
|
||||
step={step}
|
||||
value={value}
|
||||
textFieldSuffix={textFieldSuffix}
|
||||
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
||||
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
||||
(option) => !Object.keys(currentIntervalLimits).includes(option.value)
|
||||
)}
|
||||
onLimitChange={(intervalLimitKey, val) =>
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val)
|
||||
}
|
||||
onDelete={(intervalLimitKey) => {
|
||||
const current = currentIntervalLimits;
|
||||
delete current[intervalLimitKey];
|
||||
onChange(current);
|
||||
}}
|
||||
onIntervalSelect={(interval) => {
|
||||
const current = currentIntervalLimits;
|
||||
const currentValue = watchIntervalLimits[limitKey];
|
||||
|
||||
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
|
||||
delete current[bookingLimitKey];
|
||||
const newData = {
|
||||
...current,
|
||||
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
|
||||
[val?.value as BookingLimitsKey]: currentValue,
|
||||
};
|
||||
onChange(newData);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="icon"
|
||||
StartIcon={FiTrash}
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
const current = currentBookingLimits;
|
||||
delete current[key as BookingLimitsKey];
|
||||
onChange(current);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
|
||||
delete current[limitKey];
|
||||
const newData = {
|
||||
...current,
|
||||
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
|
||||
[interval?.value as IntervalLimitsKey]: currentValue,
|
||||
};
|
||||
onChange(newData);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={FiPlus}
|
||||
onClick={() => {
|
||||
if (!currentBookingLimits || !watchBookingLimits) return;
|
||||
const currentKeys = Object.keys(watchBookingLimits);
|
||||
|
||||
const rest = Object.values(BOOKING_LIMIT_OPTIONS).filter(
|
||||
(option) => !currentKeys.includes(option.value)
|
||||
);
|
||||
if (!rest || !currentKeys) return;
|
||||
//currentBookingLimits is always defined so can be casted
|
||||
|
||||
setValue("bookingLimits", {
|
||||
...watchBookingLimits,
|
||||
[rest[0].value]: 1,
|
||||
});
|
||||
}}>
|
||||
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
|
||||
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
|
||||
{t("add_limit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import type { SchedulingType } from "@prisma/client";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { ComponentProps } from "react";
|
||||
import type { Control } from "react-hook-form";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import type { Options } from "react-select";
|
||||
|
||||
@@ -35,65 +35,169 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
|
||||
return 0;
|
||||
};
|
||||
|
||||
const FixedHosts = ({
|
||||
control,
|
||||
const CheckedHostField = ({
|
||||
labelText,
|
||||
placeholder,
|
||||
options = [],
|
||||
isFixed,
|
||||
value,
|
||||
onChange,
|
||||
...rest
|
||||
}: {
|
||||
control: Control<FormValues>;
|
||||
labelText: string;
|
||||
placeholder: string;
|
||||
isFixed: boolean;
|
||||
value: { isFixed: boolean; userId: number }[];
|
||||
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
|
||||
options?: Options<CheckedSelectOption>;
|
||||
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
|
||||
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
||||
<div>
|
||||
<Label>{labelText}</Label>
|
||||
<Controller
|
||||
name="hostsFixed"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<CheckedTeamSelect
|
||||
isDisabled={false}
|
||||
onChange={(options) => {
|
||||
onChange(
|
||||
options.map((option) => ({
|
||||
isFixed: true,
|
||||
userId: parseInt(option.value, 10),
|
||||
}))
|
||||
);
|
||||
}}
|
||||
value={value
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
<CheckedTeamSelect
|
||||
isOptionDisabled={(option) => !!value.find((host) => host.userId.toString() === option.value)}
|
||||
onChange={(options) => {
|
||||
onChange &&
|
||||
onChange(
|
||||
options.map((option) => ({
|
||||
isFixed,
|
||||
userId: parseInt(option.value, 10),
|
||||
}))
|
||||
);
|
||||
}}
|
||||
value={(value || [])
|
||||
.filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
options.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={options}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const RoundRobinHosts = ({
|
||||
teamMembers,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: { isFixed: boolean; userId: number }[];
|
||||
onChange: (hosts: { isFixed: boolean; userId: number }[]) => void;
|
||||
teamMembers: {
|
||||
value: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
}[];
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<CheckedHostField
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
isFixed={true}
|
||||
onChange={(changeValue) => {
|
||||
onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
|
||||
}}
|
||||
value={value}
|
||||
placeholder={t("add_fixed_hosts")}
|
||||
labelText={t("fixed_hosts")}
|
||||
/>
|
||||
<CheckedHostField
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
onChange={(changeValue) => onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])}
|
||||
value={value}
|
||||
isFixed={false}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("round_robin_hosts")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Hosts = ({
|
||||
teamMembers,
|
||||
}: {
|
||||
teamMembers: {
|
||||
value: string;
|
||||
label: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
}[];
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
control,
|
||||
resetField,
|
||||
getValues,
|
||||
formState: { submitCount },
|
||||
} = useFormContext<FormValues>();
|
||||
const schedulingType = useWatch({
|
||||
control: formMethods.control,
|
||||
control,
|
||||
name: "schedulingType",
|
||||
});
|
||||
const initialValue = useRef<{
|
||||
hosts: FormValues["hosts"];
|
||||
schedulingType: SchedulingType | null;
|
||||
submitCount: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Handles init & out of date initial value after submission.
|
||||
if (!initialValue.current || initialValue.current?.submitCount !== submitCount) {
|
||||
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
|
||||
return;
|
||||
}
|
||||
resetField("hosts", {
|
||||
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
});
|
||||
}, [schedulingType, resetField, getValues, submitCount]);
|
||||
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
name="hosts"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const schedulingTypeRender = {
|
||||
COLLECTIVE: (
|
||||
<CheckedHostField
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isFixed={true}
|
||||
options={teamMembers.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
/>
|
||||
),
|
||||
ROUND_ROBIN: (
|
||||
<>
|
||||
<RoundRobinHosts teamMembers={teamMembers} onChange={onChange} value={value} />
|
||||
{/*<TextField
|
||||
required
|
||||
type="number"
|
||||
label={t("minimum_round_robin_hosts_count")}
|
||||
defaultValue={1}
|
||||
{...formMethods.register("minimumHostCount")}
|
||||
addOnSuffix={<>{t("hosts")}</>}
|
||||
/>*/}
|
||||
</>
|
||||
),
|
||||
};
|
||||
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const schedulingTypeOptions: {
|
||||
value: SchedulingType;
|
||||
@@ -101,17 +205,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
// description: string;
|
||||
}[] = [
|
||||
{
|
||||
value: SchedulingType.COLLECTIVE,
|
||||
value: "COLLECTIVE",
|
||||
label: t("collective"),
|
||||
// description: t("collective_description"),
|
||||
},
|
||||
{
|
||||
value: SchedulingType.ROUND_ROBIN,
|
||||
value: "ROUND_ROBIN",
|
||||
label: t("round_robin"),
|
||||
// description: t("round_robin_description"),
|
||||
},
|
||||
];
|
||||
|
||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||
return (
|
||||
<div>
|
||||
@@ -119,9 +222,8 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col">
|
||||
<Label>{t("scheduling_type")}</Label>
|
||||
<Controller
|
||||
<Controller<FormValues>
|
||||
name="schedulingType"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select
|
||||
options={schedulingTypeOptions}
|
||||
@@ -134,75 +236,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||
<FixedHosts
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
control={formMethods.control}
|
||||
/>
|
||||
)}
|
||||
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||
<>
|
||||
<FixedHosts
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||
}
|
||||
placeholder={t("add_fixed_hosts")}
|
||||
labelText={t("fixed_hosts")}
|
||||
control={formMethods.control}
|
||||
/>
|
||||
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
||||
<div>
|
||||
<Label>{t("round_robin_hosts")}</Label>
|
||||
<Controller
|
||||
name="hosts"
|
||||
control={formMethods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CheckedTeamSelect
|
||||
isDisabled={false}
|
||||
onChange={(options) =>
|
||||
onChange(
|
||||
options.map((option) => ({
|
||||
isFixed: false,
|
||||
userId: parseInt(option.value, 10),
|
||||
}))
|
||||
)
|
||||
}
|
||||
value={value
|
||||
.map(
|
||||
(host) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
teamMembers
|
||||
.map(mapUserToValue)
|
||||
.find((member) => member.value === host.userId.toString())!
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods
|
||||
.getValues("hostsFixed")
|
||||
.find((host) => host.userId.toString() === option.value)
|
||||
}
|
||||
placeholder={t("add_attendees")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/*<TextField
|
||||
required
|
||||
type="number"
|
||||
label={t("minimum_round_robin_hosts_count")}
|
||||
defaultValue={1}
|
||||
{...formMethods.register("minimumHostCount")}
|
||||
addOnSuffix={<>{t("hosts")}</>}
|
||||
/>*/}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Hosts teamMembers={teamMembersOptions} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import type { Session } from "next-auth";
|
||||
import type { GetSessionParams } from "next-auth/react";
|
||||
import { getSession as getSessionInner } from "next-auth/react";
|
||||
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export async function hashPassword(password: string) {
|
||||
const hashedPassword = await hash(password, 12);
|
||||
return hashedPassword;
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export async function verifyPassword(password: string, hashedPassword: string) {
|
||||
const isValid = await compare(password, hashedPassword);
|
||||
return isValid;
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export async function getSession(options: GetSessionParams): Promise<Session | null> {
|
||||
const session = await getSessionInner(options);
|
||||
|
||||
// that these are equal are ensured in `[...nextauth]`'s callback
|
||||
return session as Session | null;
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export enum ErrorCode {
|
||||
UserNotFound = "user-not-found",
|
||||
IncorrectPassword = "incorrect-password",
|
||||
UserMissingPassword = "missing-password",
|
||||
TwoFactorDisabled = "two-factor-disabled",
|
||||
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
|
||||
TwoFactorSetupRequired = "two-factor-setup-required",
|
||||
SecondFactorRequired = "second-factor-required",
|
||||
IncorrectTwoFactorCode = "incorrect-two-factor-code",
|
||||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
RateLimitExceeded = "rate-limit-exceeded",
|
||||
InvalidPassword = "invalid-password",
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
|
||||
[IdentityProvider.CAL]: "Cal",
|
||||
[IdentityProvider.GOOGLE]: "Google",
|
||||
[IdentityProvider.SAML]: "SAML",
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import parser from "accept-language-parser";
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { Maybe } from "@calcom/trpc/server";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { i18n } from "../../../next-i18next.config";
|
||||
|
||||
export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
@@ -15,9 +15,13 @@ export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
return preferredLocale ?? i18n.defaultLocale;
|
||||
}
|
||||
|
||||
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise<string> => {
|
||||
const prisma = (await import("@calcom/prisma")).default;
|
||||
const session = await getSession({ req });
|
||||
export const getOrSetUserLocaleFromHeaders = async (
|
||||
req: GetServerSidePropsContext["req"],
|
||||
res: GetServerSidePropsContext["res"]
|
||||
): Promise<string> => {
|
||||
const { default: prisma } = await import("@calcom/prisma");
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const preferredLocale = getLocaleFromHeaders(req);
|
||||
|
||||
if (session?.user?.id) {
|
||||
|
||||
@@ -3,21 +3,23 @@ import { RRule } from "rrule";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { detectBrowserTimeFormat, TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { parseZone } from "./parseZone";
|
||||
|
||||
const processDate = (date: string | null | Dayjs, i18n: I18n) => {
|
||||
const processDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
|
||||
const parsedZone = parseZone(date);
|
||||
if (!parsedZone?.isValid()) return "Invalid date";
|
||||
const formattedTime = parsedZone?.format(detectBrowserTimeFormat);
|
||||
const formattedTime = parsedZone?.format(
|
||||
withDefaultTimeFormat ? TimeFormat.TWELVE_HOUR : detectBrowserTimeFormat
|
||||
);
|
||||
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
|
||||
};
|
||||
|
||||
export const parseDate = (date: string | null | Dayjs, i18n: I18n) => {
|
||||
export const parseDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
|
||||
if (!date) return ["No date"];
|
||||
return processDate(date, i18n);
|
||||
return processDate(date, i18n, withDefaultTimeFormat);
|
||||
};
|
||||
|
||||
export const parseRecurringDates = (
|
||||
@@ -26,11 +28,13 @@ export const parseRecurringDates = (
|
||||
timeZone,
|
||||
recurringEvent,
|
||||
recurringCount,
|
||||
withDefaultTimeFormat,
|
||||
}: {
|
||||
startDate: string | null | Dayjs;
|
||||
timeZone?: string;
|
||||
recurringEvent: RecurringEvent | null;
|
||||
recurringCount: number;
|
||||
withDefaultTimeFormat: boolean;
|
||||
},
|
||||
i18n: I18n
|
||||
): [string[], Date[]] => {
|
||||
@@ -50,7 +54,7 @@ export const parseRecurringDates = (
|
||||
});
|
||||
const dateStrings = times.map((t) => {
|
||||
// finally; show in local timeZone again
|
||||
return processDate(t.tz(timeZone), i18n);
|
||||
return processDate(t.tz(timeZone), i18n, withDefaultTimeFormat);
|
||||
});
|
||||
|
||||
return [dateStrings, times.map((t) => t.toDate())];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.6.8",
|
||||
"version": "2.6.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -86,7 +86,7 @@
|
||||
"micro": "^10.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^13.2.1",
|
||||
"next-auth": "^4.18.8",
|
||||
"next-auth": "^4.20.1",
|
||||
"next-axiom": "^0.16.0",
|
||||
"next-collect": "^0.2.1",
|
||||
"next-i18next": "^11.3.0",
|
||||
|
||||
+30
-23
@@ -15,6 +15,7 @@ import {
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import defaultEvents, {
|
||||
getDynamicEventDescription,
|
||||
getGroupName,
|
||||
@@ -76,7 +77,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
size="sm"
|
||||
items={props.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
image: user.avatar || "",
|
||||
image: user.avatar,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
@@ -177,26 +178,28 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
className="dark:bg-darkgray-100 dark:hover:bg-darkgray-200 dark:border-darkgray-300 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50">
|
||||
<FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
className="block w-full p-5"
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
||||
{type.title}
|
||||
</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</Link>
|
||||
<div className="block w-full p-5">
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
||||
{type.title}
|
||||
</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -259,7 +262,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
const usernameList = getUsernameList(context.query.user as string);
|
||||
const dataFetchStart = Date.now();
|
||||
const users = await prisma.user.findMany({
|
||||
const usersWithoutAvatar = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernameList,
|
||||
@@ -273,7 +276,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
bio: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
avatar: true,
|
||||
theme: true,
|
||||
away: true,
|
||||
verified: true,
|
||||
@@ -281,6 +283,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
});
|
||||
|
||||
const users = usersWithoutAvatar.map((user) => ({
|
||||
...user,
|
||||
avatar: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
}));
|
||||
|
||||
if (!users.length) {
|
||||
return {
|
||||
notFound: true,
|
||||
|
||||
@@ -1,659 +1,5 @@
|
||||
import type { UserPermissionRole } from "@prisma/client";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
import { SignJWT } from "jose";
|
||||
import type { Session } from "next-auth";
|
||||
import NextAuth 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 { hostedCal, isSAMLLoginEnabled, clientSecretVerifier } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
||||
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
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";
|
||||
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
|
||||
|
||||
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
|
||||
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 })}`;
|
||||
|
||||
const providers: Provider[] = [
|
||||
CredentialsProvider({
|
||||
id: "credentials",
|
||||
name: "Cal.com",
|
||||
type: "credentials",
|
||||
credentials: {
|
||||
email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" },
|
||||
password: { label: "Password", type: "password", placeholder: "Your super secure password" },
|
||||
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials) {
|
||||
console.error(`For some reason credentials are missing`);
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: credentials.email.toLowerCase(),
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
metadata: true,
|
||||
identityProvider: true,
|
||||
password: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
teams: {
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Don't leak information about it being username or password that is invalid
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
||||
}
|
||||
|
||||
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.password) {
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||
if (!isCorrectPassword) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
}
|
||||
|
||||
if (user.twoFactorEnabled) {
|
||||
if (!credentials.totpCode) {
|
||||
throw new Error(ErrorCode.SecondFactorRequired);
|
||||
}
|
||||
|
||||
if (!user.twoFactorSecret) {
|
||||
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
|
||||
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
if (secret.length !== 32) {
|
||||
console.error(
|
||||
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
|
||||
);
|
||||
throw new Error(ErrorCode.InternalServerError);
|
||||
}
|
||||
|
||||
const isValidToken = authenticator.check(credentials.totpCode, secret);
|
||||
if (!isValidToken) {
|
||||
throw new Error(ErrorCode.IncorrectTwoFactorCode);
|
||||
}
|
||||
}
|
||||
|
||||
const limiter = rateLimit({
|
||||
intervalInMs: 60 * 1000, // 1 minute
|
||||
});
|
||||
await limiter.check(10, user.email); // 10 requests per minute
|
||||
// Check if the user you are logging into has any active teams
|
||||
const hasActiveTeams =
|
||||
user.teams.filter((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;
|
||||
|
||||
// authentication success- but does it meet the minimum password requirements?
|
||||
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: "INACTIVE_ADMIN",
|
||||
belongsToActiveTeam: hasActiveTeams,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
belongsToActiveTeam: hasActiveTeams,
|
||||
};
|
||||
},
|
||||
}),
|
||||
ImpersonationProvider,
|
||||
];
|
||||
|
||||
if (IS_GOOGLE_LOGIN_ENABLED) {
|
||||
providers.push(
|
||||
GoogleProvider({
|
||||
clientId: GOOGLE_CLIENT_ID,
|
||||
clientSecret: GOOGLE_CLIENT_SECRET,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isSAMLLoginEnabled) {
|
||||
providers.push({
|
||||
id: "saml",
|
||||
name: "BoxyHQ",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
checks: ["pkce", "state"],
|
||||
authorization: {
|
||||
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
|
||||
params: {
|
||||
scope: "",
|
||||
response_type: "code",
|
||||
provider: "saml",
|
||||
},
|
||||
},
|
||||
token: {
|
||||
url: `${WEBAPP_URL}/api/auth/saml/token`,
|
||||
params: { grant_type: "authorization_code" },
|
||||
},
|
||||
userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`,
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id || "",
|
||||
firstName: profile.firstName || "",
|
||||
lastName: profile.lastName || "",
|
||||
email: profile.email || "",
|
||||
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
|
||||
email_verified: true,
|
||||
};
|
||||
},
|
||||
options: {
|
||||
clientId: "dummy",
|
||||
clientSecret: clientSecretVerifier,
|
||||
},
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
});
|
||||
|
||||
// Idp initiated login
|
||||
providers.push(
|
||||
CredentialsProvider({
|
||||
id: "saml-idp",
|
||||
name: "IdP Login",
|
||||
credentials: {
|
||||
code: {},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { code } = credentials;
|
||||
|
||||
if (!code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
// Fetch access token
|
||||
const { access_token } = await oauthController.token({
|
||||
code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: `${process.env.NEXTAUTH_URL}`,
|
||||
client_id: "dummy",
|
||||
client_secret: clientSecretVerifier,
|
||||
});
|
||||
|
||||
if (!access_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch user info
|
||||
const userInfo = await oauthController.userInfo(access_token);
|
||||
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, firstName, lastName, email } = userInfo;
|
||||
|
||||
return {
|
||||
id: id as unknown as number,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
name: `${firstName} ${lastName}`.trim(),
|
||||
email_verified: true,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isNumber(n: string) {
|
||||
return !isNaN(parseFloat(n)) && !isNaN(+n);
|
||||
}
|
||||
|
||||
const calcomAdapter = CalComAdapter(prisma);
|
||||
|
||||
export default NextAuth({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
adapter: calcomAdapter,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
jwt: {
|
||||
// decorate the native JWT encode function
|
||||
// Impl. detail: We don't pass through as this function is called with encode/decode functions.
|
||||
encode: async ({ token, maxAge, secret }) => {
|
||||
if (token?.sub && isNumber(token.sub)) {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id: Number(token.sub) },
|
||||
select: { metadata: true },
|
||||
});
|
||||
// if no user is found, we still don't want to crash here.
|
||||
if (user) {
|
||||
const metadata = userMetadata.parse(user.metadata);
|
||||
if (metadata?.sessionTimeout) {
|
||||
maxAge = metadata.sessionTimeout / 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
return encode({ secret, token, maxAge });
|
||||
},
|
||||
},
|
||||
cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")),
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
signOut: "/auth/logout",
|
||||
error: "/auth/error", // Error code passed in query string as ?error=
|
||||
verifyRequest: "/auth/verify",
|
||||
// newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest)
|
||||
},
|
||||
providers,
|
||||
callbacks: {
|
||||
async jwt({ token, user, account }) {
|
||||
const autoMergeIdentities = async () => {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
where: { email: token.email! },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return {
|
||||
...existingUser,
|
||||
...token,
|
||||
};
|
||||
};
|
||||
if (!user) {
|
||||
return await autoMergeIdentities();
|
||||
}
|
||||
if (!account) {
|
||||
return token;
|
||||
}
|
||||
if (account.type === "credentials") {
|
||||
// return token if credentials,saml-idp
|
||||
if (account.provider === "saml-idp") {
|
||||
return token;
|
||||
}
|
||||
// any other credentials, add user info
|
||||
return {
|
||||
...token,
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
impersonatedByUID: user?.impersonatedByUID,
|
||||
belongsToActiveTeam: user?.belongsToActiveTeam,
|
||||
};
|
||||
}
|
||||
|
||||
// The arguments above are from the provider so we need to look up the
|
||||
// user based on those values in order to construct a JWT.
|
||||
if (account.type === "oauth") {
|
||||
if (!account.provider || !account.providerAccountId) {
|
||||
return token;
|
||||
}
|
||||
const idP = account.provider === "saml" ? IdentityProvider.SAML : IdentityProvider.GOOGLE;
|
||||
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
identityProvider: idP,
|
||||
},
|
||||
{
|
||||
identityProviderId: account.providerAccountId as string,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingUser) {
|
||||
return await autoMergeIdentities();
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
id: existingUser.id,
|
||||
name: existingUser.name,
|
||||
username: existingUser.username,
|
||||
email: existingUser.email,
|
||||
role: existingUser.role,
|
||||
impersonatedByUID: token.impersonatedByUID as number,
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
};
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
const calendsoSession: Session = {
|
||||
...session,
|
||||
hasValidLicense,
|
||||
user: {
|
||||
...session.user,
|
||||
id: token.id as number,
|
||||
name: token.name,
|
||||
username: token.username as string,
|
||||
role: token.role as UserPermissionRole,
|
||||
impersonatedByUID: token.impersonatedByUID as number,
|
||||
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
||||
},
|
||||
};
|
||||
return calendsoSession;
|
||||
},
|
||||
async signIn(params) {
|
||||
const { user, account, profile } = params;
|
||||
|
||||
if (account?.provider === "email") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// In this case we've already verified the credentials in the authorize
|
||||
// callback so we can sign the user in.
|
||||
// Only if provider is not saml-idp
|
||||
if (account?.provider !== "saml-idp") {
|
||||
if (account?.type === "credentials") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (account?.type !== "oauth") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!user.email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (account?.provider) {
|
||||
let idP: IdentityProvider = IdentityProvider.GOOGLE;
|
||||
if (account.provider === "saml" || account.provider === "saml-idp") {
|
||||
idP = IdentityProvider.SAML;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error TODO validate email_verified key on profile
|
||||
user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified;
|
||||
|
||||
if (!user.email_verified) {
|
||||
return "/auth/error?error=unverified-email";
|
||||
}
|
||||
|
||||
// Only google oauth on this path
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
include: {
|
||||
accounts: {
|
||||
where: {
|
||||
provider: idP,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
identityProvider: idP as IdentityProvider,
|
||||
identityProviderId: account.providerAccountId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
// In this case there's an existing user and their email address
|
||||
// hasn't changed since they last logged in.
|
||||
if (existingUser.email === user.email) {
|
||||
try {
|
||||
// If old user without Account entry we link their google account
|
||||
if (existingUser.accounts.length === 0) {
|
||||
const linkAccountWithUserData = { ...account, userId: existingUser.id };
|
||||
await calcomAdapter.linkAccount(linkAccountWithUserData);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error("Error while linking account of already existing user");
|
||||
}
|
||||
}
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the email address doesn't match, check if an account already exists
|
||||
// with the new email address. If it does, for now we return an error. If
|
||||
// not, update the email of their account and log them in.
|
||||
const userWithNewEmail = await prisma.user.findFirst({
|
||||
where: { email: user.email },
|
||||
});
|
||||
|
||||
if (!userWithNewEmail) {
|
||||
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return "/auth/error?error=new-email-conflict";
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no existing user for this identity provider and id, create
|
||||
// a new account. If an account already exists with the incoming email
|
||||
// address return an error for now.
|
||||
const existingUserWithEmail = await prisma.user.findFirst({
|
||||
where: { email: user.email },
|
||||
});
|
||||
|
||||
if (existingUserWithEmail) {
|
||||
// if self-hosted then we can allow auto-merge of identity providers if email is verified
|
||||
if (!hostedCal && existingUserWithEmail.emailVerified) {
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if user was invited
|
||||
if (
|
||||
!existingUserWithEmail.password &&
|
||||
!existingUserWithEmail.emailVerified &&
|
||||
!existingUserWithEmail.username
|
||||
) {
|
||||
await prisma.user.update({
|
||||
where: { email: user.email },
|
||||
data: {
|
||||
// Slugify the incoming name and append a few random characters to
|
||||
// prevent conflicts for users with the same name.
|
||||
username: usernameSlug(user.name),
|
||||
emailVerified: new Date(Date.now()),
|
||||
name: user.name,
|
||||
identityProvider: idP,
|
||||
identityProviderId: String(user.id),
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// User signs up with email/password and then tries to login with Google/SAML using the same email
|
||||
if (
|
||||
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
|
||||
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
|
||||
) {
|
||||
await prisma.user.update({
|
||||
where: { email: existingUserWithEmail.email },
|
||||
data: { password: null },
|
||||
});
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
|
||||
return "/auth/error?error=use-password-login";
|
||||
}
|
||||
|
||||
return "/auth/error?error=use-identity-login";
|
||||
}
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
// Slugify the incoming name and append a few random characters to
|
||||
// prevent conflicts for users with the same name.
|
||||
username: usernameSlug(user.name),
|
||||
emailVerified: new Date(Date.now()),
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
identityProvider: idP,
|
||||
identityProviderId: String(user.id),
|
||||
},
|
||||
});
|
||||
|
||||
const linkAccountNewUserData = { ...account, userId: newUser.id };
|
||||
await calcomAdapter.linkAccount(linkAccountNewUserData);
|
||||
|
||||
if (account.twoFactorEnabled) {
|
||||
return loginWithTotp(newUser);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
async redirect({ url, baseUrl }) {
|
||||
// Allows relative callback URLs
|
||||
if (url.startsWith("/")) return `${baseUrl}${url}`;
|
||||
// Allows callback URLs on the same domain
|
||||
else if (new URL(url).hostname === new URL(WEBAPP_URL).hostname) return url;
|
||||
return baseUrl;
|
||||
},
|
||||
},
|
||||
});
|
||||
export default NextAuth(AUTH_OPTIONS);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { ErrorCode, hashPassword, verifyPassword } from "../../../lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session || !session.user || !session.user.email) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { hashPassword } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(400).json({ message: "" });
|
||||
|
||||
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { ErrorCode, getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
import qrcode from "qrcode";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import { symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
const session = await getSession({ req });
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const session = await getServerSession({ req, res });
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
const booking = await handleNewBooking(req, {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
const session = await getSession({ req });
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const session = await getServerSession({ req, res });
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
return await handleCancelBooking(req);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -42,7 +42,7 @@ const defaultIntegrationAddHandler = async ({
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// Check that user is authenticated
|
||||
req.session = await getSession({ req });
|
||||
req.session = await getServerSession({ req, res });
|
||||
|
||||
const { args } = req.query;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
|
||||
let isCold = true;
|
||||
@@ -9,7 +9,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const prePrismaDate = performance.now();
|
||||
const prisma = (await import("@calcom/prisma")).default;
|
||||
const preSessionDate = performance.now();
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) return res.status(409).json({ message: "Unauthorized" });
|
||||
const preUserDate = performance.now();
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import * as trpcNext from "@calcom/trpc/server/adapters/next";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { createContext as createTrpcContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
export default trpcNext.createNextApiHandler({
|
||||
@@ -12,7 +13,11 @@ export default trpcNext.createNextApiHandler({
|
||||
/**
|
||||
* @link https://trpc.io/docs/context
|
||||
*/
|
||||
createContext,
|
||||
createContext: ({ req, res }) => {
|
||||
const sessionGetter = () => getServerSession({ req, res });
|
||||
|
||||
return createTrpcContext({ req, res }, sessionGetter);
|
||||
},
|
||||
/**
|
||||
* @link https://trpc.io/docs/error-handling
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { defaultAvatarSrc } from "@lib/profile";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import RoutingFormsRoutingConfig from "@calcom/app-store/ee/routing-forms/pages/app-routing.config";
|
||||
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
|
||||
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function getServerSideProps(
|
||||
appPages?: string[];
|
||||
}>
|
||||
) {
|
||||
const { params } = context;
|
||||
const { params, req, res } = context;
|
||||
if (!params) {
|
||||
return {
|
||||
notFound: true,
|
||||
@@ -120,7 +120,7 @@ export async function getServerSideProps(
|
||||
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
|
||||
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
|
||||
params.appPages = pages.slice(1);
|
||||
const session = await getSession({ req: context.req });
|
||||
const session = await getServerSession({ req, res });
|
||||
const user = session?.user;
|
||||
|
||||
const result = await route.getServerSideProps(
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import path from "path";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -36,11 +35,11 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
isProOnly={data.isProOnly}
|
||||
images={source.data?.items as string[] | undefined}
|
||||
isTemplate={data.isTemplate}
|
||||
dependencies={data.dependencies}
|
||||
// tos="https://zoom.us/terms"
|
||||
// privacy="https://zoom.us/privacy"
|
||||
body={
|
||||
<>
|
||||
{data.slug === "google-meet" && <ExisitingGoogleCal />}
|
||||
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { ChangeEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AppCategories } from "@calcom/prisma/client";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -86,9 +86,11 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssg = await ssgInit(context);
|
||||
|
||||
const session = await getSession(context);
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
let appStore;
|
||||
if (session?.user?.id) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ResetPasswordRequest } from "@prisma/client";
|
||||
import debounce from "lodash/debounce";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Link from "next/link";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
@@ -185,6 +186,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
},
|
||||
id,
|
||||
csrfToken: await getCsrfToken({ req: context.req }),
|
||||
...(await serverSideTranslations(context.locale || "en", ["common"])),
|
||||
},
|
||||
};
|
||||
} catch (reason) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import debounce from "lodash/debounce";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import type { SyntheticEvent } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, EmailField } from "@calcom/ui";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
@@ -132,17 +132,21 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
ForgotPassword.getInitialProps = async (context: GetServerSidePropsContext) => {
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
const session = await getSession({ req });
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (session) {
|
||||
res.writeHead(302, { Location: "/" });
|
||||
res.end();
|
||||
return;
|
||||
return { props: {} };
|
||||
}
|
||||
|
||||
return {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
props: {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
...(await serverSideTranslations(context.locale || "en", ["common"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,8 +9,9 @@ import { FormProvider, useForm } from "react-hook-form";
|
||||
import { FaGoogle } from "react-icons/fa";
|
||||
|
||||
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ErrorCode, getSession } from "@calcom/lib/auth";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -220,8 +221,9 @@ export default function Login({
|
||||
|
||||
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
|
||||
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const verifyJwt = (jwt: string) => {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -136,10 +136,12 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
const userCount = await prisma.user.count();
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getProviders, signIn, getSession, getCsrfToken } from "next-auth/react";
|
||||
import { getProviders, signIn, getCsrfToken } from "next-auth/react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
type Provider = {
|
||||
@@ -25,7 +26,9 @@ function signin({ providers }: { providers: Provider[] }) {
|
||||
export default signin;
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const session = await getSession(context);
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const csrfToken = await getCsrfToken(context);
|
||||
const providers = await getProviders();
|
||||
if (session) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import {
|
||||
hostedCal,
|
||||
@@ -16,7 +17,6 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
@@ -60,9 +60,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
throw new Error(`File is not named sso/[provider]`);
|
||||
}
|
||||
|
||||
const { req } = context;
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
if (session) {
|
||||
|
||||
@@ -199,6 +199,10 @@ export default function Success(props: SuccessProps) {
|
||||
if ((isCancellationMode || changes) && typeof window !== "undefined") {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
const tz =
|
||||
(isSuccessBookingPage
|
||||
? props.bookingInfo.attendees.find((attendee) => attendee.email === email)?.timeZone
|
||||
: props.bookingInfo.eventType?.timeZone || props.bookingInfo.user?.timeZone) || timeZone();
|
||||
|
||||
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
|
||||
|
||||
@@ -246,6 +250,7 @@ export default function Success(props: SuccessProps) {
|
||||
eventName: (props.dynamicEventName as string) || props.eventType.eventName,
|
||||
host: props.profile.name || "Nameless",
|
||||
location: location,
|
||||
bookingFields: bookingInfo.responses,
|
||||
t,
|
||||
};
|
||||
|
||||
@@ -477,6 +482,7 @@ export default function Success(props: SuccessProps) {
|
||||
date={dayjs(formerTime)}
|
||||
is24h={is24h}
|
||||
isCancelled={isCancelled}
|
||||
tz={tz}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
@@ -488,6 +494,7 @@ export default function Success(props: SuccessProps) {
|
||||
date={date}
|
||||
is24h={is24h}
|
||||
isCancelled={isCancelled}
|
||||
tz={tz}
|
||||
/>
|
||||
</div>
|
||||
{(bookingInfo?.user || bookingInfo?.attendees) && (
|
||||
@@ -785,6 +792,7 @@ type RecurringBookingsProps = {
|
||||
is24h: boolean;
|
||||
allRemainingBookings: boolean;
|
||||
isCancelled: boolean;
|
||||
tz: string;
|
||||
};
|
||||
|
||||
export function RecurringBookings({
|
||||
@@ -795,6 +803,7 @@ export function RecurringBookings({
|
||||
allRemainingBookings,
|
||||
is24h,
|
||||
isCancelled,
|
||||
tz,
|
||||
}: RecurringBookingsProps) {
|
||||
const [moreEventsVisible, setMoreEventsVisible] = useState(false);
|
||||
const {
|
||||
@@ -822,12 +831,12 @@ export function RecurringBookings({
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookingsSorted.slice(0, 4).map((dateStr: string, idx: number) => (
|
||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(dayjs.tz(dateStr, timeZone()), language, "full")}
|
||||
{formatToLocalizedDate(dayjs.tz(dateStr, tz), language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||
{formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, timeZone())})
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -842,12 +851,12 @@ export function RecurringBookings({
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
|
||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(dayjs.tz(date, timeZone()), language, "full")}
|
||||
{formatToLocalizedDate(dayjs.tz(date, tz), language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, timeZone())})
|
||||
({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -860,11 +869,11 @@ export function RecurringBookings({
|
||||
|
||||
return (
|
||||
<div className={classNames(isCancelled ? "line-through" : "")}>
|
||||
{formatToLocalizedDate(dayjs.tz(date, timeZone()), language, "full")}
|
||||
{formatToLocalizedDate(date, language, "full", tz)}
|
||||
<br />
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h)}{" "}
|
||||
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language, timeZone())})</span>
|
||||
{formatToLocalizedTime(date, language, undefined, !is24h, tz)} -{" "}
|
||||
{formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
|
||||
<span className="text-bookinglight">({formatToLocalizedTimezone(date, language, tz)})</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -901,6 +910,7 @@ const getEventTypesFromDB = async (id: number) => {
|
||||
currency: true,
|
||||
bookingFields: true,
|
||||
disableGuests: true,
|
||||
timeZone: true,
|
||||
owner: {
|
||||
select: userSelect,
|
||||
},
|
||||
@@ -1019,12 +1029,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
name: true,
|
||||
email: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
@@ -1032,6 +1044,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
select: {
|
||||
eventName: true,
|
||||
slug: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { PeriodType } from "@prisma/client";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import type { SchedulingType } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -10,22 +10,23 @@ import { z } from "zod";
|
||||
|
||||
import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import { validateBookingLimitOrder } from "@calcom/lib";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Form, showToast } from "@calcom/ui";
|
||||
|
||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
||||
@@ -85,9 +86,9 @@ export type FormValues = {
|
||||
externalId: string;
|
||||
};
|
||||
successRedirectUrl: string;
|
||||
bookingLimits?: BookingLimit;
|
||||
hosts: { userId: number }[];
|
||||
hostsFixed: { userId: number }[];
|
||||
durationLimits?: IntervalLimit;
|
||||
bookingLimits?: IntervalLimit;
|
||||
hosts: { userId: number; isFixed: boolean }[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
};
|
||||
|
||||
@@ -181,6 +182,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
delete metadata.config?.useHostSchedulesForTeamEvent;
|
||||
}
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name;
|
||||
});
|
||||
|
||||
const defaultValues = {
|
||||
title: eventType.title,
|
||||
locations: eventType.locations || [],
|
||||
@@ -188,6 +195,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
description: eventType.description ?? undefined,
|
||||
schedule: eventType.schedule || undefined,
|
||||
bookingLimits: eventType.bookingLimits || undefined,
|
||||
durationLimits: eventType.durationLimits || undefined,
|
||||
length: eventType.length,
|
||||
hidden: eventType.hidden,
|
||||
periodDates: {
|
||||
@@ -200,16 +208,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
schedulingType: eventType.schedulingType,
|
||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||
metadata,
|
||||
hosts: !!eventType.hosts?.length
|
||||
? eventType.hosts.filter((host) => !host.isFixed)
|
||||
: eventType.users
|
||||
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
|
||||
.map((user) => ({ userId: user.id })),
|
||||
hostsFixed: !!eventType.hosts?.length
|
||||
? eventType.hosts.filter((host) => host.isFixed)
|
||||
: eventType.users
|
||||
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
|
||||
.map((user) => ({ userId: user.id })),
|
||||
hosts: eventType.hosts,
|
||||
} as const;
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
@@ -221,9 +220,13 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
// Make it optional because it's not submitted from all tabs of the page
|
||||
eventName: z
|
||||
.string()
|
||||
.refine((val) => validateCustomEventName(val, t("invalid_event_name_variables")) === true, {
|
||||
message: t("invalid_event_name_variables"),
|
||||
})
|
||||
.refine(
|
||||
(val) =>
|
||||
validateCustomEventName(val, t("invalid_event_name_variables"), bookingFields) === true,
|
||||
{
|
||||
message: t("invalid_event_name_variables"),
|
||||
}
|
||||
)
|
||||
.optional(),
|
||||
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||
bookingFields: eventTypeBookingFields,
|
||||
@@ -302,12 +305,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
locations,
|
||||
metadata,
|
||||
customInputs,
|
||||
hosts: hostsInput,
|
||||
hostsFixed,
|
||||
// We don't need to send send these values to the backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
seatsPerTimeSlotEnabled,
|
||||
@@ -316,14 +318,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
...input
|
||||
} = values;
|
||||
|
||||
const hosts: ((typeof hostsInput)[number] & { isFixed?: boolean })[] = [];
|
||||
if (hostsInput || hostsFixed) {
|
||||
hosts.push(...hostsInput.concat(hostsFixed.map((host) => ({ isFixed: true, ...host }))));
|
||||
if (bookingLimits) {
|
||||
const isValid = validateIntervalLimitOrder(bookingLimits);
|
||||
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
||||
}
|
||||
|
||||
if (bookingLimits) {
|
||||
const isValid = validateBookingLimitOrder(bookingLimits);
|
||||
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
||||
if (durationLimits) {
|
||||
const isValid = validateIntervalLimitOrder(durationLimits);
|
||||
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
|
||||
}
|
||||
|
||||
if (metadata?.multipleDuration !== undefined) {
|
||||
@@ -338,7 +340,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
updateMutation.mutate({
|
||||
...input,
|
||||
hosts,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
@@ -348,6 +349,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
beforeEventBuffer: beforeBufferTime,
|
||||
afterEventBuffer: afterBufferTime,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
metadata,
|
||||
@@ -369,8 +371,10 @@ const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) =
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, query } = context;
|
||||
const session = await getSession({ req });
|
||||
const { req, res, query } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
const typeParam = parseInt(asStringOrThrow(query.type));
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
@@ -391,9 +395,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
try {
|
||||
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
// session,
|
||||
type: typeParam,
|
||||
trpcState: ssr.dehydrate(),
|
||||
initialData: {
|
||||
|
||||
@@ -338,7 +338,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.users?.length > 1 && (
|
||||
{type.team && (
|
||||
<AvatarGroup
|
||||
className="relative top-1 right-3"
|
||||
size="sm"
|
||||
|
||||
@@ -4,7 +4,7 @@ import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -140,8 +140,10 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const crypto = await import("crypto");
|
||||
const session = await getSession(context);
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { NextPageContext } from "next";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
function RedirectPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const session = await getSession(context);
|
||||
export async function getServerSideProps({ req, res }: GetServerSidePropsContext) {
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
}
|
||||
|
||||
@@ -69,7 +69,10 @@ const ConferencingLayout = () => {
|
||||
}, []);
|
||||
|
||||
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
|
||||
onSuccess: onSuccessCallback,
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
onSuccessCallback();
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(`Error: ${error.message}`, "error");
|
||||
},
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { identityProviderNameMap } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useEffect } from "react";
|
||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
@@ -48,29 +48,31 @@ function TeamPage({ team }: TeamPageProps) {
|
||||
"dark:bg-darkgray-100 dark:border-darkgray-300 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50",
|
||||
!isEmbed && "bg-white"
|
||||
)}>
|
||||
<Link
|
||||
href={`/team/${team.slug}/${type.slug}`}
|
||||
className="flex justify-between px-6 py-4"
|
||||
data-testid="event-type-link">
|
||||
<div className="flex-shrink">
|
||||
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
|
||||
<h2 className="dark:text-darkgray-700 text-sm font-semibold text-gray-700">{type.title}</h2>
|
||||
<div className="px-6 py-4 ">
|
||||
<Link
|
||||
href={`/team/${team.slug}/${type.slug}`}
|
||||
data-testid="event-type-link"
|
||||
className="flex justify-between">
|
||||
<div className="flex-shrink">
|
||||
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
|
||||
<h2 className="dark:text-darkgray-700 text-sm font-semibold text-gray-700">{type.title}</h2>
|
||||
</div>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size="sm"
|
||||
items={type.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
title: user.name || "",
|
||||
image: CAL_URL + "/" + user.username + "/avatar.png" || "",
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size="sm"
|
||||
items={type.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
title: user.name || "",
|
||||
image: CAL_URL + "/" + user.username + "/avatar.png" || "",
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
@@ -82,6 +82,8 @@ export default function JoinCall(props: JoinCallPageProps) {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
@@ -139,7 +141,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
});
|
||||
const session = await getSession();
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
// set meetingPassword to null for guests
|
||||
if (session?.user.id !== bookingObj.user?.id) {
|
||||
|
||||
@@ -1472,7 +1472,7 @@
|
||||
"add_an_option": "إضافة خيار",
|
||||
"radio": "الراديو",
|
||||
"google_meet_warning": "من أجل استخدام Google Meet، يجب عليك تعيين تقويم وجهتك على تقويم Google",
|
||||
"individual": "فردي",
|
||||
"individual": "فرد",
|
||||
"all_bookings_filter_label": "جميع الحجوزات",
|
||||
"all_users_filter_label": "جميع المستخدمين",
|
||||
"your_bookings_filter_label": "الحجوزات لديك",
|
||||
@@ -1491,13 +1491,13 @@
|
||||
"date_overrides_update_btn": "تحديث التجاوز",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-نسخ",
|
||||
"set_as_default": "تعيين كافتراضي",
|
||||
"hide_eventtype_details": "إخفاء تفاصيل نوع الفعالية",
|
||||
"hide_eventtype_details": "إخفاء تفاصيل EventType",
|
||||
"show_navigation": "إظهار التنقل",
|
||||
"hide_navigation": "إخفاء التنقل",
|
||||
"verification_code_sent": "تم إرسال رمز التحقق",
|
||||
"verified_successfully": "تم التحقق بنجاح",
|
||||
"wrong_code": "رمز تحقق خاطئ",
|
||||
"not_verified": "لم يتم التحقق بعد",
|
||||
"wrong_code": "رمز التحقق غير صحيح",
|
||||
"not_verified": "لم يتم التحقق منه بعد",
|
||||
"no_availability_in_month": "غير متاح في شهر {{month}}",
|
||||
"view_next_month": "عرض الشهر التالي",
|
||||
"send_code": "إرسال الرمز",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"upgrade_now": "Přejít na vyšší tarif",
|
||||
"accept_invitation": "Přijmout pozvánku",
|
||||
"calcom_explained": "{{appName}} je open source alternativa, která vám dává kontrolu nad vlastními daty, workflow a vzhledem.",
|
||||
"calcom_explained_new_user": "Dokončete nastavení svého účtu {{appName}}! Jste jen několik kroků od vyřešení všech svých problémů s plánováním.",
|
||||
"have_any_questions": "Máte otázky? Jsme tu, abychom vám pomohli.",
|
||||
"reset_password_subject": "{{appName}}: Pokyny pro obnovení hesla",
|
||||
"event_declined_subject": "Odmítnuto: {{title}} v {{date}}",
|
||||
@@ -23,6 +24,8 @@
|
||||
"rejection_reason_description": "Opravdu chcete rezervaci odmítnout? Osobě, která se váš čas pokusila rezervovat, dáme vědět. Níž můžete uvést důvod.",
|
||||
"rejection_confirmation": "Odmítnout rezervaci",
|
||||
"manage_this_event": "Spravovat tuto událost",
|
||||
"invite_team_member": "Pozvat člena týmu",
|
||||
"invite_team_notifcation_badge": "Pozv.",
|
||||
"your_event_has_been_scheduled": "Vaše událost byla naplánována",
|
||||
"your_event_has_been_scheduled_recurring": "Opakovaná událost byla naplánována",
|
||||
"accept_our_license": "Přijměte naši licenci změnou proměnné .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> na '{{agree}}'.",
|
||||
@@ -410,6 +413,8 @@
|
||||
"password_updated_successfully": "Heslo bylo úspěšně změněno",
|
||||
"password_has_been_changed": "Vaše heslo bylo úspěšně změněno.",
|
||||
"error_changing_password": "Chyba při změně hesla",
|
||||
"session_timeout_changed": "Konfigurace vaší relace byla aktualizována.",
|
||||
"session_timeout_change_error": "Chyba při aktualizaci konfigurace relace",
|
||||
"something_went_wrong": "Někde došlo k chybě.",
|
||||
"something_doesnt_look_right": "Něco nefunguje správně?",
|
||||
"please_try_again": "Zkuste to prosím znovu.",
|
||||
@@ -428,6 +433,7 @@
|
||||
"password_hint_num": "Obsahuje alespoň 1 číslo",
|
||||
"invalid_password_hint": "Heslo musí být minimálně {{passwordLength}} znaků dlouhé, obsahovat alespoň jedno číslo a obsahovat kombinaci velkých a malých písmen",
|
||||
"incorrect_password": "Heslo není správné.",
|
||||
"incorrect_username_password": "Uživatelské jméno nebo heslo je nesprávné.",
|
||||
"24_h": "24h",
|
||||
"use_setting": "Použít nastavení",
|
||||
"am_pm": "am/pm",
|
||||
@@ -455,6 +461,7 @@
|
||||
"slots_load_fail": "Nepodařilo se načíst dostupné časové sloty.",
|
||||
"additional_guests": "Přidat hosty",
|
||||
"your_name": "Vaše jméno",
|
||||
"your_full_name": "Vaše celé jméno",
|
||||
"email_address": "E-mailová adresa",
|
||||
"enter_valid_email": "Zadejte platný e-mail",
|
||||
"location": "Místo",
|
||||
@@ -508,6 +515,8 @@
|
||||
"admin": "Administrátor",
|
||||
"administrator_user": "Uživatel administrátor",
|
||||
"lets_create_first_administrator_user": "Pojďme vytvořit prvního uživatele administrátora.",
|
||||
"admin_user_created": "Nastavení uživatele administrátora",
|
||||
"admin_user_created_description": "Uživatele administrátora již máte vytvořeného. Teď se můžete přihlásit ke svému účtu.",
|
||||
"new_member": "Nový člen",
|
||||
"invite": "Pozvat",
|
||||
"add_team_members": "Přidat členy týmu",
|
||||
@@ -657,6 +666,7 @@
|
||||
"edit_availability": "Upravit dostupnost",
|
||||
"configure_availability": "Nastavit časy, kdy jste dostupní pro rezervace.",
|
||||
"copy_times_to": "Kopírovat časy do",
|
||||
"copy_times_to_tooltip": "Zkopírovat časy do…",
|
||||
"change_weekly_schedule": "Změnit týdenní pracovní dobu",
|
||||
"logo": "Logo",
|
||||
"error": "Chyba",
|
||||
@@ -668,6 +678,7 @@
|
||||
"add_attendees": "Přidat účastníky",
|
||||
"show_advanced_settings": "Zobrazit pokročilá nastavení",
|
||||
"event_name": "Název události",
|
||||
"event_name_in_calendar": "Název události v kalendáři",
|
||||
"event_name_tooltip": "Název, který se zobrazí v kalendářích",
|
||||
"meeting_with_user": "Schůzka s {{attendeeName}}",
|
||||
"additional_inputs": "Další pole",
|
||||
@@ -786,6 +797,7 @@
|
||||
"number_apps_one": "Počet aplikací: {{count}}",
|
||||
"number_apps_other": "Počet aplikací: {{count}}",
|
||||
"trending_apps": "Populární aplikace",
|
||||
"most_popular": "Nejoblíbenější",
|
||||
"explore_apps": "Aplikace v kategorii {{category}}",
|
||||
"installed_apps": "Nainstalované aplikace",
|
||||
"free_to_use_apps": "Zdarma",
|
||||
@@ -796,12 +808,14 @@
|
||||
"no_category_apps_description_analytics": "Přidejte analytickou aplikaci pro vaše rezervační stránky",
|
||||
"no_category_apps_description_automation": "Přidejte aplikaci pro automatizaci",
|
||||
"no_category_apps_description_other": "Přidejte jakýkoli jiný typ aplikace pro nejrůznější činnosti",
|
||||
"no_category_apps_description_web3": "Přidejte aplikaci web3 pro vaše rezervační stránky",
|
||||
"installed_app_calendar_description": "Nastavte si kalendáře, ať můžete kontrolovat konflikty a zabránit tak dvojím rezervacím.",
|
||||
"installed_app_conferencing_description": "Přidejte své oblíbené aplikace pro videokonference pro vaše schůzky",
|
||||
"installed_app_payment_description": "Nakonfigurujte služby zpracování plateb, které se mají používat při strhávání plateb od klientů.",
|
||||
"installed_app_analytics_description": "Nakonfigurujte aplikace, které chcete použít pro své rezervační stránky",
|
||||
"installed_app_other_description": "Všechny vaše nainstalované aplikace z ostatních kategorií.",
|
||||
"installed_app_automation_description": "Konfigurovat aplikace pro automatizaci",
|
||||
"installed_app_web3_description": "Nakonfigurujte aplikace web3, které se mají použít pro vaše rezervační stránky",
|
||||
"analytics": "Analytické nástroje",
|
||||
"empty_installed_apps_headline": "Nenainstalovány žádné aplikace",
|
||||
"empty_installed_apps_description": "Aplikace vám umožní zlepšit pracovní postupy a výrazně zkvalitnit plánování.",
|
||||
@@ -929,6 +943,11 @@
|
||||
"current_location": "Současné místo",
|
||||
"user_phone": "Vaše telefonní číslo",
|
||||
"new_location": "Nové místo",
|
||||
"session": "Relace",
|
||||
"session_description": "Mějte pod kontrolou relaci svého účtu",
|
||||
"session_timeout_after": "Časový limit relace vyprší po",
|
||||
"session_timeout": "Časový limit relace",
|
||||
"session_timeout_description": "Po uplynutí určitého času se relace zneplatní.",
|
||||
"no_location": "Není definováno žádné místo",
|
||||
"set_location": "Nastavit místo",
|
||||
"update_location": "Aktualizovat místo",
|
||||
@@ -1072,6 +1091,7 @@
|
||||
"broken_video_action": "Nelze přidat odkaz na schůzku <1>{{location}}</1> k vaší plánované události. Kontaktujte pozvané nebo aktualizujte událost ve svém kalendáři a přidejte podrobnosti. Můžete buď <3>změnit místo u typu události</3>, nebo zkusit <5>aplikaci odstranit a znovu přidat.</5>",
|
||||
"broken_calendar_action": "Aktualizace vašeho kalendáře <1>{{calendar}}</1> se nezdařila. <2> Zkontrolujte si nastavení kalendáře nebo kalendář odeberte a zase přidejte </2>",
|
||||
"attendee_name": "Jméno účastníka",
|
||||
"scheduler_full_name": "Celé jméno osoby provádějící rezervaci",
|
||||
"broken_integration": "Narušená integrace",
|
||||
"problem_adding_video_link": "Při přidávání odkazu na video došlo k chybě",
|
||||
"problem_updating_calendar": "Při aktualizaci kalendáře došlo k chybě",
|
||||
@@ -1214,6 +1234,7 @@
|
||||
"connect_automation_apps": "Připojit aplikace pro automatizaci",
|
||||
"connect_analytics_apps": "Připojit analytické aplikace",
|
||||
"connect_other_apps": "Propojit další aplikace",
|
||||
"connect_web3_apps": "Připojení aplikací web3",
|
||||
"current_step_of_total": "Krok {{currentStep}} / {{maxSteps}}",
|
||||
"add_variable": "Přidat proměnnou",
|
||||
"custom_phone_number": "Vlastní telefonní číslo",
|
||||
@@ -1230,6 +1251,7 @@
|
||||
"to": "Komu",
|
||||
"workflow_turned_on_successfully": "Pracovní postup {{workflowName}} byl {{offOn}}",
|
||||
"download_responses": "Stáhnout odpovědi",
|
||||
"download_responses_description": "Stáhněte si všechny odpovědi do formuláře ve formátu CSV.",
|
||||
"download": "Stáhnout",
|
||||
"create_your_first_form": "Vytvořte svůj první formulář",
|
||||
"create_your_first_form_description": "Pomocí adresných formulářů můžete klást kvalifikační otázky a adresovat je správné osobě nebo typu události.",
|
||||
@@ -1271,6 +1293,8 @@
|
||||
"routing_forms_send_email_owner": "Odeslat e-mail vlastníkovi",
|
||||
"routing_forms_send_email_owner_description": "Po odeslání formuláře bude vlastníkovi odeslán e-mail",
|
||||
"add_new_form": "Přidat nový formulář",
|
||||
"create_your_first_route": "Vytvořte svoje první směrování",
|
||||
"route_to_the_right_person": "Nasměrovat té správné osobě na základě odpovědí ve vašem formuláři",
|
||||
"form_description": "Vytvořte vlastní formulář, který adresujete rezervující osobě",
|
||||
"copy_link_to_form": "Kopírovat odkaz na formulář",
|
||||
"theme": "Motiv",
|
||||
@@ -1301,6 +1325,7 @@
|
||||
"password_reset_leading": "Pokud brzy neobdržíte e-mail, zkontrolujte, zda je zadaná e-mailová adresa správná, dále složku nevyžádané pošty nebo se obraťte na podporu, pokud problém přetrvává.",
|
||||
"password_updated": "Heslo bylo úspěšně změněno!",
|
||||
"pending_payment": "Čeká se na platbu",
|
||||
"pending_invites": "Čekající pozvánky",
|
||||
"confirmation_page_rainbow": "Zamkněte svoji událost pomocí tokenů nebo NFT na platformách Ethereum, Polygon a dalších.",
|
||||
"not_on_cal": "Nenachází se na {{appName}}",
|
||||
"no_calendar_installed": "Není nainstalován kalendář",
|
||||
@@ -1427,6 +1452,9 @@
|
||||
"disabled_calendar": "Pokud máte nainstalovaný jiný kalendář, budou do něj přidány nové rezervace. Pokud ne, připojte nový kalendář, abyste nepřišli o žádné nové rezervace.",
|
||||
"enable_apps": "Povolit aplikace",
|
||||
"enable_apps_description": "Povolte aplikace, které mohou uživatelé integrovat s Cal.com",
|
||||
"purchase_license": "Kupte si licenci",
|
||||
"already_have_key": "Klíč již mám:",
|
||||
"already_have_key_suggestion": "Zkopírujte sem svou stávající proměnnou prostředí CALCOM_LICENSE_KEY.",
|
||||
"app_is_enabled": "Aplikace {{appName}} je povolena",
|
||||
"app_is_disabled": "Aplikace {{appName}} je zakázána",
|
||||
"keys_have_been_saved": "Klíče byly uloženy",
|
||||
@@ -1447,6 +1475,7 @@
|
||||
"individual": "Jedinec",
|
||||
"all_bookings_filter_label": "Všechny rezervace",
|
||||
"all_users_filter_label": "Všichni uživatelé",
|
||||
"your_bookings_filter_label": "Vaše rezervace",
|
||||
"meeting_url_variable": "Adresa URL schůzky",
|
||||
"meeting_url_info": "Adresa URL konferenční schůzky k události",
|
||||
"date_overrides": "Změna dnů",
|
||||
@@ -1485,6 +1514,7 @@
|
||||
"round_robin_hosts": "Hostitelé typu „round robin“",
|
||||
"minimum_round_robin_hosts_count": "Počet hostitelů, kteří se musí zúčastnit",
|
||||
"hosts": "Hostitelé",
|
||||
"upgrade_to_enable_feature": "K povolení této funkce je třeba vytvořit tým. Tým vytvoříte kliknutím.",
|
||||
"new_attendee": "Nový účastník",
|
||||
"awaiting_approval": "Čeká na schválení",
|
||||
"requires_google_calendar": "Tato aplikace vyžaduje připojení ke Kalendáři Google",
|
||||
@@ -1493,9 +1523,97 @@
|
||||
"continue_to_install_google_calendar": "Pokračovat v instalaci Kalendáře Google",
|
||||
"install_google_meet": "Nainstalovat Google Meet",
|
||||
"install_google_calendar": "Nainstalovat Kalendář Google",
|
||||
"sender_name": "Jméno odesílatele",
|
||||
"already_invited": "Účastník je již pozvaný",
|
||||
"no_recordings_found": "Nenalezeny žádné nahrávky",
|
||||
"reporting": "Hlášení",
|
||||
"reporting_feature": "Zobrazení všech příchozích dat a jejich stažení ve formátu CSV",
|
||||
"teams_plan_required": "Potřebujete tarif Teams",
|
||||
"routing_forms_are_a_great_way": "Směrovací formuláře představují skvělý způsob, jak příchozí potenciální kontakty nasměrovat na tu správnou osobu. Přístup k této funkci získáte po upgradu na tarif Teams.",
|
||||
"choose_a_license": "Vyberte si licenci",
|
||||
"choose_license_description": "Cal.com se dodává s dostupnou a bezplatnou licencí AGPLv3, která má určitá omezení. Kdykoli je ale možné provést upgrade na licenci Enterprise. Upgrade je možné provést kdykoli v pozdější době.",
|
||||
"license": "Licence",
|
||||
"agplv3_license": "Licence AGPLv3",
|
||||
"ee_enterprise_license": "„/ee“ Licence Enterprise",
|
||||
"enterprise_booking_fee": "Od {{{enterprise_booking_fee}}/měsíc",
|
||||
"enterprise_license_includes": "Vše pro komerční použití",
|
||||
"no_need_to_keep_your_code_open_source": "Kód nemusíte nechávat jako open-source",
|
||||
"repackage_rebrand_resell": "Snadné přebalení, rebranding a přeprodej",
|
||||
"a_vast_suite_of_enterprise_features": "Rozsáhlá sada firemních funkcí",
|
||||
"free_license_fee": "0,00 $/měsíc",
|
||||
"forever_open_and_free": "Navždy otevřený a zdarma",
|
||||
"required_to_keep_your_code_open_source": "Svůj kód musíte nechat jako open-source",
|
||||
"cannot_repackage_and_resell": "Přebalení, rebranding a přeprodej nejsou tak snadné",
|
||||
"no_enterprise_features": "Žádné firemní funkce",
|
||||
"step_enterprise_license": "Licence Enterprise",
|
||||
"step_enterprise_license_description": "Vše pro komerční použití s privátním hostingem, přebalováním, rebrandingem a přeprodejem a přístupem k exkluzivním firemním komponentám.",
|
||||
"setup": "Nastavení",
|
||||
"setup_description": "Nastavte instanci Cal.com",
|
||||
"configure": "Konfigurovat",
|
||||
"sso_configuration": "Single Sign-On",
|
||||
"sso_configuration_description": "Konfigurace SAML/OIDC SSO a umožnění členům týmu přihlašování prostřednictvím poskytovatele identity",
|
||||
"sso_oidc_heading": "SSO s OIDC",
|
||||
"sso_oidc_description": "Konfigurace OIDC SSO s poskytovatelem identity podle vlastního výběru.",
|
||||
"sso_oidc_configuration_title": "Konfigurace OIDC",
|
||||
"sso_oidc_configuration_description": "Konfigurace připojení OIDC k poskytovateli identity. Potřebné informace najdete u svého poskytovatele identity.",
|
||||
"sso_oidc_callback_copied": "Adresa URL zpětného volání zkopírována",
|
||||
"sso_saml_heading": "SSO s protokolem SAML",
|
||||
"sso_saml_description": "Konfigurace SSO SAML s vybraným poskytovatelem identity.",
|
||||
"sso_saml_configuration_title": "Konfigurace SAML",
|
||||
"sso_saml_configuration_description": "Konfigurace připojení SAML k vašemu poskytovateli identity. Potřebné informace najdete u svého poskytovatele identity.",
|
||||
"sso_saml_acsurl_copied": "Adresa URL ACS zkopírována",
|
||||
"sso_saml_entityid_copied": "ID subjektu zkopírováno",
|
||||
"sso_connection_created_successfully": "Konfigurace {{connectionType}} vytvořena",
|
||||
"sso_connection_deleted_successfully": "Konfigurace {{connectionType}} odstraněna",
|
||||
"delete_sso_configuration": "Odstranit konfiguraci {{connectionType}}",
|
||||
"delete_sso_configuration_confirmation": "Ano, odstranit konfiguraci {{connectionType}}",
|
||||
"delete_sso_configuration_confirmation_description": "Opravdu chcete odstranit konfiguraci {{connectionType}}? Členové vašeho týmu, kteří používají přihlášení {{connectionType}}, ztratí přístup k webu Cal.com.",
|
||||
"organizer_timezone": "Časové pásmo organizátora",
|
||||
"email_no_user_cta": "Vytvořit účet",
|
||||
"email_user_cta": "Zobrazit pozvánku",
|
||||
"email_no_user_invite_heading": "Byli jste pozváni do týmu v aplikaci {{appName}}",
|
||||
"email_no_user_invite_subheading": "Uživatel {{invitedBy}} vás pozval, abyste se připojili k jeho týmu v aplikaci {{appName}}. {{appName}} je plánovač událostí, který vám a vašemu týmu umožňuje plánovat schůzky bez e-mailového ping-pongu.",
|
||||
"email_no_user_invite_steps_intro": "Provedeme vás několika krátkými kroky a za malou chvíli si budete se svým týmem užívat plánování bez stresu.",
|
||||
"email_no_user_step_one": "Vyberte si uživatelské jméno",
|
||||
"email_no_user_step_two": "Připojte svůj účet kalendáře",
|
||||
"email_no_user_step_three": "Nastavte si dostupnost",
|
||||
"email_no_user_step_four": "Připojit se k týmu {{teamName}}",
|
||||
"email_no_user_signoff": "Tým {{appName}} vám přeje ničím nerušené plánování",
|
||||
"impersonation_user_tip": "Chystáte se zosobnit uživatele, což znamená, že můžete provádět změny jeho jménem. Počínejte si obezřetně, prosím.",
|
||||
"available_variables": "Dostupné proměnné",
|
||||
"scheduler": "{Scheduler}",
|
||||
"recommended_next_steps": "Doporučené další kroky",
|
||||
"create_a_managed_event": "Vytvořte typ spravované události",
|
||||
"meetings_are_better_with_the_right": "Schůzky jsou lepší, když jsou na nich přítomni ti správní členové týmu. Pozvěte je.",
|
||||
"collective_or_roundrobin": "Kolektivní nebo round-robin",
|
||||
"book_your_team_members": "Rezervujte si členy týmu všechny spolu pomocí kolektivních událostí nebo postupně pomocí round-robinu.",
|
||||
"default_app_link_title": "Nastavte výchozí odkaz na aplikaci",
|
||||
"default_app_link_description": "Nastavení výchozího odkazu na aplikaci umožní, aby všechny nově vytvořené typy událostí používaly vámi nastavený odkaz na aplikaci.",
|
||||
"change_default_conferencing_app": "Nastavit jako výchozí",
|
||||
"booking_confirmation_failed": "Potvrzení rezervace se nezdařilo"
|
||||
"under_maintenance": "Odstávka z důvodu údržby",
|
||||
"under_maintenance_description": "Tým {{appName}} provádí plánovanou údržbu. V případě dotazů se obraťte na podporu.",
|
||||
"event_type_seats": "Počet míst: {{numberOfSeats}}",
|
||||
"booking_with_payment_cancelled": "Platba za tuto událost již není možná",
|
||||
"booking_with_payment_cancelled_already_paid": "Vrácení peněz za tuto rezervační platbu je na cestě.",
|
||||
"booking_with_payment_cancelled_refunded": "Tato rezervační platba byla vrácena.",
|
||||
"booking_confirmation_failed": "Potvrzení rezervace se nezdařilo",
|
||||
"get_started_zapier_templates": "Začněte používat šablony Zapier",
|
||||
"a_routing_form": "Směrovací formulář",
|
||||
"form_description_placeholder": "Popis formuláře",
|
||||
"keep_me_connected_with_form": "Udržujte se mnou spojení pomocí formuláře",
|
||||
"fields_in_form_duplicated": "Veškeré změny ve směrování a polích duplikovaného formuláře s promítnou duplicitě.",
|
||||
"form_deleted": "Formulář odstraněn",
|
||||
"delete_form_action": "Ano, odstranit formulář",
|
||||
"typeform_redirect_url_copied": "Adresa URL přesměrování Typeform zkopírována! Můžete jít nastavit adresu URL ve formuláři Typeform.",
|
||||
"modifications_in_fields_warning": "Úpravy v polích a směrování následujících formulářů se projeví v tomto formuláři.",
|
||||
"connected_forms": "Připojené formuláře",
|
||||
"form_modifications_warning": "Úprava polí a tras na tomto místě by se dotkla následujících formulářů.",
|
||||
"responses_collection_waiting_description": "Počkejte chvíli na shromáždění odpovědí. Formulář můžete odeslat také sami.",
|
||||
"this_is_what_your_users_would_see": "Toto uvidí vaši uživatelé",
|
||||
"identifies_name_field": "Identifikuje pole podle tohoto názvu.",
|
||||
"add_1_option_per_line": "Přidejte 1 možnost na řádek",
|
||||
"select_a_router": "Vyberte směrování",
|
||||
"add_a_new_route": "Přidejte nové směrování",
|
||||
"no_responses_yet": "Zatím žádné odpovědi",
|
||||
"this_will_be_the_placeholder": "Toto bude zástupný znak"
|
||||
}
|
||||
|
||||
@@ -1156,7 +1156,8 @@
|
||||
"event_advanced_tab_title": "Advanced",
|
||||
"event_setup_multiple_duration_error": "Event Setup: Multiple durations requires at least 1 option.",
|
||||
"event_setup_multiple_duration_default_error": "Event Setup: Please select a valid default duration.",
|
||||
"event_setup_booking_limits_error": "Booking limits must be in accending order. [day,week,month,year]",
|
||||
"event_setup_booking_limits_error": "Booking limits must be in ascending order. [day, week, month, year]",
|
||||
"event_setup_duration_limits_error": "Duration limits must be in ascending order. [day, week, month, year]",
|
||||
"select_which_cal": "Select which calendar to add bookings to",
|
||||
"custom_event_name": "Custom event name",
|
||||
"custom_event_name_description": "Create customised event names to display on calendar event",
|
||||
@@ -1252,7 +1253,7 @@
|
||||
"attendee_name_info": "The person booking's name",
|
||||
"to": "To",
|
||||
"workflow_turned_on_successfully": "{{workflowName}} workflow turned {{offOn}} successfully",
|
||||
"download_responses": "Download Responses",
|
||||
"download_responses": "Download responses",
|
||||
"download_responses_description": "Download all responses to your form in CSV format.",
|
||||
"download": "Download",
|
||||
"create_your_first_form": "Create your first form",
|
||||
@@ -1291,7 +1292,7 @@
|
||||
"exchange_authentication_standard": "Basic authentication",
|
||||
"exchange_authentication_ntlm": "NTLM authentication",
|
||||
"exchange_compression": "GZip compression",
|
||||
"routing_forms_description": "You can see all forms and routes you have created here.",
|
||||
"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",
|
||||
"add_new_form": "Add new form",
|
||||
@@ -1340,6 +1341,8 @@
|
||||
"report_app": "Report app",
|
||||
"limit_booking_frequency": "Limit booking frequency",
|
||||
"limit_booking_frequency_description": "Limit how many times this event can be booked",
|
||||
"limit_total_booking_duration": "Limit total booking duration",
|
||||
"limit_total_booking_duration_description": "Limit total amount of time that this event can be booked",
|
||||
"add_limit": "Add Limit",
|
||||
"team_name_required": "Team name required",
|
||||
"show_attendees": "Share attendee information between guests",
|
||||
@@ -1443,6 +1446,7 @@
|
||||
"calcom_is_better_with_team": "Cal.com is better with teams",
|
||||
"add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.",
|
||||
"booking_limit_reached": "Booking Limit for this event type has been reached",
|
||||
"duration_limit_reached": "Duration Limit for this event type has been reached",
|
||||
"admin_has_disabled": "An admin has disabled {{appName}}",
|
||||
"disabled_app_affects_event_type": "An admin has disabled {{appName}} which affects your event type {{eventType}}",
|
||||
"disable_payment_app": "The admin has disabled {{appName}} which affects your event type {{title}}. Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin renables your payment method.",
|
||||
@@ -1635,10 +1639,15 @@
|
||||
"add_a_new_route": "Add a new Route",
|
||||
"no_responses_yet": "No responses yet",
|
||||
"this_will_be_the_placeholder": "This will be the placeholder",
|
||||
"this_app_requires_connected_account": "{{appName}} requires a connected {{dependencyName}} account",
|
||||
"connect_app": "Connect {{dependencyName}}",
|
||||
"app_is_connected": "{{dependencyName}} is connected",
|
||||
"requires_app": "Requires {{dependencyName}}",
|
||||
"verification_code": "Verification code",
|
||||
"can_you_try_again": "Can you try again with a different time?",
|
||||
"verify": "Verify",
|
||||
"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",
|
||||
"default_conferencing_bulk_description": "Update the locations for the selected event types",
|
||||
"invalid_event_name_variables": "There is an invalid variable in your event name"
|
||||
"default_conferencing_bulk_description": "Update the locations for the selected event types"
|
||||
}
|
||||
|
||||
@@ -1156,7 +1156,8 @@
|
||||
"event_advanced_tab_title": "Avancé",
|
||||
"event_setup_multiple_duration_error": "Configuration de l'événement : plusieurs durées requièrent au moins une option.",
|
||||
"event_setup_multiple_duration_default_error": "Configuration de l'événement : veuillez sélectionner une durée par défaut valide.",
|
||||
"event_setup_booking_limits_error": "Les limites de réservation doivent être en ordre croissant. [jour,semaine,mois,année]",
|
||||
"event_setup_booking_limits_error": "Les limites de réservation doivent être dans l'ordre croissant. [jour, semaine, mois, année]",
|
||||
"event_setup_duration_limits_error": "Les limites de durée doivent être dans l'ordre croissant. [jour, semaine, mois, année]",
|
||||
"select_which_cal": "Sélectionnez le calendrier auquel ajouter les réservations.",
|
||||
"custom_event_name": "Nom d'événement personnalisé",
|
||||
"custom_event_name_description": "Créez des noms d'événement personnalisés à afficher sur l'événement du calendrier",
|
||||
@@ -1291,7 +1292,7 @@
|
||||
"exchange_authentication_standard": "Authentification de base",
|
||||
"exchange_authentication_ntlm": "Authentification NTLM",
|
||||
"exchange_compression": "Compression GZip",
|
||||
"routing_forms_description": "Vous pouvez voir tous les formulaires et routes que vous avez créés ici.",
|
||||
"routing_forms_description": "Créez des formulaires pour diriger 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",
|
||||
"add_new_form": "Ajouter un nouveau formulaire",
|
||||
@@ -1340,6 +1341,8 @@
|
||||
"report_app": "Signaler l'application",
|
||||
"limit_booking_frequency": "Limiter la fréquence de réservation",
|
||||
"limit_booking_frequency_description": "Limitez le nombre de réservations possibles pour cet événement.",
|
||||
"limit_total_booking_duration": "Limiter la durée totale de réservation",
|
||||
"limit_total_booking_duration_description": "Limitez la durée totale pendant laquelle cet événement peut être réservé.",
|
||||
"add_limit": "Ajouter une limite",
|
||||
"team_name_required": "Nom d'équipe requis",
|
||||
"show_attendees": "Partagez les informations des participants entre les invités",
|
||||
@@ -1443,6 +1446,7 @@
|
||||
"calcom_is_better_with_team": "Cal.com est meilleur en équipe",
|
||||
"add_your_team_members": "Ajoutez les membres de votre équipe à vos types d'événements. Utilisez la planification collective pour inclure tout le monde ou trouvez la personne la plus appropriée avec la planification Round-Robin.",
|
||||
"booking_limit_reached": "La limite de réservation pour ce type d'événement a été atteinte",
|
||||
"duration_limit_reached": "La limite de durée pour ce type d'événement a été atteinte",
|
||||
"admin_has_disabled": "Un administrateur a désactivé {{appName}}",
|
||||
"disabled_app_affects_event_type": "Un administrateur a désactivé {{appName}} qui affecte votre type d'événement {{eventType}}",
|
||||
"disable_payment_app": "L'administrateur a désactivé {{appName}} qui affecte votre type d'événement {{title}}. Les participants sont toujours en mesure de réserver ce type d'événement, mais ne seront pas invités à payer. Vous pouvez masquer le type d'événement pour l'empêcher jusqu'à ce que votre administrateur réactive votre moyen de paiement.",
|
||||
@@ -1619,9 +1623,10 @@
|
||||
"keep_me_connected_with_form": "Rester connecté avec le formulaire",
|
||||
"fields_in_form_duplicated": "Toute modification du routeur et des champs du formulaire dupliqué se répercutera dans la duplication.",
|
||||
"form_deleted": "Formulaire supprimé",
|
||||
"delete_form": "Supprimer le formulaire",
|
||||
"delete_form": "Voulez-vous vraiment supprimer ce formulaire ?",
|
||||
"delete_form_action": "Oui, supprimer le formulaire",
|
||||
"delete_form_confirmation": "Voulez-vous vraiment supprimer ce formulaire ? Toute personne avec qui vous avez partagé le lien ne pourra plus passer de réservation en l'utilisant. De plus, toutes les réponses associées seraient supprimées.",
|
||||
"delete_form_confirmation": "Toute personne avec qui vous avez partagé le lien ne pourra plus y accéder.",
|
||||
"delete_form_confirmation_2": "Toutes les réponses associées seront supprimées.",
|
||||
"typeform_redirect_url_copied": "Lien de redirection Typeform copié ! Vous pouvez aller définir le lien dans le formulaire Typeform.",
|
||||
"modifications_in_fields_warning": "Les modifications dans les champs et les routes des formulaires suivants seront répercutées dans ce formulaire.",
|
||||
"connected_forms": "Formulaires connectés",
|
||||
@@ -1634,10 +1639,15 @@
|
||||
"add_a_new_route": "Ajouter une nouvelle route",
|
||||
"no_responses_yet": "Aucune réponse pour l'instant",
|
||||
"this_will_be_the_placeholder": "Ce sera le texte de substitution",
|
||||
"this_app_requires_connected_account": "{{appName}} requiert un compte {{dependencyName}} connecté",
|
||||
"connect_app": "Connecter {{dependencyName}}",
|
||||
"app_is_connected": "{{dependencyName}} est connectée",
|
||||
"requires_app": "Requiert {{dependencyName}}",
|
||||
"verification_code": "Code de vérification",
|
||||
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
|
||||
"verify": "Vérifier",
|
||||
"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",
|
||||
"default_conferencing_bulk_description": "Mettez à jour les emplacements pour les types d'événements sélectionnés.",
|
||||
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement"
|
||||
"default_conferencing_bulk_description": "Mettez à jour les emplacements pour les types d'événements sélectionnés."
|
||||
}
|
||||
|
||||
@@ -478,8 +478,8 @@
|
||||
"booking_confirmation": "אשר את ה{{eventTypeTitle}} עם {{profileName}}",
|
||||
"booking_reschedule_confirmation": "שנה את המועד של ה{{eventTypeTitle}} עם {{profileName}}",
|
||||
"in_person_meeting": "פגישה אישית",
|
||||
"attendee_in_person": "פנים-אל-פנים (הכתובת של המשתתף/ת)",
|
||||
"in_person": "פנים-אל-פנים (הכתובת של המארגן/ת)",
|
||||
"attendee_in_person": "פנים-אל-פנים (הכתובת של המשתתף)",
|
||||
"in_person": "פנים-אל-פנים (הכתובת של המארגן)",
|
||||
"link_meeting": "פגישה עם קישור",
|
||||
"phone_call": "מספר הטלפון של המשתתף/ת",
|
||||
"your_number": "מספר הטלפון שלך",
|
||||
@@ -819,7 +819,7 @@
|
||||
"installed_app_web3_description": "הגדר באיזה אפליקציות web3 להשתמש בעמודי תזמון הפגישות שלך",
|
||||
"analytics": "ניתוח נתונים",
|
||||
"empty_installed_apps_headline": "אין אפליקציות מותקנות",
|
||||
"empty_installed_apps_description": "אפליקציות מאפשרות לך לשפר משמעותית את תהליכי העבודה שלך ואת תהליך קביעת המועדים.",
|
||||
"empty_installed_apps_description": "יישומים מאפשרים לך לשפר את תהליכי העבודה שלך ואת פשטות ניהול התזמונים שלך משמעותית.",
|
||||
"empty_installed_apps_button": "נא לעיין ב-App Store",
|
||||
"manage_your_connected_apps": "ניהול האפליקציות המותקנות שלך או שינוי הגדרות",
|
||||
"browse_apps": "עיון באפליקציות",
|
||||
@@ -1090,7 +1090,7 @@
|
||||
"hour_one": "{{count}} שעה",
|
||||
"hour_other": "{{count}} שעות",
|
||||
"invalid_input": "קלט לא חוקי",
|
||||
"broken_video_action": "לא יכולנו להוסיף לאירוע המתוכנן שלך את הקישור לפגישה ב-<1>{{location}}</1>. צור/י קשר עם המוזמנים או עדכן/י את הפרטים באירוע בלוח השנה. ניתן <3> לשנות את המיקום בסוג האירוע </3> או לנסות <5>להסיר את האפליקציה ולהוסיף אותה שוב.</5>",
|
||||
"broken_video_action": "לא יכולנו להוסיף את הקישור <1>{{location}}</1> פגישה שלך לארוע שתוזמן. צור קשר עם המוזמנים שלך או עדכן את הארוע בלוח השנה שלך כדי להוסיף את הפרטים. אתה יכול <3> לשנות את המיקום בסוג הארוע </3> או לנסות <5>להסיר ואז להוסיף את היישום מחדש.</5>",
|
||||
"broken_calendar_action": "לא הצלחנו לעדכן את <1>{{calendar}}</1>. <2> נא לבדוק את הגדרות לוח השנה שלך או להסיר את לוח השנה ולהוסיף אותו שוב </2>",
|
||||
"attendee_name": "שם המשתתף",
|
||||
"scheduler_full_name": "השם המלא של האדם שמבצע את התזמון",
|
||||
@@ -1463,7 +1463,7 @@
|
||||
"disable_app": "השבתת האפליקציה",
|
||||
"disable_app_description": "השבתת האפליקציה הזו עלולה לגרום לבעיות באופן שבו המשתמשים שלך יקיימו אינטראקציה עם Cal",
|
||||
"edit_keys": "עריכת מפתחות",
|
||||
"admin_apps_description": "אפשר/י לאפליקציות לפעול עם המופע של Cal",
|
||||
"admin_apps_description": "אפשר/י לאפליקציות לפעול עם מופע Cal שלך",
|
||||
"no_available_apps": "אין אפליקציות זמינות",
|
||||
"no_available_apps_description": "ודא/י שיש אפליקציות בפריסה שלך בנתיב 'packages/app-store'",
|
||||
"no_apps": "אף אפליקציה לא מופעלת במופע הזה של Cal",
|
||||
@@ -1480,7 +1480,7 @@
|
||||
"your_bookings_filter_label": "התזמונים שלך",
|
||||
"meeting_url_variable": "כתובת ה-URL של הפגישה",
|
||||
"meeting_url_info": "כתובת ה-URL של שיחת הוועידה באירוע",
|
||||
"date_overrides": "תאריכים חופפים",
|
||||
"date_overrides": "חסימת תאריכים",
|
||||
"date_overrides_subtitle": "הוסף/י תאריכים שבהם הזמינות שלך שונה משעות העבודה הרגילות שלך מדי יום.",
|
||||
"date_overrides_info": "תאריכים שסומנו למעקף מועברים לארכיון באופן אוטומטי לאחר שהתאריך חולף",
|
||||
"date_overrides_dialog_which_hours": "באילו שעות את/ה פנוי/ה?",
|
||||
@@ -1619,9 +1619,10 @@
|
||||
"keep_me_connected_with_form": "שמור אותי מחובר לטופס",
|
||||
"fields_in_form_duplicated": "כל שינוי בניתוב ובשדות הטופס שמשוכפל, ישפיעו על הטופס המשוכפל.",
|
||||
"form_deleted": "הטופס נמחק",
|
||||
"delete_form": "מחק טופס",
|
||||
"delete_form": "האם אתה בטוח שאתה רוצה למחוק את הטופס?",
|
||||
"delete_form_action": "כן, מחק את הטופס",
|
||||
"delete_form_confirmation": "אתה בטוח שאתה רוצה למחוק את הטופס? כל מי ששיתפת עמו את הקישור לא יוכל יותר לתזמן באמצעותו. בנוסף, כל התגובות הקשורות אליו ימחקו.",
|
||||
"delete_form_confirmation": "כל מי ששיתפת אתו את הקישור לא יוכל לגשת אליו יותר.",
|
||||
"delete_form_confirmation_2": "כל התגובות המשויכות ימחקו.",
|
||||
"typeform_redirect_url_copied": "קישור הפניה TypeForm הועתק! אתה יכול ללכת ולהגדיר את הקישור בטופס ה- Typeform שלך.",
|
||||
"modifications_in_fields_warning": "שינויים בשדות וניתובים של הטפסים הבאים ישפיעו על טופס זה.",
|
||||
"connected_forms": "טפסים מחוברים",
|
||||
@@ -1634,10 +1635,14 @@
|
||||
"add_a_new_route": "הוסף נתיב חדש",
|
||||
"no_responses_yet": "אין עדיין תגובות",
|
||||
"this_will_be_the_placeholder": "זה יהיה הממלא מקום",
|
||||
"this_app_requires_connected_account": "{{appName}} דורש חשבון {{dependencyName}} מחובר",
|
||||
"connect_app": "חבר את {{dependencyName}}",
|
||||
"app_is_connected": "{{dependencyName}} מחובר",
|
||||
"requires_app": "{{dependencyName}} נדרש",
|
||||
"verification_code": "קוד אימות",
|
||||
"verify": "אמת",
|
||||
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך",
|
||||
"select_all": "בחר הכל",
|
||||
"default_conferencing_bulk_title": "בצע עדכון אצווה של סוגי הארועים הקיימים",
|
||||
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו",
|
||||
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך"
|
||||
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו"
|
||||
}
|
||||
|
||||
@@ -388,7 +388,7 @@
|
||||
"go_to_billing_portal": "請求ポータルに移動",
|
||||
"need_anything_else": "他に必要なものはありませんか?",
|
||||
"further_billing_help": "請求に関するサポートが必要な場合は、サポートチームがお手伝いします。",
|
||||
"contact": "お問い合わせ",
|
||||
"contact": "問い合わせる",
|
||||
"our_support_team": "弊社のサポートチーム",
|
||||
"contact_our_support_team": "サポートチームに問い合わせる",
|
||||
"uh_oh": "おっと!",
|
||||
@@ -465,7 +465,7 @@
|
||||
"email_address": "メールアドレス",
|
||||
"enter_valid_email": "有効なメールアドレスを入力してください",
|
||||
"location": "所在地",
|
||||
"address": "住所",
|
||||
"address": "場所",
|
||||
"enter_address": "住所を入力",
|
||||
"in_person_attendee_address": "対面(出席者のアドレス)",
|
||||
"yes": "はい",
|
||||
@@ -1461,7 +1461,7 @@
|
||||
"disable_app": "アプリを無効にする",
|
||||
"disable_app_description": "このアプリを無効にすると、ユーザーが Cal を操作する際に問題が発生する可能性があります",
|
||||
"edit_keys": "キーを編集",
|
||||
"admin_apps_description": "Cal のインスタンスにアプリを有効にする",
|
||||
"admin_apps_description": "アプリを Cal インスタンスに対して有効にする",
|
||||
"no_available_apps": "利用可能なアプリはありません",
|
||||
"no_available_apps_description": "\"packages/app-store\" 直下のデプロイメントにアプリがあることをご確認ください",
|
||||
"no_apps": "Cal のこのインスタンスで有効化されているアプリはありません",
|
||||
@@ -1491,7 +1491,7 @@
|
||||
"date_overrides_update_btn": "上書きを更新",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
||||
"set_as_default": "デフォルトとして設定",
|
||||
"hide_eventtype_details": "イベントの種類の詳細情報を非表示にする",
|
||||
"hide_eventtype_details": "EventType の詳細情報を非表示にする",
|
||||
"show_navigation": "ナビゲーションを表示",
|
||||
"hide_navigation": "ナビゲーションを非表示にする",
|
||||
"verification_code_sent": "確認コードが送信されました",
|
||||
@@ -1504,7 +1504,7 @@
|
||||
"number_verified": "番号が確認されました",
|
||||
"create_your_first_team_webhook_description": "このチームイベントの種類の初めての webhook を作成します",
|
||||
"create_webhook_team_event_type": "このチームイベントの種類の webhook を作成します",
|
||||
"disable_success_page": "予約の成功ページを無効化する (リダイレクト URL がある場合にのみ有効)",
|
||||
"disable_success_page": "予約完了ページを無効化する (リダイレクト URL がある場合にのみ有効)",
|
||||
"invalid_admin_password": "あなたは管理者に設定されていますが、パスワードの長さが不足しています (15 文字以上必要です)",
|
||||
"change_password_admin": "パスワードを変更して管理者アクセス権を取得する",
|
||||
"username_already_taken": "ユーザー名が既に使用されています",
|
||||
|
||||
@@ -1529,52 +1529,93 @@
|
||||
"reporting": "Raportowanie",
|
||||
"reporting_feature": "Zobacz wszystkie elementy przychodzące z danych i pobierz je w formacie CSV",
|
||||
"teams_plan_required": "Wymagany plan dla zespołów",
|
||||
"routing_forms_are_a_great_way": "Formularze przekierowania są świetnym sposobem na przekierowanie Twoich przychodzących leadów do właściwej osoby. Przejdź na plan dla zespołów, aby uzyskać dostęp do tej funkcji.",
|
||||
"routing_forms_are_a_great_way": "Formularze przekierowania są świetnym sposobem na przekierowanie Twoich potencjalnych klientów do właściwej osoby. Przejdź na plan dla zespołów, aby uzyskać dostęp do tej funkcji.",
|
||||
"choose_a_license": "Wybierz licencję",
|
||||
"choose_license_description": "Wraz z Cal.com zyskujesz przystępną i bezpłatną licencję AGPLv3 z pewnymi ograniczeniami, która może zostać ulepszona do licencji Enterprise w dowolnym momencie. Możesz wykupić licencję wyższego poziomu w każdej chwili.",
|
||||
"license": "Licencja",
|
||||
"agplv3_license": "Licencja AGPLv3",
|
||||
"enterprise_booking_fee": "Od {{enterprise_booking_fee}}/miesięcznie",
|
||||
"ee_enterprise_license": "Licencja Enterprise „/ee”",
|
||||
"enterprise_booking_fee": "Od {{enterprise_booking_fee}}/miesiąc",
|
||||
"enterprise_license_includes": "Wszystko co niezbędne do zastosowań komercyjnych",
|
||||
"no_need_to_keep_your_code_open_source": "Twój kod nie musi pozostać na licencji typu open-source.",
|
||||
"repackage_rebrand_resell": "Łatwa modyfikacja wyglądu, marki i odsprzedaży",
|
||||
"repackage_rebrand_resell": "Łatwa modyfikacja wyglądu, marki i możliwość odsprzedaży",
|
||||
"a_vast_suite_of_enterprise_features": "Duży wybór funkcji dla przedsiębiorstw",
|
||||
"free_license_fee": "0,00 USD/miesiąc",
|
||||
"forever_open_and_free": "Zawsze za darmo i w otwartym dostępie",
|
||||
"required_to_keep_your_code_open_source": "Twój kod musi pozostać na licencji typu open-source.",
|
||||
"cannot_repackage_and_resell": "Nie da się łatwo zmodyfikować wyglądu, zmienić marki i odsprzedać.",
|
||||
"cannot_repackage_and_resell": "Nie da się łatwo zmodyfikować wyglądu, zmienić marki ani odsprzedać.",
|
||||
"no_enterprise_features": "Brak funkcji dla przedsiębiorstw",
|
||||
"step_enterprise_license": "Licencja dla przedsiębiorstw",
|
||||
"step_enterprise_license": "Licencja Enterprise",
|
||||
"step_enterprise_license_description": "Wszystko do użytku komercyjnego wraz z prywatnym hostingiem, możliwością zmiany wyglądu, marki i odsprzedaży oraz z dostępem do ekskluzywnych komponentów dla przedsiębiorstw.",
|
||||
"setup": "Konfiguracja",
|
||||
"setup_description": "Skonfiguruj wystąpienie Cal.com",
|
||||
"configure": "Konfiguruj",
|
||||
"sso_configuration": "Pojedyncze logowanie",
|
||||
"sso_configuration_description": "Skonfiguruj logowanie jednokrotne SAML/OIDC i zezwól członkom zespołu na logowanie za pomocą dostawcy tożsamości",
|
||||
"sso_oidc_heading": "Logowanie jednokrotne za pomocą OIDC",
|
||||
"sso_oidc_description": "Skonfiguruj logowanie jednokrotne OIDC za pomocą wybranego dostawcy tożsamości.",
|
||||
"sso_oidc_configuration_title": "Konfiguracja OIDC",
|
||||
"sso_oidc_configuration_description": "Skonfiguruj połączenie OIDC z dostawcą tożsamości. Wymagane informacje znajdziesz u dostawcy tożsamości.",
|
||||
"sso_oidc_callback_copied": "Skopiowano adres URL odwołania",
|
||||
"sso_saml_heading": "Logowanie jednokrotne za pomocą SAML",
|
||||
"sso_saml_description": "Skonfiguruj logowanie jednokrotne SAML za pomocą wybranego dostawcy tożsamości.",
|
||||
"sso_saml_configuration_title": "Konfiguracja SAML",
|
||||
"sso_saml_configuration_description": "Skonfiguruj połączenie SAML z dostawcą tożsamości. Wymagane informacje znajdziesz u dostawcy tożsamości.",
|
||||
"sso_saml_acsurl_copied": "Skopiowano adres URL usług ACS",
|
||||
"sso_saml_entityid_copied": "Skopiowano identyfikator obiektu",
|
||||
"sso_connection_created_successfully": "Pomyślnie utworzono konfigurację {{connectionType}}",
|
||||
"sso_connection_deleted_successfully": "Pomyślnie usunięto konfigurację {{connectionType}}",
|
||||
"delete_sso_configuration": "Usuń konfigurację {{connectionType}}",
|
||||
"delete_sso_configuration_confirmation": "Tak, usuń konfigurację {{connectionType}}",
|
||||
"delete_sso_configuration_confirmation_description": "Czy na pewno chcesz usunąć konfigurację {{connectionType}}? Członkowie zespołu, którzy używają logowania {{connectionType}}, nie będą już mogli uzyskać dostępu do Cal.com.",
|
||||
"organizer_timezone": "Strefa czasowa organizatora",
|
||||
"email_no_user_cta": "Załóż swoje konto",
|
||||
"email_user_cta": "Wyświetl zaproszenie",
|
||||
"email_no_user_invite_heading": "Zaproszono Cię do dołączenia do zespołu w aplikacji {{appName}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} zaprasza Cię do dołączenia do jego zespołu w aplikacji {{appName}}. Aplikacja {{appName}} to narzędzie do planowania wydarzeń, które umożliwi Tobie i Twojemu zespołowi zaplanowanie spotkań bez czasochłonnej wymiany wiadomości.",
|
||||
"email_no_user_invite_steps_intro": "Przeprowadzimy Cię przez kilka krótkich etapów i wkrótce zaczniesz z przyjemnością korzystać z bezstresowego narzędzia do układania grafików.",
|
||||
"email_no_user_step_one": "Wybierz nazwę użytkownika",
|
||||
"email_no_user_step_two": "Połącz swoje konto z kalendarzem",
|
||||
"email_no_user_step_three": "Ustaw swoją dostępność",
|
||||
"email_no_user_step_four": "Dołącz do zespołu {{teamName}}",
|
||||
"email_no_user_signoff": "Zespół {{appName}} życzy pomyślnego układania grafików!",
|
||||
"impersonation_user_tip": "Zamierzasz podszywać się pod użytkownika, co oznacza, że możesz wprowadzać zmiany w jego imieniu. Zachowaj ostrożność.",
|
||||
"available_variables": "Dostępne zmienne",
|
||||
"scheduler": "{Scheduler}",
|
||||
"recommended_next_steps": "Zalecane następne kroki",
|
||||
"create_a_managed_event": "Utwórz typ zarządzanego wydarzenia",
|
||||
"meetings_are_better_with_the_right": "Spotkania idą lepiej, gdy uczestniczą w nich właściwi członkowie zespołu. Zaproś ich teraz.",
|
||||
"collective_or_roundrobin": "Kolektywnie czy według algorytmu karuzelowego",
|
||||
"collective_or_roundrobin": "Kolektywnie lub według algorytmu karuzelowego",
|
||||
"book_your_team_members": "Zarezerwuj wszystkich członków zespołu za pomocą wydarzeń kolektywnych lub przeglądaj dostępne osoby, aby znaleźć tę właściwą za pomocą algorytmu karuzelowego.",
|
||||
"default_app_link_title": "Ustaw domyślny link do aplikacji",
|
||||
"default_app_link_description": "Ustawienie domyślnego linku do aplikacji pozwala wszystkim nowo utworzonym rodzajom wydarzeń na używanie ustawionego linku do aplikacji.",
|
||||
"default_app_link_description": "Ustawienie domyślnego linku do aplikacji pozwala wszystkim nowo utworzonym rodzajom wydarzeń na używanie ustawionego przez Ciebie linku do aplikacji.",
|
||||
"change_default_conferencing_app": "Ustaw jako domyślne",
|
||||
"under_maintenance": "Przerwa konserwacyjna",
|
||||
"under_maintenance_description": "Zespół {{appName}} przeprowadza zaplanowane prace konserwacyjne. Jeśli masz jakieś pytania, skontaktuj się z pomocą techniczną.",
|
||||
"event_type_seats": "Liczba miejsc: {{numberOfSeats}}",
|
||||
"booking_with_payment_cancelled": "Płatność za to wydarzenie nie jest już możliwa.",
|
||||
"booking_with_payment_cancelled_already_paid": "Zwrot za tę rezerwację jest już w drodze.",
|
||||
"booking_with_payment_cancelled_refunded": "Płatność za tę rezerwację została zwrócona.",
|
||||
"booking_confirmation_failed": "Potwierdzenie rezerwacji nie powiodło się",
|
||||
"get_started_zapier_templates": "Zacznij korzystać z szablonów Zapier",
|
||||
"a_routing_form": "Formularz przekierowania",
|
||||
"form_description_placeholder": "Opis formularza",
|
||||
"keep_me_connected_with_form": "Zachowaj połączenie z formularzem",
|
||||
"fields_in_form_duplicated": "Duplikowanie zmian w przekierowaniu i polach formularza zostaną odzwierciedlone w duplikacie.",
|
||||
"fields_in_form_duplicated": "Duplikowanie zmian w przekierowaniu i polach formularza zostanie odzwierciedlone w duplikacie.",
|
||||
"form_deleted": "Formularz usunięty",
|
||||
"delete_form": "Usuń formularz",
|
||||
"delete_form_action": "Tak, usuń formularz",
|
||||
"delete_form_confirmation": "Czy na pewno chcesz usunąć ten formularz? Osoby, którym udostępniono link, nie będą już mogły użyć go do rezerwacji. Ponadto wszystkie powiązane odpowiedzi zostaną usunięte.",
|
||||
"typeform_redirect_url_copied": "Skopiowano adres URL przekierowania typu Typeform! Możesz teraz skonfigurować adres URL w formacie Typeform.",
|
||||
"typeform_redirect_url_copied": "Skopiowano adres URL przekierowania z rozwiązania Typeform! Możesz teraz skonfigurować adres URL w formularzu Typeform.",
|
||||
"modifications_in_fields_warning": "Zmiany w polach i przekierowaniach następujących formularzy zostaną odzwierciedlone w tym formularzu.",
|
||||
"connected_forms": "Połączone formularze",
|
||||
"form_modifications_warning": "Zmiany w polach i przekierowaniach tutaj będą miały wpływ na następujące formularze.",
|
||||
"responses_collection_waiting_description": "Poczekaj na zebranie odpowiedzi. Możesz też przesłać formularz osobiście.",
|
||||
"this_is_what_your_users_would_see": "To zobaczą Twoi użytkownicy.",
|
||||
"identifies_name_field": "Identyfikuje pole za pomocą tej nazwy.",
|
||||
"this_will_be_the_placeholder": "To będzie symbol zastępczy"
|
||||
"add_1_option_per_line": "Dodaj po jednej opcji w każdym wierszu",
|
||||
"select_a_router": "Wybierz przekierowanie",
|
||||
"add_a_new_route": "Dodaj nowe przekierowanie",
|
||||
"no_responses_yet": "Brak odpowiedzi",
|
||||
"this_will_be_the_placeholder": "To będzie symbol zastępczy."
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"upgrade_now": "Atualizar agora",
|
||||
"accept_invitation": "Aceitar convite",
|
||||
"calcom_explained": "{{appName}} é a alternativa de código aberto ao Calendly que coloca você no controle dos seus dados, fluxo de trabalho e aparência.",
|
||||
"calcom_explained_new_user": "Termine de configurar sua conta do {{appName}}! Falta pouco para você solucionar todos os seus problemas de agendamento.",
|
||||
"have_any_questions": "Precisa de ajuda? Estamos aqui para ajudar.",
|
||||
"reset_password_subject": "{{appName}}: Instruções para redefinir sua senha",
|
||||
"event_declined_subject": "Recusado: {{title}} em {{date}}",
|
||||
@@ -23,6 +24,8 @@
|
||||
"rejection_reason_description": "Tem certeza de que deseja recusar a reserva? Avisaremos a pessoa que tentou reservar. Se quiser, informe um motivo abaixo.",
|
||||
"rejection_confirmation": "Recusar a reserva",
|
||||
"manage_this_event": "Gerenciar este evento",
|
||||
"invite_team_member": "Convidar membro da equipe",
|
||||
"invite_team_notifcation_badge": "Inv.",
|
||||
"your_event_has_been_scheduled": "A sua reunião foi agendada",
|
||||
"your_event_has_been_scheduled_recurring": "Seu evento recorrente foi agendado",
|
||||
"accept_our_license": "Aceite a nossa licença alterando a variável no arquivo .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> para '{{agree}}'.",
|
||||
@@ -410,6 +413,8 @@
|
||||
"password_updated_successfully": "Senha atualizada com sucesso",
|
||||
"password_has_been_changed": "Sua senha foi alterada com sucesso.",
|
||||
"error_changing_password": "Erro ao alterar senha",
|
||||
"session_timeout_changed": "Sua configuração da sessão foi atualizada com sucesso.",
|
||||
"session_timeout_change_error": "Erro ao atualizar a configuração da sessão",
|
||||
"something_went_wrong": "Ocorreu um erro.",
|
||||
"something_doesnt_look_right": "Algo parece errado?",
|
||||
"please_try_again": "Por favor, tente novamente.",
|
||||
@@ -428,6 +433,7 @@
|
||||
"password_hint_num": "Contém pelo menos um número",
|
||||
"invalid_password_hint": "A senha precisa ter pelo menos sete caracteres com pelo menos um número e ser uma combinação de letras maiúsculas e minúsculas",
|
||||
"incorrect_password": "Senha incorreta.",
|
||||
"incorrect_username_password": "A senha ou o nome de usuário estão incorretos.",
|
||||
"24_h": "24h",
|
||||
"use_setting": "Usar configurações",
|
||||
"am_pm": "manhã/tarde",
|
||||
@@ -455,10 +461,11 @@
|
||||
"slots_load_fail": "Não foi possível carregar os horários disponíveis.",
|
||||
"additional_guests": "+ Participantes Adicionais",
|
||||
"your_name": "Seu nome",
|
||||
"your_full_name": "Seu nome completo",
|
||||
"email_address": "Endereço de e-mail",
|
||||
"enter_valid_email": "Insira um e-mail válido",
|
||||
"location": "Local",
|
||||
"address": "Endereço",
|
||||
"address": "Local",
|
||||
"enter_address": "Insira o endereço",
|
||||
"in_person_attendee_address": "Pessoalmente (endereço do participante)",
|
||||
"yes": "sim",
|
||||
@@ -508,6 +515,8 @@
|
||||
"admin": "Admin",
|
||||
"administrator_user": "Usuário administrador",
|
||||
"lets_create_first_administrator_user": "Vamos criar o primeiro usuário administrador.",
|
||||
"admin_user_created": "Configuração de usuário administrador",
|
||||
"admin_user_created_description": "Você já criou um usuário administrador. Já é possível fazer login na sua conta.",
|
||||
"new_member": "Novo Membro",
|
||||
"invite": "Convidar",
|
||||
"add_team_members": "Adicionar membros da equipe",
|
||||
@@ -657,6 +666,7 @@
|
||||
"edit_availability": "Editar disponibilidade",
|
||||
"configure_availability": "Configure os horários em que estiver disponível para reservas.",
|
||||
"copy_times_to": "Copiar tipos para",
|
||||
"copy_times_to_tooltip": "Copiar horários para…",
|
||||
"change_weekly_schedule": "Altere a sua agenda semanal",
|
||||
"logo": "Logotipo",
|
||||
"error": "Erro",
|
||||
@@ -668,6 +678,7 @@
|
||||
"add_attendees": "Adicionar Participantes",
|
||||
"show_advanced_settings": "Mostrar Configurações Avançadas",
|
||||
"event_name": "Nome do Evento",
|
||||
"event_name_in_calendar": "Nome do evento no calendário",
|
||||
"event_name_tooltip": "O nome que aparecerá nos calendários",
|
||||
"meeting_with_user": "Reunião com {{attendeeName}}",
|
||||
"additional_inputs": "Campos adicionais",
|
||||
@@ -786,6 +797,7 @@
|
||||
"number_apps_one": "{{count}} Aplicativo",
|
||||
"number_apps_other": "{{count}} aplicativos",
|
||||
"trending_apps": "Apps em alta",
|
||||
"most_popular": "Mais popular",
|
||||
"explore_apps": "{{category}} aplicativos",
|
||||
"installed_apps": "Apps instalados",
|
||||
"free_to_use_apps": "Grátis",
|
||||
@@ -796,12 +808,14 @@
|
||||
"no_category_apps_description_analytics": "Adicione um aplicativo de análise às suas páginas de reservas",
|
||||
"no_category_apps_description_automation": "Adicione um aplicativo de automação para usar",
|
||||
"no_category_apps_description_other": "Adicione qualquer outro tipo de aplicativo para fazer todos os tipos de coisas",
|
||||
"no_category_apps_description_web3": "Adicione um aplicativo web3 para suas páginas de reservas",
|
||||
"installed_app_calendar_description": "Defina o(s) calendário(s) para verificar se há conflitos e evitar reservas duplas.",
|
||||
"installed_app_conferencing_description": "Adicione seus aplicativos favoritos de videoconferência para suas reuniões",
|
||||
"installed_app_payment_description": "Configure quais serviços de processamento de pagamento serão usados no carregamento de seus clientes.",
|
||||
"installed_app_analytics_description": "Configure quais aplicativos de análise serão usados nas suas páginas de reservas",
|
||||
"installed_app_other_description": "Todos os seus aplicativos instalados de outras categorias.",
|
||||
"installed_app_automation_description": "Configure quais aplicativos de automação serão usados",
|
||||
"installed_app_web3_description": "Configure quais aplicativos web3 serão usados nas suas páginas de reservas",
|
||||
"analytics": "Análise",
|
||||
"empty_installed_apps_headline": "Nenhum app instalado",
|
||||
"empty_installed_apps_description": "Os aplicativos permitem que você melhore o fluxo de trabalho e melhore significativamente sua vida de agendamento.",
|
||||
@@ -929,6 +943,11 @@
|
||||
"current_location": "Local atual",
|
||||
"user_phone": "Seu número de telefone",
|
||||
"new_location": "Novo local",
|
||||
"session": "Sessão",
|
||||
"session_description": "Controle sua sessão da conta",
|
||||
"session_timeout_after": "Tempo esgotado de sessão após",
|
||||
"session_timeout": "Tempo esgotado de sessão",
|
||||
"session_timeout_description": "Invalide sua sessão após um certo tempo.",
|
||||
"no_location": "Nenhum local definido",
|
||||
"set_location": "Definir local",
|
||||
"update_location": "Atualizar local",
|
||||
@@ -1072,6 +1091,7 @@
|
||||
"broken_video_action": "Não foi possível adicionar o link da reunião de <1>{{location}}</1> ao seu evento agendado. Entre em contato com seus convidados ou atualize o evento no seu calendário para acrescentar detalhes. Você poderá <3>alterar sua localização no tipo de evento</3> ou tentar <5>remover e adicionar o app novamente.</5>",
|
||||
"broken_calendar_action": "Não foi possível atualizar seu <1>{{calendar}}</1>. <2> Confira as configurações do seu calendário ou remova e adicione seu calendário novamente </2>",
|
||||
"attendee_name": "Nome do participante",
|
||||
"scheduler_full_name": "O nome completo da pessoa que está fazendo a reserva",
|
||||
"broken_integration": "Integração com falha",
|
||||
"problem_adding_video_link": "Ocorreu um problema ao adicionar um link de vídeo",
|
||||
"problem_updating_calendar": "Ocorreu um problema ao atualizar seu calendário",
|
||||
@@ -1214,6 +1234,7 @@
|
||||
"connect_automation_apps": "Conecte aplicativos de automação",
|
||||
"connect_analytics_apps": "Conecte aplicativos de análise",
|
||||
"connect_other_apps": "Conectar outros aplicativos",
|
||||
"connect_web3_apps": "Conecte aplicativos web3",
|
||||
"current_step_of_total": "Passo {{currentStep}} de {{maxSteps}}",
|
||||
"add_variable": "Adicionar variável",
|
||||
"custom_phone_number": "Número de telefone personalizado",
|
||||
@@ -1230,6 +1251,7 @@
|
||||
"to": "Para",
|
||||
"workflow_turned_on_successfully": "Fluxo de trabalho {{workflowName}} atualizado para {{offOn}} com sucesso",
|
||||
"download_responses": "Baixar respostas",
|
||||
"download_responses_description": "Baixe todas as respostas para seu formulário em formato CSV.",
|
||||
"download": "Baixar",
|
||||
"create_your_first_form": "Criar seu primeiro formulário",
|
||||
"create_your_first_form_description": "Com os Formulários de roteamento, você pode fazer perguntas e encaminhar para a pessoa correta ou o tipo de evento.",
|
||||
@@ -1271,6 +1293,8 @@
|
||||
"routing_forms_send_email_owner": "Enviar e-mail ao proprietário",
|
||||
"routing_forms_send_email_owner_description": "Envia um e-mail ao proprietário após o envio do formulário",
|
||||
"add_new_form": "Adicionar novo formulário",
|
||||
"create_your_first_route": "Crie sua primeira rota",
|
||||
"route_to_the_right_person": "Roteie para a pessoa certa com base nas respostas do seu formulário",
|
||||
"form_description": "Crie o seu formulário para encaminhar um reservante",
|
||||
"copy_link_to_form": "Copiar link para formulário",
|
||||
"theme": "Tema",
|
||||
@@ -1301,6 +1325,7 @@
|
||||
"password_reset_leading": "Se não receber um e-mail em breve, verifique se o endereço de e-mail digitado está correto, verifique sua pasta de spam ou entre em contato com o suporte se o problema persistir.",
|
||||
"password_updated": "Senha atualizada!",
|
||||
"pending_payment": "Pagamento pendente",
|
||||
"pending_invites": "Convites pendentes",
|
||||
"confirmation_page_rainbow": "Proteja seu evento com tokens ou NFTs no Ethereum, Polygon e muito mais.",
|
||||
"not_on_cal": "Não está na {{appName}}",
|
||||
"no_calendar_installed": "Nenhum calendário instalado",
|
||||
@@ -1427,6 +1452,9 @@
|
||||
"disabled_calendar": "Se você tiver outro calendário instalado, novas reservas serão adicionadas a ele. Caso contrário, conecte um novo calendário para não perder novas reservas.",
|
||||
"enable_apps": "Ativar aplicativos",
|
||||
"enable_apps_description": "Ative aplicativos que os usuários podem integrar com o Cal.com",
|
||||
"purchase_license": "Comprar uma licença",
|
||||
"already_have_key": "Já tenho uma chave:",
|
||||
"already_have_key_suggestion": "Copie a variável de ambiente CALCOM_LICENSE_KEY existente aqui.",
|
||||
"app_is_enabled": "{{appName}} foi ativado",
|
||||
"app_is_disabled": "{{appName}} foi desativado",
|
||||
"keys_have_been_saved": "As chaves foram salvas",
|
||||
@@ -1447,6 +1475,7 @@
|
||||
"individual": "Individual",
|
||||
"all_bookings_filter_label": "Todas as reservas",
|
||||
"all_users_filter_label": "Todos os usuários",
|
||||
"your_bookings_filter_label": "Suas reservas",
|
||||
"meeting_url_variable": "URL da reunião",
|
||||
"meeting_url_info": "O URL da conferência de reunião do evento",
|
||||
"date_overrides": "Substituições de data",
|
||||
@@ -1462,7 +1491,7 @@
|
||||
"date_overrides_update_btn": "Atualizar substituição",
|
||||
"event_type_duplicate_copy_text": "Cópia de {{slug}}",
|
||||
"set_as_default": "Definir como padrão",
|
||||
"hide_eventtype_details": "Ocultar detalhes do tipo de evento",
|
||||
"hide_eventtype_details": "Ocultar detalhes do EventType",
|
||||
"show_navigation": "Mostrar navegação",
|
||||
"hide_navigation": "Ocultar navegação",
|
||||
"verification_code_sent": "Código de verificação enviado",
|
||||
@@ -1485,6 +1514,7 @@
|
||||
"round_robin_hosts": "Hosts de Round-Robin",
|
||||
"minimum_round_robin_hosts_count": "Número de hosts necessários para participar",
|
||||
"hosts": "Hosts",
|
||||
"upgrade_to_enable_feature": "É preciso criar uma equipe para ativar este recurso. Clique para criar uma.",
|
||||
"new_attendee": "Novo participante",
|
||||
"awaiting_approval": "Aguardando aprovação",
|
||||
"requires_google_calendar": "Este aplicativo requer uma conexão com o Google Calendar",
|
||||
@@ -1493,9 +1523,97 @@
|
||||
"continue_to_install_google_calendar": "Continuar para instalar o Google Calendar",
|
||||
"install_google_meet": "Instalar o Google Meet",
|
||||
"install_google_calendar": "Instalar o Google Calendar",
|
||||
"sender_name": "Nome do remetente",
|
||||
"already_invited": "Participante já convidado",
|
||||
"no_recordings_found": "Nenhum registro encontrado",
|
||||
"reporting": "Relatando",
|
||||
"reporting_feature": "Veja todos os dados recebidos e baixe como CSV",
|
||||
"teams_plan_required": "Plano da equipe obrigatório",
|
||||
"routing_forms_are_a_great_way": "Formulários de roteamento são uma ótima maneira de direcionar os leads recebidos para a pessoa certa. Atualize para o plano Teams e acesse esse recurso.",
|
||||
"choose_a_license": "Escolha uma licença",
|
||||
"choose_license_description": "O Cal.com vem com uma licença AGPLv3 acessível e gratuita, mas tem algumas limitações, que podem ser resolvidas atualizando para a licença Enterprise a qualquer momento. Você poderá fazer isso mais tarde, quando quiser.",
|
||||
"license": "Licença",
|
||||
"agplv3_license": "Licença AGPLv3",
|
||||
"ee_enterprise_license": "“/ee” Licença Enterprise",
|
||||
"enterprise_booking_fee": "A partir de {{enterprise_booking_fee}}/mês",
|
||||
"enterprise_license_includes": "Tudo para um caso de uso comercial",
|
||||
"no_need_to_keep_your_code_open_source": "Não precisa manter seu código em código aberto",
|
||||
"repackage_rebrand_resell": "Reempacote, troque de marca e revenda com facilidade",
|
||||
"a_vast_suite_of_enterprise_features": "Uma ampla variedade de recursos empresariais",
|
||||
"free_license_fee": "$0.00/mês",
|
||||
"forever_open_and_free": "Aberto e gratuito permanentemente",
|
||||
"required_to_keep_your_code_open_source": "Obrigatório para manter seu código em código aberto",
|
||||
"cannot_repackage_and_resell": "Não é possível reempacotar, trocar de marca e revender facilmente",
|
||||
"no_enterprise_features": "Sem recursos corporativos",
|
||||
"step_enterprise_license": "Licença Enterprise",
|
||||
"step_enterprise_license_description": "Tudo para um caso de uso comercial com hospedagem privada, reempacotamento, troca de marca e revenda, além de acesso a componentes corporativos exclusivos.",
|
||||
"setup": "Configuração",
|
||||
"setup_description": "Instância de configuração do Cal.com",
|
||||
"configure": "Configurar",
|
||||
"sso_configuration": "Logon único",
|
||||
"sso_configuration_description": "Configure o SAML/OIDC SSO e permita que membros da equipe façam login com um provedor de identidade",
|
||||
"sso_oidc_heading": "SSO com OIDC",
|
||||
"sso_oidc_description": "Configure o OIDC SSO com o provedor de identidade de sua escolha.",
|
||||
"sso_oidc_configuration_title": "Configuração do OIDC",
|
||||
"sso_oidc_configuration_description": "Configure a conexão do OIDC com seu provedor de identidade. Confira as informações necessárias nele.",
|
||||
"sso_oidc_callback_copied": "URL de chamada de retorno copiada",
|
||||
"sso_saml_heading": "SSO com SAML",
|
||||
"sso_saml_description": "Configure o SAML SSO com o provedor de identidade de sua escolha.",
|
||||
"sso_saml_configuration_title": "Configuração do SAML",
|
||||
"sso_saml_configuration_description": "Configure a conexão do SAML com seu provedor de identidade. Confira as informações necessárias nele.",
|
||||
"sso_saml_acsurl_copied": "URL do ACS copiada",
|
||||
"sso_saml_entityid_copied": "ID da Entidade copiada",
|
||||
"sso_connection_created_successfully": "Configuração de {{connectionType}} criada com sucesso",
|
||||
"sso_connection_deleted_successfully": "Configuração de {{connectionType}} excluída com sucesso",
|
||||
"delete_sso_configuration": "Excluir a configuração de {{connectionType}}",
|
||||
"delete_sso_configuration_confirmation": "Sim, excluir a configuração de {{connectionType}}",
|
||||
"delete_sso_configuration_confirmation_description": "Você tem certeza que deseja remover a configuração de {{connectionType}}? Os membros do seu time que utilizam {{connectionType}} para fazer login não conseguirão acessar o Cal.com.",
|
||||
"organizer_timezone": "Fuso horário do organizador",
|
||||
"email_no_user_cta": "Criar sua conta",
|
||||
"email_user_cta": "Ver convite",
|
||||
"email_no_user_invite_heading": "Você precisa ter recebido um convite para ingressar em uma equipe de {{appName}}",
|
||||
"email_no_user_invite_subheading": "Você recebeu um convite de {{invitedBy}} para ingressar em sua equipe em {{appName}}. {{appName}} é um agendador que concilia eventos e permite que sua equipe agende reuniões sem precisar trocar e-mails.",
|
||||
"email_no_user_invite_steps_intro": "Orientaremos ao longo de alguns passos rápidos para que você comece logo a agendar eventos com sua equipe sem preocupação.",
|
||||
"email_no_user_step_one": "Escolha seu nome de usuário",
|
||||
"email_no_user_step_two": "Conecte com sua conta de calendário",
|
||||
"email_no_user_step_three": "Defina sua disponibilidade",
|
||||
"email_no_user_step_four": "Ingresse em {{teamName}}",
|
||||
"email_no_user_signoff": "Divirta-se com os agendamentos, equipe do {{appName}}",
|
||||
"impersonation_user_tip": "Você está preses a representar um usuário, ou seja, tem possibilidade de realizar mudanças em nome dele. Tenha cuidado.",
|
||||
"available_variables": "Variáveis disponíveis",
|
||||
"scheduler": "{Scheduler}",
|
||||
"recommended_next_steps": "Próximos passos recomendados",
|
||||
"create_a_managed_event": "Crie um tipo de evento gerenciado",
|
||||
"meetings_are_better_with_the_right": "Reuniões são melhores com a presença dos membros certos da equipe. Convide-os agora.",
|
||||
"collective_or_roundrobin": "Coletivo ou round-robin",
|
||||
"book_your_team_members": "Reúna os membros da sua equipe com eventos coletivos, ou selecione a pessoa certa com o round-robin.",
|
||||
"default_app_link_title": "Defina um link padrão para o app",
|
||||
"default_app_link_description": "Definir um link padrão para o app permite que todos os tipos de evento recém-criados usem o link definido para o app.",
|
||||
"change_default_conferencing_app": "Definir como padrão",
|
||||
"booking_confirmation_failed": "Falhar ao confirmar reserva"
|
||||
"under_maintenance": "Serviço interrompido para manutenção",
|
||||
"under_maintenance_description": "A equipe do {{appName}} está realizando uma manutenção programada. Se tiver alguma dúvida, fale com o suporte.",
|
||||
"event_type_seats": "{{numberOfSeats}} assentos",
|
||||
"booking_with_payment_cancelled": "Não é mais possível pagar por este evento",
|
||||
"booking_with_payment_cancelled_already_paid": "Um reembolso pelo pagamento desta reserva está a caminho.",
|
||||
"booking_with_payment_cancelled_refunded": "O pagamento desta reserva foi reembolsado.",
|
||||
"booking_confirmation_failed": "Falhar ao confirmar reserva",
|
||||
"get_started_zapier_templates": "Comece agora com modelos do Zapier",
|
||||
"a_routing_form": "Formulário de roteamento",
|
||||
"form_description_placeholder": "Descrição do formulário",
|
||||
"keep_me_connected_with_form": "Mantenha minha conexão com o formulário",
|
||||
"fields_in_form_duplicated": "Qualquer alteração no Roteador e nos campos do formulário a ser duplicado afetará a cópia duplicada.",
|
||||
"form_deleted": "Formulário excluído",
|
||||
"delete_form_action": "Sim, excluir formulário",
|
||||
"typeform_redirect_url_copied": "URL de redirecionamento do Typeform copiada! Você pode acessar e definir a URL no formulário do Typeform.",
|
||||
"modifications_in_fields_warning": "Modificações nos campos e rotas dos formulários a seguir afetarão este formulário.",
|
||||
"connected_forms": "Formulários conectados",
|
||||
"form_modifications_warning": "Os formulários a seguir serão afetados ao modificar os campos e rotas aqui.",
|
||||
"responses_collection_waiting_description": "Aguarde um pouco até as respostas serem coletadas. Você também pode acessar e enviar o formulário por conta própria.",
|
||||
"this_is_what_your_users_would_see": "Isto é o que seus usuários verão",
|
||||
"identifies_name_field": "Identifica campo por este nome.",
|
||||
"add_1_option_per_line": "Adiciona uma opção por linha",
|
||||
"select_a_router": "Selecione um roteador",
|
||||
"add_a_new_route": "Adicione uma nova Rota",
|
||||
"no_responses_yet": "Sem respostas por enquanto",
|
||||
"this_will_be_the_placeholder": "Este será o espaço reservado"
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@
|
||||
"email_address": "Endereço de email",
|
||||
"enter_valid_email": "Insira um e-mail válido",
|
||||
"location": "Localização",
|
||||
"address": "Morada",
|
||||
"address": "Local",
|
||||
"enter_address": "Especifique o endereço",
|
||||
"in_person_attendee_address": "Pessoalmente (endereço do participante)",
|
||||
"yes": "Sim",
|
||||
@@ -1113,7 +1113,7 @@
|
||||
"navigate": "Navegar",
|
||||
"open": "Abrir",
|
||||
"close": "Fechar",
|
||||
"upgrade": "Atualizar",
|
||||
"upgrade": "Upgrade",
|
||||
"upgrade_to_access_recordings_title": "Atualizar para aceder a gravações",
|
||||
"upgrade_to_access_recordings_description": "As gravações só estão disponíveis como parte do nosso plano Equipas. Atualize para começar a gravar as suas chamadas",
|
||||
"recordings_are_part_of_the_teams_plan": "As gravações fazem parte do plano Equipas",
|
||||
|
||||
@@ -1479,7 +1479,7 @@
|
||||
"meeting_url_variable": "URL ședință",
|
||||
"meeting_url_info": "Adresa URL a evenimentului ședință tip conferință",
|
||||
"date_overrides": "Suprascrieri dată",
|
||||
"date_overrides_subtitle": "Adaugă date atunci când disponibilitatea ta se schimbă în programul zilnic.",
|
||||
"date_overrides_subtitle": "Adaugă date la care disponibilitatea ta se schimbă în programul zilnic.",
|
||||
"date_overrides_info": "Datele suprascrise sunt arhivate automat după ce au trecut",
|
||||
"date_overrides_dialog_which_hours": "Între ce ore ești disponibil/ă?",
|
||||
"date_overrides_dialog_which_hours_unavailable": "Între ce ore ești ocupat/ă?",
|
||||
@@ -1534,7 +1534,7 @@
|
||||
"choose_license_description": "Cal.com include o licență AGPLv3 accesibilă și gratuită, cu unele limitări, însă care poate fi actualizată la o licență Enterprise în orice moment. Puteți face upgrade oricând mai târziu.",
|
||||
"license": "Licență",
|
||||
"agplv3_license": "Licență AGPLv3",
|
||||
"ee_enterprise_license": "Licență Enterprise „/ee”",
|
||||
"ee_enterprise_license": "Licență „/ee” pentru întreprinderi",
|
||||
"enterprise_booking_fee": "Începând de la {{enterprise_booking_fee}}/lună",
|
||||
"enterprise_license_includes": "Totul pentru uz comercial",
|
||||
"no_need_to_keep_your_code_open_source": "Nu este nevoie să vă păstrați codul open-source",
|
||||
@@ -1572,8 +1572,8 @@
|
||||
"email_no_user_cta": "Creează-ți contul",
|
||||
"email_user_cta": "Vizualizare invitație",
|
||||
"email_no_user_invite_heading": "Ați fost invitat să faceți parte dintr-o echipă de pe {{appName}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} v-a invitat să faceți parte din echipa sa de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor care vă permite dvs. și echipei dvs. să programați ședințe fără a face ping-pong prin e-mail.",
|
||||
"email_no_user_invite_steps_intro": "Vom parcurge împreună câțiva pași simpli și vă veți bucura împreună cu echipa dvs. de planificări fără probleme, în cel mai scurt timp.",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} v-a invitat să faceți parte din echipa sa de pe {{appName}}. {{appName}} este un instrument de planificare a evenimentelor, care vă permite dvs. și echipei dvs. să programați ședințe fără a face ping-pong prin e-mail.",
|
||||
"email_no_user_invite_steps_intro": "Vom parcurge împreună câțiva pași simpli și vă veți bucura alături de echipa dvs. de programări fără probleme, în cel mai scurt timp.",
|
||||
"email_no_user_step_one": "Alegeți-vă numele de utilizator",
|
||||
"email_no_user_step_two": "Conectați-vă contul de calendar",
|
||||
"email_no_user_step_three": "Setați-vă disponibilitatea",
|
||||
@@ -1597,14 +1597,14 @@
|
||||
"booking_with_payment_cancelled_already_paid": "Există o rambursare în curs pentru plata acestei rezervări.",
|
||||
"booking_with_payment_cancelled_refunded": "Plata pentru această rezervare a fost rambursată.",
|
||||
"booking_confirmation_failed": "Confirmare rezervare nereușită",
|
||||
"get_started_zapier_templates": "Fă primii pași cu șabloanele Zapier",
|
||||
"get_started_zapier_templates": "Faceți primii pași cu șabloanele Zapier",
|
||||
"a_routing_form": "Un formular de redirecționare",
|
||||
"form_description_placeholder": "Descriere formular",
|
||||
"keep_me_connected_with_form": "Vreau să primesc actualizări privind formularul",
|
||||
"fields_in_form_duplicated": "Orice modificări aduse parcursului și câmpurilor din formularul duplicat se vor reflecta în duplicat.",
|
||||
"form_deleted": "Formular șters",
|
||||
"delete_form": "Ștergere formular",
|
||||
"delete_form_action": "Da, ștergeți formularul",
|
||||
"delete_form_action": "Da, șterg formularul",
|
||||
"delete_form_confirmation": "Sigur doriți să ștergeți acest formular? Nicio persoană căreia i-ați transmis linkul nu va mai putea efectua rezervări pe baza lui. În plus, toate răspunsurile asociate vor fi șterse.",
|
||||
"typeform_redirect_url_copied": "URL-ul de redirecționare Typeform a fost copiat! Puteți accesa și seta URL-ul în formularul Typeform.",
|
||||
"modifications_in_fields_warning": "Modificările aduse câmpurilor și parcursurilor pentru următoarele formulare vor fi reproduse în acest formular.",
|
||||
|
||||
@@ -1471,7 +1471,7 @@
|
||||
"enter_option": "Ange alternativ {{index}}",
|
||||
"add_an_option": "Lägg till ett alternativ",
|
||||
"radio": "Menyknapp",
|
||||
"google_meet_warning": "För att använda Google Meet måste du ställa in din målkalender till en Google-kalender",
|
||||
"google_meet_warning": "För att använda Google Meet måste du ställa in din målkalender till en Google Kalender",
|
||||
"individual": "Individ",
|
||||
"all_bookings_filter_label": "Alla bokningar",
|
||||
"all_users_filter_label": "Alla användare",
|
||||
@@ -1491,14 +1491,14 @@
|
||||
"date_overrides_update_btn": "Uppdatera åsidosättning",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-kopia",
|
||||
"set_as_default": "Ange som standard",
|
||||
"hide_eventtype_details": "Dölj information om händelsetyp",
|
||||
"hide_eventtype_details": "Dölj EventType-information",
|
||||
"show_navigation": "Visa navigering",
|
||||
"hide_navigation": "Dölj navigering",
|
||||
"verification_code_sent": "Verifieringskod har skickats",
|
||||
"verified_successfully": "Har verifierats",
|
||||
"wrong_code": "Fel verifieringskod",
|
||||
"not_verified": "Har inte verifierats än",
|
||||
"no_availability_in_month": "Inte tillgänglig i {{month}}",
|
||||
"no_availability_in_month": "Ingen tillgänglighet i {{month}}",
|
||||
"view_next_month": "Visa nästa månad",
|
||||
"send_code": "Skicka kod",
|
||||
"number_verified": "Numret har verifierats",
|
||||
|
||||
@@ -388,7 +388,7 @@
|
||||
"go_to_billing_portal": "Fatura portalına git",
|
||||
"need_anything_else": "Başka bir şeye ihtiyacınız var mı?",
|
||||
"further_billing_help": "Faturalandırma konusunda daha fazla yardıma ihtiyacınız olursa destek ekibimiz size yardımcı olmaya hazır.",
|
||||
"contact": "İletişim",
|
||||
"contact": "İletişime geçin",
|
||||
"our_support_team": "destek ekibimiz",
|
||||
"contact_our_support_team": "Destek ekibimizle iletişime geçin",
|
||||
"uh_oh": "Hay aksi!",
|
||||
@@ -1472,7 +1472,7 @@
|
||||
"add_an_option": "Bir seçenek ekle",
|
||||
"radio": "Radio",
|
||||
"google_meet_warning": "Google Meet'i kullanmak için hedef takviminizi Google Takvimi olarak ayarlamanız gerekir",
|
||||
"individual": "Bireysel",
|
||||
"individual": "Birey",
|
||||
"all_bookings_filter_label": "Tüm Rezervasyonlar",
|
||||
"all_users_filter_label": "Tüm Kullanıcılar",
|
||||
"your_bookings_filter_label": "Rezervasyonlarınız",
|
||||
|
||||
@@ -1491,7 +1491,7 @@
|
||||
"date_overrides_update_btn": "更新替代",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-复制",
|
||||
"set_as_default": "设置为默认",
|
||||
"hide_eventtype_details": "隐藏活动类型详细信息",
|
||||
"hide_eventtype_details": "隐藏 EventType 详细信息",
|
||||
"show_navigation": "显示导航",
|
||||
"hide_navigation": "隐藏导航",
|
||||
"verification_code_sent": "验证码已发送",
|
||||
@@ -1504,7 +1504,7 @@
|
||||
"number_verified": "号码已验证",
|
||||
"create_your_first_team_webhook_description": "为此团队活动类型创建第一个 Webhook",
|
||||
"create_webhook_team_event_type": "为此团队活动类型创建 Webhook",
|
||||
"disable_success_page": "禁用成功页面 (仅当有重定向链接时才有效)",
|
||||
"disable_success_page": "禁用“成功”页面 (仅当有重定向链接时才有效)",
|
||||
"invalid_admin_password": "您被设置为管理员,但您的密码长度不足 15 个字符",
|
||||
"change_password_admin": "更改密码以获取管理员访问权限",
|
||||
"username_already_taken": "用户名已被占用",
|
||||
@@ -1617,5 +1617,6 @@
|
||||
"select_a_router": "选择导向器",
|
||||
"add_a_new_route": "添加新途径",
|
||||
"no_responses_yet": "尚无回复",
|
||||
"this_will_be_the_placeholder": "这将是占位符"
|
||||
"this_will_be_the_placeholder": "这将是占位符",
|
||||
"can_you_try_again": "可否用不同的时间再次尝试?"
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@
|
||||
"email_address": "電子郵件位址",
|
||||
"enter_valid_email": "請輸入有效的電子郵件",
|
||||
"location": "地點",
|
||||
"address": "地點",
|
||||
"address": "地址",
|
||||
"enter_address": "輸入地址",
|
||||
"in_person_attendee_address": "親自赴會 (與會者地址)",
|
||||
"yes": "是",
|
||||
@@ -1504,7 +1504,7 @@
|
||||
"number_verified": "號碼已驗證",
|
||||
"create_your_first_team_webhook_description": "為此團隊活動類型建立第一個 Webhook",
|
||||
"create_webhook_team_event_type": "為此團隊活動類型建立 Webhook",
|
||||
"disable_success_page": "停用成功頁面 (只適用於您有重新導向網址時)",
|
||||
"disable_success_page": "停用「成功」頁面 (只適用於您有重新導向網址時)",
|
||||
"invalid_admin_password": "您被設為管理者,但您的密碼長度至少須達 15 個字元",
|
||||
"change_password_admin": "變更密碼以獲得管理員存取權",
|
||||
"username_already_taken": "已有人使用此使用者名稱",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { createProxySSGHelpers } from "@calcom/trpc/react/ssg";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
@@ -12,7 +13,11 @@ import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
* Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end.
|
||||
*/
|
||||
export async function ssrInit(context: GetServerSidePropsContext) {
|
||||
const ctx = await createContext(context);
|
||||
const { req, res } = context;
|
||||
|
||||
const sessionGetter = () => getServerSession({ req, res });
|
||||
|
||||
const ctx = await createContext(context, sessionGetter);
|
||||
|
||||
const ssr = createProxySSGHelpers({
|
||||
router: appRouter,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { validateBookingLimitOrder } from "@calcom/lib/isBookingLimits";
|
||||
import { checkBookingLimits, checkLimit } from "@calcom/lib/server";
|
||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { checkBookingLimits, checkBookingLimit } from "@calcom/lib/server";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
|
||||
import { prismaMock } from "../../../../tests/config/singleton";
|
||||
|
||||
type Mockdata = {
|
||||
id: number;
|
||||
startDate: Date;
|
||||
bookingLimits: BookingLimit;
|
||||
bookingLimits: IntervalLimit;
|
||||
};
|
||||
|
||||
const MOCK_DATA: Mockdata = {
|
||||
@@ -63,7 +63,7 @@ describe("Check Booking Limits Tests", () => {
|
||||
it("Should handle mutiple limits correctly", async () => {
|
||||
prismaMock.booking.count.mockResolvedValue(1);
|
||||
expect(
|
||||
checkLimit({
|
||||
checkBookingLimit({
|
||||
key: "PER_DAY",
|
||||
limitingNumber: 2,
|
||||
eventStartDate: MOCK_DATA.startDate,
|
||||
@@ -72,7 +72,7 @@ describe("Check Booking Limits Tests", () => {
|
||||
).resolves.not.toThrow();
|
||||
prismaMock.booking.count.mockResolvedValue(3);
|
||||
expect(
|
||||
checkLimit({
|
||||
checkBookingLimit({
|
||||
key: "PER_WEEK",
|
||||
limitingNumber: 2,
|
||||
eventStartDate: MOCK_DATA.startDate,
|
||||
@@ -83,7 +83,7 @@ describe("Check Booking Limits Tests", () => {
|
||||
it("Should return busyTimes when set", async () => {
|
||||
prismaMock.booking.count.mockResolvedValue(2);
|
||||
expect(
|
||||
checkLimit({
|
||||
checkBookingLimit({
|
||||
key: "PER_DAY",
|
||||
limitingNumber: 2,
|
||||
eventStartDate: MOCK_DATA.startDate,
|
||||
@@ -99,21 +99,21 @@ describe("Check Booking Limits Tests", () => {
|
||||
|
||||
describe("Booking limit validation", () => {
|
||||
it("Should validate a correct limit", () => {
|
||||
expect(validateBookingLimitOrder({ PER_DAY: 3, PER_MONTH: 5 })).toBe(true);
|
||||
expect(validateIntervalLimitOrder({ PER_DAY: 3, PER_MONTH: 5 })).toBe(true);
|
||||
});
|
||||
|
||||
it("Should invalidate an incorrect limit", () => {
|
||||
expect(validateBookingLimitOrder({ PER_DAY: 9, PER_MONTH: 5 })).toBe(false);
|
||||
expect(validateIntervalLimitOrder({ PER_DAY: 9, PER_MONTH: 5 })).toBe(false);
|
||||
});
|
||||
|
||||
it("Should validate a correct limit with 'gaps' ", () => {
|
||||
expect(validateBookingLimitOrder({ PER_DAY: 9, PER_YEAR: 25 })).toBe(true);
|
||||
expect(validateIntervalLimitOrder({ PER_DAY: 9, PER_YEAR: 25 })).toBe(true);
|
||||
});
|
||||
|
||||
it("Should validate a correct limit with equal values ", () => {
|
||||
expect(validateBookingLimitOrder({ PER_DAY: 1, PER_YEAR: 1 })).toBe(true);
|
||||
expect(validateIntervalLimitOrder({ PER_DAY: 1, PER_YEAR: 1 })).toBe(true);
|
||||
});
|
||||
it("Should validate a correct with empty", () => {
|
||||
expect(validateBookingLimitOrder({})).toBe(true);
|
||||
expect(validateIntervalLimitOrder({})).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { checkDurationLimit, checkDurationLimits } from "@calcom/lib/server";
|
||||
|
||||
import { prismaMock } from "../../../../tests/config/singleton";
|
||||
|
||||
type MockData = {
|
||||
id: number;
|
||||
startDate: Date;
|
||||
};
|
||||
|
||||
const MOCK_DATA: MockData = {
|
||||
id: 1,
|
||||
startDate: dayjs("2022-09-30T09:00:00+01:00").toDate(),
|
||||
};
|
||||
|
||||
// Path: apps/web/test/lib/checkDurationLimits.ts
|
||||
describe("Check Duration Limits Tests", () => {
|
||||
it("Should return no errors if limit is not reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 0 }]);
|
||||
await expect(
|
||||
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should throw an error if limit is reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
await expect(
|
||||
checkDurationLimits({ PER_DAY: 60 }, MOCK_DATA.startDate, MOCK_DATA.id)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
it("Should pass with multiple duration limits", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
PER_DAY: 60,
|
||||
PER_WEEK: 120,
|
||||
},
|
||||
MOCK_DATA.startDate,
|
||||
MOCK_DATA.id
|
||||
)
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should pass with multiple duration limits with one undefined", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 30 }]);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
PER_DAY: 60,
|
||||
PER_WEEK: undefined,
|
||||
},
|
||||
MOCK_DATA.startDate,
|
||||
MOCK_DATA.id
|
||||
)
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should return no errors if limit is not reached with multiple bookings", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
PER_DAY: 90,
|
||||
PER_WEEK: 120,
|
||||
},
|
||||
MOCK_DATA.startDate,
|
||||
MOCK_DATA.id
|
||||
)
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
it("Should throw an error if one of the limit is reached with multiple bookings", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 90 }]);
|
||||
await expect(
|
||||
checkDurationLimits(
|
||||
{
|
||||
PER_DAY: 60,
|
||||
PER_WEEK: 120,
|
||||
},
|
||||
MOCK_DATA.startDate,
|
||||
MOCK_DATA.id
|
||||
)
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
// Path: apps/web/test/lib/checkDurationLimits.ts
|
||||
describe("Check Duration Limit Tests", () => {
|
||||
it("Should return no busyTimes and no error if limit is not reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
await expect(
|
||||
checkDurationLimit({
|
||||
key: "PER_DAY",
|
||||
limitingNumber: 90,
|
||||
eventStartDate: MOCK_DATA.startDate,
|
||||
eventId: MOCK_DATA.id,
|
||||
})
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
it("Should return busyTimes when set and limit is reached", async () => {
|
||||
prismaMock.$queryRaw.mockResolvedValue([{ totalMinutes: 60 }]);
|
||||
await expect(
|
||||
checkDurationLimit({
|
||||
key: "PER_DAY",
|
||||
limitingNumber: 60,
|
||||
eventStartDate: MOCK_DATA.startDate,
|
||||
eventId: MOCK_DATA.id,
|
||||
returnBusyTimes: true,
|
||||
})
|
||||
).resolves.toEqual({
|
||||
start: dayjs(MOCK_DATA.startDate).startOf("day").toDate(),
|
||||
end: dayjs(MOCK_DATA.startDate).endOf("day").toDate(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Duration limit validation", () => {
|
||||
it("Should validate limit where ranges have ascending values", () => {
|
||||
expect(validateIntervalLimitOrder({ PER_DAY: 30, PER_MONTH: 60 })).toBe(true);
|
||||
});
|
||||
it("Should invalidate limit where ranges does not have a strict ascending values", () => {
|
||||
expect(validateIntervalLimitOrder({ PER_DAY: 60, PER_WEEK: 30 })).toBe(false);
|
||||
});
|
||||
it("Should validate a correct limit with 'gaps'", () => {
|
||||
expect(validateIntervalLimitOrder({ PER_DAY: 60, PER_YEAR: 120 })).toBe(true);
|
||||
});
|
||||
it("Should validate empty limit", () => {
|
||||
expect(validateIntervalLimitOrder({})).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* !: Stops the `jose` dependency from bundling the browser version and breaking tests
|
||||
* @jest-environment node
|
||||
*/
|
||||
import type {
|
||||
EventType as PrismaEventType,
|
||||
User as PrismaUser,
|
||||
@@ -504,6 +508,7 @@ describe("getSchedule", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// FIXME: Fix minimumBookingNotice is respected test
|
||||
test.skip("minimumBookingNotice is respected", async () => {
|
||||
jest.useFakeTimers().setSystemTime(
|
||||
(() => {
|
||||
|
||||
+7
-1
@@ -35,9 +35,15 @@ const config: Config = {
|
||||
displayName: "@calcom/lib",
|
||||
roots: ["<rootDir>/packages/lib"],
|
||||
testEnvironment: "node",
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
},
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig: "<rootDir>/packages/lib/tsconfig.test.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: "@calcom/closecom",
|
||||
@@ -52,7 +58,7 @@ const config: Config = {
|
||||
},
|
||||
{
|
||||
displayName: "@calcom/routing-forms",
|
||||
roots: ["<rootDir>/packages/app-store/ee/routing-forms"],
|
||||
roots: ["<rootDir>/packages/app-store/routing-forms"],
|
||||
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)"],
|
||||
transform: {
|
||||
"^.+\\.ts?$": "ts-jest",
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
"docs-start": "turbo run start --scope=\"@calcom/docs\"",
|
||||
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
|
||||
"dx": "turbo run dx",
|
||||
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",
|
||||
"i-dx": "infisical run -- turbo run dx",
|
||||
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
|
||||
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
|
||||
"embed-tests-quick": "turbo run embed-tests-quick",
|
||||
"embed-tests": "turbo run embed-tests",
|
||||
"env-check:app-store": "dotenv-checker --schema .env.appStore.example --env .env.appStore",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import prisma, { safeAppSelect, safeCredentialSelect } from "@calcom/prisma";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { AppFrontendPayload as App } from "@calcom/types/App";
|
||||
@@ -82,6 +83,21 @@ export async function getAppRegistryWithCredentials(userId: number) {
|
||||
// Skip if app isn't installed
|
||||
/* This is now handled from the DB */
|
||||
// if (!app.installed) return apps;
|
||||
let dependencyData: {
|
||||
name?: string;
|
||||
installed?: boolean;
|
||||
}[] = [];
|
||||
if (app.dependencies) {
|
||||
dependencyData = app.dependencies.map((dependency) => {
|
||||
const dependencyInstalled = dbApps.some(
|
||||
(dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency
|
||||
);
|
||||
// If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app.
|
||||
const dependencyName = getAppFromSlug(dependency)?.name;
|
||||
return { name: dependencyName, installed: dependencyInstalled };
|
||||
});
|
||||
}
|
||||
|
||||
const { rating, reviews, trending, verified, ...remainingAppProps } = app;
|
||||
apps.push({
|
||||
rating: rating || 0,
|
||||
@@ -93,7 +109,9 @@ export async function getAppRegistryWithCredentials(userId: number) {
|
||||
credentials: dbapp.credentials,
|
||||
installed: true,
|
||||
isDefault: usersDefaultApp === dbapp.slug,
|
||||
...(app.dependencies && { dependencyData }),
|
||||
});
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"email": "support@cal.com",
|
||||
"description": "The joyful productivity app\r\r",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"]
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ export const InstallAppButtonMap = {
|
||||
exchange2016calendar: dynamic(() => import("./exchange2016calendar/components/InstallAppButton")),
|
||||
exchangecalendar: dynamic(() => import("./exchangecalendar/components/InstallAppButton")),
|
||||
googlecalendar: dynamic(() => import("./googlecalendar/components/InstallAppButton")),
|
||||
googlevideo: dynamic(() => import("./googlevideo/components/InstallAppButton")),
|
||||
hubspot: dynamic(() => import("./hubspot/components/InstallAppButton")),
|
||||
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
||||
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
Don't modify this file manually.
|
||||
**/
|
||||
import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
|
||||
import { appKeysSchema as routing_forms_zod_ts } from "./ee/routing-forms/zod";
|
||||
import { appKeysSchema as fathom_zod_ts } from "./fathom/zod";
|
||||
import { appKeysSchema as ga4_zod_ts } from "./ga4/zod";
|
||||
import { appKeysSchema as giphy_zod_ts } from "./giphy/zod";
|
||||
@@ -15,6 +14,7 @@ import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
import { appKeysSchema as plausible_zod_ts } from "./plausible/zod";
|
||||
import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod";
|
||||
import { appKeysSchema as rainbow_zod_ts } from "./rainbow/zod";
|
||||
import { appKeysSchema as routing_forms_zod_ts } from "./routing-forms/zod";
|
||||
import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod";
|
||||
import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod";
|
||||
import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
|
||||
@@ -27,7 +27,6 @@ import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
|
||||
|
||||
export const appKeysSchemas = {
|
||||
dailyvideo: dailyvideo_zod_ts,
|
||||
"routing-forms": routing_forms_zod_ts,
|
||||
fathom: fathom_zod_ts,
|
||||
ga4: ga4_zod_ts,
|
||||
giphy: giphy_zod_ts,
|
||||
@@ -39,6 +38,7 @@ export const appKeysSchemas = {
|
||||
plausible: plausible_zod_ts,
|
||||
qr_code: qr_code_zod_ts,
|
||||
rainbow: rainbow_zod_ts,
|
||||
"routing-forms": routing_forms_zod_ts,
|
||||
salesforce: salesforce_zod_ts,
|
||||
stripe: stripepayment_zod_ts,
|
||||
tandemvideo: tandemvideo_zod_ts,
|
||||
|
||||
@@ -8,8 +8,8 @@ import around_config_json from "./around/config.json";
|
||||
import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata";
|
||||
import campfire_config_json from "./campfire/config.json";
|
||||
import closecom_config_json from "./closecom/config.json";
|
||||
import cron_config_json from "./cron/config.json";
|
||||
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
|
||||
import routing_forms_config_json from "./ee/routing-forms/config.json";
|
||||
import { metadata as exchange2013calendar__metadata_ts } from "./exchange2013calendar/_metadata";
|
||||
import { metadata as exchange2016calendar__metadata_ts } from "./exchange2016calendar/_metadata";
|
||||
import exchangecalendar_config_json from "./exchangecalendar/config.json";
|
||||
@@ -33,6 +33,7 @@ import qr_code_config_json from "./qr_code/config.json";
|
||||
import rainbow_config_json from "./rainbow/config.json";
|
||||
import raycast_config_json from "./raycast/config.json";
|
||||
import riverside_config_json from "./riverside/config.json";
|
||||
import routing_forms_config_json from "./routing-forms/config.json";
|
||||
import salesforce_config_json from "./salesforce/config.json";
|
||||
import sendgrid_config_json from "./sendgrid/config.json";
|
||||
import signal_config_json from "./signal/config.json";
|
||||
@@ -64,8 +65,8 @@ export const appStoreMetadata = {
|
||||
caldavcalendar: caldavcalendar__metadata_ts,
|
||||
campfire: campfire_config_json,
|
||||
closecom: closecom_config_json,
|
||||
cron: cron_config_json,
|
||||
dailyvideo: dailyvideo__metadata_ts,
|
||||
"routing-forms": routing_forms_config_json,
|
||||
exchange2013calendar: exchange2013calendar__metadata_ts,
|
||||
exchange2016calendar: exchange2016calendar__metadata_ts,
|
||||
exchangecalendar: exchangecalendar_config_json,
|
||||
@@ -89,6 +90,7 @@ export const appStoreMetadata = {
|
||||
rainbow: rainbow_config_json,
|
||||
raycast: raycast_config_json,
|
||||
riverside: riverside_config_json,
|
||||
"routing-forms": routing_forms_config_json,
|
||||
salesforce: salesforce_config_json,
|
||||
sendgrid: sendgrid_config_json,
|
||||
signal: signal_config_json,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
Don't modify this file manually.
|
||||
**/
|
||||
import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
|
||||
import { appDataSchema as routing_forms_zod_ts } from "./ee/routing-forms/zod";
|
||||
import { appDataSchema as fathom_zod_ts } from "./fathom/zod";
|
||||
import { appDataSchema as ga4_zod_ts } from "./ga4/zod";
|
||||
import { appDataSchema as giphy_zod_ts } from "./giphy/zod";
|
||||
@@ -15,6 +14,7 @@ import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
import { appDataSchema as plausible_zod_ts } from "./plausible/zod";
|
||||
import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod";
|
||||
import { appDataSchema as rainbow_zod_ts } from "./rainbow/zod";
|
||||
import { appDataSchema as routing_forms_zod_ts } from "./routing-forms/zod";
|
||||
import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod";
|
||||
import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod";
|
||||
import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
|
||||
@@ -27,7 +27,6 @@ import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
|
||||
|
||||
export const appDataSchemas = {
|
||||
dailyvideo: dailyvideo_zod_ts,
|
||||
"routing-forms": routing_forms_zod_ts,
|
||||
fathom: fathom_zod_ts,
|
||||
ga4: ga4_zod_ts,
|
||||
giphy: giphy_zod_ts,
|
||||
@@ -39,6 +38,7 @@ export const appDataSchemas = {
|
||||
plausible: plausible_zod_ts,
|
||||
qr_code: qr_code_zod_ts,
|
||||
rainbow: rainbow_zod_ts,
|
||||
"routing-forms": routing_forms_zod_ts,
|
||||
salesforce: salesforce_zod_ts,
|
||||
stripe: stripepayment_zod_ts,
|
||||
tandemvideo: tandemvideo_zod_ts,
|
||||
|
||||
@@ -9,7 +9,7 @@ export const apiHandlers = {
|
||||
caldavcalendar: import("./caldavcalendar/api"),
|
||||
campfire: import("./campfire/api"),
|
||||
closecom: import("./closecom/api"),
|
||||
"routing-forms": import("./ee/routing-forms/api"),
|
||||
cron: import("./cron/api"),
|
||||
exchange2013calendar: import("./exchange2013calendar/api"),
|
||||
exchange2016calendar: import("./exchange2016calendar/api"),
|
||||
exchangecalendar: import("./exchangecalendar/api"),
|
||||
@@ -33,6 +33,7 @@ export const apiHandlers = {
|
||||
rainbow: import("./rainbow/api"),
|
||||
raycast: import("./raycast/api"),
|
||||
riverside: import("./riverside/api"),
|
||||
"routing-forms": import("./routing-forms/api"),
|
||||
salesforce: import("./salesforce/api"),
|
||||
sendgrid: import("./sendgrid/api"),
|
||||
signal: import("./signal/api"),
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { FiAlertCircle, FiArrowRight, FiCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
import { InstallAppButtonMap } from "./apps.browser.generated";
|
||||
import type { InstallAppButtonProps } from "./types";
|
||||
@@ -16,9 +22,16 @@ export const InstallAppButtonWithoutPlanCheck = (
|
||||
) => {
|
||||
const key = deriveAppDictKeyFromType(props.type, InstallAppButtonMap);
|
||||
const InstallAppButtonComponent = InstallAppButtonMap[key as keyof typeof InstallAppButtonMap];
|
||||
if (!InstallAppButtonComponent) return <>{props.render({ useDefaultComponent: true })}</>;
|
||||
if (!InstallAppButtonComponent)
|
||||
return <>{props.render({ useDefaultComponent: true, disabled: props.disableInstall })}</>;
|
||||
|
||||
return <InstallAppButtonComponent render={props.render} onChanged={props.onChanged} />;
|
||||
return (
|
||||
<InstallAppButtonComponent
|
||||
render={props.render}
|
||||
onChanged={props.onChanged}
|
||||
disableInstall={props.disableInstall}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InstallAppButton = (
|
||||
@@ -26,6 +39,7 @@ export const InstallAppButton = (
|
||||
isProOnly?: App["isProOnly"];
|
||||
type: App["type"];
|
||||
wrapperClassName?: string;
|
||||
disableInstall?: boolean;
|
||||
} & InstallAppButtonProps
|
||||
) => {
|
||||
const { isLoading, data: user } = trpc.viewer.me.useQuery();
|
||||
@@ -63,3 +77,79 @@ export const InstallAppButton = (
|
||||
};
|
||||
|
||||
export { AppConfiguration } from "./_components/AppConfiguration";
|
||||
|
||||
export const AppDependencyComponent = ({
|
||||
appName,
|
||||
dependencyData,
|
||||
}: {
|
||||
appName: string;
|
||||
dependencyData: RouterOutputs["viewer"]["appsRouter"]["queryForDependencies"];
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"rounded-md py-3 px-4",
|
||||
dependencyData && dependencyData.some((dependency) => !dependency.installed)
|
||||
? "bg-blue-100"
|
||||
: "bg-gray-100"
|
||||
)}>
|
||||
{dependencyData &&
|
||||
dependencyData.map((dependency) => {
|
||||
return dependency.installed ? (
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="flex items-start">
|
||||
<div>
|
||||
<FiCheck className="mt-1 mr-2 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">
|
||||
{t("app_is_connected", { dependencyName: dependency.name })}
|
||||
</span>
|
||||
<div>
|
||||
<div>
|
||||
<span>
|
||||
{t("this_app_requires_connected_account", {
|
||||
appName,
|
||||
dependencyName: dependency.name,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="flex items-start text-blue-900">
|
||||
<div>
|
||||
<FiAlertCircle className="mt-1 mr-2 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">
|
||||
{t("this_app_requires_connected_account", { appName, dependencyName: dependency.name })}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
<Link
|
||||
href={`${CAL_URL}/apps/${dependency.slug}`}
|
||||
className="flex items-center text-blue-900 underline">
|
||||
<span className="mr-1">
|
||||
{t("connect_app", { dependencyName: dependency.name })}
|
||||
</span>
|
||||
<FiArrowRight />
|
||||
</Link>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
items:
|
||||
- 1.png
|
||||
- 2.png
|
||||
- 3.png
|
||||
- 4.png
|
||||
- 5.png
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
|
||||
### Cron's most valuable features
|
||||
<!-- From "FAQs: What are Cron’s most valuable features?"
|
||||
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
|
||||
|
||||
- **Availability sharing** — mark holds on the grid, share the slots, and schedule with ease.
|
||||
- **Team overlays** — quickly overlay teammates' calendars.
|
||||
- **Time zones** — add as many as needed, drag & drop to quickly "travel" to places.
|
||||
- **Multiple accounts** — unify work and personal calendars in one place.
|
||||
- **Menu bar calendar** — tightly integrated; see and join upcoming meetings in seconds.
|
||||
- **Speed, multi-select flows, keyboard shortcuts, and gorgeous light/dark modes.**
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
redirect: {
|
||||
newTab: true,
|
||||
url: "https://cron.com",
|
||||
},
|
||||
createCredential: ({ appType, user, slug }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as add } from "./add";
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Cron",
|
||||
"slug": "cron",
|
||||
"type": "cron_other",
|
||||
"imageSrc": "logo.png",
|
||||
"logo": "logo.png",
|
||||
"url": "https://cal.com/apps/cron",
|
||||
"variant": "other",
|
||||
"categories": ["calendar"],
|
||||
"publisher": "miguelnietoa",
|
||||
"email": "support@cal.com",
|
||||
"description": "Cron is the next-generation calendar for professionals and teams.",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "link-as-an-app",
|
||||
"dependencies": ["google-calendar"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/cron",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "Cron is the next-generation calendar for professionals and teams."
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 367 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user