Compare commits

...
20 Commits
Author SHA1 Message Date
zomars bc0d83164c v2.3.8
release.yml / release (push) Canceled after 0s
2022-12-09 10:43:49 -07:00
zomars 30f0b080c5 SIU 2022-12-09 10:43:19 -07:00
Leo Giovanetti ee39ea316f Fixing app store search input (#5960) 2022-12-09 14:10:54 -03:00
Hariom BalharaandPeer Richelsen fbc2708259 Hotfix/Google Meet must be globally installed (#5948)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-09 12:51:23 +00:00
9bb83aba7e removed license consent (#5931)
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-09 11:05:42 +00:00
Omar López 3a67e5617e v2.3.7
release.yml / release (push) Canceled after 0s
2022-12-08 22:44:49 -07:00
f401aa7669 Harsh/fix avail time order (#5900)
* fix: 404 page for undefined events

* remove prev code merge

* add: sorting in available durations

* fix: sort only once

* remove: console logs

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-09 00:16:08 +00:00
a6e0678e59 feat: use next/script tag for inline scripts (#5938)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-08 23:39:15 +00:00
Omar López e832015f26 Deprecates user plan (#5942)
* Remove isMissingSeat

* Removes user plan

* Deprecates User Plan

* Updates website

* Update eventTypes.tsx
2022-12-08 16:20:24 -07:00
zomars 5e86881e15 Adds missing package 2022-12-08 16:17:04 -07:00
zomars 3b71f98460 Submodule sync 2022-12-08 15:00:29 -07:00
144d57adb2 fix: make banner responsive and cover entire app (#5934)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-08 18:42:55 +00:00
github-actions[bot]andCrowdin Bot e1f1c15318 New Crowdin translations by Github Action (#5932)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-08 19:31:16 +01:00
d88b1b2f8f remove {{appName}} in translation (#5924)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-08 18:28:22 +00:00
2e61c15f2c fix: check input is not undefined (#5927)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-08 18:16:04 +00:00
Hariom Balharaandkodiakhq[bot] df73473c7f Avoid redirect in cancellation and visit from Bookings list (#5928)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-08 17:32:44 +00:00
github-actions[bot]andCrowdin Bot 7167973afd New Crowdin translations by Github Action (#5920)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-08 15:12:24 +01:00
Omar López 3c17bc2d1a Fixes for avatar payloads (#5921) 2022-12-08 09:50:03 +00:00
a9a295dc54 Admin apps UI (#5494)
* Abstract app category navigation

* Send key schema to frontend

Co-authored-by: Omar López <zomars@users.noreply.github.com>

* Render keys for apps on admin

* Add enabled col for apps

* Save app keys to DB

* Add checks for admin role

* Abstract setup components

* Add AdminAppsList to setup wizard

* Migrate to v10 tRPC

* Default hide keys

* Display enabled apps

* Merge branch 'main' into admin-apps-ui

* Toggle calendars

* WIP

* Add params and include AppCategoryNavigation

* Refactor getEnabledApps

* Add warning for disabling apps

* Fallback to cal video when a video app is disabled

* WIP send disabled email

* Send email to all users of  event types with payment app

* Disable Stripe when app is disabled

* Disable apps in event types

* Send email to users on disabled apps

* Send email based on what app was disabled

* WIP type fix

* Disable navigation to apps list if already setup

* UI import fixes

* Waits for session data before redirecting

* Updates admin seeded password

To comply with admin password requirements

* Update yarn.lock

* Flex fixes

* Adds admin middleware

* Clean up

* WIP

* WIP

* NTS

* Add dirName to app metadata

* Upsert app if not in db

* Upsert app if not in db

* Add dirName to app metadata

* Add keys to app packages w/ keys

* Merge with main

* Toggle show keys & on enable

* Fix empty keys

* Fix lark calendar metadata

* Fix some type errors

* Fix Lark metadata & check for category when upserting

* More type fixes

* Fix types & add keys to google cal

* WIP

* WIP

* WIP

* More type fixes

* Fix type errors

* Fix type errors

* More type fixes

* More type fixes

* More type fixes

* Feedback

* Fixes default value

* Feedback

* Migrate credential invalid col default value "false"

* Upsert app on saving keys

* Clean up

* Validate app keys on frontend

* Add nonempty to app keys schemas

* Add web3

* Listlocale filter on categories / category

* Grab app metadata via category or categories

* Show empty screen if no apps are enabled

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Replace .nonempty() w/ .min(1)

* Fix type error

* Address feedback

* Added migration to keep current apps enabled

* Update apps.tsx

* Fix bug

* Add keys schema to Plausible app

* Add appKeysSchema to zod.ts template

* Update AdminAppsList.tsx

Co-authored-by: Omar López <zomars@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2022-12-07 14:47:02 -07:00
+2 21dd1f4e95 [CAL-462] Team settings dropdown menu should be open when you navigate from Teams (#5868)
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: gitstart_bot <gitstart_bot@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: gitstart_bot <gitstart_bot@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
2022-12-07 21:35:41 +00:00
184 changed files with 1882 additions and 1080 deletions
+2 -3
View File
@@ -7,14 +7,13 @@
# - E-MAIL SETTINGS
# - LICENSE *************************************************************************************************
# Set this value to 'agree' to accept our license:
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
# https://github.com/calendso/calendso/blob/main/LICENSE
#
# Summary of terms:
# - The codebase has to stay open source, whether it was modified or not
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
NEXT_PUBLIC_LICENSE_CONSENT=''
#
# To enable enterprise-only features, fill your license key in here.
# @see https://console.cal.com
CALCOM_LICENSE_KEY=
+1 -1
View File
@@ -292,7 +292,7 @@ You can deploy Cal on [Railway](https://railway.app/) using the button above. Th
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
<!-- ROADMAP -->
-5
View File
@@ -33,11 +33,6 @@
"generator": "secret",
"required": "true"
},
"NEXT_PUBLIC_LICENSE_CONSENT": {
"description": "License consent",
"value": "true",
"required": "true"
},
"CRON_API_KEY": {
"description": "ApiKey for cronjobs",
"value": ""
@@ -29,10 +29,7 @@ import { trpc } from "@calcom/trpc/react";
import { Icon, DatePicker } from "@calcom/ui";
import { timeZone as localStorageTimeZone } from "@lib/clock";
// import { timeZone } from "@lib/clock";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import Gates, { Gate, GateState } from "@components/Gates";
import AvailableTimes from "@components/booking/AvailableTimes";
@@ -280,9 +277,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
}, []);
// TODO: Improve this;
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
const telemetry = useTelemetry();
+12 -11
View File
@@ -250,23 +250,24 @@ export const EventSetupTab = (
isSearchable={false}
className="h-auto !min-h-[36px] text-sm"
options={multipleDurationOptions}
value={selectedMultipleDuration}
onChange={(options) => {
const values = options
.map((opt) => opt.value)
.sort(function (a, b) {
return a - b;
});
let newOptions = [...options];
newOptions = newOptions.sort((a, b) => {
return a?.value - b?.value;
});
const values = newOptions.map((opt) => opt.value);
setMultipleDuration(values);
setSelectedMultipleDuration(options);
if (!options.find((opt) => opt.value === defaultDuration?.value)) {
if (options.length > 0) {
setDefaultDuration(options[0]);
setSelectedMultipleDuration(newOptions);
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
if (newOptions.length > 0) {
setDefaultDuration(newOptions[0]);
} else {
setDefaultDuration(null);
}
}
if (options.length === 1 && defaultDuration === null) {
setDefaultDuration(options[0]);
if (newOptions.length === 1 && defaultDuration === null) {
setDefaultDuration(newOptions[0]);
}
formMethods.setValue("metadata.multipleDuration", values);
}}
@@ -35,23 +35,7 @@ interface ICustomUsernameProps {
setInputUsernameValue: (value: string) => void;
onSuccessMutation?: () => void;
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
user: Pick<
User,
| "username"
| "name"
| "email"
| "bio"
| "avatar"
| "timeZone"
| "weekStart"
| "hideBranding"
| "theme"
| "plan"
| "brandColor"
| "darkBrandColor"
| "timeFormat"
| "metadata"
>;
user: Pick<User, "username" | "metadata">;
readonly?: boolean;
}
@@ -14,23 +14,7 @@ export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : Premium
interface UsernameAvailabilityFieldProps {
onSuccessMutation?: () => void;
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
user: Pick<
User,
| "username"
| "name"
| "email"
| "bio"
| "avatar"
| "timeZone"
| "weekStart"
| "hideBranding"
| "theme"
| "plan"
| "brandColor"
| "darkBrandColor"
| "timeFormat"
| "metadata"
>;
user: Pick<User, "username" | "metadata">;
}
export const UsernameAvailabilityField = ({
onSuccessMutation,
@@ -1,12 +0,0 @@
import { UserPlan } from "@prisma/client";
/**
* TODO: It should be exposed at a single place.
*/
export function useExposePlanGlobally(plan: UserPlan) {
// Don't wait for component to mount. Do it ASAP. Delaying it would delay UI Configuration.
if (typeof window !== "undefined") {
// This variable is used by embed-iframe to determine if we should allow UI configuration
window.CalComPlan = plan;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.3.6",
"version": "2.3.8",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
-3
View File
@@ -28,7 +28,6 @@ import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { BadgeCheckIcon, EventTypeDescriptionLazy as EventTypeDescription, Icon } from "@calcom/ui";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr";
@@ -92,7 +91,6 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail)
useExposePlanGlobally("PRO");
const nameOrUsername = user.name || user.username || "";
const telemetry = useTelemetry();
@@ -272,7 +270,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
darkBrandColor: true,
avatar: true,
theme: true,
plan: true,
away: true,
verified: true,
allowDynamicBooking: true,
+1 -5
View File
@@ -72,7 +72,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
id: true,
username: true,
away: true,
plan: true,
name: true,
hideBranding: true,
timeZone: true,
@@ -130,7 +129,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
if (!user || !user.eventTypes.length) return { notFound: true };
const [eventType]: (typeof user.eventTypes[number] & {
users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[];
users: Pick<User, "name" | "username" | "hideBranding" | "timeZone">[];
})[] = [
{
...user.eventTypes[0],
@@ -139,7 +138,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
plan: user.plan,
timeZone: user.timeZone,
},
],
@@ -226,7 +224,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
},
},
theme: true,
plan: true,
},
});
@@ -246,7 +243,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
plan: user.plan,
timeZone: user.timeZone,
};
}),
+5 -1
View File
@@ -1,5 +1,6 @@
import { DefaultSeo } from "next-seo";
import Head from "next/head";
import Script from "next/script";
import superjson from "superjson";
import "@calcom/embed-core/src/embed-iframe";
@@ -36,8 +37,11 @@ function MyApp(props: AppProps) {
<AppProviders {...props}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler />
<Script
id="page-status"
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
/>
<Head>
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }} />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
{getLayout(
+4 -1
View File
@@ -1,4 +1,5 @@
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
import Script from "next/script";
type Props = Record<string, unknown> & DocumentProps;
@@ -69,7 +70,9 @@ class MyDocument extends Document<Props> {
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
Persist the embed mode in sessionStorage because query param might get lost during browsing.
*/}
<script
<Script
id="run-before-client"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `(${toRunBeforeReactOnClient.toString()})()`,
}}
-1
View File
@@ -46,7 +46,6 @@ async function handler(req: NextApiRequest) {
name: parsedQuery.data.full_name,
emailVerified: new Date(),
locale: "en", // TODO: We should revisit this
plan: "PRO",
identityProvider: IdentityProvider.CAL,
},
});
+1 -33
View File
@@ -1,10 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import { TRIAL_LIMIT_DAYS } from "@lib/config/constants";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
@@ -18,35 +13,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
/**
* TODO:
* We should add and extra check for non-paying customers in Stripe so we can
* downgrade them here.
* Remove this endpoint
*/
await prisma.user.updateMany({
data: {
plan: "FREE",
},
where: {
plan: "TRIAL",
OR: [
/**
* If the user doesn't have a trial end date,
* use the default 14 day trial from creation.
*/
{
createdDate: {
lt: dayjs().subtract(TRIAL_LIMIT_DAYS, "day").toDate(),
},
trialEndsAt: null,
},
/** If it does, then honor the trial end date. */
{
trialEndsAt: {
lt: dayjs().toDate(),
},
},
],
},
});
res.json({ ok: true });
}
+4 -3
View File
@@ -60,9 +60,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("AttendeeScheduledEmail", {
attendee: evt.attendees[0],
calEvent: evt,
renderEmail("DisabledAppEmail", {
appName: "Stripe",
appType: ["payment"],
t,
})
);
res.end();
@@ -47,7 +47,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
select: {
username: true,
id: true,
plan: true,
createdDate: true,
},
});
@@ -81,7 +80,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
: "No info"
}</li>
<li><b>Plan:</b>&nbsp;${user.plan}</li>
<li><b>Account created:</b>&nbsp;${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
</ul>
`,
+53 -67
View File
@@ -1,86 +1,72 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
import prisma from "@calcom/prisma";
import { defaultAvatarSrc } from "@lib/profile";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// const username = req.url?.substring(1, req.url.lastIndexOf("/"));
const username = req.query.username as string;
const teamname = req.query.teamname as string;
let identity;
let linksToThisRoute = false;
const querySchema = z
.object({
username: z.string(),
teamname: z.string(),
})
.partial();
async function getIdentityData(req: NextApiRequest) {
const { username, teamname } = querySchema.parse(req.query);
if (username) {
const user = await prisma.user.findUnique({
where: {
username: username,
},
select: {
avatar: true,
email: true,
},
where: { username },
select: { avatar: true, email: true },
});
identity = {
return {
name: username,
email: user?.email,
avatar: user?.avatar,
};
linksToThisRoute =
identity.avatar === `${CAL_URL}/${username}/avatar.png` ||
identity.avatar === `${WEBAPP_URL}/${username}/avatar.png`;
} else if (teamname) {
const team = await prisma.team.findUnique({
where: {
slug: teamname,
},
select: {
logo: true,
},
});
identity = {
name: teamname,
shouldDefaultBeNameBased: true,
avatar: team?.logo,
};
linksToThisRoute =
identity.avatar === `${CAL_URL}/team/${teamname}/avatar.png` ||
identity.avatar === `${WEBAPP_URL}/team/${teamname}/avatar.png`;
}
const emailMd5 = crypto
.createHash("md5")
.update((identity?.email as string) || "guest@example.com")
.digest("hex");
const img = identity?.avatar;
// If image isn't set or links to this route itself, use default avatar
if (!img || linksToThisRoute) {
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
if (identity?.shouldDefaultBeNameBased) {
defaultSrc = getPlaceholderAvatar(null, identity.name);
}
res.writeHead(302, {
Location: defaultSrc,
if (teamname) {
const team = await prisma.team.findUnique({
where: { slug: teamname },
select: { logo: true },
});
res.end();
} else if (!img.includes("data:image")) {
res.writeHead(302, {
Location: img,
});
res.end();
} else {
const decoded = img
.toString()
.replace("data:image/png;base64,", "")
.replace("data:image/jpeg;base64,", "");
const imageResp = Buffer.from(decoded, "base64");
res.writeHead(200, {
"Content-Type": "image/png",
"Content-Length": imageResp.length,
});
res.end(imageResp);
return {
name: teamname,
email: null,
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
};
}
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const identity = await getIdentityData(req);
const img = identity?.avatar;
// If image isn't set or links to this route itself, use default avatar
if (!img) {
res.writeHead(302, {
Location: defaultAvatarSrc({
md5: crypto
.createHash("md5")
.update(identity?.email || "guest@example.com")
.digest("hex"),
}),
});
return res.end();
}
if (!img.includes("data:image")) {
res.writeHead(302, { Location: img });
return res.end();
}
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
const imageResp = Buffer.from(decoded, "base64");
res.writeHead(200, {
"Content-Type": "image/png",
"Content-Length": imageResp.length,
});
res.end(imageResp);
}
+9 -6
View File
@@ -1,5 +1,4 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { InferGetStaticPropsType, NextPageContext } from "next";
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { ChangeEventHandler, useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
@@ -31,7 +30,10 @@ function AppsSearch({
);
}
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
export default function Apps({
categories,
appStore,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { t } = useLocale();
const [searchText, setSearchText] = useState<string | undefined>(undefined);
@@ -42,7 +44,8 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
subtitle={t("app_store_description")}
actions={(className) => (
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
)}>
)}
emptyStore={!appStore.length}>
{!searchText && (
<>
<AppStoreCategories categories={categories} />
@@ -54,7 +57,7 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
);
}
export const getServerSideProps = async (context: NextPageContext) => {
export const getServerSideProps: GetServerSideProps = async (context) => {
const ssg = await ssgInit(context);
const session = await getSession(context);
@@ -77,7 +80,6 @@ export const getServerSideProps = async (context: NextPageContext) => {
}, {} as Record<string, number>);
return {
props: {
trpcState: ssg.dehydrate(),
categories: Object.entries(categories)
.map(([name, count]): { name: AppCategories; count: number } => ({
name: name as AppCategories,
@@ -87,6 +89,7 @@ export const getServerSideProps = async (context: NextPageContext) => {
return b.count - a.count;
}),
appStore,
trpcState: ssg.dehydrate(),
},
};
};
+48
View File
@@ -0,0 +1,48 @@
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { WizardForm } from "@calcom/ui";
import SetupFormStep1 from "./steps/SetupFormStep1";
import StepDone from "./steps/StepDone";
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
const shouldDisable = props.userCount !== 0;
const steps = [
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
isLoading: isLoadingStep1,
},
{
title: t("enable_apps"),
description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup" />,
isLoading: false,
},
];
return (
<>
<main className="flex items-center bg-gray-100 print:h-full">
<WizardForm href="/auth/setup" steps={steps} disableNavigation={shouldDisable} />
</main>
</>
);
}
export const getServerSideProps = async () => {
const userCount = await prisma.user.count();
return {
props: {
userCount,
},
};
};
@@ -1,34 +1,14 @@
import { CheckIcon } from "@heroicons/react/solid";
import { zodResolver } from "@hookform/resolvers/zod";
import classNames from "classnames";
import { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import * as z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { EmailField, Label, PasswordField, TextField, WizardForm } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const StepDone = () => {
const { t } = useLocale();
return (
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<CheckIcon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
</div>
</div>
);
};
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
const router = useRouter();
@@ -83,13 +63,19 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
},
});
if (response.status === 200) {
router.replace(`/auth/login?email=${data.email_address.toLowerCase()}`);
await signIn("credentials", {
redirect: false,
callbackUrl: "/",
email: data.email_address.toLowerCase(),
password: data.password,
});
router.replace(`/auth/setup?step=2&category=calendar`);
} else {
router.replace("/auth/setup");
}
}, onError);
const longWebsiteUrl = process.env.NEXT_PUBLIC_WEBSITE_URL.length > 30;
const longWebsiteUrl = WEBSITE_URL.length > 30;
return (
<FormProvider {...formMethods}>
@@ -201,37 +187,4 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
);
};
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
const steps = [
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: props.userCount !== 0 ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
enabled: props.userCount === 0, // to check if the wizard should show buttons to navigate through more steps
isLoading: isLoadingStep1,
},
];
return (
<>
<main className="flex h-screen items-center bg-gray-100 print:h-full">
<WizardForm href="/auth/setup" steps={steps} containerClassname="max-w-sm" />
</main>
</>
);
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const userCount = await prisma.user.count();
return {
props: {
trpcState: ssr.dehydrate(),
userCount,
},
};
};
export default SetupFormStep1;
@@ -0,0 +1,19 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
const StepDone = () => {
const { t } = useLocale();
return (
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
</div>
</div>
);
};
export default StepDone;
+37 -20
View File
@@ -57,26 +57,41 @@ function redirectToExternalUrl(url: string) {
function RedirectionToast({ url }: { url: string }) {
const [timeRemaining, setTimeRemaining] = useState(10);
const [isToastVisible, setIsToastVisible] = useState(true);
const parsedSuccessUrl = new URL(document.URL);
const parsedExternalUrl = new URL(url);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
parsedSuccessUrl.searchParams.set(name, value);
}
const urlWithSuccessParams =
parsedExternalUrl.origin +
parsedExternalUrl.pathname +
"?" +
parsedSuccessUrl.searchParams.toString() +
parsedExternalUrl.hash;
const { t } = useLocale();
const timerRef = useRef<number | null>(null);
const router = useRouter();
const { cancel: isCancellationMode } = querySchema.parse(router.query);
const urlWithSuccessParamsRef = useRef<string | null>();
if (isCancellationMode && timerRef.current) {
setIsToastVisible(false);
}
useEffect(() => {
if (!isToastVisible && timerRef.current) {
window.clearInterval(timerRef.current);
}
}, [isToastVisible]);
useEffect(() => {
const parsedExternalUrl = new URL(url);
const parsedSuccessUrl = new URL(document.URL);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
parsedSuccessUrl.searchParams.set(name, value);
}
const urlWithSuccessParams =
parsedExternalUrl.origin +
parsedExternalUrl.pathname +
"?" +
parsedSuccessUrl.searchParams.toString() +
parsedExternalUrl.hash;
urlWithSuccessParamsRef.current = urlWithSuccessParams;
timerRef.current = window.setInterval(() => {
if (timeRemaining > 0) {
setTimeRemaining((timeRemaining) => {
@@ -90,7 +105,7 @@ function RedirectionToast({ url }: { url: string }) {
return () => {
window.clearInterval(timerRef.current as number);
};
}, [timeRemaining, urlWithSuccessParams]);
}, [timeRemaining, url]);
if (!isToastVisible) {
return null;
@@ -113,7 +128,9 @@ function RedirectionToast({ url }: { url: string }) {
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
<button
onClick={() => {
redirectToExternalUrl(urlWithSuccessParams);
if (urlWithSuccessParamsRef.current) {
redirectToExternalUrl(urlWithSuccessParamsRef.current);
}
}}
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
{t("continue")}
@@ -124,7 +141,6 @@ function RedirectionToast({ url }: { url: string }) {
type="button"
onClick={() => {
setIsToastVisible(false);
window.clearInterval(timerRef.current as number);
}}
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
<Icon.FiX className="h-6 w-6 text-white" />
@@ -349,7 +365,9 @@ export default function Success(props: SuccessProps) {
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
{eventType.successRedirectUrl ? <RedirectionToast url={eventType.successRedirectUrl} /> : null}{" "}
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
<RedirectionToast url={eventType.successRedirectUrl} />
) : null}{" "}
<div
className={classNames(
shouldAlignCentrally ? "text-center" : "",
@@ -840,7 +858,6 @@ const getEventTypesFromDB = async (id: number) => {
name: true,
username: true,
hideBranding: true,
plan: true,
theme: true,
brandColor: true,
darkBrandColor: true,
-3
View File
@@ -90,7 +90,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
},
theme: true,
plan: true,
},
});
@@ -117,7 +116,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
name: u.name,
username: u.username,
hideBranding: u.hideBranding,
plan: u.plan,
timeZone: u.timeZone,
})),
});
@@ -158,7 +156,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
away: user.away,
isDynamicGroup: false,
profile,
plan: user.plan,
date,
eventType: eventTypeObject,
workingHours,
+8 -4
View File
@@ -8,9 +8,10 @@ import { useForm } from "react-hook-form";
import { z } from "zod";
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
import getApps, { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
import { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
import { EventLocationType, LocationObject } from "@calcom/core/location";
import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib";
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
import { CAL_URL } from "@calcom/lib/constants";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
@@ -282,7 +283,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!metadata?.multipleDuration?.includes(input.length)) {
if (input.length && !metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@@ -340,7 +341,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
id: true,
avatar: true,
email: true,
plan: true,
locale: true,
defaultScheduleId: true,
});
@@ -467,6 +467,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const credentials = await prisma.credential.findMany({
where: {
userId: session.user.id,
app: {
enabled: true,
},
},
select: {
id: true,
@@ -474,6 +477,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
key: true,
userId: true,
appId: true,
invalid: true,
},
});
@@ -526,7 +530,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}
const currentUser = eventType.users.find((u) => u.id === session.user.id);
const t = await getTranslation(currentUser?.locale ?? "en", "common");
const integrations = getApps(credentials);
const integrations = await getEnabledApps(credentials);
const locationOptions = getLocationOptions(integrations, t);
const eventTypeObject = Object.assign({}, eventType, {
@@ -164,7 +164,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
weekStart: true,
hideBranding: true,
theme: true,
plan: true,
brandColor: true,
darkBrandColor: true,
metadata: true,
@@ -1,5 +1,6 @@
import { GetServerSidePropsContext } from "next";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
@@ -10,7 +11,7 @@ function AdminAppsView() {
return (
<>
<Meta title={t("apps")} description={t("apps_description")} />
<h1>{t("apps_listing")}</h1>
<AdminAppsList baseURL="/settings/admin/apps" className="w-0" />
</>
);
}
+1 -7
View File
@@ -36,7 +36,6 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
const BillingView = () => {
const { t } = useLocale();
const { data: user } = trpc.viewer.me.useQuery();
const isPro = user?.plan === "PRO";
const [, loadChat] = useChat();
const [showChat, setShowChat] = useState(false);
const router = useRouter();
@@ -53,14 +52,9 @@ const BillingView = () => {
<Meta title={t("billing")} description={t("manage_billing_description")} />
<div className="space-y-6 text-sm sm:space-y-8">
<CtaRow
className={classNames(!isPro && "pointer-events-none opacity-30")}
title={t("billing_manage_details_title")}
description={t("billing_manage_details_description")}>
<Button
color={isPro ? "primary" : "secondary"}
href={billingHref}
target="_blank"
EndIcon={Icon.FiExternalLink}>
<Button color="primary" href={billingHref} target="_blank" EndIcon={Icon.FiExternalLink}>
{t("billing_portal")}
</Button>
</CtaRow>
+164 -154
View File
@@ -2,7 +2,7 @@ import { IdentityProvider } from "@prisma/client";
import crypto from "crypto";
import { GetServerSidePropsContext } from "next";
import { signOut } from "next-auth/react";
import { BaseSyntheticEvent, useEffect, useRef, useState } from "react";
import { BaseSyntheticEvent, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
@@ -63,14 +63,25 @@ interface DeleteAccountValues {
totpCode: string;
}
type FormValues = {
username: string;
avatar: string;
name: string;
email: string;
bio: string;
};
const ProfileView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { data: avatar, isLoading: isLoadingAvatar } = trpc.viewer.avatar.useQuery();
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: () => {
showToast(t("settings_updated_successfully"), "success");
utils.viewer.me.invalidate();
utils.viewer.avatar.invalidate();
setTempFormValues(null);
},
onError: () => {
showToast(t("error_updating_settings"), "error");
@@ -78,18 +89,14 @@ const ProfileView = () => {
});
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
const [tempFormValues, setTempFormValues] = useState<FormValues | null>(null);
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
const form = useForm<DeleteAccountValues>();
const emailMd5 = crypto
.createHash("md5")
.update(user?.email || "example@example.com")
.digest("hex");
const onDeleteMeSuccessMutation = async () => {
await utils.viewer.me.invalidate();
showToast(t("Your account was deleted"), "success");
@@ -104,7 +111,7 @@ const ProfileView = () => {
const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({
onSuccess() {
mutation.mutate(formMethods.getValues());
if (tempFormValues) mutation.mutate(tempFormValues);
setConfirmPasswordOpen(false);
},
onError() {
@@ -150,6 +157,7 @@ const ProfileView = () => {
deleteMeWithoutPasswordMutation.mutate();
}
};
const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => {
e?.preventDefault();
if (isCALIdentityProviver) {
@@ -160,38 +168,6 @@ const ProfileView = () => {
}
};
const formMethods = useForm({
defaultValues: {
username: "",
avatar: "",
name: "",
email: "",
bio: "",
},
});
const {
reset,
formState: { isSubmitting, isDirty },
} = formMethods;
useEffect(() => {
if (user) {
reset(
{
avatar: user?.avatar || "",
username: user?.username || "",
name: user?.name || "",
email: user?.email || "",
bio: user?.bio || "",
},
{
keepDirtyValues: true,
}
);
}
}, [reset, user]);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const passwordRef = useRef<HTMLInputElement>(null!);
@@ -203,133 +179,100 @@ const ProfileView = () => {
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
};
const onSuccessfulUsernameUpdate = async () => {
showToast(t("settings_updated_successfully"), "success");
await utils.viewer.me.invalidate();
};
const onErrorInUsernameUpdate = () => {
showToast(t("error_updating_settings"), "error");
};
if (isLoading || !user)
if (isLoading || !user || isLoadingAvatar || !avatar)
return <SkeletonLoader title={t("profile")} description={t("profile_description")} />;
const isDisabled = isSubmitting || !isDirty;
const defaultValues = {
username: user.username || "",
avatar: avatar.avatar || "",
name: user.name || "",
email: user.email || "",
bio: user.bio || "",
};
return (
<>
<Meta title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
<Form
form={formMethods}
handleSubmit={(values) => {
if (values.email !== user?.email && isCALIdentityProviver) {
<ProfileForm
key={JSON.stringify(defaultValues)}
defaultValues={defaultValues}
onSubmit={(values) => {
if (values.email !== user.email && isCALIdentityProviver) {
setTempFormValues(values);
setConfirmPasswordOpen(true);
} else {
mutation.mutate(values);
}
}}>
<div className="flex items-center">
<Controller
control={formMethods.control}
name="avatar"
render={({ field: { value } }) => (
<>
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
<div className="ml-4">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("change_avatar")}
handleAvatarChange={(newAvatar) => {
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
}}
imageSrc={value}
/>
</div>
</>
}}
extraField={
<div className="mt-8">
<UsernameAvailabilityField
user={user}
onSuccessMutation={async () => {
showToast(t("settings_updated_successfully"), "success");
await utils.viewer.me.invalidate();
}}
onErrorMutation={() => {
showToast(t("error_updating_settings"), "error");
}}
/>
</div>
}
/>
<hr className="my-6 border-neutral-200" />
<Label>{t("danger_zone")}</Label>
{/* Delete account Dialog */}
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
<DialogTrigger asChild>
<Button
data-testid="delete-account"
color="destructive"
className="mt-1 border-2"
StartIcon={Icon.FiTrash2}>
{t("delete_account")}
</Button>
</DialogTrigger>
<DialogContent
title={t("delete_account_modal_title")}
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
type="creation"
Icon={Icon.FiAlertTriangle}>
<>
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
{isCALIdentityProviver && (
<PasswordField
data-testid="password"
name="password"
id="password"
autoComplete="current-password"
required
label="Password"
ref={passwordRef}
/>
)}
/>
</div>
<div className="mt-8">
<UsernameAvailabilityField
user={user}
onSuccessMutation={onSuccessfulUsernameUpdate}
onErrorMutation={onErrorInUsernameUpdate}
/>
</div>
<div className="mt-8">
<TextField label={t("full_name")} {...formMethods.register("name")} />
</div>
<div className="mt-8">
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
</div>
<div className="mt-8">
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
</div>
<Button
disabled={isDisabled}
color="primary"
className="mt-8"
type="submit"
loading={mutation.isLoading}>
{t("update")}
</Button>
{user?.twoFactorEnabled && isCALIdentityProviver && (
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
<TwoFactor center={false} />
</Form>
)}
<hr className="my-6 border-neutral-200" />
<Label>{t("danger_zone")}</Label>
{/* Delete account Dialog */}
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
<DialogTrigger asChild>
<Button
data-testid="delete-account"
color="destructive"
className="mt-1 border-2"
StartIcon={Icon.FiTrash2}>
{t("delete_account")}
</Button>
</DialogTrigger>
<DialogContent
title={t("delete_account_modal_title")}
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
type="creation"
Icon={Icon.FiAlertTriangle}>
<>
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
{isCALIdentityProviver && (
<PasswordField
data-testid="password"
name="password"
id="password"
autoComplete="current-password"
required
label="Password"
ref={passwordRef}
/>
)}
{user?.twoFactorEnabled && isCALIdentityProviver && (
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
<TwoFactor center={false} />
</Form>
)}
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
<DialogFooter>
<Button
color="primary"
data-testid="delete-account-confirm"
onClick={(e) => onConfirmButton(e)}>
{t("delete_my_account")}
</Button>
<DialogClose />
</DialogFooter>
</>
</DialogContent>
</Dialog>
</Form>
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
<DialogFooter>
<Button
color="primary"
data-testid="delete-account-confirm"
onClick={(e) => onConfirmButton(e)}>
{t("delete_my_account")}
</Button>
<DialogClose />
</DialogFooter>
</>
</DialogContent>
</Dialog>
{/* If changing email, confirm password */}
<Dialog open={confirmPasswordOpen} onOpenChange={setConfirmPasswordOpen}>
@@ -363,6 +306,73 @@ const ProfileView = () => {
);
};
const ProfileForm = ({
defaultValues,
onSubmit,
extraField,
}: {
defaultValues: FormValues;
onSubmit: (values: FormValues) => void;
extraField?: React.ReactNode;
}) => {
const { t } = useLocale();
const emailMd5 = crypto
.createHash("md5")
.update(defaultValues.email || "example@example.com")
.digest("hex");
const formMethods = useForm<FormValues>({
defaultValues,
});
const {
formState: { isSubmitting, isDirty },
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
return (
<Form form={formMethods} handleSubmit={onSubmit}>
<div className="flex items-center">
<Controller
control={formMethods.control}
name="avatar"
render={({ field: { value } }) => (
<>
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
<div className="ml-4">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("change_avatar")}
handleAvatarChange={(newAvatar) => {
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
}}
imageSrc={value || undefined}
/>
</div>
</>
)}
/>
</div>
{extraField}
<div className="mt-8">
<TextField label={t("full_name")} {...formMethods.register("name")} />
</div>
<div className="mt-8">
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
</div>
<div className="mt-8">
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
</div>
<Button disabled={isDisabled} color="primary" className="mt-8" type="submit">
{t("update")}
</Button>
</Form>
);
};
ProfileView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
-7
View File
@@ -1,4 +1,3 @@
import { UserPlan } from "@prisma/client";
import classNames from "classnames";
import { GetServerSidePropsContext } from "next";
import Link from "next/link";
@@ -14,7 +13,6 @@ import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { Avatar, Button, EventTypeDescription, Icon } from "@calcom/ui";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -27,7 +25,6 @@ function TeamPage({ team }: TeamPageProps) {
useTheme();
const showMembers = useToggleQuery("members");
const { t } = useLocale();
useExposePlanGlobally("PRO");
const isEmbed = useIsEmbed();
const telemetry = useTelemetry();
const router = useRouter();
@@ -137,10 +134,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (!team) return { notFound: true } as { notFound: true };
const members = team.members.filter((member) => member.plan !== UserPlan.FREE);
team.members = members ?? [];
team.eventTypes = team.eventTypes.map((type) => ({
...type,
users: type.users.map((user) => ({
-5
View File
@@ -1,4 +1,3 @@
import { UserPlan } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
@@ -60,7 +59,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
username: true,
timeZone: true,
hideBranding: true,
plan: true,
brandColor: true,
darkBrandColor: true,
},
@@ -130,7 +128,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
plan: user.plan,
timeZone: user.timeZone,
})),
});
@@ -144,8 +141,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
// Team is always pro
plan: "PRO" as UserPlan,
profile: {
name: team.name || team.slug,
slug: team.slug,
+1 -1
View File
@@ -13,7 +13,7 @@ test.describe.configure({ mode: "parallel" });
test.describe("free user", () => {
test.beforeEach(async ({ page, users }) => {
const free = await users.create({ plan: "FREE" });
const free = await users.create();
await page.goto(`/${free.username}`);
});
test.afterEach(async ({ users }) => {
@@ -1,7 +1,5 @@
import { expect } from "@playwright/test";
import { UserPlan } from "@prisma/client";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -10,7 +10,7 @@ import {
test("dynamic booking", async ({ page, users }) => {
const pro = await users.create();
await pro.login();
const free = await users.create({ plan: "FREE" });
const free = await users.create({ username: "free" });
await page.goto(`/${pro.username}+${free.username}`);
await test.step("book an event first day in next month", async () => {
+1 -31
View File
@@ -120,37 +120,7 @@ test.describe("Event Types tests", () => {
});
await page.locator("[data-testid=update-eventtype]").click();
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
await expect(toast).toBeTruthy();
});
});
test.describe("free user", () => {
test.beforeEach(async ({ page, users }) => {
const free = await users.create({ plan: "FREE" });
await free.login();
await page.goto("/event-types");
// We wait until loading is finished
await page.waitForSelector('[data-testid="event-types"]');
});
test("has at least 2 events where first is enabled", async ({ page }) => {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const count = await $eventTypes.count();
expect(count).toBeGreaterThanOrEqual(2);
});
test("edit first event", async ({ page }) => {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
await page.waitForNavigation({
url: (url) => {
return !!url.pathname.match(/\/event-types\/.+/);
},
});
await page.locator("[data-testid=update-eventtype]").click();
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
await expect(toast).toBeTruthy();
expect(toast).toBeTruthy();
});
});
});
+4 -7
View File
@@ -1,6 +1,6 @@
import type { Page, WorkerInfo } from "@playwright/test";
import type Prisma from "@prisma/client";
import { Prisma as PrismaType, UserPlan } from "@prisma/client";
import { Prisma as PrismaType } from "@prisma/client";
import { hash } from "bcryptjs";
import dayjs from "@calcom/dayjs";
@@ -238,7 +238,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
};
};
type CustomUserOptsKeys = "username" | "password" | "plan" | "completedOnboarding" | "locale" | "name";
type CustomUserOptsKeys = "username" | "password" | "completedOnboarding" | "locale" | "name";
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & { timeZone?: TimeZoneEnum };
// creates the actual user in the db.
@@ -247,13 +247,10 @@ const createUser = async (
opts?: CustomUserOpts | null
): Promise<PrismaType.UserCreateInput> => {
// build a unique name for our user
const uname = `${opts?.username ?? opts?.plan?.toLocaleLowerCase() ?? UserPlan.PRO.toLowerCase()}-${
workerInfo.workerIndex
}-${Date.now()}`;
const uname = `${opts?.username}-${workerInfo.workerIndex}-${Date.now()}`;
return {
username: uname,
name: opts?.name === undefined ? (opts?.plan ?? UserPlan.PRO).toUpperCase() : opts?.name,
plan: opts?.plan ?? UserPlan.PRO,
name: opts?.name,
email: `${uname}@example.com`,
password: await hashPassword(uname),
emailVerified: new Date(),
+2 -29
View File
@@ -1,5 +1,4 @@
import { expect } from "@playwright/test";
import { UserPlan } from "@prisma/client";
import { login } from "./fixtures/users";
import { test } from "./lib/fixtures";
@@ -13,10 +12,10 @@ test.describe("user can login & logout succesfully", async () => {
test.afterAll(async ({ users }) => {
await users.deleteAll();
});
test("login flow TRAIL user & logout using dashboard", async ({ page, users }) => {
test("login flow user & logout using dashboard", async ({ page, users }) => {
// log in trail user
await test.step("Log in", async () => {
const user = await users.create({ plan: UserPlan.TRIAL });
const user = await users.create();
await user.login();
const shellLocator = page.locator(`[data-testid=dashboard-shell]`);
@@ -24,11 +23,6 @@ test.describe("user can login & logout succesfully", async () => {
// expects the home page for an authorized user
await page.goto("/");
await expect(shellLocator).toBeVisible();
// Asserts to read the tested plan
const planLocator = shellLocator.locator(`[data-testid=plan-trial]`);
await expect(planLocator).toBeVisible();
await expect(planLocator).toHaveText("-TRIAL");
});
//
@@ -64,27 +58,6 @@ test.describe("Login and logout tests", () => {
await expect(page.locator(`[data-testid=login-form]`)).toBeVisible();
});
// Test login with all plans
const plans = [UserPlan.PRO, UserPlan.FREE];
plans.forEach((plan) => {
test(`Should login with a ${plan} account`, async ({ page, users }) => {
// Create user and login
const user = await users.create({ plan });
await user.login();
const shellLocator = page.locator(`[data-testid=dashboard-shell]`);
// expects the home page for an authorized user
await page.goto("/");
await expect(shellLocator).toBeVisible();
// Asserts to read the tested plan
const planLocator = shellLocator.locator(`[data-testid=plan-${plan.toLowerCase()}]`);
await expect(planLocator).toBeVisible();
await expect(planLocator).toHaveText(`-${plan}`);
});
});
test.describe("Login flow validations", async () => {
test("Should warn when user does not exist", async ({ page }) => {
const alertMessage = (await localize("en"))("no_account_exists");
+1 -2
View File
@@ -1,6 +1,5 @@
/* eslint-disable playwright/no-skipped-test */
import { expect } from "@playwright/test";
import { UserPlan } from "@prisma/client";
import { test } from "./lib/fixtures";
@@ -9,7 +8,7 @@ test.describe.configure({ mode: "serial" });
test.describe("Onboarding", () => {
test.describe("Onboarding v2", () => {
test("Onboarding Flow", async ({ page, users }) => {
const user = await users.create({ plan: UserPlan.TRIAL, completedOnboarding: false, name: null });
const user = await users.create({ completedOnboarding: false, name: null });
await user.login();
// tests whether the user makes it to /getting-started
@@ -1 +1 @@
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between Nameless and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Nameless","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
+11 -10
View File
@@ -14,7 +14,7 @@
"event_request_cancelled": "Ihr geplanter Termin wurde abgesagt",
"organizer": "Organisator",
"need_to_reschedule_or_cancel": "Möchten Sie einen neuen Termin finden oder absagen?",
"cancellation_reason": "Grund für Ihre Absage",
"cancellation_reason": "Grund für Ihre Absage (optional)",
"cancellation_reason_placeholder": "Warum sagen Sie ab? (optional)",
"rejection_reason": "Grund für die Ablehnung",
"rejection_reason_title": "Die Buchungsanfrage ablehnen?",
@@ -406,7 +406,7 @@
"forgotten_secret_description": "Wenn Sie dieses Passwort verloren oder vergessen haben, können Sie es ändern. Bitte beachten Sie, dass alle Integrationen, die dieses Geheimnis nutzen, aktualisiert werden müssen",
"current_incorrect_password": "Aktuelles Passwort ist falsch",
"password_hint_caplow": "Mischung aus Groß- und Kleinbuchstaben",
"password_hint_min": "Mindestens 7 Zeichen lang",
"password_hint_min": "Mindestens 8 Zeichen lang",
"password_hint_num": "Enthält mindestens 1 Zahl",
"invalid_password_hint": "Das Passwort muss mindestens 7 Zeichen lang sein und mindestens eine Nummer beinhalten, sowie eine Mischung aus Groß- und Kleinbuchstaben sein",
"incorrect_password": "Passwort ist falsch",
@@ -746,6 +746,7 @@
"trending_apps": "Angesagte Apps",
"explore_apps": "{{category}} Apps",
"installed_apps": "Installierte Apps",
"free_to_use_apps": "Kostenlos",
"no_category_apps": "Keine {{category}} Apps",
"no_category_apps_description_calendar": "Fügen Sie eine Kalender-App hinzu, in der nach Konflikten gesucht werden soll, um Doppelbuchungen zu vermeiden",
"no_category_apps_description_conferencing": "Versuchen Sie, eine Konferenz-App hinzuzufügen, um Videoanrufe mit Ihren Kunden zu integrieren",
@@ -762,7 +763,7 @@
"analytics": "Analyse",
"empty_installed_apps_headline": "Keine Apps installiert",
"empty_installed_apps_description": "Apps erlauben es Ihnen, Ihren Arbeitsfluss zu verbessern und Ihre Planung viel besser zu gestalten.",
"empty_installed_apps_button": "Entdecken Sie den App Store",
"empty_installed_apps_button": "Durchsuchen Sie den App Store",
"manage_your_connected_apps": "Verwalten Sie Ihre installierten Apps oder ändern Sie die Einstellungen",
"browse_apps": "Apps durchsuchen",
"features": "Funktionen",
@@ -784,6 +785,8 @@
"verify_wallet": "Wallet verifizieren",
"connect_metamask": "Metamask verbinden",
"create_events_on": "Erstelle Termine in:",
"enterprise_license": "Das ist eine Enterprise-Funktion",
"enterprise_license_description": "Um diese Funktion zu aktivieren, holen Sie sich einen Deployment-Schlüssel von der {{consoleUrl}}-Konsole und fügen Sie ihn als CALCOM_LICENSE_KEY zu Ihrer .env hinzu. Wenn Ihr Team bereits eine Lizenz hat, wenden Sie sich bitte an {{supportMail}} für Hilfe.",
"missing_license": "Lizenz fehlt",
"signup_requires": "Kommerzielle Lizenz erforderlich",
"next_steps": "Nächste Schritte",
@@ -894,6 +897,7 @@
"generate_api_key": "Api Key generieren",
"your_unique_api_key": "Ihr eindeutiger API-Key",
"copy_safe_api_key": "Kopieren Sie diesen API-Schlüssel und speichern Sie ihn an einem sicheren Ort. Wenn Sie diesen Schlüssel verlieren, müssen Sie einen neuen generieren.",
"zapier_setup_instructions": "<0>Gehen Sie zu: <1>Zapier-Einladungs-Link</1></0><1>Logge Sie sich in Ihr Zapier-Konto ein und erstelle einen neuen Zap.</1><2>Wählen Sie Cal.com als ihre Trigger-App aus. Wählen Sie auch ein Auslöserereignis.</2><3>Wähle Sie ihr Konto aus und geben dann ihren eindeutigen API-Key ein.</3><4>Teste Sie den Trigger.</4><5>Alles fertig!</5>",
"install_zapier_app": "Bitte installieren Sie zuerst die Zapier-App im App Store.",
"connect_apple_server": "Mit Apple-Server verbinden",
"connect_caldav_server": "Mit CalDav-Server verbinden",
@@ -1300,10 +1304,6 @@
"saml_sp_acs_url_copied": "ACS-URL kopiert!",
"saml_sp_entity_id_copied": "SP-Entitäts-ID kopiert!",
"saml_btn_configure": "Konfigurieren",
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie...",
"free_to_use_apps": "Kostenlos",
"enterprise_license": "Das ist eine Enterprise-Funktion",
"enterprise_license_description": "Um diese Funktion zu aktivieren, holen Sie sich einen Deployment-Schlüssel von der {{consoleUrl}}-Konsole und fügen Sie ihn als CALCOM_LICENSE_KEY zu Ihrer .env hinzu. Wenn Ihr Team bereits eine Lizenz hat, wenden Sie sich bitte an {{supportMail}} für Hilfe.",
"add_calendar": "Kalender hinzufügen",
"limit_future_bookings": "Zukünftige Termine begrenzen",
"limit_future_bookings_description": "Begrenzt, wie weit in die Zukunft dieser Termin gebucht werden kann",
@@ -1327,7 +1327,8 @@
"number_sms_notifications": "Telefonnummer (SMS-Benachrichtigungen)",
"attendee_email_workflow": "Teilnehmer E-Mail",
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
"invalid_credential": "Oh nein! Es sieht so aus, als ob die Berechtigung abgelaufen sind oder entzogen wurden. Bitte erneut installieren.",
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie...",
"invalid_credential": "Oh nein! Es sieht so aus, als ob die Berechtigung abgelaufen ist oder entzogen wurden. Bitte erneut installieren.",
"choose_common_schedule_team_event": "Gemeinsamen Zeitplan auswählen",
"choose_common_schedule_team_event_description": "Aktivieren Sie dies, wenn Sie einen gemeinsamen Zeitplan zwischen Hosts verwenden möchten. Wenn deaktiviert, ist jeder Host basierend auf seinen Standardplan buchbar.",
"reason": "Grund",
@@ -1335,7 +1336,7 @@
"sender_id_error_message": "Nur Buchstaben, Zahlen und Leerzeichen erlaubt (max. 11 Zeichen)",
"test_routing_form": "Testweiterleitungsformular",
"test_preview": "Vorschau testen",
"route_to": "Route zu",
"test_preview_description": "Testen Sie Ihr Leitungsformular, ohne Daten zu senden",
"route_to": "Weiterleiten zu",
"test_preview_description": "Testen Sie Ihr Weiterleitungsformular, ohne Daten zu senden",
"test_routing": "Testweiterleitung"
}
+25 -3
View File
@@ -940,7 +940,7 @@
"generate_api_key_description": "Generate an API key to use with {{appName}} at",
"your_unique_api_key": "Your unique API key",
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select {{appName}} as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"install_zapier_app": "Please first install the Zapier App in the app store.",
"connect_apple_server": "Connect to Apple Server",
"connect_caldav_server": "Connect to CalDav (Beta)",
@@ -1403,8 +1403,8 @@
"route_to": "Route to",
"test_preview_description": "Test your routing form without submitting any data",
"test_routing": "Test Routing",
"booking_limit_reached": "Booking Limit for this event type has been reached",
"fill_this_field": "Please fill in this field",
"payment_app_disabled": "An admin has disabled a payment app",
"edit_event_type": "Edit event type",
"collective_scheduling": "Collective Scheduling",
"make_it_easy_to_book": "Make it easy to book your team when everyone is available.",
"find_the_best_person": "Find the best person available and cycle through your team.",
@@ -1413,6 +1413,28 @@
"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",
"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.",
"payment_disabled_still_able_to_book": "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 reenables your payment method.",
"app_disabled_with_event_type": "The admin has disabled {{appName}} which affects your event type {{title}}.",
"app_disabled_video": "The admin has disabled {{appName}} which may affect your event types. If you have event types with {{appName}} as the location then it will default to Cal Video.",
"app_disabled_subject": "{{appName}} has been disabled",
"navigate_installed_apps": "Go to installed apps",
"disabled_calendar": "If you have another calendar installed new bookings will be added to it. If not then connect a new calendar so you do not miss any new bookings.",
"enable_apps": "Enable Apps",
"enable_apps_description": "Enable apps that users can integrate with Cal.com",
"app_is_enabled": "{{appName}} is enabled",
"app_is_disabled": "{{appName}} is disabled",
"keys_have_been_saved": "Keys have been saved",
"disable_app": "Disable App",
"disable_app_description": "Disabling this app could cause problems with how your users interact with Cal",
"edit_keys": "Edit Keys",
"apps_description": "Enable apps for your instance of Cal",
"no_available_apps": "There are no available apps",
"no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'",
"no_apps": "There are no apps enabled in this instance of Cal",
"apps_settings": "Apps settings",
"fill_this_field": "Please fill in this field",
"options": "Options",
"enter_option": "Enter Option {{index}}",
@@ -16,8 +16,8 @@
"event_request_cancelled": "Tu evento programado fue cancelado",
"organizer": "Organizador",
"need_to_reschedule_or_cancel": "¿Necesita reprogramar o cancelar?",
"cancellation_reason": "Motivo de la cancelación",
"cancellation_reason_placeholder": "¿Por qué razón estás cancelando? (opcional)",
"cancellation_reason": "Motivo de la cancelación (opcional)",
"cancellation_reason_placeholder": "¿Por qué razón está cancelando?",
"rejection_reason": "Motivo del rechazo",
"rejection_reason_title": "¿Rechazar la solicitud de reserva?",
"rejection_reason_description": "¿Estás seguro de que deseas rechazar la reserva? Le haremos saber a la persona que ha intentado reservar. Puedes indicar una razón a continuación.",
@@ -1228,8 +1228,8 @@
"exchange_authentication_ntlm": "Autenticación NTLM",
"exchange_compression": "Compresión GZip",
"routing_forms_description": "Aquí puedes ver todos los formularios y rutas que has creado.",
"routing_forms_send_email_owner": "Enviar email al propietario",
"routing_forms_send_email_owner_description": "Envía un correo electrónico al propietario cuando se envía el formulario",
"routing_forms_send_email_owner": "Enviar correo electrónico al propietario",
"routing_forms_send_email_owner_description": "Mande un correo electrónico al propietario cuando se envía el formulario",
"add_new_form": "Añadir formulario nuevo",
"form_description": "Crea tu formulario para dirigirlo a un agente de reservas",
"copy_link_to_form": "Copiar enlace al formulario",
@@ -1361,13 +1361,13 @@
"attendee_email_workflow": "Correo electrónico del asistente",
"attendee_email_info": "El correo electrónico de la persona que reserva",
"invalid_credential": "¡Oh no! Parece que el permiso ha caducado o se ha revocado. Vuelva a instalarlo.",
"choose_common_schedule_team_event": "Elija un horario común",
"choose_common_schedule_team_event_description": "Habilite esto si desea utilizar un cronograma común entre anfitriones. Cuando esté desactivado, cada anfitrión se reservará con base en su horario predeterminado.",
"choose_common_schedule_team_event": "Elige un horario común para todos",
"choose_common_schedule_team_event_description": "Habilite esto si desea utilizar un cronograma común entre anfitriones. Cuando esté desactivado, cada anfitrión se reservará en relación a su horario predeterminado.",
"reason": "Motivo",
"sender_id": "ID del remitente",
"sender_id_error_message": "Sólo se permiten letras, números y espacios (máx. 11 caracteres)",
"test_routing_form": "Prueba de formulario de ruta",
"test_preview": "Vista previa de prueba",
"test_preview": "Vista previa de la prueba",
"route_to": "Ruta hacia",
"test_preview_description": "Pruebe su formulario de ruta sin enviar ningún dato",
"test_routing": "Prueba de ruta"
+10 -10
View File
@@ -16,8 +16,8 @@
"event_request_cancelled": "Il tuo evento programmato è stato cancellato",
"organizer": "Organizzatore",
"need_to_reschedule_or_cancel": "È necessario riprogrammare o annullare?",
"cancellation_reason": "Motivo dell'annullamento",
"cancellation_reason_placeholder": "Perché stai annullando? (facoltativo)",
"cancellation_reason": "Motivo dell'annullamento (facoltativo)",
"cancellation_reason_placeholder": "Perché stai annullando?",
"rejection_reason": "Motivo del rifiuto",
"rejection_reason_title": "Rifiutare la richiesta di prenotazione?",
"rejection_reason_description": "Sei sicuro di voler rifiutare la prenotazione? Informeremo la persona che ha cercato di prenotare. Puoi indicare il motivo qui sotto.",
@@ -419,7 +419,7 @@
"forgotten_secret_description": "Nel caso la parola segreta venga persa o dimenticata, è possibile cambiarla, ma tieni presente che tutte le integrazioni che la utilizzano dovranno essere aggiornate",
"current_incorrect_password": "La password attuale non è corretta",
"password_hint_caplow": "Combinazione di caratteri maiuscoli e minuscoli",
"password_hint_min": "La lunghezza minima è di 7 caratteri",
"password_hint_min": "La lunghezza minima è di 8 caratteri",
"password_hint_num": "Deve contenere almeno 1 numero",
"invalid_password_hint": "La password deve avere una lunghezza minima di 7 caratteri e deve contenere almeno 1 numero e una combinazione di caratteri maiuscoli e minuscoli",
"incorrect_password": "La password non è corretta.",
@@ -504,7 +504,7 @@
"add_team_members_description": "Invita altri utenti a unirsi al tuo team",
"add_team_member": "Aggiungi un membro del team",
"invite_new_member": "Invita un nuovo membro",
"invite_new_member_description": "Nota: il <1>costo di un posto aggiuntivo (15$/m)</1> verrà addebitato sul tuo abbonamento.",
"invite_new_member_description": "Nota: Sul tuo abbonamento verrà addebitato <1>un posto aggiuntivo (15$/m)</1>.",
"invite_new_team_member": "Invita qualcuno nel tuo team.",
"change_member_role": "Cambia il ruolo del membro del team",
"disable_cal_branding": "Disabilita il branding {{appName}}",
@@ -601,7 +601,7 @@
"monthly_other": "mesi",
"yearly_one": "anno",
"yearly_other": "anni",
"plus_more": "altri {{count}}",
"plus_more": "Altri {{count}}",
"max": "Max",
"single_theme": "Tema Singolo",
"brand_color": "Colore del Marchio",
@@ -784,7 +784,7 @@
"analytics": "Analisi",
"empty_installed_apps_headline": "Nessuna app installata",
"empty_installed_apps_description": "Le app consentono di ottimizzare il flusso di lavoro e di migliorare significativamente la pianificazione.",
"empty_installed_apps_button": "Esplora l'App Store o installa dalle app sottostanti",
"empty_installed_apps_button": "Esplora l'App Store",
"manage_your_connected_apps": "Gestisci le app installate o modifica le impostazioni",
"browse_apps": "Sfoglia app",
"features": "Funzionalità",
@@ -1345,16 +1345,16 @@
"billing_frequency": "Frequenza di fatturazione",
"monthly": "Mensile",
"yearly": "Annuale",
"checkout": "Checkout",
"checkout": "Pagamento",
"your_team_disbanded_successfully": "Il tuo team è stato sciolto correttamente",
"error_creating_team": "Errore durante la creazione del team",
"you": "Tu",
"send_email": "Invia e-mail",
"member_already_invited": "Membro già invitato",
"enter_email_or_username": "Immettere un indirizzo e-mail o un nome utente",
"team_name_taken": "Nome già occupato",
"must_enter_team_name": "Immettere un nome del team",
"team_url_required": "Immettere un URL del team",
"team_name_taken": "Nome già utilizzato",
"must_enter_team_name": "Necessario immettere un nome per il team",
"team_url_required": "Immettere un URL per il team",
"team_url_taken": "URL già occupato",
"team_publish": "Pubblica team",
"number_sms_notifications": "Numero di telefono (notifiche SMS)",
+21 -21
View File
@@ -16,8 +16,8 @@
"event_request_cancelled": "スケジュールされていた予定がキャンセルされました",
"organizer": "主催者",
"need_to_reschedule_or_cancel": "再スケジュールまたはキャンセルが必要ですか?",
"cancellation_reason": "キャンセルの理由",
"cancellation_reason_placeholder": "キャンセルが必要な理由を教えてください (任意)",
"cancellation_reason": "キャンセルの理由 (オプション)",
"cancellation_reason_placeholder": "キャンセルが必要な理由をお教えください",
"rejection_reason": "拒否の理由",
"rejection_reason_title": "予約のリクエストを拒否しますか?",
"rejection_reason_description": "この予約を拒否してよろしいですか?予約を試みたユーザーに通知を送ります。拒否の理由は、下にご記入いただけます。",
@@ -108,7 +108,7 @@
"link_expires": "追記: 有効期限は {{expiresIn}} 時間後に切れます。",
"upgrade_to_per_seat": "座席ごとにアップグレードする",
"team_upgrade_seats_details": "あなたのチームに所属する {{memberCount}} 人のメンバーのうち、{{unpaidCount}} 名分のライセンスが未払いとなっています。ライセンスごとの料金は、${{seatPrice}} / 月ですので、あなたのメンバーシップの合計金額は、${{totalCost}} / 月になると予想されます。",
"team_upgrade_banner_description": "新しいチームプランをお試しいただきありがとうございます。あなたのチーム「{{teamName}}」はアップグレードが必要なようです。",
"team_upgrade_banner_description": "新しいチームプランをお試しいただき、ありがとうございます。あなたのチーム「{{teamName}}」には、アップグレードが必要なようです。",
"team_upgrade_banner_action": "アップグレードはこちら",
"team_upgraded_successfully": "チームのアップグレードが正常に完了しました!",
"use_link_to_reset_password": "以下のリンクを使用してパスワードをリセットしてください",
@@ -419,7 +419,7 @@
"forgotten_secret_description": "このシークレットを紛失または忘れた場合は変更が可能です。しかしこのシークレットを使っている連携はすべて最新のものにする必要があります",
"current_incorrect_password": "現在のパスワードが正しくありません",
"password_hint_caplow": "アルファベットの大文字と小文字を両方使用してください",
"password_hint_min": "7 文字以上入力してください",
"password_hint_min": "8 文字以上入力してください",
"password_hint_num": "少なくとも 1 文字は数字を含めてください",
"invalid_password_hint": "パスワードには少なくとも 1 文字以上数字を含め、アルファベットの大文字と小文字を両方使用した上で 7 文字以上の長さに設定してください",
"incorrect_password": "パスワードが正しくありません。",
@@ -504,7 +504,7 @@
"add_team_members_description": "他のユーザーをチームに招待する",
"add_team_member": "チームメンバーを追加",
"invite_new_member": "新しいチームメンバーを招待",
"invite_new_member_description": "注意:これによりサブスクリプションに<1>追加座席分(月15ドル)の費用が加算されます。</1>",
"invite_new_member_description": "注: これにより、サブスクリプションに<1>追加の座席料金 (15 米ドル/メンバー) が加算されます。</1>",
"invite_new_team_member": "チームに誰かを招待する。",
"change_member_role": "チームメンバーの役割を変更する",
"disable_cal_branding": "{{appName}} のブランディングを無効化",
@@ -601,7 +601,7 @@
"monthly_other": "月",
"yearly_one": "年",
"yearly_other": "年",
"plus_more": "{{count}} 回以上",
"plus_more": "あと {{count}} 回",
"max": "最大",
"single_theme": "シングルテーマ",
"brand_color": "ブランドカラー",
@@ -784,7 +784,7 @@
"analytics": "分析",
"empty_installed_apps_headline": "アプリがインストールされていません",
"empty_installed_apps_description": "アプリを使えば、ワークフローの質を高め、スケジュールライフを大幅に改善することができます。",
"empty_installed_apps_button": "App Store で探す",
"empty_installed_apps_button": "App Store を見る",
"manage_your_connected_apps": "インストール済みアプリの管理や設定の変更を行う",
"browse_apps": "アプリを閲覧",
"features": "特徴",
@@ -1229,7 +1229,7 @@
"exchange_compression": "GZip 圧縮",
"routing_forms_description": "作成したすべてのフォームとルートをここで確認できます。",
"routing_forms_send_email_owner": "所有者にメールを送信",
"routing_forms_send_email_owner_description": "フォーム送信時に所有者にメールを送信します",
"routing_forms_send_email_owner_description": "フォームの送信時に所有者にメールを送信します",
"add_new_form": "新しいフォームを追加",
"form_description": "予約者をルーティングするフォームを作成します",
"copy_link_to_form": "フォームへのリンクをコピー",
@@ -1296,7 +1296,7 @@
"fetching_calendars_error": "カレンダーの取得中にエラーが発生しました。<1>もう一度お試しいただく</1>か、カスタマーサポートまでお問い合わせください。",
"calendar_connection_fail": "カレンダーの接続に失敗しました",
"booking_confirmation_success": "正常に予約を確認しました",
"booking_rejection_success": "正常に予約を拒否しました",
"booking_rejection_success": "予約を正常に拒否しました",
"booking_confirmation_fail": "予約の確認に失敗しました",
"we_wont_show_again": "次回から表示しない",
"couldnt_update_timezone": "タイムゾーンを更新できませんでした",
@@ -1342,33 +1342,33 @@
"limit_future_bookings_description": "今後いつまでこのイベントを予約できるかを設定します",
"no_event_types": "イベントタイプは設定されていません",
"no_event_types_description": "{{name}}はあなたが予約するイベントのタイプを設定していません。",
"billing_frequency": "請求頻度",
"monthly": "月ごと",
"yearly": "年ごと",
"checkout": "精算",
"your_team_disbanded_successfully": "チームは正常に解散しました",
"billing_frequency": "請求の頻度",
"monthly": "毎月",
"yearly": "毎年",
"checkout": "チェックアウト",
"your_team_disbanded_successfully": "チームは正常に解散されました",
"error_creating_team": "チームの作成中にエラーが発生しました",
"you": "あなた",
"send_email": "メールを送信",
"member_already_invited": "メンバーは既に招待されています",
"enter_email_or_username": "メールアドレスまたはユーザー名を入力してください",
"team_name_taken": "この名前は既に使用されています",
"must_enter_team_name": "チーム名を入力する必要があります",
"team_url_required": "チームの URL を入力する必要があります",
"must_enter_team_name": "チーム名の入力は必須です",
"team_url_required": "チームの URL の入力は必須です",
"team_url_taken": "この URL は既に使用されています",
"team_publish": "チームを公開",
"number_sms_notifications": "電話番号(SMS 通知)",
"attendee_email_workflow": "出席者のメールアドレス",
"attendee_email_info": "予約者のメールアドレス",
"invalid_credential": "おっと!許可が期限切れになったか、取り消されたようです。もう一度インストールし直してください。",
"invalid_credential": "アクセス許可の有効期限が切れたか、取り消されたようです。もう一度インストールしてください。",
"choose_common_schedule_team_event": "共通のスケジュールを選択",
"choose_common_schedule_team_event_description": "ホスト間で共通のスケジュールを使用する場合は、これを有効にします。無効にすると、各ホストはデフォルトのスケジュールに基づいて予約されます。",
"choose_common_schedule_team_event_description": "ホスト間で共通のスケジュールを使用する場合には、これを有効にしてください。無効にすると、各ホストに対してはデフォルトのスケジュールに基づいて予約が行われます。",
"reason": "理由",
"sender_id": "送信者 ID",
"sender_id_error_message": "文字、数字、スペースのみ使用できます(最大 11 文字)",
"sender_id_error_message": "文字、数字、スペースのみが使用可能です (最大 11 文字)",
"test_routing_form": "ルーティングフォームをテスト",
"test_preview": "テストプレビュー",
"route_to": "ルート先",
"test_preview": "プレビューをテスト",
"route_to": "ルーティング先",
"test_preview_description": "データを送信せずにルーティングフォームをテストします",
"test_routing": "ルーティングをテスト"
}
+14 -14
View File
@@ -16,8 +16,8 @@
"event_request_cancelled": "회의 일정이 취소되었습니다.",
"organizer": "주최자",
"need_to_reschedule_or_cancel": "다시 일정을 잡거나 취소하시겠습니까?",
"cancellation_reason": "취소 사유",
"cancellation_reason_placeholder": "왜 취소를 선택하셨나요? (선택사항)",
"cancellation_reason": "취소 사유(선택사항)",
"cancellation_reason_placeholder": "취소하는 이유가 무엇인가요?",
"rejection_reason": "거부 사유",
"rejection_reason_title": "예약 요청을 거부하시겠습니까?",
"rejection_reason_description": "예약을 거부하시겠습니까? 예약하려고 시도하셨던 다른 분께 알려드릴 것입니다. 해당 사유는 아래에서 제공하실 수 있습니다.",
@@ -108,8 +108,8 @@
"link_expires": "{{expiresIn}} 시간 안에 만료됩니다.",
"upgrade_to_per_seat": "시트 단위로 업그레이드",
"team_upgrade_seats_details": "귀하의 팀원 {{memberCount}}명 중 {{unpaidCount}}명의 시트 비용이 미지급되었습니다. 시트당 ${{seatPrice}}/m로 계산시, 예상 총 멤버십 비용은 ${{totalCost}}/m입니다.",
"team_upgrade_banner_description": "당사의 새로운 팀 플랜을 사용해 주셔서 감사합니다. \"{{teamName}}\" 팀을 업그레이드해야 합니다.",
"team_upgrade_banner_action": "여기에서 업그레이드",
"team_upgrade_banner_description": "당사의 새로운 팀 플랜을 사용해 주셔서 감사합니다. 귀하의 \"{{teamName}}\" 팀에 업그레이드가 필요한 것으로 확인되었습니다.",
"team_upgrade_banner_action": "여기서 업그레이드",
"team_upgraded_successfully": "팀이 성공적으로 업그레이드되었습니다!",
"use_link_to_reset_password": "비밀번호를 재설정하시려면 아래 링크를 사용하세요.",
"hey_there": "안녕하세요.",
@@ -504,7 +504,7 @@
"add_team_members_description": "다른 사람을 팀에 초대",
"add_team_member": "팀원 추가",
"invite_new_member": "새 팀원 초대",
"invite_new_member_description": "참고: 이렇게 하면 해당 구독에 <1>추가 좌석 비용($15/m)</1>이 발생합니다.",
"invite_new_member_description": "참고: 이로 인해 해당 구독에 <1>추가 시트 비용($15/m)</1>이 발생합니다.",
"invite_new_team_member": "누군가를 팀에 초대하기",
"change_member_role": "팀원 역할 변경",
"disable_cal_branding": "{{appName}} 브랜딩 비활성화",
@@ -784,7 +784,7 @@
"analytics": "분석",
"empty_installed_apps_headline": "설치된 앱 없음",
"empty_installed_apps_description": "앱을 통해 워크플로를 강화하고 일정 관리 수명을 대폭 향상할 수 있습니다.",
"empty_installed_apps_button": "앱 스토어를 둘러보거나 아래 앱 중에서 설치",
"empty_installed_apps_button": "앱 스토어 둘러보기",
"manage_your_connected_apps": "설치된 앱 관리 또는 설정 변경",
"browse_apps": "앱 찾아보기",
"features": "기능",
@@ -1296,7 +1296,7 @@
"fetching_calendars_error": "캘린더를 가져오는 중 문제가 발생했습니다. <1>다시 시도</1>하거나 고객 지원에 문의하십시오.",
"calendar_connection_fail": "캘린더 연결 실패",
"booking_confirmation_success": "예약 확인 성공",
"booking_rejection_success": "예약 거부 성공",
"booking_rejection_success": "예약이 거부됨",
"booking_confirmation_fail": "예약 확인 실패",
"we_wont_show_again": "다시 표시 안 함",
"couldnt_update_timezone": "시간대를 업데이트할 수 없습니다",
@@ -1347,27 +1347,27 @@
"yearly": "연간",
"checkout": "체크아웃",
"your_team_disbanded_successfully": "귀하의 팀이 성공적으로 해산되었습니다",
"error_creating_team": "팀을 만드는 중 오류가 발생했습니다",
"you": "나",
"error_creating_team": "팀을 만드는 중 오류 발생",
"you": "귀하",
"send_email": "이메일 보내기",
"member_already_invited": "구성원이 이미 초대되었습니다",
"enter_email_or_username": "이메일 또는 사용자 이름을 입력하세요",
"team_name_taken": "이미 사용 중인 이름입니다",
"must_enter_team_name": "팀 이름을 입력하세요",
"team_url_required": "팀 URL을 입력해야 합니다",
"team_url_required": "팀 URL을 입력하세요",
"team_url_taken": "이미 사용 중인 URL입니다",
"team_publish": "팀 게시",
"number_sms_notifications": "전화번호 (SMS notifications)",
"attendee_email_workflow": "참석자 이메일",
"attendee_email_info": "예약자 이메일",
"invalid_credential": "죄송합니다! 권한이 만료되었거나 취소된 것 같습니다. 다시 설치하십시오.",
"choose_common_schedule_team_event": "공통 일정을 선택하세요",
"choose_common_schedule_team_event_description": "호스트 간에 공통 일정을 사용하려면 이 옵션을 활성화하십시오. 비활성화되면 각 호스트는 기본 일정에 따라 예약됩니다.",
"reason": "이유",
"choose_common_schedule_team_event": "공통 일정 선택",
"choose_common_schedule_team_event_description": "호스트 간에 공통 일정을 사용하려면 이 옵션을 활성화하세요. 비활성화되면 각 호스트는 기본 일정에 따라 예약됩니다.",
"reason": "사유",
"sender_id": "발신자 ID",
"sender_id_error_message": "영문, 숫자, 공백만 허용(최대 11자)",
"test_routing_form": "라우팅 양식 테스트",
"test_preview": "테스트 미리보기",
"test_preview": "미리보기 테스트",
"route_to": "목표 경로",
"test_preview_description": "데이터를 제출하지 않고 라우팅 양식 테스트",
"test_routing": "라우팅 테스트"
@@ -17,7 +17,7 @@
"organizer": "Organizador",
"need_to_reschedule_or_cancel": "Precisa reagendar ou cancelar?",
"cancellation_reason": "Motivo do cancelamento",
"cancellation_reason_placeholder": "Por que você está cancelando? (opcional)",
"cancellation_reason_placeholder": "Por que você está cancelando?",
"rejection_reason": "Motivo da recusa",
"rejection_reason_title": "Recusar a solicitação de reserva?",
"rejection_reason_description": "Tem certeza de que deseja recusar a reserva? Avisaremos a pessoa que tentou reservar. Se quiser, informe um motivo abaixo.",
@@ -419,7 +419,7 @@
"forgotten_secret_description": "Caso tenha perdido ou esquecido o segredo, você poderá alterá-lo. Mas fique ciente de que todas as integrações com esse segredo precisarão ser atualizadas",
"current_incorrect_password": "Senha atual está incorreta",
"password_hint_caplow": "Mistura de letras maiúsculas e minúsculas",
"password_hint_min": "Mínimo de sete caracteres",
"password_hint_min": "Mínimo de oito caracteres",
"password_hint_admin_min": "No mínimo 15 caracteres",
"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",
@@ -1356,7 +1356,7 @@
"team_name_taken": "Este nome já está em uso",
"must_enter_team_name": "É preciso inserir um nome de equipe",
"team_url_required": "É preciso inserir uma URL de equipe",
"team_url_taken": "Esta URL já está em uso",
"team_url_taken": "Este URL já está em uso",
"team_publish": "Publicar equipe",
"number_sms_notifications": "Número de telefone (notificações de SMS)",
"attendee_email_workflow": "E-mail do participante",
@@ -1368,7 +1368,7 @@
"sender_id": "ID do remetente",
"sender_id_error_message": "Apenas letras, números e espaços permitidos (no máx. 11 caracteres)",
"test_routing_form": "Testar formulário de roteamento",
"test_preview": "Testar prévia",
"test_preview": "Visualizar teste",
"route_to": "Direcionar para",
"test_preview_description": "Teste seu formulário de roteamento sem enviar dados",
"test_routing": "Testar roteamento"
+19 -19
View File
@@ -16,8 +16,8 @@
"event_request_cancelled": "Ваша запланированная встреча была отменена",
"organizer": "Организатор",
"need_to_reschedule_or_cancel": "Хотите перенести или отменить?",
"cancellation_reason": "Причина отмены",
"cancellation_reason_placeholder": "Укажите причину отмены (необязательное поле)",
"cancellation_reason": "Причина отмены (необязательно)",
"cancellation_reason_placeholder": "Укажите причину отмены подписки",
"rejection_reason": "Причина отказа",
"rejection_reason_title": "Отказаться от бронирования?",
"rejection_reason_description": "Отказаться от брони? Мы сообщим пользователю, который пытался оформить бронь. Вы можете указать причину отказа ниже.",
@@ -108,7 +108,7 @@
"link_expires": "p.s. Это истекает через {{expiresIn}} часов.",
"upgrade_to_per_seat": "Перейти на оплату из расчета за количество мест",
"team_upgrade_seats_details": "В вашей команде {{memberCount}} пользователей; из них не оплачено еще {{unpaidCount}} мест. При оплате ${{seatPrice}} в месяц за место общая сумма к оплате составляет ${{totalCost}} в месяц.",
"team_upgrade_banner_description": "Спасибо, что перешли на наш новый тариф team. Мы заметили, что вам нужно обновить \"{{teamName}}\" команды.",
"team_upgrade_banner_description": "Спасибо, что решили попробовать наш новый тариф Team. Необходимо обновить команду «{{teamName}}».",
"team_upgrade_banner_action": "Обновить",
"team_upgraded_successfully": "Вы успешно перешли на новый тариф для своей команды!",
"use_link_to_reset_password": "Используйте ссылку ниже, чтобы сбросить пароль",
@@ -420,7 +420,7 @@
"current_incorrect_password": "Неверный текущий пароль",
"password_hint_caplow": "Используйте смесь заглавных и строчных букв",
"password_hint_min": "Не короче 7 символов",
"password_hint_admin_min": "Минимум 15 символов",
"password_hint_admin_min": "Не короче 15 символов",
"password_hint_num": "Не менее 1 цифры",
"invalid_password_hint": "Пароль должен быть не короче 7 символов и содержать заглавные и строчные буквы, а также не менее одной цифры",
"incorrect_password": "Неверный пароль",
@@ -505,7 +505,7 @@
"add_team_members_description": "Пригласите других пользователей в свою команду",
"add_team_member": "Добавить участника команды",
"invite_new_member": "Пригласить нового участника команды",
"invite_new_member_description": "Примечание. Стоимость подписки увеличится на <1>$15/месяц за дополнительное место</1>.",
"invite_new_member_description": "Примечание: стоимость подписки увеличится на <1>15 $/месяц за дополнительное место</1>.",
"invite_new_team_member": "Пригласите кого-нибудь в вашу команду.",
"change_member_role": "Изменить роль члена команды",
"disable_cal_branding": "Отключить брендинг {{appName}}",
@@ -602,7 +602,7 @@
"monthly_other": "мес.",
"yearly_one": "год",
"yearly_other": "г.",
"plus_more": "еще {{count}}",
"plus_more": "Еще {{count}}",
"max": "Макс.",
"single_theme": "Тема оформления",
"brand_color": "Цвет бренда",
@@ -785,7 +785,7 @@
"analytics": "Аналитика",
"empty_installed_apps_headline": "Нет установленных приложений",
"empty_installed_apps_description": "Приложения позволяют значительно улучшить рабочий процесс и жить по расписанию.",
"empty_installed_apps_button": "Перейти в App Store или установить одно из приложений ниже",
"empty_installed_apps_button": "Перейти в App Store",
"manage_your_connected_apps": "Управление установленными приложениями и изменение настроек",
"browse_apps": "Обзор приложений",
"features": "Возможности",
@@ -1230,7 +1230,7 @@
"exchange_compression": "Сжатие GZip",
"routing_forms_description": "Здесь показываются все созданные вами формы и маршруты.",
"routing_forms_send_email_owner": "Отправить письмо владельцу",
"routing_forms_send_email_owner_description": "Отправляет сообщение владельцу при отправке формы",
"routing_forms_send_email_owner_description": "Отправляет владельцу электронное письмо при отправке формы",
"add_new_form": "Добавить новую форму",
"form_description": "Создайте форму для перенаправления пользователей при бронировании",
"copy_link_to_form": "Скопировать ссылку на форму",
@@ -1344,8 +1344,8 @@
"no_event_types": "Типы событий не установлены",
"no_event_types_description": "{{name}} не установил(а) для вас типы событий, которые вы можете бронировать.",
"billing_frequency": "Периодичность расчетов",
"monthly": "Ежемесячно",
"yearly": "Ежегодно",
"monthly": "Ежемесячная",
"yearly": "Ежегодная",
"checkout": "Оплатить заказ",
"your_team_disbanded_successfully": "Ваша команда расформирована",
"error_creating_team": "Ошибка при создании команды",
@@ -1353,24 +1353,24 @@
"send_email": "Отправить письмо",
"member_already_invited": "Участник уже приглашен",
"enter_email_or_username": "Введите адрес электронной почты или имя пользователя",
"team_name_taken": "Это имя уже занято",
"team_name_taken": "Это название уже занято",
"must_enter_team_name": "Необходимо ввести название команды",
"team_url_required": "Необходимо ввести URL команды",
"team_url_taken": "Этот URL уже занят",
"team_url_required": "Необходимо ввести URL-адрес команды",
"team_url_taken": "Этот URL-адрес уже занят",
"team_publish": "Опубликовать команду",
"number_sms_notifications": "Номер телефона (SMS-уведомления)",
"attendee_email_workflow": "Электронная почта участника",
"attendee_email_info": "Электронная почта участника, на которого оформляется бронирование",
"invalid_credential": "Похоже, разрешение истекло или отозвано. Возобновите его.",
"invalid_credential": "Похоже, разрешение истекло или было отозвано. Необходима переустановка.",
"choose_common_schedule_team_event": "Выберите общее расписание",
"choose_common_schedule_team_event_description": "Позволяет организаторам использовать общее расписание. Если общее расписание отключено, бронирование выполняется для каждого организатора в расписании по умолчанию.",
"choose_common_schedule_team_event_description": "Позволяет организаторам использовать общее расписание. Если общее расписание отключено, то для каждого организатора бронирование выполняется в соответствии с его индивидуальным расписанием по умолчанию.",
"reason": "Причина",
"sender_id": "ID отправителя",
"sender_id_error_message": "Только буквы, цифры и пробелы (макс. 11 символов)",
"test_routing_form": "Тестировать форму машрутизации",
"test_preview": "Тестировать предварительный просмотр",
"route_to": "Путь к",
"test_routing_form": "Проверить форму машрутизации",
"test_preview": "Проверить предварительный просмотр",
"route_to": "Цель маршрута",
"test_preview_description": "Проверить форму маршрутизации без отправки данных",
"test_routing": "Тестировать машрутизацию",
"test_routing": "Проверить машрутизацию",
"booking_limit_reached": "Достигнут лимит бронирования для этого типа события"
}
@@ -108,7 +108,7 @@
"link_expires": "p.s. Ističe za {{expiresIn}} sati.",
"upgrade_to_per_seat": "Pretplati se na Po-Mestu",
"team_upgrade_seats_details": "Od {{memberCount}} članova u vašem timu, {{unpaidCount}} mesta/o nisu plaćena. Za ${{seatPrice}}/mesečno po mestu procenjena celokupna cena vašeg članstva je ${{totalCost}}/mesečno.",
"team_upgrade_banner_description": "Hvala što probate naš novi plan tima. Primetili smo da vaš tim „{{teamName}}“ treba da se nadogradi.",
"team_upgrade_banner_description": "Hvala vam što isprobavate naš novi plan za timove. Primetili smo da vaš tim „{{teamName}}“ treba da se nadogradi.",
"team_upgrade_banner_action": "Nadogradite ovde",
"team_upgraded_successfully": "Vaš tim je uspešno pretplaćen!",
"use_link_to_reset_password": "Resetujte lozinku koristeći link ispod",
@@ -504,7 +504,7 @@
"add_team_members_description": "Pozovite druge da se pridruže vašem timu",
"add_team_member": "Dodaj člana tima",
"invite_new_member": "Pozovite novog člana",
"invite_new_member_description": "Napomena: Ovim će <1>biti naplaćeno dodatno mesto (15 $/m)</1> putem vaše pretplate.",
"invite_new_member_description": "</1>Napomena: Ovo će <1>povećati vašu pretplatu za dodatno mesto (15 $/mesečno)</1>.",
"invite_new_team_member": "Pozovite nekog u vaš tim.",
"change_member_role": "Promenite ulogu članu tima",
"disable_cal_branding": "Onemogući brendiranje {{appName}}",
@@ -1366,7 +1366,7 @@
"reason": "Razlog",
"sender_id": "ID pošiljaoca",
"sender_id_error_message": "Dozvoljena su samo slova, brojevi i razmaci (maks. 11 karaktera)",
"test_routing_form": "Obrazac testiranja usmeravanja",
"test_routing_form": "Testiranje obrasca za usmeravanje",
"test_preview": "Pregled testa",
"route_to": "Usmeri na",
"test_preview_description": "Proverite obrazac za usmeravanje bez slanja bilo kog podatka",
+13 -13
View File
@@ -109,7 +109,7 @@
"upgrade_to_per_seat": "Перейдіть на версію з оплатою за місце",
"team_upgrade_seats_details": "У вас стільки неоплачених місць для учасників команди: {{unpaidCount}} із {{memberCount}}. Одне місце коштує {{seatPrice}} $ за місяць, тому загальна вартість вашого членства складатиме приблизно {{totalCost}} $ за місяць.",
"team_upgrade_banner_description": "Дякуємо, що спробували наш новий план для команд! Ми помітили, що вашу команду «{{teamName}}» потрібно перевести на план вищого рівня.",
"team_upgrade_banner_action": "Перейдіть на інший план тут",
"team_upgrade_banner_action": "Перейдіть на інший план",
"team_upgraded_successfully": "Версію для вашої команди оновлено!",
"use_link_to_reset_password": "Скористайтеся посиланням нижче, щоб скинути пароль",
"hey_there": "Привіт!",
@@ -419,7 +419,7 @@
"forgotten_secret_description": "Якщо ви загубили або забули цей таємний код, його можна змінити, але в такому випадку потрібно буде оновити всі інтеграції, що використовують його.",
"current_incorrect_password": "Поточний пароль неправильний",
"password_hint_caplow": "Комбінація великих і малих літер",
"password_hint_min": "Не менше ніж 7 символів завдовжки",
"password_hint_min": "Щонайменше 8 символів завдовжки",
"password_hint_num": "Містить принаймні 1 цифру",
"invalid_password_hint": "Пароль має містити не менше ніж 7 символів: принаймні одну цифру та комбінацію великих і малих літер",
"incorrect_password": "Пароль неправильний.",
@@ -784,7 +784,7 @@
"analytics": "Аналітика",
"empty_installed_apps_headline": "Не встановлено жодних додатків",
"empty_installed_apps_description": "Додатки дають змогу оптимізувати робочий процес і спростити роботу з графіком.",
"empty_installed_apps_button": "Переглянути магазин додатків або встановити наведені нижче додатки",
"empty_installed_apps_button": "Переглянути магазин додатків",
"manage_your_connected_apps": "Керуйте встановленими додатками й налаштуваннями",
"browse_apps": "Огляд додатків",
"features": "Функції",
@@ -1228,7 +1228,7 @@
"exchange_authentication_ntlm": "Автентифікація NTLM",
"exchange_compression": "Стиснення GZip",
"routing_forms_description": "Тут можна переглянути всі створені вами форми й переспрямування.",
"routing_forms_send_email_owner": "Надсилати власнику електронний лист",
"routing_forms_send_email_owner": "Надіслати власнику електронний лист",
"routing_forms_send_email_owner_description": "Коли надсилається форма, власник отримує електронний лист",
"add_new_form": "Додати нову форму",
"form_description": "Створіть власну форму для переспрямування автора бронювання",
@@ -1345,30 +1345,30 @@
"billing_frequency": "Періодичність виставлення рахунків",
"monthly": "Щомісяця",
"yearly": "Щороку",
"checkout": "Оформлення замовлення",
"checkout": "Оплата",
"your_team_disbanded_successfully": "Вашу команду розформовано",
"error_creating_team": "Помилка створення команди",
"you": "Ви",
"send_email": "Надіслати ел. лист",
"send_email": "Надіслати лист",
"member_already_invited": "Учасника вже запрошено",
"enter_email_or_username": "Введіть адресу електронної пошти або ім’я користувача",
"enter_email_or_username": "Введіть електронну адресу або ім’я користувача",
"team_name_taken": "Це ім’я вже зайнято",
"must_enter_team_name": "Необхідно ввести назву команди",
"team_url_required": "Необхідно ввести URL-адресу команди",
"must_enter_team_name": "Потрібно ввести назву команди",
"team_url_required": "Потрібно ввести URL-адресу команди",
"team_url_taken": "Цю URL-адресу вже зайнято",
"team_publish": "Опублікувати команду",
"number_sms_notifications": "Номер телефону (SMS-сповіщення)",
"attendee_email_workflow": "Адреса ел. пошти учасника",
"attendee_email_info": "Адреса ел. пошти особи, яка бронює",
"invalid_credential": "Отакої! Схоже, дозвіл більше не дійсний або його відкликано. Повторіть інсталяцію.",
"invalid_credential": "Отакої! Схоже, дозвіл більше не дійсний або його відкликано. Перевстановіть додаток знову.",
"choose_common_schedule_team_event": "Виберіть спільний розклад",
"choose_common_schedule_team_event_description": "Увімкніть цей параметр, щоб використовувати спільний для двох ведучих розклад. Якщо цей параметр вимкнено, бронювання для кожного з ведучих відбуватиметься за їхніми власними графіками.",
"reason": "Причина",
"sender_id": "Ідентифікатор відправника",
"sender_id": "ID відправника",
"sender_id_error_message": "Дозволяються тільки літери, цифри та пробіли (макс. 11 символів)",
"test_routing_form": "Перевірка форми переспрямування",
"test_preview": "Перевірити попередній перегляд",
"route_to": "Куди переспрямувати",
"test_preview_description": "Перевірка вашої форми переспрямування без надсилання даних",
"route_to": "Кінцева точка",
"test_preview_description": "Перевірте свою форму переспрямування без надсилання даних",
"test_routing": "Перевірка переспрямування"
}
@@ -108,7 +108,7 @@
"link_expires": "p.s. Link hết hạn trong vòng {{expiresIn}} tiếng.",
"upgrade_to_per_seat": "Nâng cấp từng tài khoản",
"team_upgrade_seats_details": "Trong tổng {{memberCount}} thành viên của nhóm bạn, {{unpaidCount}} thành viên chưa được thanh toán. Với {{seatPrice}}$/tháng mỗi thành viên, tổng chi phí cho nhóm bạn là {{totalCost}}$/tháng.",
"team_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói mới dành cho nhóm. Chúng tôi để ý thấy nhóm \"{{teamName}}\" của bạn cần được nâng cấp.",
"team_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói kế hoạch mới dành cho nhóm. Chúng tôi để ý thấy nhóm \"{{teamName}}\" của bạn cần được nâng cấp.",
"team_upgrade_banner_action": "Nâng cấp tại đây",
"team_upgraded_successfully": "Nhóm bạn đã được nâng cấp thành công!",
"use_link_to_reset_password": "Sử dụng đường link dưới đây để thay đổi mật khẩu của bạn",
@@ -504,7 +504,7 @@
"add_team_members_description": "Mời người khác tham gia nhóm của bạn",
"add_team_member": "Thêm thành viên nhóm",
"invite_new_member": "Mời một thành viên mới",
"invite_new_member_description": "Lưu ý: Cái này sẽ <1>tốn phí thêm thành viên (15$/tháng)</1> cho gói đăng ký của bạn.",
"invite_new_member_description": "Lưu ý: Việc này sẽ <1>mất phí bổ sung chỗ ngồi (15$/tháng)</1> cho gói đăng ký của bạn.",
"invite_new_team_member": "Mời ai đó vào nhóm của bạn.",
"change_member_role": "Thay đổi vai trò thành viên trong nhóm",
"disable_cal_branding": "Tắt thương hiệu {{appName}}",
@@ -16,8 +16,8 @@
"event_request_cancelled": "預定的活動取消了",
"organizer": "主辦",
"need_to_reschedule_or_cancel": "要重新規劃還是取消嗎?",
"cancellation_reason": "取消的原因",
"cancellation_reason_placeholder": "為什麼打算取消?(選填)",
"cancellation_reason": "取消原因(選填)",
"cancellation_reason_placeholder": "您為什麼要取消?",
"rejection_reason": "拒絕的原因",
"rejection_reason_title": "拒絕預約嗎?",
"rejection_reason_description": "確定要拒絕預約嗎?我們會讓想要進行預約的對方知道。可以在下方提供原因。",
@@ -109,7 +109,7 @@
"upgrade_to_per_seat": "升級為算座位數",
"team_upgrade_seats_details": "團隊裡一共有 {{memberCount}} 位成員,{{unpaidCount}} 個座位不是付費的。每個月每個座位的費用是 ${{seatPrice}},因此會員資格總共是每個月 ${{totalCost}}。",
"team_upgrade_banner_description": "謝謝您試用我們新的團隊方案。我們注意到您的團隊「{{teamName}}」需要升級。",
"team_upgrade_banner_action": "到這裡升級",
"team_upgrade_banner_action": "在此處升級",
"team_upgraded_successfully": "團隊成功升級!",
"use_link_to_reset_password": "使用以下連結重置密碼",
"hey_there": "哈囉,",
@@ -419,7 +419,7 @@
"forgotten_secret_description": "如果您已遺失或忘記此密碼,可以變更密碼,但請注意,所有使用此密碼的整合都需更新",
"current_incorrect_password": "現有密碼不正確",
"password_hint_caplow": "大小寫字母組合",
"password_hint_min": "至少 7 個字元長",
"password_hint_min": "至少 8 個字元長",
"password_hint_num": "至少包含 1 個數字",
"invalid_password_hint": "密碼長度至少要有 7 個字元,其中包含至少一個數字和大小寫字母組合",
"incorrect_password": "密碼不正確。",
@@ -601,7 +601,7 @@
"monthly_other": "月",
"yearly_one": "年",
"yearly_other": "年",
"plus_more": "{{count}} 個以上",
"plus_more": "多 {{count}} 個",
"max": "最大",
"single_theme": "主題",
"brand_color": "品牌顏色",
@@ -784,7 +784,7 @@
"analytics": "分析",
"empty_installed_apps_headline": "沒有已安裝的應用程式",
"empty_installed_apps_description": "應用程式使您能夠增強您的工作流程並顯著改善您的日程安排。",
"empty_installed_apps_button": "探索 App Store 或從下列應用程式中選擇安裝",
"empty_installed_apps_button": "瀏覽 App Store",
"manage_your_connected_apps": "管理已安裝的應用程式或是變更設定",
"browse_apps": "瀏覽應用程式",
"features": "功能",
@@ -1229,7 +1229,7 @@
"exchange_compression": "GZip 壓縮",
"routing_forms_description": "您可以在這裡查看您已建立的所有表單和引導路徑。",
"routing_forms_send_email_owner": "傳送電子郵件給擁有者",
"routing_forms_send_email_owner_description": "表單提交時傳送電子郵件給擁有者",
"routing_forms_send_email_owner_description": "表單提交後傳送電子郵件給擁有者",
"add_new_form": "新增新表單",
"form_description": "建立您的表單來引導預約者",
"copy_link_to_form": "複製連結至表單",
@@ -1343,10 +1343,10 @@
"no_event_types": "未設定活動類型",
"no_event_types_description": "{{name}} 尚未設定任何活動類型供您預約。",
"billing_frequency": "付費頻率",
"monthly": "月",
"yearly": "年",
"monthly": "每月",
"yearly": "每年",
"checkout": "結帳",
"your_team_disbanded_successfully": "解散團隊成功",
"your_team_disbanded_successfully": "已成功解散您的團隊",
"error_creating_team": "建立團隊時發生錯誤",
"you": "您",
"send_email": "傳送電子郵件",
@@ -1360,15 +1360,15 @@
"number_sms_notifications": "電話號碼 (簡訊通知)",
"attendee_email_workflow": "與會者電子郵件",
"attendee_email_info": "預約人電子郵件",
"invalid_credential": "糟糕!權限似乎過期或遭到撤銷了。請再次重新安裝。",
"choose_common_schedule_team_event": "選擇一般行程表",
"choose_common_schedule_team_event_description": "若要在多個主辦者之間使用一般行程表,請啟用此選項。停用時,會根據每個主辦者預設的行程表預約對方的時間。",
"invalid_credential": "糟糕!權限似乎過期或遭到撤銷。請重新安裝。",
"choose_common_schedule_team_event": "選擇公用行程表",
"choose_common_schedule_team_event_description": "若要在多個主辦者之間使用公用行程表,請啟用此選項。停用時,會根據每個主辦者預設的行程表預約對方的時間。",
"reason": "原因",
"sender_id": "傳送者 ID",
"sender_id_error_message": "只能使用字母、數字和空格 (最多 11 個字元)",
"test_routing_form": "測試引導表單",
"test_preview": "測試預覽",
"route_to": "引導至",
"test_preview_description": "不提交任何資料並測試引導表單",
"test_preview_description": "測試引導表單而不提交任何資料",
"test_routing": "測試引導"
}
+16
View File
@@ -62,6 +62,7 @@ function generateFiles() {
const browserOutput = [`import dynamic from "next/dynamic"`];
const metadataOutput = [];
const schemasOutput = [];
const appKeysSchemasOutput = [];
const serverOutput = [];
const appDirs: { name: string; path: string }[] = [];
@@ -190,6 +191,20 @@ function generateFiles() {
})
);
appKeysSchemasOutput.push(
...getObjectExporter("appKeysSchemas", {
fileToBeImported: "zod.ts",
// Import path must have / even for windows and not \
importBuilder: (app) =>
`import { appKeysSchema as ${getVariableName(app.name)}_keys_schema } from "./${app.path.replace(
/\\/g,
"/"
)}/zod";`,
// Key must be appId as this is used by eventType metadata and lookup is by appId
entryBuilder: (app) => ` "${getAppId(app)}":${getVariableName(app.name)}_keys_schema ,`,
})
);
browserOutput.push(
...getObjectExporter("InstallAppButtonMap", {
fileToBeImported: "components/InstallAppButton.tsx",
@@ -225,6 +240,7 @@ function generateFiles() {
["apps.server.generated.ts", serverOutput],
["apps.browser.generated.tsx", browserOutput],
["apps.schemas.generated.ts", schemasOutput],
["apps.keys-schemas.generated.ts", appKeysSchemasOutput],
];
filesToGenerate.forEach(([fileName, output]) => {
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
+8 -2
View File
@@ -26,7 +26,10 @@ export async function getAppWithMetadata(app: { dirName: string }) {
/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
export async function getAppRegistry() {
const dbApps = await prisma.app.findMany({ select: { dirName: true, slug: true, categories: true } });
const dbApps = await prisma.app.findMany({
where: { enabled: true },
select: { dirName: true, slug: true, categories: true, enabled: true },
});
const apps = [] as Omit<App, "key">[];
for await (const dbapp of dbApps) {
const app = await getAppWithMetadata(dbapp);
@@ -51,7 +54,10 @@ export async function getAppRegistry() {
}
export async function getAppRegistryWithCredentials(userId: number) {
const dbApps = await prisma.app.findMany({ include: { credentials: { where: { userId } } } });
const dbApps = await prisma.app.findMany({
where: { enabled: true },
include: { credentials: { where: { userId } } },
});
const apps = [] as (Omit<App, "key"> & {
credentials: Credential[];
})[];
+3 -2
View File
@@ -26,7 +26,7 @@ export default function AppCard({
const [animationRef] = useAutoAnimate<HTMLDivElement>();
return (
<div className="mb-4 mt-2 rounded-md border border-gray-200">
<div className={`mb-4 mt-2 rounded-md border border-gray-200 ${!app.enabled && "grayscale"}`}>
<div className="p-4 text-sm sm:p-8">
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
@@ -44,6 +44,7 @@ export default function AppCard({
{app?.isInstalled ? (
<div className="ml-auto flex items-center">
<Switch
disabled={!app.enabled}
onCheckedChange={(enabled) => {
if (switchOnClick) {
switchOnClick(enabled);
@@ -54,7 +55,7 @@ export default function AppCard({
/>
</div>
) : (
<OmniInstallAppButton className="ml-auto flex items-center" appId={app?.slug} />
<OmniInstallAppButton className="ml-auto flex items-center" appId={app.slug} />
)}
</div>
</div>
@@ -0,0 +1,34 @@
import { useMemo } from "react";
import { classNames } from "@calcom/lib";
import { HorizontalTabs, VerticalTabs } from "@calcom/ui";
import getAppCategories from "../_utils/getAppCategories";
const AppCategoryNavigation = ({
baseURL,
children,
containerClassname,
className,
}: {
baseURL: string;
children: React.ReactNode;
containerClassname: string;
className?: string;
}) => {
const appCategories = useMemo(() => getAppCategories(baseURL), [baseURL]);
return (
<div className={classNames("flex flex-col p-2 md:p-0 xl:flex-row", className)}>
<div className="hidden xl:block">
<VerticalTabs tabs={appCategories} sticky linkProps={{ shallow: true }} />
</div>
<div className="block overflow-x-scroll xl:hidden">
<HorizontalTabs tabs={appCategories} linkProps={{ shallow: true }} />
</div>
<main className={containerClassname}>{children}</main>
</div>
);
};
export default AppCategoryNavigation;
+2
View File
@@ -7,3 +7,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
isSunrise: z.boolean(),
})
);
export const appKeysSchema = z.object({});
@@ -0,0 +1,43 @@
import { Icon } from "@calcom/ui";
const getAppCategories = (baseURL: string) => {
return [
{
name: "calendar",
href: `${baseURL}/calendar`,
icon: Icon.FiCalendar,
},
{
name: "conferencing",
href: `${baseURL}/conferencing`,
icon: Icon.FiVideo,
},
{
name: "payment",
href: `${baseURL}/payment`,
icon: Icon.FiCreditCard,
},
{
name: "automation",
href: `${baseURL}/automation`,
icon: Icon.FiShare2,
},
{
name: "analytics",
href: `${baseURL}/analytics`,
icon: Icon.FiBarChart,
},
{
name: "web3",
href: `${baseURL}/web3`,
icon: Icon.FiBarChart,
},
{
name: "other",
href: `${baseURL}/other`,
icon: Icon.FiGrid,
},
];
};
export default getAppCategories;
@@ -20,6 +20,7 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
dirName: "applecalendar",
} as AppMeta;
export default metadata;
@@ -25,6 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
key: symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY!),
userId: user.id,
appId: "apple-calendar",
invalid: false,
};
try {
@@ -0,0 +1,47 @@
/**
This file is autogenerated using the command `yarn app-store:build --watch`.
Don't modify this file manually.
**/
import { appKeysSchema as dailyvideo_keys_schema } from "./dailyvideo/zod";
import { appKeysSchema as routing_forms_keys_schema } from "./ee/routing-forms/zod";
import { appKeysSchema as fathom_keys_schema } from "./fathom/zod";
import { appKeysSchema as ga4_keys_schema } from "./ga4/zod";
import { appKeysSchema as giphy_keys_schema } from "./giphy/zod";
import { appKeysSchema as googlecalendar_keys_schema } from "./googlecalendar/zod";
import { appKeysSchema as hubspot_keys_schema } from "./hubspot/zod";
import { appKeysSchema as larkcalendar_keys_schema } from "./larkcalendar/zod";
import { appKeysSchema as office365calendar_keys_schema } from "./office365calendar/zod";
import { appKeysSchema as office365video_keys_schema } from "./office365video/zod";
import { appKeysSchema as plausible_keys_schema } from "./plausible/zod";
import { appKeysSchema as qr_code_keys_schema } from "./qr_code/zod";
import { appKeysSchema as rainbow_keys_schema } from "./rainbow/zod";
import { appKeysSchema as salesforce_keys_schema } from "./salesforce/zod";
import { appKeysSchema as stripepayment_keys_schema } from "./stripepayment/zod";
import { appKeysSchema as tandemvideo_keys_schema } from "./tandemvideo/zod";
import { appKeysSchema as vital_keys_schema } from "./vital/zod";
import { appKeysSchema as wordpress_keys_schema } from "./wordpress/zod";
import { appKeysSchema as zapier_keys_schema } from "./zapier/zod";
import { appKeysSchema as zoomvideo_keys_schema } from "./zoomvideo/zod";
export const appKeysSchemas = {
dailyvideo: dailyvideo_keys_schema,
"routing-forms": routing_forms_keys_schema,
fathom: fathom_keys_schema,
ga4: ga4_keys_schema,
giphy: giphy_keys_schema,
googlecalendar: googlecalendar_keys_schema,
hubspot: hubspot_keys_schema,
larkcalendar: larkcalendar_keys_schema,
office365calendar: office365calendar_keys_schema,
office365video: office365video_keys_schema,
plausible: plausible_keys_schema,
qr_code: qr_code_keys_schema,
rainbow: rainbow_keys_schema,
salesforce: salesforce_keys_schema,
stripe: stripepayment_keys_schema,
tandemvideo: tandemvideo_keys_schema,
vital: vital_keys_schema,
wordpress: wordpress_keys_schema,
zapier: zapier_keys_schema,
zoomvideo: zoomvideo_keys_schema,
};
@@ -2,24 +2,46 @@
This file is autogenerated using the command `yarn app-store:build --watch`.
Don't modify this file manually.
**/
import { appDataSchema as dailyvideo_schema } from "./dailyvideo/zod";
import { appDataSchema as routing_forms_schema } from "./ee/routing-forms/zod";
import { appDataSchema as fathom_schema } from "./fathom/zod";
import { appDataSchema as ga4_schema } from "./ga4/zod";
import { appDataSchema as giphy_schema } from "./giphy/zod";
import { appDataSchema as googlecalendar_schema } from "./googlecalendar/zod";
import { appDataSchema as hubspot_schema } from "./hubspot/zod";
import { appDataSchema as larkcalendar_schema } from "./larkcalendar/zod";
import { appDataSchema as office365calendar_schema } from "./office365calendar/zod";
import { appDataSchema as office365video_schema } from "./office365video/zod";
import { appDataSchema as plausible_schema } from "./plausible/zod";
import { appDataSchema as qr_code_schema } from "./qr_code/zod";
import { appDataSchema as rainbow_schema } from "./rainbow/zod";
import { appDataSchema as salesforce_schema } from "./salesforce/zod";
import { appDataSchema as stripepayment_schema } from "./stripepayment/zod";
import { appDataSchema as tandemvideo_schema } from "./tandemvideo/zod";
import { appDataSchema as vital_schema } from "./vital/zod";
import { appDataSchema as wordpress_schema } from "./wordpress/zod";
import { appDataSchema as zapier_schema } from "./zapier/zod";
import { appDataSchema as zoomvideo_schema } from "./zoomvideo/zod";
export const appDataSchemas = {
dailyvideo: dailyvideo_schema,
"routing-forms": routing_forms_schema,
fathom: fathom_schema,
ga4: ga4_schema,
giphy: giphy_schema,
googlecalendar: googlecalendar_schema,
hubspot: hubspot_schema,
larkcalendar: larkcalendar_schema,
office365calendar: office365calendar_schema,
office365video: office365video_schema,
plausible: plausible_schema,
qr_code: qr_code_schema,
rainbow: rainbow_schema,
salesforce: salesforce_schema,
stripe: stripepayment_schema,
tandemvideo: tandemvideo_schema,
vital: vital_schema,
wordpress: wordpress_schema,
zapier: zapier_schema,
zoomvideo: zoomvideo_schema,
};
+1
View File
@@ -4,6 +4,7 @@ import config from "./config.json";
export const metadata = {
category: "other",
dirName: "around",
appData: {
location: {
linkType: "static",
@@ -20,6 +20,7 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "ali@cal.com",
dirName: "caldavcalendar",
} as AppMeta;
export default metadata;
@@ -28,6 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
),
userId: user.id,
appId: "caldav-calendar",
invalid: false,
};
try {
+1
View File
@@ -4,6 +4,7 @@ import config from "./config.json";
export const metadata = {
category: "other",
dirName: "caldavcalendar",
...config,
} as AppMeta;
+1
View File
@@ -12,6 +12,7 @@ export const metadata = {
verified: true,
licenseRequired: true,
isProOnly: true,
dirName: "closecom",
...config,
} as App;
@@ -29,6 +29,7 @@ export const metadata = {
},
},
key: { apikey: process.env.DAILY_API_KEY },
dirName: "dailyvideo",
} as AppMeta;
export default metadata;
+8
View File
@@ -0,0 +1,8 @@
import { z } from "zod";
export const appKeysSchema = z.object({
api_key: z.string().min(1),
scale_plan: z.string(),
});
export const appDataSchema = z.object({});
@@ -12,6 +12,7 @@ export const metadata = {
verified: true,
licenseRequired: true,
isProOnly: true,
dirName: "routing-forms",
...config,
} as AppMeta;
@@ -14,8 +14,6 @@ import { AppGetServerSidePropsContext, AppPrisma } from "@calcom/types/AppGetSer
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button, showToast } from "@calcom/ui";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import FormInputFields from "../../components/FormInputFields";
import { getSerializableForm } from "../../lib/getSerializableForm";
import { processRoute } from "../../lib/processRoute";
@@ -26,7 +24,6 @@ function RoutingForm({ form, profile, ...restProps }: inferSSRProps<typeof getSe
const formFillerIdRef = useRef(uuidv4());
const isEmbed = useIsEmbed(restProps.isEmbed);
useTheme(profile.theme);
useExposePlanGlobally(profile.plan);
// TODO: We might want to prevent spam from a single user by having same formFillerId across pageviews
// But technically, a user can fill form multiple times due to any number of reasons and we currently can't differentiate b/w that.
// - like a network error
@@ -183,7 +180,6 @@ export const getServerSideProps = async function getServerSideProps(
theme: true,
brandColor: true,
darkBrandColor: true,
plan: true,
},
},
},
@@ -202,7 +198,6 @@ export const getServerSideProps = async function getServerSideProps(
theme: form.user.theme,
brandColor: form.user.brandColor,
darkBrandColor: form.user.darkBrandColor,
plan: form.user.plan,
},
form: getSerializableForm(form),
},
@@ -44,3 +44,5 @@ export const zodRoutes = z
// TODO: This is a requirement right now that zod.ts file (if it exists) must have appDataSchema export(which is only required by apps having EventTypeAppCard interface)
// This is a temporary solution and will be removed in future
export const appDataSchema = z.any();
export const appKeysSchema = z.object({});
@@ -21,6 +21,7 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
dirName: "exchange2013calendar",
} as AppMeta;
export default metadata;
@@ -34,6 +34,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!),
userId: user.id,
appId: "exchange2013-calendar",
invalid: false,
};
try {
@@ -21,6 +21,7 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
dirName: "exchange2016calendar",
} as AppMeta;
export default metadata;
@@ -34,6 +34,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!),
userId: user.id,
appId: "exchange2016-calendar",
invalid: false,
};
try {
@@ -9,6 +9,7 @@ export const metadata = {
reviews: 0,
trending: true,
verified: true,
dirName: "exchangecalendar",
...config,
} as App;
@@ -25,7 +25,13 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const session = checkSession(req);
const body = formSchema.parse(req.body);
const encrypted = symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY || "");
const data = { type: "exchange_calendar", key: encrypted, userId: session.user?.id, appId: "exchange" };
const data = {
type: "exchange_calendar",
key: encrypted,
userId: session.user?.id,
appId: "exchange",
invalid: false,
};
try {
const service = new CalendarService({ id: 0, ...data });
+1
View File
@@ -4,6 +4,7 @@ import config from "./config.json";
export const metadata = {
category: "analytics",
dirName: "fathom",
...config,
} as AppMeta;
+2
View File
@@ -7,3 +7,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
trackingId: z.string(),
})
);
export const appKeysSchema = z.object({});
+1
View File
@@ -4,6 +4,7 @@ import config from "./config.json";
export const metadata = {
category: "other",
dirName: "ga4",
...config,
} as AppMeta;
+2
View File
@@ -7,3 +7,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
trackingId: z.string(),
})
);
export const appKeysSchema = z.object({});
+1
View File
@@ -22,6 +22,7 @@ export const metadata = {
verified: true,
extendsFeature: "EventType",
email: "help@cal.com",
dirName: "giphy",
} as AppMeta;
export default metadata;
+4
View File
@@ -7,3 +7,7 @@ export const appDataSchema = eventTypeAppCardZod.merge(
thankYouPage: z.string().optional(),
})
);
export const appKeysSchema = z.object({
app_key: z.string().min(1),
});
@@ -21,6 +21,7 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
dirName: "googlecalendar",
} as AppMeta;
export default metadata;
+9
View File
@@ -0,0 +1,9 @@
import { z } from "zod";
export const appDataSchema = z.object({});
export const appKeysSchema = z.object({
client_id: z.string().min(1),
client_secret: z.string().min(1),
redirect_uris: z.string().min(1),
});
@@ -29,6 +29,7 @@ export const metadata = {
label: "Google Meet",
},
},
dirName: "googlevideo",
} as AppMeta;
export default metadata;
+1
View File
@@ -21,6 +21,7 @@ export const metadata = {
title: "HubSpot CRM",
trending: true,
email: "help@cal.com",
dirName: "hubspot",
} as AppMeta;
export default metadata;
+8
View File
@@ -0,0 +1,8 @@
import { z } from "zod";
export const appKeysSchema = z.object({
client_id: z.string().min(1),
client_secret: z.string().min(1),
});
export const appDataSchema = z.object({});
@@ -30,6 +30,7 @@ export const metadata = {
},
},
key: { apikey: randomString(12) },
dirName: "huddle01video",
} as AppMeta;
export default metadata;
@@ -28,6 +28,7 @@ export const metadata = {
label: "Jitsi Video",
},
},
dirName: "jitsivideo",
} as AppMeta;
export default metadata;
@@ -20,6 +20,7 @@ export const metadata = {
url: "https://larksuite.com/",
verified: true,
email: "alan@larksuite.com",
dirName: "larkcalendar",
} as AppMeta;
export default metadata;
+9
View File
@@ -0,0 +1,9 @@
import { z } from "zod";
export const appDataSchema = z.object({});
export const appKeysSchema = z.object({
app_id: z.string().min(1),
app_secret: z.string().min(1),
open_verfication_token: z.string().min(1),
});
+1
View File
@@ -4,6 +4,7 @@ import config from "./config.json";
export const metadata = {
category: "automation",
dirName: "n8n",
...config,
} as AppMeta;
@@ -19,6 +19,7 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
dirName: "office365calendar",
} as AppMeta;
export default metadata;
@@ -0,0 +1,11 @@
import { z } from "zod";
export const appKeysSchema = z.object({
client_id: z.string().min(1),
client_secret: z.string().min(1),
});
export const appDataSchema = z.object({
client_id: z.string(),
client_secret: z.string(),
});
+3 -24
View File
@@ -1,31 +1,10 @@
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
import config from "./config.json";
export const metadata = {
name: "Microsoft 365/Teams (Requires work/school account)",
description: _package.description,
type: "office365_video",
imageSrc: "/api/app-store/office365video/icon.svg",
variant: "conferencing",
logo: "/api/app-store/office365video/icon.svg",
publisher: "Cal.com",
url: "https://www.microsoft.com/en-ca/microsoft-teams/group-chat-software",
verified: true,
rating: 4.3, // TODO: placeholder for now, pull this from TrustPilot or G2
reviews: 69, // TODO: placeholder for now, pull this from TrustPilot or G2
category: "video",
slug: "msteams",
title: "MS Teams (Requires work/school account)",
trending: true,
email: "help@cal.com",
appData: {
location: {
linkType: "dynamic",
type: "integrations:office365_video",
label: "MS Teams",
},
},
dirName: "office365video",
...config,
} as AppMeta;
export default metadata;
@@ -0,0 +1,25 @@
{
"name": "Microsoft 365/Teams (Requires work/school account)",
"description": "Microsoft Teams is a business communication platform and collaborative workspace included in Microsoft 365. It offers workspace chat and video conferencing, file storage, and application integration. Both web versions and desktop/mobile applications are available. NOTE: MUST HAVE A WORK / SCHOOL ACCOUNT",
"type": "office365_video",
"imageSrc": "/api/app-store/office365video/icon.svg",
"variant": "conferencing",
"logo": "/api/app-store/office365video/icon.svg",
"publisher": "Cal.com",
"url": "https://www.microsoft.com/en-ca/microsoft-teams/group-chat-software",
"verified": true,
"rating": 4.3,
"reviews": 69,
"category": "video",
"slug": "msteams",
"title": "MS Teams (Requires work/school account)",
"trending": true,
"email": "help@cal.com",
"appData": {
"location": {
"linkType": "dynamic",
"type": "integrations:office365_video",
"label": "MS Teams"
}
}
}

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