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
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
NEXT_PUBLIC_LICENSE_CONSENT=''
# To enable enterprise-only features, fill your license key in here
CALCOM_LICENSE_KEY=
# ***********************************************************************************************************
# - DATABASE ************************************************************************************************
@ -25,6 +27,7 @@ NEXT_PUBLIC_LICENSE_CONSENT=''
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
# Change to 'http://localhost:3001' if running the website simultaneously
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'
# To enable SAML login, set both these variables

View File

@ -16,7 +16,8 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Use Node ${{ matrix.node }}
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:
- main
paths-ignore:
- public/static/locales/**
- apps/web/public/static/locales/**
jobs:
test:
timeout-minutes: 20

18
.vscode/tasks.json vendored
View File

@ -15,7 +15,9 @@
"Website(3001)",
"Embed Core(3100)",
"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
"group": {
@ -65,6 +67,20 @@
"command": "yarn db-studio",
"isBackground": false,
"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.
## 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
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://github.com/calcom/cal.com/issues">Issues</a>
·
<a href="https://cal.com/roadmap">Roadmap</a>
</p>
</p>
<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://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://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/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://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>
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=social"></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>
<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://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://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>
<img src="https://api.checklyhq.com/v1/badges/checks/5e048048-1b51-47ba-9209-60607507622e?responseTime=true" alt="Checkly Availability" />
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></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>
<!-- 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 React from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
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 }) {
const { t } = useLocale();
const { status } = useSession();
const tabs = [
{
name: t("app_store"),
href: "/apps",
},
{
name: t("installed_apps"),
href: "/apps/installed",
},
];
return (
<>

View File

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

View File

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

View File

@ -9,17 +9,17 @@ export default function EmptyScreen({
}: {
Icon: SVGComponent;
headline: string;
description: string;
description: string | React.ReactElement;
}) {
return (
<>
<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">
<Icon className="inline-block h-10 w-10 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 text-white dark:bg-white dark:text-gray-600" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium">{headline}</h2>
<p className="text-sm leading-6 text-gray-600">{description}</p>
<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 dark:text-gray-300">{description}</p>
</div>
</div>
</>

View File

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

View File

@ -1,48 +1,54 @@
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 Shell from "./Shell";
export default function SettingsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const tabs = [
{
name: t("profile"),
href: "/settings/profile",
icon: UserIcon,
},
{
name: t("security"),
href: "/settings/security",
icon: KeyIcon,
},
{
name: t("teams"),
href: "/settings/teams",
icon: UserGroupIcon,
},
{
name: t("billing"),
href: "/settings/billing",
icon: CreditCardIcon,
},
{
name: t("admin"),
href: "/settings/admin",
icon: LockClosedIcon,
adminRequired: true,
},
];
const tabs = [
{
name: "profile",
href: "/settings/profile",
icon: UserIcon,
},
{
name: "security",
href: "/settings/security",
icon: KeyIcon,
},
{
name: "teams",
href: "/settings/teams",
icon: UserGroupIcon,
},
{
name: "billing",
href: "/settings/billing",
icon: CreditCardIcon,
},
{
name: "admin",
href: "/settings/admin",
icon: LockClosedIcon,
adminRequired: true,
},
];
export default function SettingsShell({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return (
<>
<Shell {...rest}>
<div className="sm:mx-auto">
<NavTabs tabs={tabs} />
</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 { 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 Button from "@calcom/ui/Button";
import Dropdown, {
@ -32,6 +32,7 @@ import LicenseBanner from "@ee/components/LicenseBanner";
import TrialBanner from "@ee/components/TrialBanner";
import HelpMenuItem from "@ee/components/support/HelpMenuItem";
import ErrorBoundary from "@lib/ErrorBoundary";
import classNames from "@lib/classNames";
import { WEBAPP_URL } from "@lib/config/constants";
import { shouldShowOnboarding } from "@lib/getting-started";
@ -349,7 +350,7 @@ const Layout = ({
"px-4 sm:px-6 md:px-8",
props.flexChildrenContainer && "flex flex-1 flex-col"
)}>
{!props.isLoading ? props.children : props.customLoader}
<ErrorBoundary>{!props.isLoading ? props.children : props.customLoader}</ErrorBoundary>
</div>
{/* show bottom navigation for md and smaller (tablet and phones) */}
{status === "authenticated" && (
@ -477,7 +478,7 @@ function UserDropdown({ small }: { small?: boolean }) {
// eslint-disable-next-line @next/next/no-img-element
<img
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"}
/>
}

View File

@ -46,20 +46,17 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
</Button>
</DialogTrigger>
<DialogContent>
<div className="mb-4">
<div className="mb-8">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("add_new_schedule")}
</h3>
<div>
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
</div>
</div>
<Form
form={form}
handleSubmit={(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">
{t("name")}
</label>

View File

@ -2,6 +2,7 @@ import {
BanIcon,
CheckIcon,
ClockIcon,
LocationMarkerIcon,
PaperAirplaneIcon,
PencilAltIcon,
XIcon,
@ -16,6 +17,7 @@ import { Frequency as RRuleFrequency } from "rrule";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
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 useMeQuery from "@lib/hooks/useMeQuery";
import { LocationType } from "@lib/location";
import { parseRecurringDates } from "@lib/parseDate";
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions";
@ -72,6 +76,7 @@ function BookingListItem(booking: BookingItemProps) {
if (!res.ok) {
throw new HttpError({ statusCode: res.status });
}
setRejectionDialogIsOpen(false);
},
{
async onSettled() {
@ -89,8 +94,7 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("reject_all")
: t("reject"),
onClick: (e) => {
e.stopPropagation();
onClick: () => {
setRejectionDialogIsOpen(true);
},
icon: BanIcon,
@ -102,8 +106,7 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("confirm_all")
: t("confirm"),
onClick: (e) => {
e.stopPropagation();
onClick: () => {
mutation.mutate(true);
},
icon: CheckIcon,
@ -120,25 +123,33 @@ function BookingListItem(booking: BookingItemProps) {
icon: XIcon,
},
{
id: "reschedule",
label: t("reschedule"),
icon: ClockIcon,
id: "edit_booking",
label: t("edit_booking"),
icon: PencilAltIcon,
actions: [
{
id: "edit",
icon: PencilAltIcon,
label: t("edit_booking"),
id: "reschedule",
icon: ClockIcon,
label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}`,
},
{
id: "reschedule_request",
icon: ClockIcon,
icon: PaperAirplaneIcon,
iconClassName: "rotate-45 w-[18px] -ml-[2px]",
label: t("send_reschedule_request"),
onClick: (e) => {
e.stopPropagation();
onClick: () => {
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 [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)
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 (
<>
<RescheduleDialog
@ -175,6 +230,12 @@ function BookingListItem(booking: BookingItemProps) {
setIsOpenDialog={setIsOpenRescheduleDialog}
bookingUId={booking.uid}
/>
<EditLocationDialog
booking={booking}
saveLocation={saveLocation}
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
/>
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
@ -209,115 +270,103 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr
className="flex cursor-pointer hover:bg-neutral-50"
onClick={() =>
router.push({
pathname: "/success",
query: {
date: booking.startTime,
type: booking.eventType.id,
eventSlug: booking.eventType.slug,
user: user?.username || "",
name: booking.attendees[0].name,
email: booking.attendees[0].email,
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,
status: booking.listingStatus,
},
})
}>
<td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
{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(
<tr className="flex hover:bg-neutral-50">
<td
className="hidden whitespace-nowrap align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56"
onClick={onClick}>
<div className="cursor-pointer py-4">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
{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]
.toString()
.toLowerCase()}`
),
})} ${booking.recurringCount} ${t(
`${RRuleFrequency[booking.eventType.recurringEvent.freq]
.toString()
.toLowerCase()}`
),
})} ${booking.recurringCount} ${t(
`${RRuleFrequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`,
{ count: booking.recurringCount }
)}`}
</p>
</Tooltip>
.toLowerCase()}`,
{ count: booking.recurringCount }
)}`}
</p>
</Tooltip>
</div>
</div>
</div>
)}
)}
</div>
</div>
</td>
<td className={"flex-1 py-4 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}>
<div className="sm:hidden">
{!booking.confirmed && !booking.rejected && (
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
)}
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag>
)}
<div className="text-sm font-medium text-gray-900">
{startTime}:{" "}
<small className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
</small>
<td
className={"flex-1 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}
onClick={onClick}>
<div className="cursor-pointer py-4">
<div className="sm:hidden">
{!booking.confirmed && !booking.rejected && (
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
)}
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag>
)}
<div className="text-sm font-medium text-gray-900">
{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
title={booking.title}
className={classNames(
"max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
isCancelled ? "line-through" : ""
)}>
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
{booking.title}
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
)}
{!booking.confirmed && !booking.rejected && (
<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
title={booking.title}
className={classNames(
"max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
isCancelled ? "line-through" : ""
)}>
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
{booking.title}
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
)}
{!booking.confirmed && !booking.rejected && (
<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>
)}
{booking.attendees.length !== 0 && (
<a
className="text-sm text-gray-900 hover:text-blue-500"
href={"mailto:" + booking.attendees[0].email}
onClick={(e) => e.stopPropagation()}>
{booking.attendees[0].email}
</a>
)}
{isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block text-left text-sm md:hidden">
<RequestSentMessage />
</div>
)}
{booking.attendees.length !== 0 && (
<a
className="text-sm text-gray-900 hover:text-blue-500"
href={"mailto:" + booking.attendees[0].email}
onClick={(e) => e.stopPropagation()}>
{booking.attendees[0].email}
</a>
)}
{isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block text-left text-sm md:hidden">
<RequestSentMessage />
</div>
)}
</div>
</td>
<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 { 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 classNames from "@lib/classNames";

View File

@ -30,9 +30,9 @@ import {
useIsBackgroundTransparent,
sdkActionManager,
useEmbedNonStylesConfig,
} from "@calcom/embed-core";
} from "@calcom/embed-core/embed-iframe";
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 { localStorage } from "@calcom/lib/webstorage";
@ -68,6 +68,8 @@ export const locationKeyToString = (location: LocationObject, t: TFunction) => {
case LocationType.Link:
return location.link || "Link"; // If disabled link won't exist on the object
case LocationType.Phone:
return t("your_number");
case LocationType.UserPhone:
return t("phone_call");
case LocationType.GoogleMeet:
return "Google Meet";
@ -234,7 +236,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
.filter((user) => user.name !== profile.name)
.map((user) => ({
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,
})),
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
@ -328,7 +330,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
.map((user) => ({
title: 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 }[]
}
@ -370,7 +372,9 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
return (
<span key={el.type}>
{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>
);
})}
@ -424,7 +428,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
{previousPage === `${WEBAPP_URL}/${profile.slug}` && (
<div className="flex h-full flex-col justify-end">
<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()}
/>
<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 { 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 { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
@ -79,6 +83,7 @@ type BookingFormValues = {
customInputs?: {
[key: string]: string | boolean;
};
rescheduleReason?: string;
};
const BookingPage = ({
@ -251,6 +256,7 @@ const BookingPage = ({
email: primaryAttendee.email || "",
guests: guestListEmails,
notes: booking.description || "",
rescheduleReason: "",
customInputs: eventType.customInputs.reduce(
(customInputs, input) => ({
...customInputs,
@ -782,18 +788,31 @@ const BookingPage = ({
<label
htmlFor="notes"
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>
<textarea
{...bookingForm.register("notes")}
id="notes"
name="notes"
rows={3}
className={inputClassName}
placeholder={t("share_additional_notes")}
disabled={disabledExceptForOwner}
/>
{rescheduleUid ? (
<textarea
{...bookingForm.register("rescheduleReason")}
id="rescheduleReason"
name="rescheduleReason"
rows={3}
className={inputClassName}
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 className="flex items-start space-x-2 rtl:space-x-reverse">
<Button
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 { InformationCircleIcon } from "@heroicons/react/solid";
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 { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
import { TextField } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";

View File

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

View File

@ -18,7 +18,7 @@ export default function ModalContainer(props: Props) {
<DialogContent>
<div
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-4xl sm:max-w-4xl": props.wide,

View File

@ -1,6 +1,6 @@
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";

View File

@ -9,6 +9,7 @@ import { SVGComponent } from "@lib/types/SVGComponent";
export type ActionType = {
id: string;
icon?: SVGComponent;
iconClassName?: string;
label: string;
disabled?: boolean;
color?: "primary" | "secondary";
@ -52,6 +53,7 @@ const DropdownActions = ({
className="w-full rounded-none font-normal"
href={action.href}
StartIcon={action.icon}
startIconClassName={action.iconClassName}
onClick={action.onClick || defaultAction}
data-testid={action.id}>
{action.label}
@ -81,6 +83,7 @@ const TableActions: FC<Props> = ({ actions }) => {
href={action.href}
onClick={action.onClick || defaultAction}
StartIcon={action.icon}
startIconClassName={action.iconClassName}
{...(action?.actions ? { EndIcon: ChevronDownIcon } : null)}
disabled={action.disabled}
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 classNames from "@calcom/lib/classNames";
import InfoBadge from "@components/ui/InfoBadge";
type Props = InputHTMLAttributes<HTMLInputElement> & {
label?: React.ReactNode;
description: string;
descriptionAsLabel?: boolean;
infomationIconText?: string;
informationIconText?: string;
};
const CheckboxField = forwardRef<HTMLInputElement, Props>(
({ label, description, infomationIconText, descriptionAsLabel, ...rest }, ref) => {
({ label, description, informationIconText, ...rest }, ref) => {
const descriptionAsLabel = !label || rest.descriptionAsLabel;
return (
<div className="block items-center sm:flex">
{label && !descriptionAsLabel && (
{label && (
<div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={rest.id} className="flex text-sm font-medium text-neutral-700">
{label}
</label>
</div>
)}
{label && descriptionAsLabel && (
<div className="min-w-48 mb-4 sm:mb-0">
<span className="flex text-sm font-medium text-neutral-700">{label}</span>
{React.createElement(
descriptionAsLabel ? "div" : "label",
{
className: "flex text-sm font-medium text-neutral-700",
...(!descriptionAsLabel
? {
htmlFor: rest.id,
}
: {}),
},
label
)}
</div>
)}
<div className="w-full">
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
{...rest}
disabled={rest.disabled}
ref={ref}
type="checkbox"
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">
{!label || descriptionAsLabel ? (
<label htmlFor={rest.id} className="text-neutral-700">
{description}
</label>
) : (
<p className="text-neutral-900">{description}</p>
)}
</div>
{infomationIconText && <InfoBadge content={infomationIconText}></InfoBadge>}
{React.createElement(
descriptionAsLabel ? "label" : "div",
{
className: classNames(
"relative flex items-start",
descriptionAsLabel ? "text-neutral-700" : "text-neutral-900"
),
},
<>
<div className="flex h-5 items-center">
<input
{...rest}
ref={ref}
type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
/>
</div>
<span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span>
</>
)}
{informationIconText && <InfoBadge content={informationIconText}></InfoBadge>}
</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 { TApiKeys } from "./ApiKeyListItem";
import LicenseRequired from "../LicenseRequired";
import type { TApiKeys } from "./ApiKeyListItem";
export default function ApiKeyDialogForm(props: {
title: string;
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean };
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean };
handleClose: () => void;
}) {
const { t } = useLocale();
@ -49,7 +50,7 @@ export default function ApiKeyDialogForm(props: {
const watchNeverExpires = form.watch("neverExpires");
return (
<>
<LicenseRequired>
{successfulNewApiKeyModal ? (
<>
<div className="mb-10">
@ -92,12 +93,12 @@ export default function ApiKeyDialogForm(props: {
</DialogFooter>
</>
) : (
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean }>
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires?: boolean }>
form={form}
handleSubmit={async (event) => {
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
setApiKey(apiKey);
setApiKeyDetails({ ...event });
setApiKeyDetails({ ...event, neverExpires: !!event.neverExpires });
await utils.invalidateQueries(["viewer.apiKeys.list"]);
setSuccessfulNewApiKeyModal(true);
}}
@ -146,6 +147,6 @@ export default function ApiKeyDialogForm(props: {
</DialogFooter>
</Form>
)}
</>
</LicenseRequired>
);
}

View File

@ -12,66 +12,76 @@ import { trpc } from "@lib/trpc";
import { List } from "@components/List";
export default function ApiKeyListContainer() {
import LicenseRequired from "../LicenseRequired";
function ApiKeyListContainer() {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.apiKeys.list"]);
const [newApiKeyModal, setNewApiKeyModal] = 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 (
<QueryCell
query={query}
success={({ data }) => (
<>
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
<div className="mt-9">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
</div>
<div className="mb-9 sm:self-center">
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
{t("generate_new_api_key")}
</Button>
</div>
</div>
{data.length > 0 && (
<List className="pb-6">
{data.map((item: any) => (
<ApiKeyListItem
key={item.id}
apiKey={item}
onEditApiKey={() => {
setApiKeyToEdit(item);
setEditModalOpen(true);
}}
/>
))}
</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}
/>
<>
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
<div className="mt-9">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
</div>
<div className="mb-9 sm:self-center">
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
{t("generate_new_api_key")}
</Button>
</div>
</div>
<LicenseRequired>
<QueryCell
query={query}
success={({ data }) => (
<>
{data.length > 0 && (
<List className="pb-6">
{data.map((item) => (
<ApiKeyListItem
key={item.id}
apiKey={item}
onEditApiKey={() => {
setApiKeyToEdit(item);
setEditModalOpen(true);
}}
/>
))}
</List>
)}
</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 { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { TextArea } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Badge from "@components/ui/Badge";
import LicenseRequired from "../LicenseRequired";
export default function SAMLConfiguration({
teamsView,
teamId,
@ -92,7 +94,7 @@ export default function SAMLConfiguration({
return (
<>
{isSAMLLoginEnabled ? (
<>
<LicenseRequired>
<hr className="mt-8" />
<div className="mt-6">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
@ -157,7 +159,7 @@ export default function SAMLConfiguration({
</div>
<hr className="mt-4" />
</form>
</>
</LicenseRequired>
) : null}
</>
);

View File

@ -6,10 +6,10 @@ import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
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 { sdkActionManager, useIsEmbed } from "@calcom/embed-core";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import getStripe from "@calcom/stripe/client";
import PaymentComponent from "@ee/components/stripe/Payment";
import { PaymentPageProps } from "@ee/pages/payment/[uid]";

View File

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

View File

@ -2,9 +2,10 @@ import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
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 { DatePicker } from "@components/ui/form/DatePicker";
@ -33,62 +34,66 @@ export default function TeamAvailabilityModal(props: Props) {
}, [utils, selectedTimeZone, selectedDate]);
return (
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
<div className="min-w-64 w-64 space-y-5 p-5 pr-0">
<div className="flex">
<Avatar
imageSrc={WEBSITE_URL + "/" + props.member?.username + "/avatar.png"}
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>
<span className="-mt-1 block text-sm text-gray-400">{props.member?.email}</span>
<LicenseRequired>
<div className="flex max-h-[500px] min-h-[500px] flex-row space-x-8 rtl:space-x-reverse">
<div className="min-w-64 w-64 space-y-5 p-5 pr-0">
<div className="flex">
<Avatar
imageSrc={WEBAPP_URL + "/" + props.member?.username + "/avatar.png"}
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>
<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 className="flex flex-col">
<span className="font-bold text-gray-600">Date</span>
<DatePicker
date={selectedDate.toDate()}
onDatesChange={(newDate) => {
setSelectedDate(dayjs(newDate));
}}
{props.team && props.member && (
<TeamAvailabilityTimes
className="overflow-scroll"
teamId={props.team.id}
memberId={props.member.id}
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>
{props.team && props.member && (
<TeamAvailabilityTimes
className="overflow-auto"
teamId={props.team.id}
memberId={props.member.id}
frequency={frequency}
selectedDate={selectedDate}
selectedTimeZone={selectedTimeZone}
/>
)}
</div>
</LicenseRequired>
);
}

View File

@ -3,7 +3,7 @@ import React, { useState, useEffect, CSSProperties } from "react";
import AutoSizer from "react-virtualized-auto-sizer";
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";
@ -46,7 +46,7 @@ export default function TeamAvailabilityScreen(props: Props) {
HeaderComponent={
<div className="mb-6 flex items-center">
<Avatar
imageSrc={WEBSITE_URL + "/" + member.username + "/avatar.png"}
imageSrc={CAL_URL + "/" + member.username + "/avatar.png"}
alt={member?.name || ""}
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 genericAbi from "../../../web3/abis/abiWithGetBalance.json";
import verifyAccount, { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount";
import { withLicenseRequired } from "../LicenseRequired";
interface Window {
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 Stripe from "stripe";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { getErrorFromUnknown } from "@calcom/lib/errors";
@ -17,8 +18,15 @@ export type PaymentInfo = {
id?: string | null;
};
let paymentFeePercentage: number | undefined;
let paymentFeeFixed: number | undefined;
const stripeKeysSchema = z.object({
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(
evt: CalendarEvent,
@ -35,13 +43,10 @@ export async function handlePayment(
}
) {
const appKeys = await getAppKeysFromSlug("stripe");
if (typeof appKeys.payment_fee_fixed === "number") paymentFeePercentage = appKeys.payment_fee_fixed;
if (typeof appKeys.payment_fee_percentage === "number") paymentFeeFixed = appKeys.payment_fee_percentage;
const { payment_fee_fixed, payment_fee_percentage } = stripeKeysSchema.parse(appKeys);
const paymentFee = Math.round(
selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`)
);
const { stripe_user_id, stripe_publishable_key } = stripeCredential.key as Stripe.OAuthToken;
const paymentFee = Math.round(selectedEventType.price * payment_fee_percentage + payment_fee_fixed);
const { stripe_user_id, stripe_publishable_key } = stripeCredentialSchema.parse(stripeCredential.key);
const params: Stripe.PaymentIntentCreateParams = {
amount: selectedEventType.price,

View File

@ -2,6 +2,7 @@ import { useRouter } from "next/router";
import { useMemo, useState } from "react";
import { Alert } from "@calcom/ui/Alert";
import LicenseRequired from "@ee/components/LicenseRequired";
import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
@ -49,17 +50,19 @@ export function TeamAvailabilityPage() {
/>
)
}>
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
{isLoading && <Loader />}
{isFreeUser ? (
<Alert
className="-mt-24 border"
severity="warning"
title="This is a pro feature. Upgrade to pro to see your team's availability."
/>
) : (
TeamAvailability
)}
<LicenseRequired>
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
{isLoading && <Loader />}
{isFreeUser ? (
<Alert
className="-mt-24 border"
severity="warning"
title="This is a pro feature. Upgrade to pro to see your team's availability."
/>
) : (
TeamAvailability
)}
</LicenseRequired>
</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
export type AppProps = NextAppProps & {
export type AppProps = Omit<NextAppProps, "Component"> & {
Component: NextAppProps["Component"] & { requiresLicense?: boolean };
/** Will be defined only is there was an 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 AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-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 AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-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 ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-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 OrganizerRequestEmail from "@lib/emails/templates/organizer-request-email";
import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-request-reminder-email";
@ -268,6 +270,38 @@ export const sendRequestRescheduleEmail = async (
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) => {
await new Promise((resolve, reject) => {
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", {
organizer: this.calEvent.organizer.name,
})},
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()}
${this.getWhen()}
${this.getAdditionalNotes()}
@ -151,6 +152,7 @@ ${getCancelLink(this.calEvent)}
<tr>
<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;">
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()}
${this.getWhen()}
${this.getWho()}

View File

@ -52,6 +52,7 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
return `
${this.attendee.language.translate("event_has_been_rescheduled")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
@ -112,6 +113,7 @@ ${this.getCustomInputs()}
<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.calEvent.cancellationReason && this.getReason()}
${this.getWhat()}
${this.getWhen()}
${this.getWho()}

View File

@ -452,4 +452,12 @@ ${getRichDescription(this.calEvent)}
protected getInviteeEnd(): Dayjs {
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", {
attendee: this.calEvent.attendees[0].name,
})},
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
@ -163,6 +164,7 @@ ${getCancelLink(this.calEvent)}
<tr>
<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;">
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()}
${this.getWhen()}
${this.getWho()}

View File

@ -59,6 +59,7 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
return `
${this.calEvent.organizer.language.translate("event_has_been_rescheduled")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${this.calEvent.cancellationReason && this.getReason()}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
@ -108,6 +109,7 @@ ${getCancelLink(this.calEvent)}
<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.calEvent.cancellationReason && this.getReason()}
${this.getWhat()}
${this.getWhen()}
${this.getWho()}

View File

@ -439,4 +439,12 @@ ${getRichDescription(this.calEvent)}
protected getOrganizerEnd(): Dayjs {
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 { useEffect, useState } from "react";
import { useEmbedTheme } from "@calcom/embed-core";
import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
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";
/**
* Use this function for translating event location to a readable string
* @param location
* @param t
* @returns string
*/
export const LocationOptionsToString = (location: string, t: TFunction) => {
switch (location) {
case LocationType.InPerson:

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "1.6.1",
"version": "1.6.2",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@ -76,6 +76,7 @@
"jimp": "^0.16.1",
"libphonenumber-js": "^1.9.53",
"lodash": "^4.17.21",
"memory-cache": "^0.2.0",
"micro": "^9.3.4",
"mime-types": "^2.1.35",
"next": "^12.1.6",
@ -125,6 +126,7 @@
"@types/glidejs__glide": "^3.4.2",
"@types/jest": "^27.5.1",
"@types/lodash": "^4.14.182",
"@types/memory-cache": "^0.2.2",
"@types/micro": "7.3.6",
"@types/mime-types": "^2.1.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 { 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, {
getDynamicEventDescription,
getGroupName,
@ -337,11 +342,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
weekStart: "Sunday",
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: users.some((user) => {
allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking;
})
? false
: true,
}),
}
: {
name: user.name || user.username,

View File

@ -3,7 +3,7 @@ import { UserPlan } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
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 { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar";
@ -307,11 +307,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
weekStart: "Sunday",
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: users.some((user) => {
allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking;
})
? false
: true,
}),
}
: {
name: user.name || user.username,

View File

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

View File

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

View File

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

View File

@ -201,7 +201,11 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
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) {
const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody;
@ -677,7 +681,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (originalRescheduledBooking?.uid) {
// 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
// to the default description when we are sending the emails.
evt.description = eventType.description;
@ -711,7 +720,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{
...evt,
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) : {}
);

View File

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

View File

@ -7,7 +7,7 @@ import { RecurringEvent } from "@calcom/types/Calendar";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
import { GetBookingType } from "@lib/getBooking";
import { AppStoreLocationType, locationHiddenFilter, LocationObject } from "@lib/location";
import { locationHiddenFilter, LocationObject } from "@lib/location";
import prisma from "@lib/prisma";
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 getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { StripeData } from "@calcom/stripe/server";
@ -64,6 +65,7 @@ import Shell from "@components/Shell";
import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import RecurringEventController from "@components/eventtype/RecurringEventController";
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
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 { DateRangePicker } from "@components/ui/form/DateRangePicker";
import MinutesField from "@components/ui/form/MinutesField";
import PhoneInput from "@components/ui/form/PhoneInput";
import Select from "@components/ui/form/Select";
import * as RadioArea from "@components/ui/form/radio-area";
import WebhookListContainer from "@components/webhook/WebhookListContainer";
@ -179,7 +180,7 @@ const SuccessRedirectEdit = <T extends UseFormReturn<FormValues>>({
}}
readOnly={proUpgradeRequired}
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")}
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
@ -323,10 +324,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
);
const [tokensList, setTokensList] = useState<Array<Token>>([]);
const defaultSeats = 2;
const defaultSeatsInput = 6;
const defaultSeatsPro = 6;
const minSeats = 2;
const [enableSeats, setEnableSeats] = useState(!!eventType.seatsPerTimeSlot);
const [inputSeatNumber, setInputSeatNumber] = useState(eventType.seatsPerTimeSlot! >= defaultSeatsInput);
const 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 })
);
}
};
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;
}
setShowLocationModal(false);
};
const removeCustom = (index: number) => {
@ -580,11 +453,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
endDate: new Date(eventType.periodEndDate || Date.now()),
});
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${
team ? `team/${team.slug}` : eventType.users[0].username
}/${eventType.slug}`;
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
eventType.slug
}`;
const placeholderHashedLink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/d/${hashedUrl}/${eventType.slug}`;
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const mapUserToValue = ({
id,
@ -597,7 +470,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
}) => ({
value: `${id || ""}`,
label: `${name || ""}`,
avatar: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${username}/avatar.png`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
});
const formMethods = useForm<FormValues>({
@ -641,7 +514,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<Select
options={locationOptions}
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) => {
if (e?.value) {
const newLocationType: LocationType = e.value;
@ -669,7 +542,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="flex flex-grow items-center">
<LocationMarkerIcon className="h-6 w-6" />
<span className="w-full border-0 bg-transparent text-sm ltr:ml-2 rtl:mr-2">
{location.link}
{location.address}
</span>
</div>
)}
@ -1081,7 +954,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="w-full">
<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">
{process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}/
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
{team ? "team/" + team.slug : eventType.users[0].username}/
</span>
<input
@ -1089,7 +962,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
id="slug"
aria-labelledby="slug-label"
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}
{...formMethods.register("slug", {
setValueAs: (v) => slugify(v),
@ -1155,7 +1028,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="w-full">
<textarea
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")}
{...formMethods.register("description")}
defaultValue={asStringOrUndefined(eventType.description)}></textarea>
@ -1312,7 +1185,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="relative mt-1 rounded-sm">
<input
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")}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
@ -1334,7 +1207,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
{
<input
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")}
defaultValue={(eventType.metadata.smartContractAddress || "") as string}
{...formMethods.register("smartContractAddress")}
@ -1439,9 +1312,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<Controller
name="requiresConfirmation"
control={formMethods.control}
defaultValue={eventType.requiresConfirmation}
render={() => (
render={({ field: { value, onChange } }) => (
<CheckboxField
id="requiresConfirmation"
descriptionAsLabel
@ -1450,10 +1322,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
description={t("opt_in_booking_description")}
defaultChecked={eventType.requiresConfirmation}
disabled={enableSeats}
checked={formMethods.watch("disableGuests")}
onChange={(e) => {
formMethods.setValue("requiresConfirmation", e?.target.checked);
}}
checked={value}
onChange={(e) => onChange(e?.target.checked)}
/>
)}
/>
@ -1518,7 +1388,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
name="hashedLink"
data-testid="generated-hash-url"
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}
/>
<Tooltip
@ -1654,7 +1524,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
/>
<select
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")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>
@ -1728,7 +1598,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
return (
<Select
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) => {
if (val) onChange(val.value);
}}
@ -1766,7 +1636,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
return (
<Select
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) => {
if (val) onChange(val.value);
}}
@ -1802,7 +1672,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (e?.target.checked) {
setEnableSeats(true);
// Want to disable individuals from taking multiple seats
formMethods.setValue("seatsPerTimeSlot", defaultSeats);
formMethods.setValue("seatsPerTimeSlot", defaultSeatsPro);
formMethods.setValue("disableGuests", true);
formMethods.setValue("requiresConfirmation", false);
} else {
@ -1860,14 +1730,14 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</label>
<input
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"
placeholder={`${defaultSeatsInput}`}
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={`${defaultSeatsPro}`}
min={minSeats}
{...formMethods.register("seatsPerTimeSlot", {
valueAsNumber: true,
min: defaultSeatsInput,
})}
defaultValue={
eventType.seatsPerTimeSlot || defaultSeatsInput
eventType.seatsPerTimeSlot || defaultSeatsPro
}
/>
</div>
@ -1883,25 +1753,18 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
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 "
onChange={(val) => {
if (val!.value === -1) {
formMethods.setValue(
"seatsPerTimeSlot",
defaultSeatsInput
);
setInputSeatNumber(true);
if (!val) {
return;
}
if (val.value === -1) {
formMethods.setValue("seatsPerTimeSlot", minSeats);
} else {
setInputSeatNumber(false);
formMethods.setValue(
"seatsPerTimeSlot",
val!.value
);
formMethods.setValue("seatsPerTimeSlot", val.value);
}
}}
defaultValue={{
value: eventType.seatsPerTimeSlot || defaultSeats,
label: `${
eventType.seatsPerTimeSlot || defaultSeats
}`,
value: eventType.seatsPerTimeSlot || minSeats,
label: `${eventType.seatsPerTimeSlot || minSeats}`,
}}
options={selectSeatsPerTimeSlotOptions}
/>
@ -1954,7 +1817,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
id="requirePayment"
name="requirePayment"
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}
/>
</div>
@ -1992,12 +1855,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
min="0.5"
type="number"
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"
onChange={(e) => {
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>
<Dialog open={showLocationModal} onOpenChange={setShowLocationModal}>
<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>
<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>
<EditLocationDialog
isOpenDialog={showLocationModal}
setShowLocationModal={setShowLocationModal}
saveLocation={addLocation}
defaultValues={formMethods.getValues("locations")}
selection={
selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
}
setSelectedLocation={setSelectedLocation}
/>
<Controller
name="customInputs"
control={formMethods.control}
@ -2294,6 +2095,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const session = await getSession({ req });
const typeParam = parseInt(asStringOrThrow(query.type));
if (Number.isNaN(typeParam)) {
return {
notFound: true,
};
}
if (!session?.user?.id) {
return {
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({
where: {
@ -2478,7 +2289,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const teamMembers = eventTypeObject.team
? eventTypeObject.team.members.map((member) => {
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;
})
: [];

View File

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

View File

@ -36,7 +36,6 @@ import { ClientSuspense } from "@components/ClientSuspense";
import Loader from "@components/Loader";
import Schedule from "@components/availability/Schedule";
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
import Text from "@components/ui/Text";
import TimezoneSelect from "@components/ui/form/TimezoneSelect";
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">
{t("timezone")}
</label>
<Text variant="caption">
<p className="text-sm leading-tight text-gray-500 dark:text-white">
{t("current_time")}:&nbsp;
<span className="text-black">{dayjs().tz(selectedTimeZone).format("LT")}</span>
</Text>
</p>
</section>
<TimezoneSelect
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"
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")}
</Text>
</p>
</fieldset>
</section>
</form>
@ -582,17 +581,13 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
<article className="relative">
<section className="space-y-4 sm:mx-auto sm:w-full sm:max-w-lg">
<header>
<Text className="text-white" variant="largetitle">
{steps[currentStep].title}
</Text>
<Text className="text-white" variant="subtitle">
{steps[currentStep].description}
</Text>
<p className="font-cal mb-2 text-3xl tracking-wider text-white">{steps[currentStep].title}</p>
<p className="text-sm font-normal text-white">{steps[currentStep].description}</p>
</header>
<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}
</Text>
</p>
{error && <Alert severity="error" message={error?.message} />}

View File

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

View File

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

View File

@ -9,7 +9,6 @@ import { identityProviderNameMap } from "@lib/auth";
import { trpc } from "@lib/trpc";
import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
import ChangePasswordSection from "@components/security/ChangePasswordSection";
import DisableUserImpersonation from "@components/security/DisableUserImpersonation";
import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection";
@ -18,8 +17,8 @@ export default function Security() {
const user = trpc.useQuery(["viewer.me"]).data;
const { t } = useLocale();
return (
<Shell heading={t("security")} subtitle={t("manage_account_security")}>
<SettingsShell>
<SettingsShell heading={t("security")} subtitle={t("manage_account_security")}>
<>
{user && user.identityProvider !== IdentityProvider.CAL ? (
<>
<div className="mt-6">
@ -45,7 +44,7 @@ export default function Security() {
)}
<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 { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import useMeQuery from "@lib/hooks/useMeQuery";
import { trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen";
import Loader from "@components/Loader";
import SettingsShell from "@components/SettingsShell";
import Shell from "@components/Shell";
import TeamCreateModal from "@components/team/TeamCreateModal";
import TeamList from "@components/team/TeamList";
export default function Teams() {
function Teams() {
const { t } = useLocale();
const { status } = useSession();
const loading = status === "loading";
@ -40,8 +39,8 @@ export default function Teams() {
const isFreePlan = me.data?.plan === "FREE";
return (
<Shell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
<SettingsShell>
<SettingsShell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
<>
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
{isFreePlan && (
<Alert
@ -87,7 +86,11 @@ export default function Teams() {
/>
)}
{teams.length > 0 && <TeamList teams={teams}></TeamList>}
</SettingsShell>
</Shell>
</>
</SettingsShell>
);
}
Teams.requiresLicense = false;
export default Teams;

View File

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

View File

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

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