Merge remote-tracking branch 'origin/main' into feature/app-store-cli

This commit is contained in:
Hariom Balhara 2022-05-31 10:57:48 +05:30
commit bcfd22614b
192 changed files with 4961 additions and 1683 deletions

View File

@ -15,6 +15,8 @@
# - You can not repackage or sell the codebase # - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales # - Acquire a commercial license to remove these terms by visiting: cal.com/sales
NEXT_PUBLIC_LICENSE_CONSENT='' NEXT_PUBLIC_LICENSE_CONSENT=''
# To enable enterprise-only features, fill your license key in here
CALCOM_LICENSE_KEY=
# *********************************************************************************************************** # ***********************************************************************************************************
# - DATABASE ************************************************************************************************ # - DATABASE ************************************************************************************************
@ -25,6 +27,7 @@ NEXT_PUBLIC_LICENSE_CONSENT=''
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000' NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
# Change to 'http://localhost:3001' if running the website simultaneously # Change to 'http://localhost:3001' if running the website simultaneously
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000' NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js' NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
# To enable SAML login, set both these variables # To enable SAML login, set both these variables

View File

@ -16,7 +16,8 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Use Node ${{ matrix.node }} - name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v3 uses: actions/setup-node@v3

110
.github/workflows/e2e-embed.yml vendored Normal file
View File

@ -0,0 +1,110 @@
name: E2E test - embed
on:
push:
branches: [ tests/ci-embed ]
pull_request_target: # So we can test on forks
branches:
- main
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
paths-ignore:
- apps/api/**
- apps/console/**
- apps/docs/**
- apps/swagger/**
- apps/website/**
- apps/web/public/**
- tests/**
- playwright/**
jobs:
test:
timeout-minutes: 20
name: Testing Embeds
strategy:
matrix:
node: ["14.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
NEXTAUTH_SECRET: secret
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: true
# CRON_API_KEY: xxx
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
PAYMENT_FEE_PERCENTAGE: 0.005
PAYMENT_FEE_FIXED: 10
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
SAML_ADMINS: pro@example.com
NEXTAUTH_URL: http://localhost:3000/api/auth
NEXT_PUBLIC_IS_E2E: 1
# EMAIL_FROM: e2e@cal.com
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
# MS_GRAPH_CLIENT_ID: xxx
# MS_GRAPH_CLIENT_SECRET: xxx
# ZOOM_CLIENT_ID: xxx
# ZOOM_CLIENT_SECRET: xxx
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
services:
postgres:
image: postgres:12.1
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
ports:
- 5432:5432
steps:
- name: Checkout repo
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
fetch-depth: 2
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: "yarn"
- name: Cache playwright binaries
uses: actions/cache@v2
id: playwright-cache
with:
path: |
~/Library/Caches/ms-playwright
~/.cache/ms-playwright
${{ github.workspace }}/node_modules/playwright
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
restore-keys: cache-playwright-
- run: yarn --frozen-lockfile
- name: Install playwright deps
# if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps
- run: yarn embed-tests-prepare
- run: yarn workspace @calcom/embed-core embed-tests-update-snapshots:ci
- run: yarn workspace @calcom/embed-react embed-tests-update-snapshots:ci
- name: Upload embed-core results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results-core
path: packages/embeds/embed-core/playwright/results
- name: Upload embed-react results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results-react
path: packages/embeds/embed-react/playwright/results

View File

@ -4,7 +4,7 @@ on:
branches: branches:
- main - main
paths-ignore: paths-ignore:
- public/static/locales/** - apps/web/public/static/locales/**
jobs: jobs:
test: test:
timeout-minutes: 20 timeout-minutes: 20

18
.vscode/tasks.json vendored
View File

@ -15,7 +15,9 @@
"Website(3001)", "Website(3001)",
"Embed Core(3100)", "Embed Core(3100)",
"Embed React(3101)", "Embed React(3101)",
"Prisma Studio(5555)" "Prisma Studio(5555)",
"Maildev(587)",
"AppStoreCli:Watch"
], ],
// Mark as the default build task so cmd/ctrl+shift+b will create them // Mark as the default build task so cmd/ctrl+shift+b will create them
"group": { "group": {
@ -65,6 +67,20 @@
"command": "yarn db-studio", "command": "yarn db-studio",
"isBackground": false, "isBackground": false,
"problemMatcher": [] "problemMatcher": []
},
{
"label": "Maildev(587)",
"type": "shell",
"command": "maildev -s 587",
"isBackground": false,
"problemMatcher": []
},
{
"label": "AppStoreCli:Watch",
"type": "shell",
"command": "cd packages/app-store-cli && yarn build:watch",
"isBackground": false,
"problemMatcher": []
} }
] ]
} }

View File

@ -4,6 +4,26 @@ Contributions are what make the open source community such an amazing place to b
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission. - Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
## Areas of expertise
### Legend
✅ = has knowledge
🥇 = is their main priority
⚠️ = is the only one with knowledge
👀 = has no knowledge but wants to be onboarded
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://dynamic-svgs.vercel.app/image.svg?dark">
<img alt="Areas of expertise table" src="https://dynamic-svgs.vercel.app/image.svg">
</picture>
## Developing ## Developing
The development branch is `main`. This is the branch that all pull The development branch is `main`. This is the branch that all pull

View File

@ -18,22 +18,26 @@
<a href="https://cal.com">Website</a> <a href="https://cal.com">Website</a>
· ·
<a href="https://github.com/calcom/cal.com/issues">Issues</a> <a href="https://github.com/calcom/cal.com/issues">Issues</a>
·
<a href="https://cal.com/roadmap">Roadmap</a>
</p> </p>
</p> </p>
<p align="center"> <p align="center">
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a> <a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a> <a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a> <a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a> <a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a> <a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a> <a href="https://github.com/calcom/cal.com/pulse"><img src="https://img.shields.io/github/commit-activity/m/calcom/cal.com" alt="Commits-per-month"></a>
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-%2412%2Fmonth-brightgreen" alt="Pricing"></a> <a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-%2412%2Fmonth-brightgreen" alt="Pricing"></a>
<a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a> <a href="https://jitsu.com?utm_source=github/calcom/cal.com"><img src="https://img.shields.io/badge/Metrics_tracked_by-JITSU-AA00FF?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACKSURBVHgBrZDRCYAwDEQv6gCOoKO4hOCXI9QVnEZwiY5iF5GaVClaBNtioCSUvCR3tMJaxIfZgW4AGUoEPVwgPZoS0Dmgg3NBVDFNbMIsmYCak3J1jDk9iCQvsKJvkzr71N81Gj6vDT/LU2P6RhY63jcafk3YJEbgeZpiFyc/5HJKv8Ef273NSfABGbQfUZhnOSAAAAAASUVORK5CYII=" alt="Jitsu Tracked"></a>
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a> <img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=social"></a> <a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a> <a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a> <a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
</p> </p>
<!-- ABOUT THE PROJECT --> <!-- ABOUT THE PROJECT -->

@ -1 +0,0 @@
Subproject commit 8474e2baa27ef665b69491fa000ccb5d94257363

@ -1 +1 @@
Subproject commit 14aca7ef2b81421bdf4c95020bf54255abe34dcb Subproject commit ed2f42fb0195b1afa0bf2edbab1df2126038b273

@ -1 +1 @@
Subproject commit dfa050650c1507f24ec81d972c0c697ec07934d9 Subproject commit b6b26f47922a5404086bf34635338dc6afa9c1d3

View File

@ -1,23 +1,21 @@
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import React from "react"; import React from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import NavTabs from "./NavTabs"; import NavTabs from "./NavTabs";
const tabs = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
export default function AppsShell({ children }: { children: React.ReactNode }) { export default function AppsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const { status } = useSession(); const { status } = useSession();
const tabs = [
{
name: t("app_store"),
href: "/apps",
},
{
name: t("installed_apps"),
href: "/apps/installed",
},
];
return ( return (
<> <>

View File

@ -1,30 +1,27 @@
import React from "react"; import React from "react";
import { useLocale } from "@lib/hooks/useLocale";
import NavTabs from "./NavTabs"; import NavTabs from "./NavTabs";
export default function BookingsShell({ children }: { children: React.ReactNode }) { const tabs = [
const { t } = useLocale(); {
const tabs = [ name: "upcoming",
{ href: "/bookings/upcoming",
name: t("upcoming"), },
href: "/bookings/upcoming", {
}, name: "recurring",
{ href: "/bookings/recurring",
name: t("recurring"), },
href: "/bookings/recurring", {
}, name: "past",
{ href: "/bookings/past",
name: t("past"), },
href: "/bookings/past", {
}, name: "cancelled",
{ href: "/bookings/cancelled",
name: t("cancelled"), },
href: "/bookings/cancelled", ];
},
];
export default function BookingsShell({ children }: { children: React.ReactNode }) {
return ( return (
<> <>
<NavTabs tabs={tabs} linkProps={{ shallow: true }} /> <NavTabs tabs={tabs} linkProps={{ shallow: true }} />

View File

@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useBrandColors } from "@calcom/embed-core"; import { useBrandColors } from "@calcom/embed-core/embed-iframe";
const brandColor = "#292929"; const brandColor = "#292929";
const brandTextColor = "#ffffff"; const brandTextColor = "#ffffff";

View File

@ -9,17 +9,17 @@ export default function EmptyScreen({
}: { }: {
Icon: SVGComponent; Icon: SVGComponent;
headline: string; headline: string;
description: string; description: string | React.ReactElement;
}) { }) {
return ( return (
<> <>
<div className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed"> <div className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-white"> <div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon className="inline-block h-10 w-10 bg-white" /> <Icon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div> </div>
<div className="max-w-[420px] text-center"> <div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium">{headline}</h2> <h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{headline}</h2>
<p className="text-sm leading-6 text-gray-600">{description}</p> <p className="text-sm leading-6 text-gray-600 dark:text-gray-300">{description}</p>
</div> </div>
</div> </div>
</> </>

View File

@ -4,6 +4,8 @@ import Link, { LinkProps } from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { FC, Fragment, MouseEventHandler } from "react"; import { FC, Fragment, MouseEventHandler } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { SVGComponent } from "@lib/types/SVGComponent"; import { SVGComponent } from "@lib/types/SVGComponent";
@ -22,6 +24,7 @@ export interface NavTabProps {
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => { const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
const router = useRouter(); const router = useRouter();
const { t } = useLocale();
return ( return (
<> <>
<nav <nav
@ -77,7 +80,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
aria-hidden="true" aria-hidden="true"
/> />
)} )}
<span>{tab.name}</span> <span>{t(tab.name)}</span>
</a> </a>
</Link> </Link>
</Component> </Component>

View File

@ -1,48 +1,54 @@
import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid"; import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
import React from "react"; import React, { ComponentProps } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import ErrorBoundary from "@lib/ErrorBoundary";
import NavTabs from "./NavTabs"; import NavTabs from "./NavTabs";
import Shell from "./Shell";
export default function SettingsShell({ children }: { children: React.ReactNode }) { const tabs = [
const { t } = useLocale(); {
name: "profile",
const tabs = [ href: "/settings/profile",
{ icon: UserIcon,
name: t("profile"), },
href: "/settings/profile", {
icon: UserIcon, name: "security",
}, href: "/settings/security",
{ icon: KeyIcon,
name: t("security"), },
href: "/settings/security", {
icon: KeyIcon, name: "teams",
}, href: "/settings/teams",
{ icon: UserGroupIcon,
name: t("teams"), },
href: "/settings/teams", {
icon: UserGroupIcon, name: "billing",
}, href: "/settings/billing",
{ icon: CreditCardIcon,
name: t("billing"), },
href: "/settings/billing", {
icon: CreditCardIcon, name: "admin",
}, href: "/settings/admin",
{ icon: LockClosedIcon,
name: t("admin"), adminRequired: true,
href: "/settings/admin", },
icon: LockClosedIcon, ];
adminRequired: true,
},
];
export default function SettingsShell({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return ( return (
<> <Shell {...rest}>
<div className="sm:mx-auto"> <div className="sm:mx-auto">
<NavTabs tabs={tabs} /> <NavTabs tabs={tabs} />
</div> </div>
<main className="max-w-4xl">{children}</main> <main className="max-w-4xl">
</> <>
<ErrorBoundary>{children}</ErrorBoundary>
</>
</main>
</Shell>
); );
} }

View File

@ -19,7 +19,7 @@ import { useRouter } from "next/router";
import React, { Fragment, ReactNode, useEffect, useState } from "react"; import React, { Fragment, ReactNode, useEffect, useState } from "react";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { useIsEmbed } from "@calcom/embed-core"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import Dropdown, { import Dropdown, {
@ -32,6 +32,7 @@ import LicenseBanner from "@ee/components/LicenseBanner";
import TrialBanner from "@ee/components/TrialBanner"; import TrialBanner from "@ee/components/TrialBanner";
import HelpMenuItem from "@ee/components/support/HelpMenuItem"; import HelpMenuItem from "@ee/components/support/HelpMenuItem";
import ErrorBoundary from "@lib/ErrorBoundary";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
import { WEBAPP_URL } from "@lib/config/constants"; import { WEBAPP_URL } from "@lib/config/constants";
import { shouldShowOnboarding } from "@lib/getting-started"; import { shouldShowOnboarding } from "@lib/getting-started";
@ -349,7 +350,7 @@ const Layout = ({
"px-4 sm:px-6 md:px-8", "px-4 sm:px-6 md:px-8",
props.flexChildrenContainer && "flex flex-1 flex-col" props.flexChildrenContainer && "flex flex-1 flex-col"
)}> )}>
{!props.isLoading ? props.children : props.customLoader} <ErrorBoundary>{!props.isLoading ? props.children : props.customLoader}</ErrorBoundary>
</div> </div>
{/* show bottom navigation for md and smaller (tablet and phones) */} {/* show bottom navigation for md and smaller (tablet and phones) */}
{status === "authenticated" && ( {status === "authenticated" && (
@ -477,7 +478,7 @@ function UserDropdown({ small }: { small?: boolean }) {
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img <img
className="rounded-full" className="rounded-full"
src={process.env.NEXT_PUBLIC_WEBSITE_URL + "/" + user?.username + "/avatar.png"} src={WEBAPP_URL + "/" + user?.username + "/avatar.png"}
alt={user?.username || "Nameless User"} alt={user?.username || "Nameless User"}
/> />
} }

View File

@ -46,20 +46,17 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<div className="mb-4"> <div className="mb-8">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title"> <h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("add_new_schedule")} {t("add_new_schedule")}
</h3> </h3>
<div>
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
</div>
</div> </div>
<Form <Form
form={form} form={form}
handleSubmit={(values) => { handleSubmit={(values) => {
createMutation.mutate(values); createMutation.mutate(values);
}}> }}>
<div className="mt-3 space-y-4"> <div className="mt-3 space-y-2">
<label htmlFor="label" className="block text-sm font-medium text-gray-700"> <label htmlFor="label" className="block text-sm font-medium text-gray-700">
{t("name")} {t("name")}
</label> </label>

View File

@ -2,6 +2,7 @@ import {
BanIcon, BanIcon,
CheckIcon, CheckIcon,
ClockIcon, ClockIcon,
LocationMarkerIcon,
PaperAirplaneIcon, PaperAirplaneIcon,
PencilAltIcon, PencilAltIcon,
XIcon, XIcon,
@ -16,6 +17,7 @@ import { Frequency as RRuleFrequency } from "rrule";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { Tooltip } from "@calcom/ui/Tooltip"; import { Tooltip } from "@calcom/ui/Tooltip";
@ -23,9 +25,11 @@ import { TextArea } from "@calcom/ui/form/fields";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
import useMeQuery from "@lib/hooks/useMeQuery"; import useMeQuery from "@lib/hooks/useMeQuery";
import { LocationType } from "@lib/location";
import { parseRecurringDates } from "@lib/parseDate"; import { parseRecurringDates } from "@lib/parseDate";
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc"; import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions"; import TableActions, { ActionType } from "@components/ui/TableActions";
@ -72,6 +76,7 @@ function BookingListItem(booking: BookingItemProps) {
if (!res.ok) { if (!res.ok) {
throw new HttpError({ statusCode: res.status }); throw new HttpError({ statusCode: res.status });
} }
setRejectionDialogIsOpen(false);
}, },
{ {
async onSettled() { async onSettled() {
@ -89,8 +94,7 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "upcoming" && booking.recurringEventId !== null booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("reject_all") ? t("reject_all")
: t("reject"), : t("reject"),
onClick: (e) => { onClick: () => {
e.stopPropagation();
setRejectionDialogIsOpen(true); setRejectionDialogIsOpen(true);
}, },
icon: BanIcon, icon: BanIcon,
@ -102,8 +106,7 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "upcoming" && booking.recurringEventId !== null booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("confirm_all") ? t("confirm_all")
: t("confirm"), : t("confirm"),
onClick: (e) => { onClick: () => {
e.stopPropagation();
mutation.mutate(true); mutation.mutate(true);
}, },
icon: CheckIcon, icon: CheckIcon,
@ -120,25 +123,33 @@ function BookingListItem(booking: BookingItemProps) {
icon: XIcon, icon: XIcon,
}, },
{ {
id: "reschedule", id: "edit_booking",
label: t("reschedule"), label: t("edit_booking"),
icon: ClockIcon, icon: PencilAltIcon,
actions: [ actions: [
{ {
id: "edit", id: "reschedule",
icon: PencilAltIcon, icon: ClockIcon,
label: t("edit_booking"), label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}`, href: `/reschedule/${booking.uid}`,
}, },
{ {
id: "reschedule_request", id: "reschedule_request",
icon: ClockIcon, icon: PaperAirplaneIcon,
iconClassName: "rotate-45 w-[18px] -ml-[2px]",
label: t("send_reschedule_request"), label: t("send_reschedule_request"),
onClick: (e) => { onClick: () => {
e.stopPropagation();
setIsOpenRescheduleDialog(true); setIsOpenRescheduleDialog(true);
}, },
}, },
{
id: "change_location",
label: t("edit_location"),
onClick: () => {
setIsOpenLocationDialog(true);
},
icon: LocationMarkerIcon,
},
], ],
}, },
]; ];
@ -154,6 +165,26 @@ function BookingListItem(booking: BookingItemProps) {
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY"); const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false); const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const setLocationMutation = trpc.useMutation("viewer.bookings.editLocation", {
onSuccess: () => {
showToast(t("location_updated"), "success");
setIsOpenLocationDialog(false);
utils.invalidateQueries("viewer.bookings");
},
});
const saveLocation = (newLocationType: LocationType, details: { [key: string]: string }) => {
let newLocation = newLocationType as string;
if (
newLocationType === LocationType.InPerson ||
newLocationType === LocationType.Link ||
newLocationType === LocationType.UserPhone
) {
newLocation = details[Object.keys(details)[0]];
}
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
};
// Calculate the booking date(s) // Calculate the booking date(s)
let recurringStrings: string[] = []; let recurringStrings: string[] = [];
@ -168,6 +199,30 @@ function BookingListItem(booking: BookingItemProps) {
); );
} }
const onClick = () => {
router.push({
pathname: "/success",
query: {
date: booking.startTime,
type: booking.eventType.id,
eventSlug: booking.eventType.slug,
user: user?.username || "",
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
location: booking.location
? booking.location.includes("integration")
? (t("web_conferencing_details_to_follow") as string)
: booking.location
: "",
eventName: booking.eventType.eventName || "",
bookingId: booking.id,
recur: booking.recurringEventId,
reschedule: booking.confirmed,
listingStatus: booking.listingStatus,
status: booking.status,
},
});
};
return ( return (
<> <>
<RescheduleDialog <RescheduleDialog
@ -175,6 +230,12 @@ function BookingListItem(booking: BookingItemProps) {
setIsOpenDialog={setIsOpenRescheduleDialog} setIsOpenDialog={setIsOpenRescheduleDialog}
bookingUId={booking.uid} bookingUId={booking.uid}
/> />
<EditLocationDialog
booking={booking}
saveLocation={saveLocation}
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
/>
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */} {/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}> <Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
@ -209,115 +270,103 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<tr <tr className="flex hover:bg-neutral-50">
className="flex cursor-pointer hover:bg-neutral-50" <td
onClick={() => className="hidden whitespace-nowrap align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56"
router.push({ onClick={onClick}>
pathname: "/success", <div className="cursor-pointer py-4">
query: { <div className="text-sm leading-6 text-gray-900">{startTime}</div>
date: booking.startTime, <div className="text-sm text-gray-500">
type: booking.eventType.id, {dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
eventSlug: booking.eventType.slug, {dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
user: user?.username || "", </div>
name: booking.attendees[0].name, <div className="text-sm text-gray-400">
email: booking.attendees[0].email, {booking.recurringCount &&
location: booking.location booking.eventType?.recurringEvent?.freq &&
? booking.location.includes("integration") booking.listingStatus === "upcoming" && (
? (t("web_conferencing_details_to_follow") as string) <div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
: booking.location <div className="flex">
: "", <Tooltip
eventName: booking.eventType.eventName || "", content={recurringStrings.map((aDate, key) => (
bookingId: booking.id, <p key={key}>{aDate}</p>
recur: booking.recurringEventId, ))}>
reschedule: booking.confirmed, <p className="text-gray-600 dark:text-white">
status: booking.listingStatus, <RefreshIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
}, {`${t("every_for_freq", {
}) freq: t(
}> `${RRuleFrequency[booking.eventType.recurringEvent.freq]
<td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56"> .toString()
<div className="text-sm leading-6 text-gray-900">{startTime}</div> .toLowerCase()}`
<div className="text-sm text-gray-500"> ),
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "} })} ${booking.recurringCount} ${t(
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
</div>
<div className="text-sm text-gray-400">
{booking.recurringCount &&
booking.eventType?.recurringEvent?.freq &&
booking.listingStatus === "upcoming" && (
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
<div className="flex">
<Tooltip
content={recurringStrings.map((aDate, key) => (
<p key={key}>{aDate}</p>
))}>
<p className="text-gray-600 dark:text-white">
<RefreshIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
{`${t("every_for_freq", {
freq: t(
`${RRuleFrequency[booking.eventType.recurringEvent.freq] `${RRuleFrequency[booking.eventType.recurringEvent.freq]
.toString() .toString()
.toLowerCase()}` .toLowerCase()}`,
), { count: booking.recurringCount }
})} ${booking.recurringCount} ${t( )}`}
`${RRuleFrequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`, </p>
{ count: booking.recurringCount } </Tooltip>
)}`} </div>
</p>
</Tooltip>
</div> </div>
</div> )}
)} </div>
</div> </div>
</td> </td>
<td className={"flex-1 py-4 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}> <td
<div className="sm:hidden"> className={"flex-1 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}
{!booking.confirmed && !booking.rejected && ( onClick={onClick}>
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag> <div className="cursor-pointer py-4">
)} <div className="sm:hidden">
{!!booking?.eventType?.price && !booking.paid && ( {!booking.confirmed && !booking.rejected && (
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag> <Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
)} )}
<div className="text-sm font-medium text-gray-900"> {!!booking?.eventType?.price && !booking.paid && (
{startTime}:{" "} <Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag>
<small className="text-sm text-gray-500"> )}
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")} <div className="text-sm font-medium text-gray-900">
</small> {startTime}:{" "}
<small className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
</small>
</div>
</div> </div>
</div> <div
<div title={booking.title}
title={booking.title} className={classNames(
className={classNames( "max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
"max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max", isCancelled ? "line-through" : ""
isCancelled ? "line-through" : "" )}>
)}> {booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>} {booking.title}
{booking.title} {!!booking?.eventType?.price && !booking.paid && (
{!!booking?.eventType?.price && !booking.paid && ( <Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag> )}
)} {!booking.confirmed && !booking.rejected && (
{!booking.confirmed && !booking.rejected && ( <Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag> )}
)}
</div>
{booking.description && (
<div className="max-w-52 md:max-w-96 truncate text-sm text-gray-500" title={booking.description}>
&quot;{booking.description}&quot;
</div> </div>
)} {booking.description && (
<div
className="max-w-52 md:max-w-96 truncate text-sm text-gray-500"
title={booking.description}>
&quot;{booking.description}&quot;
</div>
)}
{booking.attendees.length !== 0 && ( {booking.attendees.length !== 0 && (
<a <a
className="text-sm text-gray-900 hover:text-blue-500" className="text-sm text-gray-900 hover:text-blue-500"
href={"mailto:" + booking.attendees[0].email} href={"mailto:" + booking.attendees[0].email}
onClick={(e) => e.stopPropagation()}> onClick={(e) => e.stopPropagation()}>
{booking.attendees[0].email} {booking.attendees[0].email}
</a> </a>
)} )}
{isCancelled && booking.rescheduled && ( {isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block text-left text-sm md:hidden"> <div className="mt-2 inline-block text-left text-sm md:hidden">
<RequestSentMessage /> <RequestSentMessage />
</div> </div>
)} )}
</div>
</td> </td>
<td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4"> <td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">

View File

@ -7,7 +7,7 @@ import utc from "dayjs/plugin/utc";
import { memoize } from "lodash"; import { memoize } from "lodash";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useEmbedStyles } from "@calcom/embed-core"; import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";

View File

@ -30,9 +30,9 @@ import {
useIsBackgroundTransparent, useIsBackgroundTransparent,
sdkActionManager, sdkActionManager,
useEmbedNonStylesConfig, useEmbedNonStylesConfig,
} from "@calcom/embed-core"; } from "@calcom/embed-core/embed-iframe";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage"; import { localStorage } from "@calcom/lib/webstorage";
@ -68,6 +68,8 @@ export const locationKeyToString = (location: LocationObject, t: TFunction) => {
case LocationType.Link: case LocationType.Link:
return location.link || "Link"; // If disabled link won't exist on the object return location.link || "Link"; // If disabled link won't exist on the object
case LocationType.Phone: case LocationType.Phone:
return t("your_number");
case LocationType.UserPhone:
return t("phone_call"); return t("phone_call");
case LocationType.GoogleMeet: case LocationType.GoogleMeet:
return "Google Meet"; return "Google Meet";
@ -234,7 +236,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
.filter((user) => user.name !== profile.name) .filter((user) => user.name !== profile.name)
.map((user) => ({ .map((user) => ({
title: user.name, title: user.name,
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`, image: `${CAL_URL}/${user.username}/avatar.png`,
alt: user.name || undefined, alt: user.name || undefined,
})), })),
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[] ].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
@ -328,7 +330,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
.map((user) => ({ .map((user) => ({
title: user.name, title: user.name,
alt: user.name, alt: user.name,
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`, image: `${CAL_URL}/${user.username}/avatar.png`,
})), })),
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[] ].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
} }
@ -370,7 +372,9 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
return ( return (
<span key={el.type}> <span key={el.type}>
{locationKeyToString(el, t)}{" "} {locationKeyToString(el, t)}{" "}
{arr.length - 1 !== i && <span className="font-light"> or </span>} {arr.length - 1 !== i && (
<span className="font-light"> {t("or_lowercase")} </span>
)}
</span> </span>
); );
})} })}
@ -424,7 +428,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
{previousPage === `${WEBAPP_URL}/${profile.slug}` && ( {previousPage === `${WEBAPP_URL}/${profile.slug}` && (
<div className="flex h-full flex-col justify-end"> <div className="flex h-full flex-col justify-end">
<ArrowLeftIcon <ArrowLeftIcon
className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white" className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white"
onClick={() => router.back()} onClick={() => router.back()}
/> />
<p className="sr-only">Go Back</p> <p className="sr-only">Go Back</p>

View File

@ -24,7 +24,11 @@ import { Frequency as RRuleFrequency } from "rrule";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { z } from "zod"; import { z } from "zod";
import { useEmbedNonStylesConfig, useIsBackgroundTransparent, useIsEmbed } from "@calcom/embed-core"; import {
useEmbedNonStylesConfig,
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
@ -79,6 +83,7 @@ type BookingFormValues = {
customInputs?: { customInputs?: {
[key: string]: string | boolean; [key: string]: string | boolean;
}; };
rescheduleReason?: string;
}; };
const BookingPage = ({ const BookingPage = ({
@ -251,6 +256,7 @@ const BookingPage = ({
email: primaryAttendee.email || "", email: primaryAttendee.email || "",
guests: guestListEmails, guests: guestListEmails,
notes: booking.description || "", notes: booking.description || "",
rescheduleReason: "",
customInputs: eventType.customInputs.reduce( customInputs: eventType.customInputs.reduce(
(customInputs, input) => ({ (customInputs, input) => ({
...customInputs, ...customInputs,
@ -782,18 +788,31 @@ const BookingPage = ({
<label <label
htmlFor="notes" htmlFor="notes"
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white"> className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
{t("additional_notes")} {rescheduleUid ? t("reschedule_optional") : t("additional_notes")}
</label> </label>
<textarea {rescheduleUid ? (
{...bookingForm.register("notes")} <textarea
id="notes" {...bookingForm.register("rescheduleReason")}
name="notes" id="rescheduleReason"
rows={3} name="rescheduleReason"
className={inputClassName} rows={3}
placeholder={t("share_additional_notes")} className={inputClassName}
disabled={disabledExceptForOwner} placeholder={t("reschedule_placeholder")}
/> disabled={disabledExceptForOwner}
/>
) : (
<textarea
{...bookingForm.register("notes")}
id="notes"
name="notes"
rows={3}
className={inputClassName}
placeholder={t("share_additional_notes")}
disabled={disabledExceptForOwner}
/>
)}
</div> </div>
<div className="flex items-start space-x-2 rtl:space-x-reverse"> <div className="flex items-start space-x-2 rtl:space-x-reverse">
<Button <Button
type="submit" type="submit"

View File

@ -0,0 +1,336 @@
import { LocationMarkerIcon } from "@heroicons/react/solid";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import dynamic from "next/dynamic";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { Form } from "@calcom/ui/form/fields";
import { QueryCell } from "@lib/QueryCell";
import { linkValueToString } from "@lib/linkValueToString";
import { LocationType } from "@lib/location";
import { LocationOptionsToString } from "@lib/locationOptions";
import { inferQueryOutput, trpc } from "@lib/trpc";
import CheckboxField from "@components/ui/form/CheckboxField";
import type PhoneInputType from "@components/ui/form/PhoneInput";
import Select from "@components/ui/form/Select";
const PhoneInput = dynamic(
() => import("@components/ui/form/PhoneInput")
) as unknown as typeof PhoneInputType;
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type OptionTypeBase = {
label: string;
value: LocationType;
disabled?: boolean;
};
type LocationFormValues = {
locationType: LocationType;
locationAddress?: string;
locationLink?: string;
locationPhoneNumber?: string;
displayLocationPublicly?: boolean;
};
interface ISetLocationDialog {
saveLocation: (newLocationType: LocationType, details: { [key: string]: string }) => void;
selection?: OptionTypeBase;
booking?: BookingItem;
defaultValues?: {
type: LocationType;
address?: string | undefined;
link?: string | undefined;
hostPhoneNumber?: string | undefined;
displayLocationPublicly?: boolean | undefined;
}[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
}
export const EditLocationDialog = (props: ISetLocationDialog) => {
const {
saveLocation,
selection,
booking,
setShowLocationModal,
isOpenDialog,
defaultValues,
setSelectedLocation,
} = props;
const { t } = useLocale();
const locationsQuery = trpc.useQuery(["viewer.locationOptions"]);
useEffect(() => {
if (selection) {
locationFormMethods.setValue("locationType", selection?.value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selection]);
const locationFormSchema = z.object({
locationType: z.string(),
locationAddress: z.string().optional(),
locationLink: z.string().url().optional(),
displayLocationPublicly: z.boolean().optional(),
locationPhoneNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
});
const locationFormMethods = useForm<LocationFormValues>({
mode: "onSubmit",
resolver: zodResolver(locationFormSchema),
});
const selectedLocation = useWatch({
control: locationFormMethods.control,
name: "locationType",
});
const LocationOptions =
selectedLocation === LocationType.InPerson ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_address_place")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationAddress")}
id="address"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.InPerson
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.InPerson)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}></CheckboxField>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Link ? (
<div>
<label htmlFor="link" className="block text-sm font-medium text-gray-700">
{t("set_link_meeting")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="link"
required
className="block w-full rounded-sm border-gray-300 sm:text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Link
)?.link
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("url_start_with_https")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
description={t("display_location_label")}
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Link)
?.displayLocationPublicly
: undefined
}
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
informationIconText={t("display_location_info_badge")}></CheckboxField>
)}
/>
</div>
)}
</div>
) : selectedLocation === LocationType.UserPhone ? (
<div>
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
{t("set_your_phone_number")}
{locationFormMethods.formState?.errors?.locationPhoneNumber?.message}
</label>
<div className="mt-1">
<PhoneInput<LocationFormValues>
control={locationFormMethods.control}
name="locationPhoneNumber"
required
id="locationPhoneNumber"
placeholder={t("host_phone_number")}
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.UserPhone
)?.hostPhoneNumber
: undefined
}
/>
{locationFormMethods.formState.errors.locationPhoneNumber && (
<p className="mt-1 text-sm text-red-500">Invalid input</p>
)}
</div>
</div>
) : (
<p className="text-sm">{LocationOptionsToString(selectedLocation, t)}</p>
);
return (
<Dialog open={isOpenDialog}>
<DialogContent asChild>
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<LocationMarkerIcon className="text-primary-600 h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("edit_location")}
</h3>
{!booking && (
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
)}
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"></div>
</div>
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">{linkValueToString(booking.location, t)}</p>
</>
)}
<Form
form={locationFormMethods}
handleSubmit={async (values) => {
const { locationType: newLocation, displayLocationPublicly } = values;
let details = {};
if (newLocation === LocationType.InPerson) {
details = {
address: values.locationAddress,
displayLocationPublicly,
};
}
if (newLocation === LocationType.Link) {
details = { link: values.locationLink, displayLocationPublicly };
}
if (newLocation === LocationType.UserPhone) {
details = { hostPhoneNumber: values.locationPhoneNumber };
}
saveLocation(newLocation, details);
setShowLocationModal(false);
setSelectedLocation?.(undefined);
locationFormMethods.unregister([
"locationType",
"locationLink",
"locationAddress",
"locationPhoneNumber",
]);
}}>
<QueryCell
query={locationsQuery}
success={({ data: locationOptions }) => {
if (!locationOptions.length) return null;
return (
<Controller
name="locationType"
control={locationFormMethods.control}
render={() => (
<Select
maxMenuHeight={150}
name="location"
defaultValue={selection}
options={
booking
? locationOptions.filter((location) => location.value !== "phone")
: locationOptions
}
isSearchable={false}
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
onChange={(val) => {
if (val) {
locationFormMethods.setValue("locationType", val.value);
locationFormMethods.unregister([
"locationLink",
"locationAddress",
"locationPhoneNumber",
]);
locationFormMethods.clearErrors([
"locationLink",
"locationPhoneNumber",
"locationAddress",
]);
setSelectedLocation?.(val);
}
}}
/>
)}
/>
);
}}
/>
{selectedLocation && LocationOptions}
<div className="mt-4 flex justify-end space-x-2">
<Button
onClick={() => {
setShowLocationModal(false);
setSelectedLocation?.(undefined);
locationFormMethods.unregister("locationType");
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button type="submit">{t("update")}</Button>
</div>
</Form>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -1,10 +1,10 @@
import { UserIcon } from "@heroicons/react/outline"; import { UserIcon } from "@heroicons/react/outline";
import { InformationCircleIcon } from "@heroicons/react/solid"; import { InformationCircleIcon } from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client"; import { MembershipRole } from "@prisma/client";
import React, { useState, useEffect, SyntheticEvent, useMemo } from "react"; import React, { useState, SyntheticEvent, useMemo } from "react";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog"; import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
import { TextField } from "@calcom/ui/form/fields"; import { TextField } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";

View File

@ -4,7 +4,7 @@ import { MembershipRole } from "@prisma/client";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification"; import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
@ -74,7 +74,7 @@ export default function MemberListItem(props: Props) {
<div className="flex w-full flex-col justify-between sm:flex-row"> <div className="flex w-full flex-col justify-between sm:flex-row">
<div className="flex"> <div className="flex">
<Avatar <Avatar
imageSrc={WEBSITE_URL + "/" + props.member.username + "/avatar.png"} imageSrc={WEBAPP_URL + "/" + props.member.username + "/avatar.png"}
alt={name || ""} alt={name || ""}
className="h-9 w-9 rounded-full" className="h-9 w-9 rounded-full"
/> />

View File

@ -5,13 +5,12 @@ import Link from "next/link";
import { TeamPageProps } from "pages/team/[slug]"; import { TeamPageProps } from "pages/team/[slug]";
import React from "react"; import React from "react";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import Text from "@components/ui/Text";
type TeamType = TeamPageProps["team"]; type TeamType = TeamPageProps["team"];
type MembersType = TeamType["members"]; type MembersType = TeamType["members"];
@ -52,14 +51,14 @@ const Team = ({ team }: TeamPageProps) => {
<div> <div>
<Avatar <Avatar
alt={member.name || ""} alt={member.name || ""}
imageSrc={WEBSITE_URL + "/" + member.username + "/avatar.png"} imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
className="-mt-4 h-12 w-12" className="-mt-4 h-12 w-12"
/> />
<section className="mt-2 w-full space-y-1"> <section className="line-clamp-4 mt-2 w-full space-y-1">
<Text variant="title">{member.name}</Text> <p className="font-medium text-neutral-900 dark:text-white">{member.name}</p>
<Text variant="subtitle" className=""> <p className="text-sm font-normal text-neutral-500 dark:text-white">
{member.bio || t("user_from_team", { user: member.name, team: team.name })} {member.bio || t("user_from_team", { user: member.name, team: team.name })}
</Text> </p>
</section> </section>
</div> </div>
</div> </div>

View File

@ -18,7 +18,7 @@ export default function ModalContainer(props: Props) {
<DialogContent> <DialogContent>
<div <div
className={classNames( className={classNames(
"inline-block transform bg-white text-left align-bottom transition-all sm:align-middle", "inline-block w-full transform bg-white text-left align-bottom transition-all sm:align-middle",
{ {
"sm:w-full sm:max-w-lg ": !props.wide, "sm:w-full sm:max-w-lg ": !props.wide,
"sm:w-4xl sm:max-w-4xl": props.wide, "sm:w-4xl sm:max-w-4xl": props.wide,

View File

@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import { useIsEmbed } from "@calcom/embed-core"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";

View File

@ -9,6 +9,7 @@ import { SVGComponent } from "@lib/types/SVGComponent";
export type ActionType = { export type ActionType = {
id: string; id: string;
icon?: SVGComponent; icon?: SVGComponent;
iconClassName?: string;
label: string; label: string;
disabled?: boolean; disabled?: boolean;
color?: "primary" | "secondary"; color?: "primary" | "secondary";
@ -52,6 +53,7 @@ const DropdownActions = ({
className="w-full rounded-none font-normal" className="w-full rounded-none font-normal"
href={action.href} href={action.href}
StartIcon={action.icon} StartIcon={action.icon}
startIconClassName={action.iconClassName}
onClick={action.onClick || defaultAction} onClick={action.onClick || defaultAction}
data-testid={action.id}> data-testid={action.id}>
{action.label} {action.label}
@ -81,6 +83,7 @@ const TableActions: FC<Props> = ({ actions }) => {
href={action.href} href={action.href}
onClick={action.onClick || defaultAction} onClick={action.onClick || defaultAction}
StartIcon={action.icon} StartIcon={action.icon}
startIconClassName={action.iconClassName}
{...(action?.actions ? { EndIcon: ChevronDownIcon } : null)} {...(action?.actions ? { EndIcon: ChevronDownIcon } : null)}
disabled={action.disabled} disabled={action.disabled}
color={action.color || "secondary"}> color={action.color || "secondary"}>

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Body: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-gray-900 dark:text-white", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Body;

View File

@ -1,3 +0,0 @@
import Body from "./Body";
export default Body;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Caption: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-sm text-gray-500 dark:text-white leading-tight", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Caption;

View File

@ -1,3 +0,0 @@
import Caption from "./Caption";
export default Caption;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Caption2: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-xs italic text-gray-500 dark:text-white leading-tight", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Caption2;

View File

@ -1,3 +0,0 @@
import Caption2 from "./Caption2";
export default Caption2;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Footnote: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-xs font-medium text-gray-500 dark:text-white", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Footnote;

View File

@ -1,3 +0,0 @@
import Footnote from "./Footnote";
export default Footnote;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Headline: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("font-cal text-xl text-gray-900 dark:text-white", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Headline;

View File

@ -1,3 +0,0 @@
import Headline from "./Headline";
export default Headline;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Largetitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("font-cal tracking-wider text-3xl mb-2", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Largetitle;

View File

@ -1,3 +0,0 @@
import Largetitle from "./Largetitle";
export default Largetitle;

View File

@ -1,15 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Overline: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames(
"text-sm capitalize font-medium text-gray-900 dark:text-white",
props?.className
);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Overline;

View File

@ -1,3 +0,0 @@
import Overline from "./Overline";
export default Overline;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Subheadline: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-xl text-gray-500 dark:text-white leading-relaxed", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Subheadline;

View File

@ -1,3 +0,0 @@
import Subheadline from "./Subheadline";
export default Subheadline;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Subtitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-sm font-normal text-neutral-500 dark:text-white", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Subtitle;

View File

@ -1,3 +0,0 @@
import Subtitle from "./Subtitle";
export default Subtitle;

View File

@ -1,164 +0,0 @@
/**
* @deprecated create new a new set of components, waiting for designs
*/
import React from "react";
import Body from "./Body";
import Caption from "./Caption";
import Caption2 from "./Caption2";
import Footnote from "./Footnote";
import Headline from "./Headline";
import Largetitle from "./Largetitle";
import Overline from "./Overline";
import Subheadline from "./Subheadline";
import Subtitle from "./Subtitle";
import Title from "./Title";
import Title2 from "./Title2";
import Title3 from "./Title3";
type Props = {
variant?:
| "overline"
| "caption"
| "body"
| "caption2"
| "footnote"
| "headline"
| "largetitle"
| "subheadline"
| "subtitle"
| "title"
| "title2"
| "title3";
children: any;
text?: string;
tx?: string;
className?: string;
};
export type TextProps = {
children: any;
text?: string;
tx?: string;
className?: string;
};
/**
* static let largeTitle: Font
* A font with the large title text style.
*
* static let title: Font
* A font with the title text style.
*
* static let title2: Font
* Create a font for second level hierarchical headings.
*
* static let title3: Font
* Create a font for third level hierarchical headings.
*
* static let headline: Font
* A font with the headline text style.
*
* static let subheadline: Font
* A font with the subheadline text style.
*
* static let body: Font
* A font with the body text style.
*
* static let callout: Font
* A font with the callout text style.
*
* static let caption: Font
* A font with the caption text style.
*
* static let caption2: Font
* Create a font with the alternate caption text style.
*
* static let footnote: Font
* A font with the footnote text style.
*/
const Text: React.FunctionComponent<Props> = (props: Props) => {
switch (props?.variant) {
case "overline":
return (
<Overline text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Overline>
);
case "body":
return (
<Body text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Body>
);
case "caption":
return (
<Caption text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Caption>
);
case "caption2":
return (
<Caption2 text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Caption2>
);
case "footnote":
return (
<Footnote text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Footnote>
);
case "headline":
return (
<Headline text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Headline>
);
case "largetitle":
return (
<Largetitle text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Largetitle>
);
case "subheadline":
return (
<Subheadline text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Subheadline>
);
case "subtitle":
return (
<Subtitle text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Subtitle>
);
case "title":
return (
<Title text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Title>
);
case "title2":
return (
<Title2 text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Title2>
);
case "title3":
return (
<Title3 text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Title3>
);
default:
return (
<Body text={props?.text} tx={props?.tx} className={props?.className}>
{props.children}
</Body>
);
}
};
export default Text;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Title: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("font-medium text-neutral-900 dark:text-white", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Title;

View File

@ -1,3 +0,0 @@
import Title from "./Title";
export default Title;

View File

@ -1,12 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Title2: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-base font-normal text-gray-900 dark:text-white", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Title2;

View File

@ -1,3 +0,0 @@
import Title2 from "./Title2";
export default Title2;

View File

@ -1,15 +0,0 @@
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Title3: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames(
"text-xs font-semibold leading-tight text-gray-900 dark:text-white",
props?.className
);
return <p className={classes}>{props?.text || props.children}</p>;
};
export default Title3;

View File

@ -1,3 +0,0 @@
import Title3 from "./Title3";
export default Title3;

View File

@ -1,40 +0,0 @@
import Body from "./Body";
import Caption from "./Caption";
import Caption2 from "./Caption2";
import Footnote from "./Footnote";
import Headline from "./Headline";
import Largetitle from "./Largetitle";
import Overline from "./Overline";
import Subheadline from "./Subheadline";
import Subtitle from "./Subtitle";
import Text from "./Text";
import Title from "./Title";
import Title2 from "./Title2";
import Title3 from "./Title3";
export { Text };
export default Text;
export { Title };
export { Title2 };
export { Title3 };
export { Largetitle };
export { Subtitle };
export { Headline };
export { Subheadline };
export { Caption };
export { Caption2 };
export { Footnote };
export { Overline };
export { Body };

View File

@ -1,51 +1,60 @@
import React, { forwardRef, InputHTMLAttributes } from "react"; import React, { forwardRef, InputHTMLAttributes } from "react";
import classNames from "@calcom/lib/classNames";
import InfoBadge from "@components/ui/InfoBadge"; import InfoBadge from "@components/ui/InfoBadge";
type Props = InputHTMLAttributes<HTMLInputElement> & { type Props = InputHTMLAttributes<HTMLInputElement> & {
label?: React.ReactNode; label?: React.ReactNode;
description: string; description: string;
descriptionAsLabel?: boolean; descriptionAsLabel?: boolean;
infomationIconText?: string; informationIconText?: string;
}; };
const CheckboxField = forwardRef<HTMLInputElement, Props>( const CheckboxField = forwardRef<HTMLInputElement, Props>(
({ label, description, infomationIconText, descriptionAsLabel, ...rest }, ref) => { ({ label, description, informationIconText, ...rest }, ref) => {
const descriptionAsLabel = !label || rest.descriptionAsLabel;
return ( return (
<div className="block items-center sm:flex"> <div className="block items-center sm:flex">
{label && !descriptionAsLabel && ( {label && (
<div className="min-w-48 mb-4 sm:mb-0"> <div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={rest.id} className="flex text-sm font-medium text-neutral-700"> {React.createElement(
{label} descriptionAsLabel ? "div" : "label",
</label> {
</div> className: "flex text-sm font-medium text-neutral-700",
)} ...(!descriptionAsLabel
{label && descriptionAsLabel && ( ? {
<div className="min-w-48 mb-4 sm:mb-0"> htmlFor: rest.id,
<span className="flex text-sm font-medium text-neutral-700">{label}</span> }
: {}),
},
label
)}
</div> </div>
)} )}
<div className="w-full"> <div className="w-full">
<div className="relative flex items-start"> <div className="relative flex items-start">
<div className="flex h-5 items-center"> {React.createElement(
<input descriptionAsLabel ? "label" : "div",
{...rest} {
disabled={rest.disabled} className: classNames(
ref={ref} "relative flex items-start",
type="checkbox" descriptionAsLabel ? "text-neutral-700" : "text-neutral-900"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300" ),
/> },
</div> <>
<div className="text-sm ltr:ml-3 rtl:mr-3"> <div className="flex h-5 items-center">
{!label || descriptionAsLabel ? ( <input
<label htmlFor={rest.id} className="text-neutral-700"> {...rest}
{description} ref={ref}
</label> type="checkbox"
) : ( className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
<p className="text-neutral-900">{description}</p> />
)} </div>
</div> <span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span>
{infomationIconText && <InfoBadge content={infomationIconText}></InfoBadge>} </>
)}
{informationIconText && <InfoBadge content={informationIconText}></InfoBadge>}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,57 @@
import { ExclamationIcon } from "@heroicons/react/solid";
import { useSession } from "next-auth/react";
import React, { AriaRole, ComponentType, FC, Fragment } from "react";
import { CONSOLE_URL } from "@calcom/lib/constants";
import EmptyScreen from "@components/EmptyScreen";
type LicenseRequiredProps = {
as?: keyof JSX.IntrinsicElements | "";
className?: string;
role?: AriaRole | undefined;
children: React.ReactNode;
};
/**
* This component will only render it's children if the installation has a valid
* license.
*/
const LicenseRequired: FC<LicenseRequiredProps> = ({ children, as = "", ...rest }) => {
const session = useSession();
const Component = as || Fragment;
return (
<Component {...rest}>
{session.data?.hasValidLicense ? (
children
) : (
<EmptyScreen
Icon={ExclamationIcon}
headline="This is an enterprise feature"
description={
<>
To enable this feature, get a deployment key at{" "}
<a href={CONSOLE_URL} target="_blank" rel="noopener noreferrer" className="underline">
Cal.com console
</a>
.
</>
}
/>
)}
</Component>
);
};
export function withLicenseRequired<T>(Component: ComponentType<T>) {
// eslint-disable-next-line react/display-name
return (hocProps: T) => {
return (
<LicenseRequired>
<Component {...(hocProps as T)} />;
</LicenseRequired>
);
};
}
export default LicenseRequired;

View File

@ -15,11 +15,12 @@ import { trpc } from "@lib/trpc";
import { DatePicker } from "@components/ui/form/DatePicker"; import { DatePicker } from "@components/ui/form/DatePicker";
import { TApiKeys } from "./ApiKeyListItem"; import LicenseRequired from "../LicenseRequired";
import type { TApiKeys } from "./ApiKeyListItem";
export default function ApiKeyDialogForm(props: { export default function ApiKeyDialogForm(props: {
title: string; title: string;
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean }; defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean };
handleClose: () => void; handleClose: () => void;
}) { }) {
const { t } = useLocale(); const { t } = useLocale();
@ -49,7 +50,7 @@ export default function ApiKeyDialogForm(props: {
const watchNeverExpires = form.watch("neverExpires"); const watchNeverExpires = form.watch("neverExpires");
return ( return (
<> <LicenseRequired>
{successfulNewApiKeyModal ? ( {successfulNewApiKeyModal ? (
<> <>
<div className="mb-10"> <div className="mb-10">
@ -92,12 +93,12 @@ export default function ApiKeyDialogForm(props: {
</DialogFooter> </DialogFooter>
</> </>
) : ( ) : (
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean }> <Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean }>
form={form} form={form}
handleSubmit={async (event) => { handleSubmit={async (event) => {
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event); const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
setApiKey(apiKey); setApiKey(apiKey);
setApiKeyDetails({ ...event }); setApiKeyDetails({ ...event, neverExpires: !!event.neverExpires });
await utils.invalidateQueries(["viewer.apiKeys.list"]); await utils.invalidateQueries(["viewer.apiKeys.list"]);
setSuccessfulNewApiKeyModal(true); setSuccessfulNewApiKeyModal(true);
}} }}
@ -146,6 +147,6 @@ export default function ApiKeyDialogForm(props: {
</DialogFooter> </DialogFooter>
</Form> </Form>
)} )}
</> </LicenseRequired>
); );
} }

View File

@ -12,66 +12,76 @@ import { trpc } from "@lib/trpc";
import { List } from "@components/List"; import { List } from "@components/List";
export default function ApiKeyListContainer() { import LicenseRequired from "../LicenseRequired";
function ApiKeyListContainer() {
const { t } = useLocale(); const { t } = useLocale();
const query = trpc.useQuery(["viewer.apiKeys.list"]); const query = trpc.useQuery(["viewer.apiKeys.list"]);
const [newApiKeyModal, setNewApiKeyModal] = useState(false); const [newApiKeyModal, setNewApiKeyModal] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires: boolean }) | null>(null); const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | null>(null);
return ( return (
<QueryCell <>
query={query} <div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
success={({ data }) => ( <div className="mt-9">
<> <h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row"> <p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
<div className="mt-9"> </div>
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2> <div className="mb-9 sm:self-center">
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p> <Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
</div> {t("generate_new_api_key")}
<div className="mb-9 sm:self-center"> </Button>
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}> </div>
{t("generate_new_api_key")} </div>
</Button> <LicenseRequired>
</div> <QueryCell
</div> query={query}
success={({ data }) => (
{data.length > 0 && ( <>
<List className="pb-6"> {data.length > 0 && (
{data.map((item: any) => ( <List className="pb-6">
<ApiKeyListItem {data.map((item) => (
key={item.id} <ApiKeyListItem
apiKey={item} key={item.id}
onEditApiKey={() => { apiKey={item}
setApiKeyToEdit(item); onEditApiKey={() => {
setEditModalOpen(true); setApiKeyToEdit(item);
}} setEditModalOpen(true);
/> }}
))} />
</List> ))}
)} </List>
{/* New api key dialog */}
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
<DialogContent>
<ApiKeyDialogForm title={t("create_api_key")} handleClose={() => setNewApiKeyModal(false)} />
</DialogContent>
</Dialog>
{/* Edit api key dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent>
{apiKeyToEdit && (
<ApiKeyDialogForm
title={t("edit_api_key")}
key={apiKeyToEdit.id}
handleClose={() => setEditModalOpen(false)}
defaultValues={apiKeyToEdit}
/>
)} )}
</DialogContent>
</Dialog> {/* New api key dialog */}
</> <Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
)} <DialogContent>
/> <ApiKeyDialogForm
title={t("create_api_key")}
handleClose={() => setNewApiKeyModal(false)}
/>
</DialogContent>
</Dialog>
{/* Edit api key dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent>
{apiKeyToEdit && (
<ApiKeyDialogForm
title={t("edit_api_key")}
key={apiKeyToEdit.id}
handleClose={() => setEditModalOpen(false)}
defaultValues={apiKeyToEdit}
/>
)}
</DialogContent>
</Dialog>
</>
)}
/>
</LicenseRequired>
</>
); );
} }
export default ApiKeyListContainer;

View File

@ -1,18 +1,20 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification"; import showToast from "@calcom/lib/notification";
import { Alert } from "@calcom/ui/Alert"; import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog"; import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { TextArea } from "@calcom/ui/form/fields"; import { TextArea } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc"; import { trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Badge from "@components/ui/Badge"; import Badge from "@components/ui/Badge";
import LicenseRequired from "../LicenseRequired";
export default function SAMLConfiguration({ export default function SAMLConfiguration({
teamsView, teamsView,
teamId, teamId,
@ -92,7 +94,7 @@ export default function SAMLConfiguration({
return ( return (
<> <>
{isSAMLLoginEnabled ? ( {isSAMLLoginEnabled ? (
<> <LicenseRequired>
<hr className="mt-8" /> <hr className="mt-8" />
<div className="mt-6"> <div className="mt-6">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900"> <h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
@ -157,7 +159,7 @@ export default function SAMLConfiguration({
</div> </div>
<hr className="mt-4" /> <hr className="mt-4" />
</form> </form>
</> </LicenseRequired>
) : null} ) : null}
</> </>
); );

View File

@ -6,10 +6,10 @@ import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray"; import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import Head from "next/head"; import Head from "next/head";
import React, { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl"; import { FormattedNumber, IntlProvider } from "react-intl";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core"; import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import getStripe from "@calcom/stripe/client"; import getStripe from "@calcom/stripe/client";
import PaymentComponent from "@ee/components/stripe/Payment"; import PaymentComponent from "@ee/components/stripe/Payment";
import { PaymentPageProps } from "@ee/pages/payment/[uid]"; import { PaymentPageProps } from "@ee/pages/payment/[uid]";

View File

@ -13,7 +13,6 @@ import ContactMenuItem from "./ContactMenuItem";
export default function HelpMenuItem() { export default function HelpMenuItem() {
const [rating, setRating] = useState<null | string>(null); const [rating, setRating] = useState<null | string>(null);
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
// const [errorMessage, setErrorMessage] = useState(false);
const [disableSubmit, setDisableSubmit] = useState(true); const [disableSubmit, setDisableSubmit] = useState(true);
const { t } = useLocale(); const { t } = useLocale();

View File

@ -2,9 +2,10 @@ import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import LicenseRequired from "@ee/components/LicenseRequired";
import { trpc, inferQueryOutput } from "@lib/trpc"; import { inferQueryOutput, trpc } from "@lib/trpc";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import { DatePicker } from "@components/ui/form/DatePicker"; import { DatePicker } from "@components/ui/form/DatePicker";
@ -33,62 +34,66 @@ export default function TeamAvailabilityModal(props: Props) {
}, [utils, selectedTimeZone, selectedDate]); }, [utils, selectedTimeZone, selectedDate]);
return ( return (
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse"> <LicenseRequired>
<div className="min-w-64 w-64 space-y-5 p-5 pr-0"> <div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
<div className="flex"> <div className="min-w-64 w-64 space-y-5 p-5 pr-0">
<Avatar <div className="flex">
imageSrc={WEBSITE_URL + "/" + props.member?.username + "/avatar.png"} <Avatar
alt={props.member?.name || ""} imageSrc={WEBAPP_URL + "/" + props.member?.username + "/avatar.png"}
className="h-14 w-14 rounded-full" alt={props.member?.name || ""}
/> className="h-14 w-14 rounded-full"
<div className="ml-3 inline-block pt-1"> />
<span className="text-lg font-bold text-neutral-700">{props.member?.name}</span> <div className="ml-3 inline-block pt-1">
<span className="-mt-1 block text-sm text-gray-400">{props.member?.email}</span> <span className="text-lg font-bold text-neutral-700">{props.member?.name}</span>
<span className="-mt-1 block text-sm text-gray-400">{props.member?.email}</span>
</div>
</div>
<div className="flex flex-col">
<span className="font-bold text-gray-600">Date</span>
<DatePicker
date={selectedDate.toDate()}
onDatesChange={(newDate) => {
setSelectedDate(dayjs(newDate));
}}
/>
</div>
<div>
<span className="font-bold text-gray-600">Timezone</span>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
classNamePrefix="react-select"
className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
/>
</div>
<div>
<span className="font-bold text-gray-600">Slot Length</span>
<Select
options={[
{ value: 15, label: "15 minutes" },
{ value: 30, label: "30 minutes" },
{ value: 60, label: "60 minutes" },
]}
isSearchable={false}
classNamePrefix="react-select"
className="react-select-container focus:ring-primary-500 focus:border-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
value={{ value: frequency, label: `${frequency} minutes` }}
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
/>
</div> </div>
</div> </div>
<div className="flex flex-col"> {props.team && props.member && (
<span className="font-bold text-gray-600">Date</span> <TeamAvailabilityTimes
<DatePicker className="overflow-scroll"
date={selectedDate.toDate()} teamId={props.team.id}
onDatesChange={(newDate) => { memberId={props.member.id}
setSelectedDate(dayjs(newDate)); frequency={frequency}
}} selectedDate={selectedDate}
selectedTimeZone={selectedTimeZone}
/> />
</div> )}
<div>
<span className="font-bold text-gray-600">Timezone</span>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={(timezone) => setSelectedTimeZone(timezone.value)}
className="mt-1 block w-full rounded-sm border border-gray-300 shadow-sm sm:text-sm"
/>
</div>
<div>
<span className="font-bold text-gray-600">Slot Length</span>
<Select
options={[
{ value: 15, label: "15 minutes" },
{ value: 30, label: "30 minutes" },
{ value: 60, label: "60 minutes" },
]}
isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
value={{ value: frequency, label: `${frequency} minutes` }}
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
/>
</div>
</div> </div>
{props.team && props.member && ( </LicenseRequired>
<TeamAvailabilityTimes
className="overflow-auto"
teamId={props.team.id}
memberId={props.member.id}
frequency={frequency}
selectedDate={selectedDate}
selectedTimeZone={selectedTimeZone}
/>
)}
</div>
); );
} }

View File

@ -3,7 +3,7 @@ import React, { useState, useEffect, CSSProperties } from "react";
import AutoSizer from "react-virtualized-auto-sizer"; import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeList as List } from "react-window"; import { FixedSizeList as List } from "react-window";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { inferQueryOutput, trpc } from "@lib/trpc"; import { inferQueryOutput, trpc } from "@lib/trpc";
@ -46,7 +46,7 @@ export default function TeamAvailabilityScreen(props: Props) {
HeaderComponent={ HeaderComponent={
<div className="mb-6 flex items-center"> <div className="mb-6 flex items-center">
<Avatar <Avatar
imageSrc={WEBSITE_URL + "/" + member.username + "/avatar.png"} imageSrc={CAL_URL + "/" + member.username + "/avatar.png"}
alt={member?.name || ""} alt={member?.name || ""}
className="min-w-10 min-h-10 mt-1 h-10 w-10 rounded-full" className="min-w-10 min-h-10 mt-1 h-10 w-10 rounded-full"
/> />

View File

@ -11,6 +11,7 @@ import { Button } from "@calcom/ui/Button";
import { useContracts } from "../../../contexts/contractsContext"; import { useContracts } from "../../../contexts/contractsContext";
import genericAbi from "../../../web3/abis/abiWithGetBalance.json"; import genericAbi from "../../../web3/abis/abiWithGetBalance.json";
import verifyAccount, { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount"; import verifyAccount, { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount";
import { withLicenseRequired } from "../LicenseRequired";
interface Window { interface Window {
ethereum: AbstractProvider & { selectedAddress: string }; ethereum: AbstractProvider & { selectedAddress: string };
@ -150,4 +151,4 @@ const CryptoSection = (props: CryptoSectionProps) => {
); );
}; };
export default CryptoSection; export default withLicenseRequired(CryptoSection);

View File

@ -1,6 +1,7 @@
import { PaymentType, Prisma } from "@prisma/client"; import { PaymentType, Prisma } from "@prisma/client";
import Stripe from "stripe"; import Stripe from "stripe";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getErrorFromUnknown } from "@calcom/lib/errors";
@ -17,8 +18,15 @@ export type PaymentInfo = {
id?: string | null; id?: string | null;
}; };
let paymentFeePercentage: number | undefined; const stripeKeysSchema = z.object({
let paymentFeeFixed: number | undefined; payment_fee_fixed: z.number(),
payment_fee_percentage: z.number(),
});
const stripeCredentialSchema = z.object({
stripe_user_id: z.string(),
stripe_publishable_key: z.string(),
});
export async function handlePayment( export async function handlePayment(
evt: CalendarEvent, evt: CalendarEvent,
@ -35,13 +43,10 @@ export async function handlePayment(
} }
) { ) {
const appKeys = await getAppKeysFromSlug("stripe"); const appKeys = await getAppKeysFromSlug("stripe");
if (typeof appKeys.payment_fee_fixed === "number") paymentFeePercentage = appKeys.payment_fee_fixed; const { payment_fee_fixed, payment_fee_percentage } = stripeKeysSchema.parse(appKeys);
if (typeof appKeys.payment_fee_percentage === "number") paymentFeeFixed = appKeys.payment_fee_percentage;
const paymentFee = Math.round( const paymentFee = Math.round(selectedEventType.price * payment_fee_percentage + payment_fee_fixed);
selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`) const { stripe_user_id, stripe_publishable_key } = stripeCredentialSchema.parse(stripeCredential.key);
);
const { stripe_user_id, stripe_publishable_key } = stripeCredential.key as Stripe.OAuthToken;
const params: Stripe.PaymentIntentCreateParams = { const params: Stripe.PaymentIntentCreateParams = {
amount: selectedEventType.price, amount: selectedEventType.price,

View File

@ -2,6 +2,7 @@ import { useRouter } from "next/router";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Alert } from "@calcom/ui/Alert"; import { Alert } from "@calcom/ui/Alert";
import LicenseRequired from "@ee/components/LicenseRequired";
import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen"; import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
@ -49,17 +50,19 @@ export function TeamAvailabilityPage() {
/> />
) )
}> }>
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />} <LicenseRequired>
{isLoading && <Loader />} {!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
{isFreeUser ? ( {isLoading && <Loader />}
<Alert {isFreeUser ? (
className="-mt-24 border" <Alert
severity="warning" className="-mt-24 border"
title="This is a pro feature. Upgrade to pro to see your team's availability." severity="warning"
/> title="This is a pro feature. Upgrade to pro to see your team's availability."
) : ( />
TeamAvailability ) : (
)} TeamAvailability
)}
</LicenseRequired>
</Shell> </Shell>
); );
} }

View File

@ -0,0 +1,35 @@
import React, { ErrorInfo } from "react";
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ error: Error | null; errorInfo: ErrorInfo | null }
> {
constructor(props: { children: React.ReactNode } | Readonly<{ children: React.ReactNode }>) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch?(error: Error, errorInfo: ErrorInfo) {
// Catch errors in any components below and re-render with error message
this.setState({ error, errorInfo });
// You can also log error messages to an error reporting service here
}
render() {
if (this.state.errorInfo) {
// Error path
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: "pre-wrap" }}>
{this.state.error && this.state.error.toString()}
</details>
</div>
);
}
// Normally, just render children
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -16,7 +16,8 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps & { children: React.Rea
)); ));
// Workaround for https://github.com/vercel/next.js/issues/8592 // Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = NextAppProps & { export type AppProps = Omit<NextAppProps, "Component"> & {
Component: NextAppProps["Component"] & { requiresLicense?: boolean };
/** Will be defined only is there was an error */ /** Will be defined only is there was an error */
err?: Error; err?: Error;
}; };

View File

@ -3,6 +3,7 @@ import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calend
import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email"; import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email";
import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email"; import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email";
import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email"; import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email";
import AttendeeLocationChangeEmail from "@lib/emails/templates/attendee-location-change-email";
import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email"; import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email";
import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email"; import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email";
import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email"; import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email";
@ -10,6 +11,7 @@ import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-ema
import FeedbackEmail, { Feedback } from "@lib/emails/templates/feedback-email"; import FeedbackEmail, { Feedback } from "@lib/emails/templates/feedback-email";
import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email"; import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email";
import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-email"; import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-email";
import OrganizerLocationChangeEmail from "@lib/emails/templates/organizer-location-change-email";
import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email"; import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email";
import OrganizerRequestEmail from "@lib/emails/templates/organizer-request-email"; import OrganizerRequestEmail from "@lib/emails/templates/organizer-request-email";
import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-request-reminder-email"; import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-request-reminder-email";
@ -268,6 +270,38 @@ export const sendRequestRescheduleEmail = async (
await Promise.all(emailsToSend); await Promise.all(emailsToSend);
}; };
export const sendLocationChangeEmails = async (
calEvent: CalendarEvent,
recurringEvent: RecurringEvent = {}
) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return new Promise((resolve, reject) => {
try {
const scheduledEmail = new AttendeeLocationChangeEmail(calEvent, attendee, recurringEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeLocationChangeEmail.sendEmail failed", e));
}
});
})
);
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const scheduledEmail = new OrganizerLocationChangeEmail(calEvent, recurringEvent);
resolve(scheduledEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerLocationChangeEmail.sendEmail failed", e));
}
})
);
await Promise.all(emailsToSend);
};
export const sendFeedbackEmail = async (feedback: Feedback) => { export const sendFeedbackEmail = async (feedback: Feedback) => {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
try { try {

View File

@ -0,0 +1,165 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.attendee.language.translate("location_changed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
// Only the original attendee can make changes to the event
// Guests cannot
if (this.attendee === this.calEvent.attendees[0]) {
return `
${this.attendee.language.translate("event_location_changed")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.attendee.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
return `
${this.attendee.language.translate("event_location_changed")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.attendee.language.translate("location_changed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.attendee.language.translate("event_location_changed"),
this.attendee.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}

View File

@ -102,6 +102,7 @@ ${this.calEvent.organizer.language.translate("request_reschedule_title_attendee"
${this.calEvent.organizer.language.translate("request_reschedule_subtitle", { ${this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
organizer: this.calEvent.organizer.name, organizer: this.calEvent.organizer.name,
})}, })},
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getAdditionalNotes()} ${this.getAdditionalNotes()}
@ -151,6 +152,7 @@ ${getCancelLink(this.calEvent)}
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;"> <div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getWho()} ${this.getWho()}

View File

@ -52,6 +52,7 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
return ` return `
${this.attendee.language.translate("event_has_been_rescheduled")} ${this.attendee.language.translate("event_has_been_rescheduled")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")} ${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
@ -112,6 +113,7 @@ ${this.getCustomInputs()}
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;"> <div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getWho()} ${this.getWho()}

View File

@ -452,4 +452,12 @@ ${getRichDescription(this.calEvent)}
protected getInviteeEnd(): Dayjs { protected getInviteeEnd(): Dayjs {
return dayjs(this.calEvent.endTime).tz(this.getTimezone()); return dayjs(this.calEvent.endTime).tz(this.getTimezone());
} }
protected getReason(): string {
return `
<div style="line-height: 6px; margin-bottom: 24px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("reschedule_reason")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p>
</div>`;
}
} }

View File

@ -0,0 +1,162 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("location_changed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("event_location_changed")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("location_changed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("event_location_changed"),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}

View File

@ -111,6 +111,7 @@ ${this.calEvent.organizer.language.translate("request_reschedule_title_organizer
${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { ${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
attendee: this.calEvent.attendees[0].name, attendee: this.calEvent.attendees[0].name,
})}, })},
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
@ -163,6 +164,7 @@ ${getCancelLink(this.calEvent)}
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;"> <div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getWho()} ${this.getWho()}

View File

@ -59,6 +59,7 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
return ` return `
${this.calEvent.organizer.language.translate("event_has_been_rescheduled")} ${this.calEvent.organizer.language.translate("event_has_been_rescheduled")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")} ${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getLocation()} ${this.getLocation()}
@ -108,6 +109,7 @@ ${getCancelLink(this.calEvent)}
<tr> <tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;"> <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;"> <div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()} ${this.getWhat()}
${this.getWhen()} ${this.getWhen()}
${this.getWho()} ${this.getWho()}

View File

@ -439,4 +439,12 @@ ${getRichDescription(this.calEvent)}
protected getOrganizerEnd(): Dayjs { protected getOrganizerEnd(): Dayjs {
return dayjs(this.calEvent.endTime).tz(this.getTimezone()); return dayjs(this.calEvent.endTime).tz(this.getTimezone());
} }
protected getReason(): string {
return `
<div style="line-height: 6px; margin-bottom: 24px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("reschedule_reason")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p>
</div>`;
}
} }

View File

@ -1,7 +1,7 @@
import Head from "next/head"; import Head from "next/head";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useEmbedTheme } from "@calcom/embed-core"; import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
import { Maybe } from "@trpc/server"; import { Maybe } from "@trpc/server";

View File

@ -0,0 +1,41 @@
import { TFunction } from "next-i18next";
import { LocationType } from "./location";
/**
* Use this function to translate booking location value to a readable string
* @param linkValue
* @param translationFunction
* @returns
*/
export const linkValueToString = (
linkValue: string | undefined | null,
translationFunction: TFunction
): string => {
const t = translationFunction;
if (!linkValue) {
return translationFunction("no_location");
}
switch (linkValue) {
case LocationType.InPerson:
return t("in_person_meeting");
case LocationType.UserPhone:
return t("user_phone");
case LocationType.GoogleMeet:
return `Google Meet: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Zoom:
return `Zoom: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Daily:
return `Cal Video: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Jitsi:
return `Jitsi: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Huddle01:
return `Huddle01t: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Tandem:
return `Tandem: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Teams:
return `Teams: ${t("meeting_url_in_conformation_email")}`;
default:
return linkValue || "";
}
};

View File

@ -2,6 +2,12 @@ import { TFunction } from "next-i18next";
import { LocationType } from "./location"; import { LocationType } from "./location";
/**
* Use this function for translating event location to a readable string
* @param location
* @param t
* @returns string
*/
export const LocationOptionsToString = (location: string, t: TFunction) => { export const LocationOptionsToString = (location: string, t: TFunction) => {
switch (location) { switch (location) {
case LocationType.InPerson: case LocationType.InPerson:

View File

@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "1.6.1", "version": "1.6.2",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@ -76,6 +76,7 @@
"jimp": "^0.16.1", "jimp": "^0.16.1",
"libphonenumber-js": "^1.9.53", "libphonenumber-js": "^1.9.53",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"memory-cache": "^0.2.0",
"micro": "^9.3.4", "micro": "^9.3.4",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"next": "^12.1.6", "next": "^12.1.6",
@ -125,6 +126,7 @@
"@types/glidejs__glide": "^3.4.2", "@types/glidejs__glide": "^3.4.2",
"@types/jest": "^27.5.1", "@types/jest": "^27.5.1",
"@types/lodash": "^4.14.182", "@types/lodash": "^4.14.182",
"@types/memory-cache": "^0.2.2",
"@types/micro": "7.3.6", "@types/micro": "7.3.6",
"@types/mime-types": "^2.1.1", "@types/mime-types": "^2.1.1",
"@types/module-alias": "^2.0.1", "@types/module-alias": "^2.0.1",

View File

@ -10,7 +10,12 @@ import { useEffect, useState } from "react";
import { Toaster } from "react-hot-toast"; import { Toaster } from "react-hot-toast";
import { JSONObject } from "superjson/dist/types"; import { JSONObject } from "superjson/dist/types";
import { sdkActionManager, useEmbedNonStylesConfig, useEmbedStyles, useIsEmbed } from "@calcom/embed-core"; import {
sdkActionManager,
useEmbedNonStylesConfig,
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import defaultEvents, { import defaultEvents, {
getDynamicEventDescription, getDynamicEventDescription,
getGroupName, getGroupName,
@ -337,11 +342,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
weekStart: "Sunday", weekStart: "Sunday",
brandColor: "", brandColor: "",
darkBrandColor: "", darkBrandColor: "",
allowDynamicBooking: users.some((user) => { allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking; return !user.allowDynamicBooking;
}) }),
? false
: true,
} }
: { : {
name: user.name || user.username, name: user.name || user.username,

View File

@ -3,7 +3,7 @@ import { UserPlan } from "@prisma/client";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types"; import { JSONObject } from "superjson/dist/types";
import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@calcom/app-store/locations"; import { locationHiddenFilter, LocationObject } from "@calcom/app-store/locations";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents"; import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar"; import { RecurringEvent } from "@calcom/types/Calendar";
@ -307,11 +307,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
weekStart: "Sunday", weekStart: "Sunday",
brandColor: "", brandColor: "",
darkBrandColor: "", darkBrandColor: "",
allowDynamicBooking: users.some((user) => { allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking; return !user.allowDynamicBooking;
}) }),
? false
: true,
} }
: { : {
name: user.name || user.username, name: user.name || user.username,

View File

@ -200,11 +200,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
theme: null, theme: null,
brandColor: "", brandColor: "",
darkBrandColor: "", darkBrandColor: "",
allowDynamicBooking: users.some((user) => { allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking; return !user.allowDynamicBooking;
}) }),
? false
: true,
eventName: getDynamicEventName(dynamicNames, eventTypeSlug), eventName: getDynamicEventName(dynamicNames, eventTypeSlug),
} }
: { : {

View File

@ -3,6 +3,7 @@ import Head from "next/head";
import superjson from "superjson"; import superjson from "superjson";
import "@calcom/embed-core/src/embed-iframe"; import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@ee/components/LicenseRequired";
import AppProviders, { AppProps } from "@lib/app-providers"; import AppProviders, { AppProps } from "@lib/app-providers";
import { seoConfig } from "@lib/config/next-seo.config"; import { seoConfig } from "@lib/config/next-seo.config";
@ -37,7 +38,13 @@ function MyApp(props: AppProps) {
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script> <script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head> </Head>
<Component {...pageProps} err={err} /> {Component.requiresLicense ? (
<LicenseRequired>
<Component {...pageProps} err={err} />
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
)}
</AppProviders> </AppProviders>
</ContractsProvider> </ContractsProvider>
); );

View File

@ -10,6 +10,7 @@ import nodemailer, { TransportOptions } from "nodemailer";
import { authenticator } from "otplib"; import { authenticator } from "otplib";
import path from "path"; import path from "path";
import checkLicense from "@calcom/ee/server/checkLicense";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { WEBSITE_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto"; import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies"; import { defaultCookies } from "@calcom/lib/default-cookies";
@ -276,8 +277,10 @@ export default NextAuth({
return token; return token;
}, },
async session({ session, token }) { async session({ session, token }) {
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
const calendsoSession: Session = { const calendsoSession: Session = {
...session, ...session,
hasValidLicense,
user: { user: {
...session.user, ...session.user,
id: token.id as number, id: token.id as number,

View File

@ -201,7 +201,11 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
type User = Prisma.UserGetPayload<typeof userSelect>; type User = Prisma.UserGetPayload<typeof userSelect>;
type ExtendedBookingCreateBody = BookingCreateBody & { noEmail?: boolean; recurringCount?: number }; type ExtendedBookingCreateBody = BookingCreateBody & {
noEmail?: boolean;
recurringCount?: number;
rescheduleReason?: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody; const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody;
@ -677,7 +681,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (originalRescheduledBooking?.uid) { if (originalRescheduledBooking?.uid) {
// Use EventManager to conditionally use all needed integrations. // Use EventManager to conditionally use all needed integrations.
const updateManager = await eventManager.update(evt, originalRescheduledBooking.uid, booking?.id); const updateManager = await eventManager.update(
evt,
originalRescheduledBooking.uid,
booking?.id,
reqBody.rescheduleReason
);
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back // This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails. // to the default description when we are sending the emails.
evt.description = eventType.description; evt.description = eventType.description;
@ -711,7 +720,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{ {
...evt, ...evt,
additionInformation: metadata, additionInformation: metadata,
additionalNotes, // Resets back to the addtionalNote input and not the overriden value additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: reqBody.rescheduleReason,
}, },
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {} reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
); );

View File

@ -1,4 +1,3 @@
import { ClipboardIcon } from "@heroicons/react/solid";
import Image from "next/image"; import Image from "next/image";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { JSONObject } from "superjson/dist/types"; import { JSONObject } from "superjson/dist/types";

View File

@ -7,7 +7,7 @@ import { RecurringEvent } from "@calcom/types/Calendar";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability"; import { getWorkingHours } from "@lib/availability";
import { GetBookingType } from "@lib/getBooking"; import { GetBookingType } from "@lib/getBooking";
import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@lib/location"; import { locationHiddenFilter, LocationObject } from "@lib/location";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";

View File

@ -34,6 +34,7 @@ import { z } from "zod";
import { SelectGifInput } from "@calcom/app-store/giphy/components"; import { SelectGifInput } from "@calcom/app-store/giphy/components";
import getApps, { getLocationOptions } from "@calcom/app-store/utils"; import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification"; import showToast from "@calcom/lib/notification";
import { StripeData } from "@calcom/stripe/server"; import { StripeData } from "@calcom/stripe/server";
@ -64,6 +65,7 @@ import Shell from "@components/Shell";
import { UpgradeToProDialog } from "@components/UpgradeToProDialog"; import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader"; import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import RecurringEventController from "@components/eventtype/RecurringEventController"; import RecurringEventController from "@components/eventtype/RecurringEventController";
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm"; import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
import Badge from "@components/ui/Badge"; import Badge from "@components/ui/Badge";
@ -72,7 +74,6 @@ import CheckboxField from "@components/ui/form/CheckboxField";
import CheckedSelect from "@components/ui/form/CheckedSelect"; import CheckedSelect from "@components/ui/form/CheckedSelect";
import { DateRangePicker } from "@components/ui/form/DateRangePicker"; import { DateRangePicker } from "@components/ui/form/DateRangePicker";
import MinutesField from "@components/ui/form/MinutesField"; import MinutesField from "@components/ui/form/MinutesField";
import PhoneInput from "@components/ui/form/PhoneInput";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
import * as RadioArea from "@components/ui/form/radio-area"; import * as RadioArea from "@components/ui/form/radio-area";
import WebhookListContainer from "@components/webhook/WebhookListContainer"; import WebhookListContainer from "@components/webhook/WebhookListContainer";
@ -179,7 +180,7 @@ const SuccessRedirectEdit = <T extends UseFormReturn<FormValues>>({
}} }}
readOnly={proUpgradeRequired} readOnly={proUpgradeRequired}
type="url" type="url"
className=" block w-full rounded-sm border-gray-300 sm:text-sm" className="block w-full rounded-sm border-gray-300 sm:text-sm"
placeholder={t("external_redirect_url")} placeholder={t("external_redirect_url")}
defaultValue={eventType.successRedirectUrl || ""} defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")} {...formMethods.register("successRedirectUrl")}
@ -323,10 +324,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
); );
const [tokensList, setTokensList] = useState<Array<Token>>([]); const [tokensList, setTokensList] = useState<Array<Token>>([]);
const defaultSeats = 2; const defaultSeatsPro = 6;
const defaultSeatsInput = 6; const minSeats = 2;
const [enableSeats, setEnableSeats] = useState(!!eventType.seatsPerTimeSlot); const [enableSeats, setEnableSeats] = useState(!!eventType.seatsPerTimeSlot);
const [inputSeatNumber, setInputSeatNumber] = useState(eventType.seatsPerTimeSlot! >= defaultSeatsInput);
const periodType = const periodType =
PERIOD_TYPES.find((s) => s.type === eventType.periodType) || PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
@ -422,134 +422,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
formMethods.getValues("locations").concat({ type: newLocationType, ...details }) formMethods.getValues("locations").concat({ type: newLocationType, ...details })
); );
} }
}; setShowLocationModal(false);
const LocationOptions = () => {
if (!selectedLocation) {
return null;
}
switch (selectedLocation.value) {
case LocationType.InPerson:
return (
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_address_place")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationAddress")}
id="address"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
formMethods
.getValues("locations")
.find((location) => location.type === LocationType.InPerson)?.address
}
/>
</div>
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={({ field: { onChange, value } }) => (
<CheckboxField
description={t("display_location_label")}
onChange={(e) => onChange(e.target.checked)}
infomationIconText={t("display_location_info_badge")}></CheckboxField>
)}
/>
</div>
</div>
);
case LocationType.Link:
return (
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_link_meeting")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="address"
required
className=" block w-full rounded-sm border-gray-300 sm:text-sm"
defaultValue={
formMethods.getValues("locations").find((location) => location.type === LocationType.Link)
?.link
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-red-500">
{locationFormMethods.formState.errors.locationLink.message}
</p>
)}
</div>
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={({ field: { onChange, value } }) => (
<CheckboxField
description={t("display_location_label")}
onChange={(e) => onChange(e.target.checked)}
infomationIconText={t("display_location_info_badge")}></CheckboxField>
)}
/>
</div>
</div>
);
case LocationType.UserPhone:
return (
<div>
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
{t("set_your_phone_number")}
</label>
<div className="mt-1">
<PhoneInput
control={locationFormMethods.control}
name="locationPhoneNumber"
required
id="locationPhoneNumber"
placeholder={t("host_phone_number")}
rules={{}}
defaultValue={
formMethods
.getValues("locations")
.find((location) => location.type === LocationType.UserPhone)?.hostPhoneNumber
}
/>
{locationFormMethods.formState.errors.locationPhoneNumber && (
<p className="mt-1 text-red-500">
{locationFormMethods.formState.errors.locationPhoneNumber.message}
</p>
)}
</div>
</div>
);
case LocationType.Phone:
return <p className="text-sm">{t("cal_invitee_phone_number_scheduling")}</p>;
/* TODO: Render this dynamically from App Store */
case LocationType.GoogleMeet:
return <p className="text-sm">{t("cal_provide_google_meet_location")}</p>;
case LocationType.Zoom:
return <p className="text-sm">{t("cal_provide_zoom_meeting_url")}</p>;
case LocationType.Daily:
return <p className="text-sm">{t("cal_provide_video_meeting_url")}</p>;
case LocationType.Jitsi:
return <p className="text-sm">{t("cal_provide_jitsi_meeting_url")}</p>;
case LocationType.Huddle01:
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
case LocationType.Tandem:
return <p className="text-sm">{t("cal_provide_tandem_meeting_url")}</p>;
case LocationType.Teams:
return <p className="text-sm">{t("cal_provide_teams_meeting_url")}</p>;
default:
return null;
}
}; };
const removeCustom = (index: number) => { const removeCustom = (index: number) => {
@ -580,11 +453,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
endDate: new Date(eventType.periodEndDate || Date.now()), endDate: new Date(eventType.periodEndDate || Date.now()),
}); });
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${ const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
team ? `team/${team.slug}` : eventType.users[0].username eventType.slug
}/${eventType.slug}`; }`;
const placeholderHashedLink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/d/${hashedUrl}/${eventType.slug}`; const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const mapUserToValue = ({ const mapUserToValue = ({
id, id,
@ -597,7 +470,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
}) => ({ }) => ({
value: `${id || ""}`, value: `${id || ""}`,
label: `${name || ""}`, label: `${name || ""}`,
avatar: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${username}/avatar.png`, avatar: `${WEBAPP_URL}/${username}/avatar.png`,
}); });
const formMethods = useForm<FormValues>({ const formMethods = useForm<FormValues>({
@ -641,7 +514,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<Select <Select
options={locationOptions} options={locationOptions}
isSearchable={false} isSearchable={false}
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm" className="block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
onChange={(e) => { onChange={(e) => {
if (e?.value) { if (e?.value) {
const newLocationType: LocationType = e.value; const newLocationType: LocationType = e.value;
@ -669,7 +542,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="flex flex-grow items-center"> <div className="flex flex-grow items-center">
<LocationMarkerIcon className="h-6 w-6" /> <LocationMarkerIcon className="h-6 w-6" />
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2"> <span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
{location.link} {location.address}
</span> </span>
</div> </div>
)} )}
@ -1081,7 +954,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="w-full"> <div className="w-full">
<div className="flex rounded-sm"> <div className="flex rounded-sm">
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500"> <span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
{process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}/ {CAL_URL?.replace(/^(https?:|)\/\//, "")}/
{team ? "team/" + team.slug : eventType.users[0].username}/ {team ? "team/" + team.slug : eventType.users[0].username}/
</span> </span>
<input <input
@ -1089,7 +962,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
id="slug" id="slug"
aria-labelledby="slug-label" aria-labelledby="slug-label"
required required
className=" block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm" className="block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm"
defaultValue={eventType.slug} defaultValue={eventType.slug}
{...formMethods.register("slug", { {...formMethods.register("slug", {
setValueAs: (v) => slugify(v), setValueAs: (v) => slugify(v),
@ -1155,7 +1028,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="w-full"> <div className="w-full">
<textarea <textarea
id="description" id="description"
className=" block w-full rounded-sm border-gray-300 text-sm" className="block w-full rounded-sm border-gray-300 text-sm "
placeholder={t("quick_video_meeting")} placeholder={t("quick_video_meeting")}
{...formMethods.register("description")} {...formMethods.register("description")}
defaultValue={asStringOrUndefined(eventType.description)}></textarea> defaultValue={asStringOrUndefined(eventType.description)}></textarea>
@ -1312,7 +1185,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="relative mt-1 rounded-sm"> <div className="relative mt-1 rounded-sm">
<input <input
type="text" type="text"
className=" block w-full rounded-sm border-gray-300 text-sm" className="block w-full rounded-sm border-gray-300 text-sm "
placeholder={t("meeting_with_user")} placeholder={t("meeting_with_user")}
defaultValue={eventType.eventName || ""} defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")} {...formMethods.register("eventName")}
@ -1334,7 +1207,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
{ {
<input <input
type="text" type="text"
className=" block w-full rounded-sm border-gray-300 text-sm" className="block w-full rounded-sm border-gray-300 text-sm "
placeholder={t("Example: 0x71c7656ec7ab88b098defb751b7401b5f6d8976f")} placeholder={t("Example: 0x71c7656ec7ab88b098defb751b7401b5f6d8976f")}
defaultValue={(eventType.metadata.smartContractAddress || "") as string} defaultValue={(eventType.metadata.smartContractAddress || "") as string}
{...formMethods.register("smartContractAddress")} {...formMethods.register("smartContractAddress")}
@ -1439,9 +1312,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<Controller <Controller
name="requiresConfirmation" name="requiresConfirmation"
control={formMethods.control}
defaultValue={eventType.requiresConfirmation} defaultValue={eventType.requiresConfirmation}
render={() => ( render={({ field: { value, onChange } }) => (
<CheckboxField <CheckboxField
id="requiresConfirmation" id="requiresConfirmation"
descriptionAsLabel descriptionAsLabel
@ -1450,10 +1322,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
description={t("opt_in_booking_description")} description={t("opt_in_booking_description")}
defaultChecked={eventType.requiresConfirmation} defaultChecked={eventType.requiresConfirmation}
disabled={enableSeats} disabled={enableSeats}
checked={formMethods.watch("disableGuests")} checked={value}
onChange={(e) => { onChange={(e) => onChange(e?.target.checked)}
formMethods.setValue("requiresConfirmation", e?.target.checked);
}}
/> />
)} )}
/> />
@ -1518,7 +1388,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
name="hashedLink" name="hashedLink"
data-testid="generated-hash-url" data-testid="generated-hash-url"
type="text" type="text"
className=" grow select-none border-gray-300 bg-gray-50 text-sm text-gray-500 ltr:rounded-l-sm rtl:rounded-r-sm" className="grow select-none border-gray-300 bg-gray-50 text-sm text-gray-500 ltr:rounded-l-sm rtl:rounded-r-sm"
defaultValue={placeholderHashedLink} defaultValue={placeholderHashedLink}
/> />
<Tooltip <Tooltip
@ -1654,7 +1524,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
/> />
<select <select
id="" id=""
className=" block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm" className="block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
{...formMethods.register("periodCountCalendarDays")} {...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}> defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option> <option value="1">{t("calendar_days")}</option>
@ -1728,7 +1598,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
return ( return (
<Select <Select
isSearchable={false} isSearchable={false}
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm" className="block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
onChange={(val) => { onChange={(val) => {
if (val) onChange(val.value); if (val) onChange(val.value);
}} }}
@ -1766,7 +1636,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
return ( return (
<Select <Select
isSearchable={false} isSearchable={false}
className=" block w-full min-w-0 flex-1 rounded-sm sm:text-sm" className="block w-full min-w-0 flex-1 rounded-sm sm:text-sm"
onChange={(val) => { onChange={(val) => {
if (val) onChange(val.value); if (val) onChange(val.value);
}} }}
@ -1802,7 +1672,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (e?.target.checked) { if (e?.target.checked) {
setEnableSeats(true); setEnableSeats(true);
// Want to disable individuals from taking multiple seats // Want to disable individuals from taking multiple seats
formMethods.setValue("seatsPerTimeSlot", defaultSeats); formMethods.setValue("seatsPerTimeSlot", defaultSeatsPro);
formMethods.setValue("disableGuests", true); formMethods.setValue("disableGuests", true);
formMethods.setValue("requiresConfirmation", false); formMethods.setValue("requiresConfirmation", false);
} else { } else {
@ -1860,14 +1730,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</label> </label>
<input <input
type="number" type="number"
className="focus:border-primary-500 focus:ring-primary-500 py- block w-20 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm" className="focus:border-primary-500 focus:ring-primary-500 py- block w-20 rounded-sm border-gray-300 [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
placeholder={`${defaultSeatsInput}`} placeholder={`${defaultSeatsPro}`}
min={minSeats}
{...formMethods.register("seatsPerTimeSlot", { {...formMethods.register("seatsPerTimeSlot", {
valueAsNumber: true, valueAsNumber: true,
min: defaultSeatsInput,
})} })}
defaultValue={ defaultValue={
eventType.seatsPerTimeSlot || defaultSeatsInput eventType.seatsPerTimeSlot || defaultSeatsPro
} }
/> />
</div> </div>
@ -1883,25 +1753,18 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
classNamePrefix="react-select" classNamePrefix="react-select"
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-auto rounded-sm border border-gray-300 sm:text-sm " className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-auto rounded-sm border border-gray-300 sm:text-sm "
onChange={(val) => { onChange={(val) => {
if (val!.value === -1) { if (!val) {
formMethods.setValue( return;
"seatsPerTimeSlot", }
defaultSeatsInput if (val.value === -1) {
); formMethods.setValue("seatsPerTimeSlot", minSeats);
setInputSeatNumber(true);
} else { } else {
setInputSeatNumber(false); formMethods.setValue("seatsPerTimeSlot", val.value);
formMethods.setValue(
"seatsPerTimeSlot",
val!.value
);
} }
}} }}
defaultValue={{ defaultValue={{
value: eventType.seatsPerTimeSlot || defaultSeats, value: eventType.seatsPerTimeSlot || minSeats,
label: `${ label: `${eventType.seatsPerTimeSlot || minSeats}`,
eventType.seatsPerTimeSlot || defaultSeats
}`,
}} }}
options={selectSeatsPerTimeSlotOptions} options={selectSeatsPerTimeSlotOptions}
/> />
@ -1954,7 +1817,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
id="requirePayment" id="requirePayment"
name="requirePayment" name="requirePayment"
type="checkbox" type="checkbox"
className="text-primary-600 h-4 w-4 rounded border-gray-300" className="text-primary-600 h-4 w-4 rounded border-gray-300"
defaultChecked={requirePayment} defaultChecked={requirePayment}
/> />
</div> </div>
@ -1992,12 +1855,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
min="0.5" min="0.5"
type="number" type="number"
required required
className=" block w-full rounded-sm border-gray-300 pl-2 pr-12 sm:text-sm" className="block w-full rounded-sm border-gray-300 pl-2 pr-12 sm:text-sm"
placeholder="Price" placeholder="Price"
onChange={(e) => { onChange={(e) => {
field.onChange(e.target.valueAsNumber * 100); field.onChange(e.target.valueAsNumber * 100);
}} }}
value={field.value > 0 ? field.value / 100 : 0} value={field.value > 0 ? field.value / 100 : undefined}
/> />
)} )}
/> />
@ -2145,78 +2008,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</div> </div>
</div> </div>
</div> </div>
<Dialog open={showLocationModal} onOpenChange={setShowLocationModal}> <EditLocationDialog
<DialogContent asChild> isOpenDialog={showLocationModal}
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle"> setShowLocationModal={setShowLocationModal}
<div className="mb-4 sm:flex sm:items-start"> saveLocation={addLocation}
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10"> defaultValues={formMethods.getValues("locations")}
<LocationMarkerIcon className="text-primary-600 h-6 w-6" /> selection={
</div> selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> }
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title"> setSelectedLocation={setSelectedLocation}
{t("edit_location")} />
</h3>
<div>
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
</div>
</div>
</div>
<Form
form={locationFormMethods}
handleSubmit={async (values) => {
const { locationType: newLocation, displayLocationPublicly } = values;
let details = {};
if (newLocation === LocationType.InPerson) {
details = {
address: values.locationAddress,
displayLocationPublicly,
};
}
if (newLocation === LocationType.Link) {
details = { link: values.locationLink, displayLocationPublicly };
}
if (newLocation === LocationType.UserPhone) {
details = { hostPhoneNumber: values.locationPhoneNumber };
}
addLocation(newLocation, details);
setShowLocationModal(false);
}}>
<div>
<Controller
name="locationType"
control={locationFormMethods.control}
render={() => (
<Select
maxMenuHeight={100}
name="location"
defaultValue={selectedLocation}
options={locationOptions}
isSearchable={false}
className=" my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
onChange={(val) => {
if (val) {
locationFormMethods.setValue("locationType", val.value);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
setSelectedLocation(val);
}
}}
/>
)}
/>
</div>
<LocationOptions />
<div className="mt-4 flex justify-end space-x-2">
<Button onClick={() => setShowLocationModal(false)} type="button" color="secondary">
{t("cancel")}
</Button>
<Button type="submit">{t("update")}</Button>
</div>
</Form>
</div>
</DialogContent>
</Dialog>
<Controller <Controller
name="customInputs" name="customInputs"
control={formMethods.control} control={formMethods.control}
@ -2294,6 +2095,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const session = await getSession({ req }); const session = await getSession({ req });
const typeParam = parseInt(asStringOrThrow(query.type)); const typeParam = parseInt(asStringOrThrow(query.type));
if (Number.isNaN(typeParam)) {
return {
notFound: true,
};
}
if (!session?.user?.id) { if (!session?.user?.id) {
return { return {
redirect: { redirect: {
@ -2406,7 +2213,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}, },
}); });
if (!rawEventType) throw Error("Event type not found"); if (!rawEventType) {
return {
notFound: true,
};
}
const credentials = await prisma.credential.findMany({ const credentials = await prisma.credential.findMany({
where: { where: {
@ -2478,7 +2289,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const teamMembers = eventTypeObject.team const teamMembers = eventTypeObject.team
? eventTypeObject.team.members.map((member) => { ? eventTypeObject.team.members.map((member) => {
const user = member.user; const user = member.user;
user.avatar = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`; user.avatar = `${CAL_URL}/${user.username}/avatar.png`;
return user; return user;
}) })
: []; : [];

View File

@ -19,12 +19,12 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react"; import React, { Fragment, useEffect, useState } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification"; import showToast from "@calcom/lib/notification";
import { Button } from "@calcom/ui"; import { Button } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert"; import { Alert } from "@calcom/ui/Alert";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog"; import { Dialog } from "@calcom/ui/Dialog";
import Dropdown, { import Dropdown, {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@ -246,7 +246,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
truncateAfter={4} truncateAfter={4}
items={type.users.map((organizer) => ({ items={type.users.map((organizer) => ({
alt: organizer.name || "", alt: organizer.name || "",
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${organizer.username}/avatar.png`, image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
}))} }))}
/> />
)} )}
@ -257,7 +257,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
)}> )}>
<Tooltip content={t("preview") as string}> <Tooltip content={t("preview") as string}>
<a <a
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`} href={`${CAL_URL}/${group.profile.slug}/${type.slug}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={classNames("btn-icon appearance-none", type.$disabled && " opacity-30")}> className={classNames("btn-icon appearance-none", type.$disabled && " opacity-30")}>
@ -271,9 +271,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<button <button
onClick={() => { onClick={() => {
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
navigator.clipboard.writeText( navigator.clipboard.writeText(`${CAL_URL}/${group.profile.slug}/${type.slug}`);
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`
);
}} }}
className={classNames("btn-icon", type.$disabled && " opacity-30")}> className={classNames("btn-icon", type.$disabled && " opacity-30")}>
<LinkIcon <LinkIcon
@ -354,8 +352,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent portalled> <DropdownMenuContent portalled>
<DropdownMenuItem> <DropdownMenuItem>
<Link <Link href={`${CAL_URL}/${group.profile.slug}/${type.slug}`}>
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`}>
<a target="_blank"> <a target="_blank">
<Button <Button
color="minimal" color="minimal"
@ -376,9 +373,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={ClipboardCopyIcon} StartIcon={ClipboardCopyIcon}
onClick={() => { onClick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(`${CAL_URL}/${group.profile.slug}/${type.slug}`);
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`
);
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
}}> }}>
{t("copy_link") as string} {t("copy_link") as string}
@ -398,7 +393,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
.share({ .share({
title: t("share"), title: t("share"),
text: t("share_event"), text: t("share_event"),
url: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`, url: `${CAL_URL}/${group.profile.slug}/${type.slug}`,
}) })
.then(() => showToast(t("link_shared"), "success")) .then(() => showToast(t("link_shared"), "success"))
.catch(() => showToast(t("failed"), "error")); .catch(() => showToast(t("failed"), "error"));
@ -500,11 +495,10 @@ const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeading
</span> </span>
)} )}
{profile?.slug && ( {profile?.slug && (
<Link href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${profile.slug}`}> <Link href={`${CAL_URL}/${profile.slug}`}>
<a className="block text-xs text-neutral-500">{`${process.env.NEXT_PUBLIC_WEBSITE_URL?.replace( <a className="block text-xs text-neutral-500">{`${CAL_URL?.replace("https://", "")}/${
"https://", profile.slug
"" }`}</a>
)}/${profile.slug}`}</a>
</Link> </Link>
)} )}
</div> </div>

View File

@ -36,7 +36,6 @@ import { ClientSuspense } from "@components/ClientSuspense";
import Loader from "@components/Loader"; import Loader from "@components/Loader";
import Schedule from "@components/availability/Schedule"; import Schedule from "@components/availability/Schedule";
import { CalendarListContainer } from "@components/integrations/CalendarListContainer"; import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
import Text from "@components/ui/Text";
import TimezoneSelect from "@components/ui/form/TimezoneSelect"; import TimezoneSelect from "@components/ui/form/TimezoneSelect";
import getEventTypes from "../lib/queries/event-types/get-event-types"; import getEventTypes from "../lib/queries/event-types/get-event-types";
@ -398,10 +397,10 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700"> <label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")} {t("timezone")}
</label> </label>
<Text variant="caption"> <p className="text-sm leading-tight text-gray-500 dark:text-white">
{t("current_time")}:&nbsp; {t("current_time")}:&nbsp;
<span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span> <span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span>
</Text> </p>
</section> </section>
<TimezoneSelect <TimezoneSelect
id="timeZone" id="timeZone"
@ -529,9 +528,9 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm" className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
defaultValue={props.user.bio || undefined} defaultValue={props.user.bio || undefined}
/> />
<Text variant="caption" className="mt-2"> <p className="mt-2 text-sm leading-tight text-gray-500 dark:text-white">
{t("few_sentences_about_yourself")} {t("few_sentences_about_yourself")}
</Text> </p>
</fieldset> </fieldset>
</section> </section>
</form> </form>
@ -582,17 +581,13 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
<article className="relative"> <article className="relative">
<section className="space-y-4 sm:mx-auto sm:w-full sm:max-w-lg"> <section className="space-y-4 sm:mx-auto sm:w-full sm:max-w-lg">
<header> <header>
<Text className="text-white" variant="largetitle"> <p className="font-cal mb-2 text-3xl tracking-wider text-white">{steps[currentStep].title}</p>
{steps[currentStep].title} <p className="text-sm font-normal text-white">{steps[currentStep].description}</p>
</Text>
<Text className="text-white" variant="subtitle">
{steps[currentStep].description}
</Text>
</header> </header>
<section className="space-y-2 pt-4"> <section className="space-y-2 pt-4">
<Text variant="footnote"> <p className="text-xs font-medium text-gray-500 dark:text-white">
Step {currentStep + 1} of {steps.length} Step {currentStep + 1} of {steps.length}
</Text> </p>
{error && <Alert severity="error" message={error?.message} />} {error && <Alert severity="error" message={error?.message} />}

View File

@ -1,14 +1,13 @@
import { ExternalLinkIcon } from "@heroicons/react/solid"; import { ExternalLinkIcon } from "@heroicons/react/solid";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import { useIntercom } from "@ee/lib/intercom/useIntercom"; import { useIntercom } from "@ee/lib/intercom/useIntercom";
import { useLocale } from "@lib/hooks/useLocale";
import useMeQuery from "@lib/hooks/useMeQuery"; import useMeQuery from "@lib/hooks/useMeQuery";
import SettingsShell from "@components/SettingsShell"; import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
type CardProps = { title: string; description: string; className?: string; children: ReactNode }; type CardProps = { title: string; description: string; className?: string; children: ReactNode };
const Card = ({ title, description, className = "", children }: CardProps): JSX.Element => ( const Card = ({ title, description, className = "", children }: CardProps): JSX.Element => (
@ -30,8 +29,8 @@ export default function Billing() {
const { boot, show } = useIntercom(); const { boot, show } = useIntercom();
return ( return (
<Shell heading={t("billing")} subtitle={t("manage_your_billing_info")}> <SettingsShell heading={t("billing")} subtitle={t("manage_your_billing_info")}>
<SettingsShell> <>
<div className="py-6 lg:col-span-9 lg:pb-8"> <div className="py-6 lg:col-span-9 lg:pb-8">
{data?.plan && ["FREE", "TRIAL"].includes(data.plan) && ( {data?.plan && ["FREE", "TRIAL"].includes(data.plan) && (
<Card <Card
@ -72,7 +71,7 @@ export default function Billing() {
</div> </div>
</div> </div>
</div> </div>
</SettingsShell> </>
</Shell> </SettingsShell>
); );
} }

View File

@ -5,6 +5,7 @@ import { signOut } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { ComponentProps, FormEvent, RefObject, useEffect, useMemo, useRef, useState } from "react"; import { ComponentProps, FormEvent, RefObject, useEffect, useMemo, useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification"; import showToast from "@calcom/lib/notification";
import { Alert } from "@calcom/ui/Alert"; import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
@ -15,7 +16,6 @@ import { withQuery } from "@lib/QueryCell";
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull"; import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth"; import { getSession } from "@lib/auth";
import { nameOfDay } from "@lib/core/i18n/weekday"; import { nameOfDay } from "@lib/core/i18n/weekday";
import { useLocale } from "@lib/hooks/useLocale";
import { isBrandingHidden } from "@lib/isBrandingHidden"; import { isBrandingHidden } from "@lib/isBrandingHidden";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { trpc } from "@lib/trpc"; import { trpc } from "@lib/trpc";
@ -23,7 +23,6 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import ImageUploader from "@components/ImageUploader"; import ImageUploader from "@components/ImageUploader";
import SettingsShell from "@components/SettingsShell"; import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import Badge from "@components/ui/Badge"; import Badge from "@components/ui/Badge";
@ -488,11 +487,9 @@ export default function Settings(props: Props) {
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<Shell heading={t("profile")} subtitle={t("edit_profile_info_description")}> <SettingsShell heading={t("profile")} subtitle={t("edit_profile_info_description")}>
<SettingsShell> <WithQuery success={({ data }) => <SettingsView {...props} localeProp={data.locale} />} />
<WithQuery success={({ data }) => <SettingsView {...props} localeProp={data.locale} />} /> </SettingsShell>
</SettingsShell>
</Shell>
); );
} }

View File

@ -9,7 +9,6 @@ import { identityProviderNameMap } from "@lib/auth";
import { trpc } from "@lib/trpc"; import { trpc } from "@lib/trpc";
import SettingsShell from "@components/SettingsShell"; import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
import ChangePasswordSection from "@components/security/ChangePasswordSection"; import ChangePasswordSection from "@components/security/ChangePasswordSection";
import DisableUserImpersonation from "@components/security/DisableUserImpersonation"; import DisableUserImpersonation from "@components/security/DisableUserImpersonation";
import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection"; import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection";
@ -18,8 +17,8 @@ export default function Security() {
const user = trpc.useQuery(["viewer.me"]).data; const user = trpc.useQuery(["viewer.me"]).data;
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<Shell heading={t("security")} subtitle={t("manage_account_security")}> <SettingsShell heading={t("security")} subtitle={t("manage_account_security")}>
<SettingsShell> <>
{user && user.identityProvider !== IdentityProvider.CAL ? ( {user && user.identityProvider !== IdentityProvider.CAL ? (
<> <>
<div className="mt-6"> <div className="mt-6">
@ -45,7 +44,7 @@ export default function Security() {
)} )}
<SAMLConfiguration teamsView={false} teamId={null} /> <SAMLConfiguration teamsView={false} teamId={null} />
</SettingsShell> </>
</Shell> </SettingsShell>
); );
} }

View File

@ -4,21 +4,20 @@ import { useSession } from "next-auth/react";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import { useState } from "react"; import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert"; import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import useMeQuery from "@lib/hooks/useMeQuery"; import useMeQuery from "@lib/hooks/useMeQuery";
import { trpc } from "@lib/trpc"; import { trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen"; import EmptyScreen from "@components/EmptyScreen";
import Loader from "@components/Loader"; import Loader from "@components/Loader";
import SettingsShell from "@components/SettingsShell"; import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
import TeamCreateModal from "@components/team/TeamCreateModal"; import TeamCreateModal from "@components/team/TeamCreateModal";
import TeamList from "@components/team/TeamList"; import TeamList from "@components/team/TeamList";
export default function Teams() { function Teams() {
const { t } = useLocale(); const { t } = useLocale();
const { status } = useSession(); const { status } = useSession();
const loading = status === "loading"; const loading = status === "loading";
@ -40,8 +39,8 @@ export default function Teams() {
const isFreePlan = me.data?.plan === "FREE"; const isFreePlan = me.data?.plan === "FREE";
return ( return (
<Shell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}> <SettingsShell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
<SettingsShell> <>
{!!errorMessage && <Alert severity="error" title={errorMessage} />} {!!errorMessage && <Alert severity="error" title={errorMessage} />}
{isFreePlan && ( {isFreePlan && (
<Alert <Alert
@ -87,7 +86,11 @@ export default function Teams() {
/> />
)} )}
{teams.length > 0 && <TeamList teams={teams}></TeamList>} {teams.length > 0 && <TeamList teams={teams}></TeamList>}
</SettingsShell> </>
</Shell> </SettingsShell>
); );
} }
Teams.requiresLicense = false;
export default Teams;

View File

@ -22,7 +22,7 @@ import {
useEmbedNonStylesConfig, useEmbedNonStylesConfig,
useIsBackgroundTransparent, useIsBackgroundTransparent,
useIsEmbed, useIsEmbed,
} from "@calcom/embed-core"; } from "@calcom/embed-core/embed-iframe";
import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage"; import { localStorage } from "@calcom/lib/webstorage";
@ -148,7 +148,7 @@ type SuccessProps = inferSSRProps<typeof getServerSideProps>;
export default function Success(props: SuccessProps) { export default function Success(props: SuccessProps) {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const { location: _location, name, reschedule, status } = router.query; const { location: _location, name, reschedule, listingStatus, status } = router.query;
const location = Array.isArray(_location) ? _location[0] : _location; const location = Array.isArray(_location) ? _location[0] : _location;
const [is24h, setIs24h] = useState(isBrowserLocale24h()); const [is24h, setIs24h] = useState(isBrowserLocale24h());
const { data: session } = useSession(); const { data: session } = useSession();
@ -176,6 +176,7 @@ export default function Success(props: SuccessProps) {
const eventName = getEventName(eventNameObject); const eventName = getEventName(eventNameObject);
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true"; const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
const isCancelled = status === "CANCELLED" || status === "REJECTED";
const telemetry = useTelemetry(); const telemetry = useTelemetry();
useEffect(() => { useEffect(() => {
telemetry.withJitsu((jitsu) => telemetry.withJitsu((jitsu) =>
@ -238,6 +239,9 @@ export default function Success(props: SuccessProps) {
function getTitle(): string { function getTitle(): string {
const titleSuffix = props.recurringBookings ? "_recurring" : ""; const titleSuffix = props.recurringBookings ? "_recurring" : "";
if (isCancelled) {
return t("emailed_information_about_cancelled_event");
}
if (needsConfirmation) { if (needsConfirmation) {
if (props.profile.name !== null) { if (props.profile.name !== null) {
return t("user_needs_to_confirm_or_reject_booking" + titleSuffix, { return t("user_needs_to_confirm_or_reject_booking" + titleSuffix, {
@ -298,25 +302,31 @@ export default function Success(props: SuccessProps) {
<div <div
className={classNames( className={classNames(
"mx-auto flex items-center justify-center", "mx-auto flex items-center justify-center",
!giphyImage ? "h-12 w-12 rounded-full bg-green-100" : "" !giphyImage && !isCancelled ? "h-12 w-12 rounded-full bg-green-100" : "",
isCancelled ? "h-12 w-12 rounded-full bg-red-100" : ""
)}> )}>
{giphyImage && !needsConfirmation && ( {giphyImage && !needsConfirmation && (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img src={giphyImage} alt={"Gif from Giphy"} /> <img src={giphyImage} alt={"Gif from Giphy"} />
)} )}
{!giphyImage && !needsConfirmation && ( {!giphyImage && !needsConfirmation && !isCancelled && (
<CheckIcon className="h-8 w-8 text-green-600" /> <CheckIcon className="h-8 w-8 text-green-600" />
)} )}
{needsConfirmation && <ClockIcon className="h-8 w-8 text-green-600" />} {needsConfirmation && !isCancelled && (
<ClockIcon className="h-8 w-8 text-green-600" />
)}
{isCancelled && <XIcon className="h-8 w-8 text-red-600" />}
</div> </div>
<div className="mt-3 text-center sm:mt-5"> <div className="mt-3 text-center sm:mt-5">
<h3 <h3
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white" className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
id="modal-headline"> id="modal-headline">
{needsConfirmation {needsConfirmation && !isCancelled
? props.recurringBookings ? props.recurringBookings
? t("submitted_recurring") ? t("submitted_recurring")
: t("submitted") : t("submitted")
: isCancelled
? t("event_cancelled")
: props.recurringBookings : props.recurringBookings
? t("meeting_is_scheduled_recurring") ? t("meeting_is_scheduled_recurring")
: t("meeting_is_scheduled")} : t("meeting_is_scheduled")}
@ -333,32 +343,36 @@ export default function Success(props: SuccessProps) {
isReschedule={reschedule === "true"} isReschedule={reschedule === "true"}
eventType={props.eventType} eventType={props.eventType}
recurringBookings={props.recurringBookings} recurringBookings={props.recurringBookings}
status={(status as string) || "upcoming"} listingStatus={(listingStatus as string) || "upcoming"}
date={date} date={date}
is24h={is24h} is24h={is24h}
/> />
</div> </div>
<div className="font-medium">{t("who")}</div> {(bookingInfo?.user || bookingInfo?.attendees) && (
<div className="col-span-2 mb-6"> <>
{bookingInfo?.user && ( <div className="font-medium">{t("who")}</div>
<div className="mb-3"> <div className="col-span-2 mb-6">
<p>{bookingInfo.user.name}</p> {bookingInfo?.user && (
<p className="text-bookinglight">{bookingInfo.user.email}</p> <div className="mb-3">
<p>{bookingInfo.user.name}</p>
<p className="text-bookinglight">{bookingInfo.user.email}</p>
</div>
)}
{bookingInfo?.attendees.map((attendee, index) => (
<div
key={attendee.name}
className={index === bookingInfo.attendees.length - 1 ? "" : "mb-3"}>
<p>{attendee.name}</p>
<p className="text-bookinglight">{attendee.email}</p>
</div>
))}
</div> </div>
)} </>
{bookingInfo?.attendees.map((attendee, index) => ( )}
<div
key={attendee.name}
className={index === bookingInfo.attendees.length - 1 ? "" : "mb-3"}>
<p>{attendee.name}</p>
<p className="text-bookinglight">{attendee.email}</p>
</div>
))}
</div>
{location && ( {location && (
<> <>
<div className="mt-6 font-medium">{t("where")}</div> <div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-6"> <div className="col-span-2 mt-3">
{location.startsWith("http") ? ( {location.startsWith("http") ? (
<a title="Meeting Link" href={location}> <a title="Meeting Link" href={location}>
{location} {location}
@ -401,6 +415,7 @@ export default function Success(props: SuccessProps) {
</div> </div>
</div> </div>
{!needsConfirmation && {!needsConfirmation &&
!isCancelled &&
(!isCancellationMode ? ( (!isCancellationMode ? (
<div className="border-bookinglightest text-bookingdark mt-2 grid grid-cols-3 border-b py-4 text-left dark:border-gray-900"> <div className="border-bookinglightest text-bookingdark mt-2 grid grid-cols-3 border-b py-4 text-left dark:border-gray-900">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50"> <span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
@ -425,7 +440,7 @@ export default function Success(props: SuccessProps) {
theme={userIsOwner ? "light" : props.profile.theme} theme={userIsOwner ? "light" : props.profile.theme}
/> />
))} ))}
{userIsOwner && !needsConfirmation && !isCancellationMode && ( {userIsOwner && !needsConfirmation && !isCancellationMode && !isCancelled && (
<div className="border-bookinglightest mt-9 flex border-b pt-2 pb-4 text-center dark:border-gray-900 sm:mt-0 sm:pt-4"> <div className="border-bookinglightest mt-9 flex border-b pt-2 pb-4 text-center dark:border-gray-900 sm:mt-0 sm:pt-4">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50"> <span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
{t("add_to_calendar")} {t("add_to_calendar")}
@ -591,7 +606,7 @@ type RecurringBookingsProps = {
recurringBookings: SuccessProps["recurringBookings"]; recurringBookings: SuccessProps["recurringBookings"];
date: dayjs.Dayjs; date: dayjs.Dayjs;
is24h: boolean; is24h: boolean;
status: string; listingStatus: string;
}; };
function RecurringBookings({ function RecurringBookings({
@ -599,11 +614,11 @@ function RecurringBookings({
eventType, eventType,
recurringBookings, recurringBookings,
date, date,
status, listingStatus,
}: RecurringBookingsProps) { }: RecurringBookingsProps) {
const [moreEventsVisible, setMoreEventsVisible] = useState(false); const [moreEventsVisible, setMoreEventsVisible] = useState(false);
const { t } = useLocale(); const { t } = useLocale();
return !isReschedule && recurringBookings && status === "upcoming" ? ( return !isReschedule && recurringBookings && listingStatus === "upcoming" ? (
<> <>
{eventType.recurringEvent?.count && {eventType.recurringEvent?.count &&
recurringBookings.slice(0, 4).map((dateStr, idx) => ( recurringBookings.slice(0, 4).map((dateStr, idx) => (

View File

@ -5,8 +5,8 @@ import { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useIsEmbed } from "@calcom/embed-core"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { WEBSITE_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import Button from "@calcom/ui/Button"; import Button from "@calcom/ui/Button";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
@ -23,7 +23,6 @@ import { HeadSeo } from "@components/seo/head-seo";
import Team from "@components/team/screens/Team"; import Team from "@components/team/screens/Team";
import Avatar from "@components/ui/Avatar"; import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup"; import AvatarGroup from "@components/ui/AvatarGroup";
import Text from "@components/ui/Text";
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>; export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({ team }: TeamPageProps) { function TeamPage({ team }: TeamPageProps) {
@ -68,7 +67,7 @@ function TeamPage({ team }: TeamPageProps) {
size={10} size={10}
items={type.users.map((user) => ({ items={type.users.map((user) => ({
alt: user.name || "", alt: user.name || "",
image: WEBSITE_URL + "/" + user.username + "/avatar.png" || "", image: CAL_URL + "/" + user.username + "/avatar.png" || "",
}))} }))}
/> />
</div> </div>
@ -93,12 +92,8 @@ function TeamPage({ team }: TeamPageProps) {
imageSrc={getPlaceholderAvatar(team.logo, team.name)} imageSrc={getPlaceholderAvatar(team.logo, team.name)}
className="mx-auto mb-4 h-20 w-20 rounded-full" className="mx-auto mb-4 h-20 w-20 rounded-full"
/> />
<Text variant="largetitle" className="text-gray-900 dark:text-white"> <p className="font-cal mb-2 text-3xl tracking-wider text-gray-900 dark:text-white">{teamName}</p>
{teamName} <p className="mt-2 text-sm font-normal text-neutral-500 dark:text-white">{team.bio}</p>
</Text>
<Text variant="subtitle" className="mt-2">
{team.bio}
</Text>
</div> </div>
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />} {(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
{!showMembers.isOn && team.eventTypes.length > 0 && ( {!showMembers.isOn && team.eventTypes.length > 0 && (
@ -147,7 +142,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
...type, ...type,
users: type.users.map((user) => ({ users: type.users.map((user) => ({
...user, ...user,
avatar: WEBSITE_URL + "/" + user.username + "/avatar.png", avatar: CAL_URL + "/" + user.username + "/avatar.png",
})), })),
})); }));

View File

@ -7,7 +7,7 @@ import { RecurringEvent } from "@calcom/types/Calendar";
import { asStringOrNull } from "@lib/asStringOrNull"; import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability"; import { getWorkingHours } from "@lib/availability";
import getBooking, { GetBookingType } from "@lib/getBooking"; import getBooking, { GetBookingType } from "@lib/getBooking";
import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@lib/location"; import { locationHiddenFilter, LocationObject } from "@lib/location";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";

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