Compare commits

..
43 Commits
Author SHA1 Message Date
Crowdin Bot 0fa2a6aea3 New Crowdin translations by Github Action
release.yml / release (push) Canceled after 0s
2023-04-15 09:21:02 +00:00
Peer Richelsen 440251f9c3 Update package.json 2023-04-15 11:20:06 +02:00
Omar LópezandLeo Giovanetti 7f6847f7c7 In-app users admin (#8035)
* WIP

* WIP

* Update trpc-router.ts

* Fixes

* Update trpc-router.ts

* Update zod-utils.ts

* Apply suggestions from code review

Co-authored-by: Leo Giovanetti <hello@leog.me>

* Feedback and dark mode fixes

* Feedback

* Fixes

* Update UserForm.tsx

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-04-14 17:04:48 -07:00
zomars 9f1fa83a97 Update pr.yml 2023-04-14 17:04:35 -07:00
Joe Au-Yeung f00b112792 Change Stripe <CardElement /> to <PaymentElement /> (#8268)
* Use Stripe PaymentElement

* Only show needs confirmation, if the booking needs it

* Clean up

* Type fix

* More type fixes
2023-04-14 21:56:16 +00:00
Yagiz Nizipli 5339a489f5 perf: reduce array iteration on bookings router (#8276) 2023-04-14 21:29:43 +00:00
Yagiz Nizipli f7df204e0b perf: parallelize database calls to improve speed (#8275) 2023-04-14 21:27:10 +00:00
Alex van Andel 4975aba5e0 Move some dependencies into async (#8270)
* Move some dependencies into async

* Also remove jackson & jose from global scope
2023-04-14 14:25:15 -07:00
Omar López 29d61ad67b Update CODEOWNERS 2023-04-14 14:18:06 -07:00
Udit Takkar 6e555e64fa fix: spacing in booking item mobile (#8279)
release.yml / release (push) Canceled after 0s
2023-04-14 22:35:17 +02:00
Peer Richelsen c748bbc7e6 Update package.json 2023-04-14 22:34:32 +02:00
Leo Giovanetti 0681359bf3 Removing unneeded endpoint (#8280) 2023-04-14 20:27:37 +00:00
Yagiz Nizipli 4c9d4d7972 refactor: remove unnecessary or operation (#8274) 2023-04-14 11:03:24 -07:00
Crowdin Bot 67f8de42b3 New Crowdin translations by Github Action 2023-04-14 11:00:31 +00:00
Peer Richelsen 87584132b5 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-14 12:59:31 +02:00
Crowdin Bot 5b943742e0 New Crowdin translations by Github Action 2023-04-14 08:05:36 +00:00
Hariom Balhara fe155b0b97 Fix: Sylaps app shown uninstalled after installation (#8256) 2023-04-14 10:04:41 +02:00
Udit TakkarandOmar López 6639588c2d fix: selectfield bug and min height (#8253)
* fix: selectfield bug and min height

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: remove overflow

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Omar López <zomars@me.com>
2023-04-13 21:45:18 +00:00
zomars 7f9e8ca7a6 Runs pipelines only when a PR has a requested review 2023-04-13 14:24:26 -07:00
e478a46358 Feature: Reserve slots currently being booked (#6909)
* Reserving slot picked up on cache

* change memory-cache to database table to block slots while reservation completes

* remove memory-cache

* update realeaseAt field when same user change te selected Slot

* Change default time to book

Co-authored-by: alannnc <alannnc@gmail.com>

* remove ip field and renews the session when the user remains in the booking form

* Remove duplicate router

* types fixes

* nit picks

* Update turbo.json

* Revert unrelated change

* Uses constant

* Constant already has a fallback

* Update slots.ts

* Unit test fixes

* slot reservation on user level and support seats

* types fixes and reserve slots on click

* Fix nit var name

---------

Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-13 19:55:26 +00:00
Udit Takkar 7c9012738a feat: add isRecordingExist field and create api handler (#6777) 2023-04-13 19:07:10 +00:00
alannnc 5c763389f7 Fix workflows on seated events (#8034)
* Fix workflows on seated events

* Fix new args required in handleConfirmation

* Update function import to new name

* added email to override for workflows
2023-04-13 19:03:08 +00:00
Efraín Rochín 7fde30edf9 avoid get error when a user has no schedule defined in getUserAvailability function (#8170) 2023-04-13 18:26:45 +00:00
Hariom Balharaandsean-brydon 0e22e92cab Feat: Support setting custom css variables through embed API. (#8169)
* Add booker border specific custom variables

* Support configuring cssVars through embed API

* Update apps/web/pages/booking/[uid].tsx

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-04-13 18:26:31 +00:00
JoeandPeer Richelsen 17f48614ff fix: reccuring input width (#8227)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-13 18:26:13 +00:00
zomars 2f33b57eb7 Update yarn.lock 2023-04-13 11:12:25 -07:00
Nafees Nazik 6e5c3a88ee fix: clock icon size in booker page (#8249) 2023-04-13 20:08:09 +02:00
Alex van Andel 9024914f23 v2.7.16
release.yml / release (push) Canceled after 0s
2023-04-13 19:41:45 +02:00
sean-brydon 966bc08eb4 Fix radio item on future limits (#8247) 2023-04-13 11:58:08 +00:00
sean-brydon c545331cfe Few darkmode fixes - Avatar Tooltip | Forgot password page (#8242) 2023-04-13 10:59:32 +00:00
Peer Richelsen ce763f30f6 readded vibrancy for desktop app (#8245) 2023-04-13 10:59:09 +00:00
Peer Richelsen d453ae9f2b use brand for highlight (#8240) 2023-04-13 11:41:12 +01:00
Nafees Nazik 9f2b41e597 fix: Dark mode fixes (#8243)
* fix: event type card

* fix: text

* fix: border
2023-04-13 10:34:44 +01:00
GitStart-Cal.com 86e903ce4a show the link only if there's a username (#8238) 2023-04-13 07:04:52 +00:00
Crowdin Bot 9d3963436b New Crowdin translations by Github Action 2023-04-13 05:28:45 +00:00
alannnc d8218a6243 Fix payment app credential search (#8235) 2023-04-13 10:57:54 +05:30
Crowdin Bot cdc3833e09 New Crowdin translations by Github Action 2023-04-13 02:12:21 +00:00
Crowdin Bot 52b7e401a1 New Crowdin translations by Github Action 2023-04-13 02:11:27 +00:00
5170fc2424 Managed event-types (#6876)
* WIP

* Locked fields manager

* Leftovers

* Bad merge fix

* Type import fix

* Moving away from classes

* Progress refactoring locked logic

* Covering apps, webhooks and workflows

* Supporting webhooks and workflows (TBT)

* Restoring yarn.lock

* Progress

* Refactoring code, adding default values

* Fixing CRUD for children

* Connect app link and case-sensitive lib renaming

* Translation missing

* Locked indicators, empty screens, locations

* Member card and hidden status + missing i18n

* Missing existent children shown

* Showing preview for already created children

* Email notification almost in place

* Making progress over notif email

* Fixing nodemailer by mixed FE/BE mixup

* Delete dialog

* Adding tests

* New test

* Reverting unneeded change

* Removed console.log

* Tweaking email

* Reverting not applicable webhook changes

* Reverting dev email api

* Fixing last changes due to tests

* Changing user-evType relationship

* Availability and slug replacement tweaks

* Fixing event type delete

* Sometimes slug is not there...

* Removing old webhooks references
Changed slug hint

* Fixing types

* Fixing hiding event types actions

* Changing delete dialog text

* Removing unneeded code

* Applying feedback

* Update yarn.lock

* Making sure locked fields values are static

* Applying feedback

* Feedback + relying on children list, not users

* Removing console.log

* PR Feedback

* Telemetry for slug replacement action

* More unit tests

* Relying on schedule and editor tweaks

* Fixing conteiner classname

* PR Feedback

* PR Feedback

* Updating unit tests

* Moving stuff to ee, added feature flag

* type fix

* Including e2e

* Reverting unneeded changes in EmptyScreen

* Fixing some UI issues after merging tokens

* Fixing missing disabled locked fields

* Theme fixes + e2e potential fix

* Fixing e2e

* Fixing login relying on network

* Tweaking e2e

* Removing unneeded code

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-12 23:10:23 -03:00
Nafees NazikandHariom Balhara 7349fb9f6d test: add other fields to routing form e2e (#8112)
* chore: add test id

* chore: add test

* remove console log

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* fix: linting

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-04-12 13:58:03 -07:00
Syed Ali Shahbaz a8368aac79 Caldav/refactor: Mutiple Fixes and General code improvement (#8031)
* code cleanup and fixes for unsupported objects and fetch requests

* improves sanitizer function

* adds comment to the utility function sanitizeCalendarObjects

* adds ICAL.parse in try catch-- fixes code breaking for all objects due to one bad ICAL body

* adds a function that ensures expand=false fallback for unsupporting calendars

* --WIP

* improves code to now support multi-event calendar ics parsing

* Handle failed calls to not break all selected calendars

* removes debug remnants

* removes further unnecessary comments
2023-04-12 14:30:39 -03:00
Abdallah AlsammanandAlex van Andel 91f381bce9 Replace react icons with lucidedev (#8146)
* migrate from react-icons to lucide-react

* replace react-icon with lucide-dev: Webhook Icon

* add lucide transformer

* Fix LinkIcon import

* Update yarn.lock to include monorepo deps

* Migrated icons in ChargeCardDialog

* Port Storybook to new icons as well

* Adjust Info & Globe icons size to match react-icons size

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-12 17:26:31 +02:00
Peer Richelsen fb9c1b18e0 remove leading-none (#8232) 2023-04-12 12:15:08 +00:00
310 changed files with 6078 additions and 2453 deletions
+2
View File
@@ -177,3 +177,5 @@ CSP_POLICY=
# Vercel Edge Config
EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
+2 -1
View File
@@ -5,7 +5,8 @@
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
* @calcom/reviewers
# ⬇️ Removed so we can manually trigger checks
# * @calcom/reviewers
.github/ @calcom/core
.husky/ @calcom/core
+1
View File
@@ -2,6 +2,7 @@ name: PR Update
on:
pull_request_target:
types: [ready_for_review, review_requested]
branches:
- main
paths-ignore:
-53
View File
@@ -103,59 +103,6 @@ paths:
summary: Reset a user's password
tags:
- Authentication
"/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}":
get:
description: "Gets the busy times for a particular user, by username."
summary: Gets the busy times for a user
tags:
- Availability
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
description: ""
minItems: 1
uniqueItems: true
x-examples:
example-1:
- start: "Fri, 03 Sep 2021 17:00:00 GMT"
end: "Fri, 03 Sep 2021 17:40:00 GMT"
items:
type: object
properties:
start:
type: string
minLength: 1
end:
type: string
minLength: 1
required:
- start
- end
"500":
description: Internal Server Error
parameters:
- schema:
type: string
name: user
in: path
required: true
description: The username of who you want to check availability for
- schema:
type: string
name: dateFrom
in: path
required: true
description: The timestamp of which time you want to get busy times from
- schema:
type: string
name: dateTo
in: path
required: true
description: The timestamp of which time you want to get busy times until
/api/availability/calendar:
get:
description: Gets the user's selected calendars.
+2 -2
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
export default function AddToHomescreen() {
const { t } = useLocale();
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
type="button"
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">{t("dismiss")}</span>
<FiX className="text-inverted h-6 w-6" aria-hidden="true" />
<X className="text-inverted h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
+2 -2
View File
@@ -6,7 +6,7 @@ import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon";
import { AlertCircle } from "@calcom/ui/components/icon";
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
@@ -77,7 +77,7 @@ export default function AppListCard(props: AppListCardProps) {
<ListItemText component="p">{description}</ListItemText>
{invalidCredential && (
<div className="flex gap-x-2 pt-2">
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
</ListItemText>
+6 -6
View File
@@ -22,7 +22,7 @@ import {
TextField,
ColorPicker,
} from "@calcom/ui";
import { FiCode, FiTrello, FiSun, FiArrowLeft } from "@calcom/ui/components/icon";
import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select";
@@ -493,7 +493,7 @@ const tabs = [
{
name: "HTML",
href: "embedTabName=embed-code",
icon: FiCode,
icon: Code,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -546,7 +546,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
{
name: "React",
href: "embedTabName=embed-react",
icon: FiCode,
icon: Code,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -586,7 +586,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
{
name: "Preview",
href: "embedTabName=embed-preview",
icon: FiTrello,
icon: Trello,
type: "iframe",
Component: forwardRef<
HTMLIFrameElement | HTMLTextAreaElement | null,
@@ -622,7 +622,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
return (
<components.Control {...props}>
<FiSun className="text-subtle ml-2 h-4 w-4" />
<Sun className="text-subtle ml-2 h-4 w-4" />
{children}
</components.Control>
);
@@ -830,7 +830,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]);
}}>
<FiArrowLeft className="mr-4 w-4" />
<ArrowLeft className="mr-4 w-4" />
</button>
{embed.title}
</h3>
+7 -7
View File
@@ -3,7 +3,7 @@ import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui";
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
import { CreditCard, Key, Lock, Terminal, User, Users } from "@calcom/ui/components/icon";
import NavTabs from "./NavTabs";
@@ -11,32 +11,32 @@ const tabs = [
{
name: "profile",
href: "/settings/my-account/profile",
icon: FiUser,
icon: User,
},
{
name: "teams",
href: "/settings/teams",
icon: FiUsers,
icon: Users,
},
{
name: "security",
href: "/settings/security",
icon: FiKey,
icon: Key,
},
{
name: "developer",
href: "/settings/developer",
icon: FiTerminal,
icon: Terminal,
},
{
name: "billing",
href: "/settings/billing",
icon: FiCreditCard,
icon: CreditCard,
},
{
name: "admin",
href: "/settings/admin",
icon: FiLock,
icon: Lock,
adminRequired: true,
},
];
@@ -9,7 +9,7 @@ import {
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -40,7 +40,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
return (
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
{t("add")}
</Button>
</DropdownMenuTrigger>
@@ -48,7 +48,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
{options.map((data) => (
<DropdownMenuItem key={data.slug} className="focus:outline-none">
{data.slug === "add-new" ? (
<DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
{t("install_new_calendar_app")}
</DropdownItem>
) : (
+9 -18
View File
@@ -14,16 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App as AppType } from "@calcom/types/App";
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
import {
FiBookOpen,
FiCheck,
FiExternalLink,
FiFile,
FiFlag,
FiMail,
FiPlus,
FiShield,
} from "@calcom/ui/components/icon";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
/* These app slugs all require Google Cal to be installed */
@@ -153,7 +144,7 @@ const Component = ({
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={FiCheck} color="secondary" disabled>
<Button StartIcon={Check} color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("default")}
@@ -175,7 +166,7 @@ const Component = ({
}
return (
<Button
StartIcon={FiPlus}
StartIcon={Plus}
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
@@ -275,7 +266,7 @@ const Component = ({
rel="noreferrer"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<FiBookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
<BookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
@@ -287,7 +278,7 @@ const Component = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<FiExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
<ExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
@@ -299,7 +290,7 @@ const Component = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}>
<FiMail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
<Mail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{email}
</a>
@@ -312,7 +303,7 @@ const Component = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<FiFile className="text-subtle mr-1 -mt-px inline h-4 w-4" />
<File className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
@@ -324,7 +315,7 @@ const Component = ({
rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<FiShield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
<Shield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
@@ -335,7 +326,7 @@ const Component = ({
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
<Flag className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
@@ -17,7 +17,7 @@ import {
Switch,
ShellSubHeading,
} from "@calcom/ui";
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
import { ArrowLeft, Calendar, Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -100,7 +100,7 @@ function CalendarSwitch(props: {
/>
{!!props.destination && (
<span className="bg-subtle text-default ml-8 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm font-normal sm:ml-4">
<FiArrowLeft className="h-4 w-4" />
<ArrowLeft className="h-4 w-4" />
{t("adding_events_to")}
</span>
)}
@@ -282,8 +282,8 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
<div className="bg-default border-subtle relative rounded-md border p-1.5">
<FiCalendar className="text-default h-8 w-8" strokeWidth="1" />
<FiPlus
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
<Plus
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
strokeWidth="4"
/>
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</>
) : (
<EmptyScreen
Icon={FiCalendar}
Icon={Calendar}
headline={t("no_category_apps", {
category: t("calendar").toLowerCase(),
})}
@@ -6,7 +6,7 @@ import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon";
import { AlertCircle } from "@calcom/ui/components/icon";
type AppsLayoutProps = {
children: React.ReactNode;
@@ -27,7 +27,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
<main className="w-full">
{emptyStore ? (
<EmptyScreen
Icon={FiAlertCircle}
Icon={AlertCircle}
headline={t("no_apps")}
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
@@ -6,38 +6,38 @@ import type { InstalledAppVariants } from "@calcom/app-store/utils";
import Shell from "@calcom/features/shell/Shell";
import { trpc } from "@calcom/trpc/react";
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "calendar",
href: "/apps/installed/calendar",
icon: FiCalendar,
icon: Calendar,
},
{
name: "conferencing",
href: "/apps/installed/conferencing",
icon: FiVideo,
icon: Video,
},
{
name: "payment",
href: "/apps/installed/payment",
icon: FiCreditCard,
icon: CreditCard,
},
{
name: "automation",
href: "/apps/installed/automation",
icon: FiShare2,
icon: Share2,
},
{
name: "analytics",
href: "/apps/installed/analytics",
icon: FiBarChart,
icon: BarChart,
},
{
name: "other",
href: "/apps/installed/other",
icon: FiGrid,
icon: Grid,
},
];
@@ -1,7 +1,7 @@
import React from "react";
import { Button, SkeletonText } from "@calcom/ui";
import { FiMoreHorizontal } from "@calcom/ui/components/icon";
import { MoreHorizontal } from "@calcom/ui/components/icon";
import classNames from "@lib/classNames";
@@ -31,7 +31,7 @@ function SkeletonItem() {
type="button"
variant="icon"
color="secondary"
StartIcon={FiMoreHorizontal}
StartIcon={MoreHorizontal}
disabled
/>
</div>
@@ -4,7 +4,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "@calcom/ui";
import { FiLink } from "@calcom/ui/components/icon";
import { Link } from "@calcom/ui/components/icon";
import type { Props } from "./pages/AvailabilityPage";
@@ -41,7 +41,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<FiLink className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
<Link className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
) : (
<img
src={eventLocationType.iconUrl}
@@ -10,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
@@ -28,6 +29,7 @@ type AvailableTimesProps = {
slots?: Slot[];
isLoading: boolean;
ethSignature?: string;
duration: number;
};
const AvailableTimes: FC<AvailableTimesProps> = ({
@@ -42,7 +44,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
seatsPerTimeSlot,
bookingAttendees,
ethSignature,
duration,
}) => {
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
const { t, i18n } = useLocale();
const router = useRouter();
@@ -63,6 +67,14 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
[isMobile]
);
const reserveSlot = (slot: Slot) => {
reserveSlotMutation.mutate({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
});
};
return (
<div ref={slotPickerRef}>
{!!date ? (
@@ -150,6 +162,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
onClick={() => reserveSlot(slot)}
data-testid="time">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
@@ -6,7 +6,7 @@ import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui";
import { FiCheckSquare, FiClock } from "@calcom/ui/components/icon";
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -103,7 +103,7 @@ const BookingDescription: FC<Props> = (props) => {
{requiresConfirmation && (
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
<div>
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<CheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
</div>
{requiresConfirmationText}
</div>
@@ -117,9 +117,9 @@ const BookingDescription: FC<Props> = (props) => {
isBookingPage && "text-default",
!eventType.metadata?.multipleDuration && "items-center"
)}>
<FiClock
<Clock
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "mt-[2px]"
)}
/>
@@ -2,7 +2,7 @@ import type { TFunction } from "next-i18next";
import { FormattedNumber, IntlProvider } from "react-intl";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { FiCreditCard } from "@calcom/ui/components/icon";
import { CreditCard } from "@calcom/ui/components/icon";
const BookingDescriptionPayment = (props: {
eventType: Parameters<typeof getPaymentAppData>[0];
@@ -13,7 +13,7 @@ const BookingDescriptionPayment = (props: {
return (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>
{props.t("no_show_fee_amount", {
+11 -21
View File
@@ -28,16 +28,7 @@ import {
TableActions,
TextAreaField,
} from "@calcom/ui";
import {
FiCheck,
FiClock,
FiMapPin,
FiRefreshCcw,
FiSend,
FiSlash,
FiX,
FiCreditCard,
} from "@calcom/ui/components/icon";
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery";
@@ -125,7 +116,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setRejectionDialogIsOpen(true);
},
icon: FiSlash,
icon: Slash,
disabled: mutation.isLoading,
},
// For bookings with payment, only confirm if the booking is paid for
@@ -137,7 +128,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
bookingConfirm(true);
},
icon: FiCheck,
icon: Check,
disabled: mutation.isLoading,
},
]
@@ -165,7 +156,7 @@ function BookingListItem(booking: BookingItemProps) {
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
`,
icon: FiX,
icon: X,
},
{
id: "edit_booking",
@@ -173,7 +164,7 @@ function BookingListItem(booking: BookingItemProps) {
actions: [
{
id: "reschedule",
icon: FiClock,
icon: Clock,
label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}${
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
@@ -181,7 +172,7 @@ function BookingListItem(booking: BookingItemProps) {
},
{
id: "reschedule_request",
icon: FiSend,
icon: Send,
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
label: t("send_reschedule_request"),
onClick: () => {
@@ -194,7 +185,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setIsOpenLocationDialog(true);
},
icon: FiMapPin,
icon: MapPin,
},
],
},
@@ -208,7 +199,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setChargeCardDialogIsOpen(true);
},
icon: FiCreditCard,
icon: CreditCard,
},
];
@@ -222,7 +213,7 @@ function BookingListItem(booking: BookingItemProps) {
const RequestSentMessage = () => {
return (
<Badge startIcon={FiSend} size="md" variant="gray" data-testid="request_reschedule_sent">
<Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
{t("reschedule_request_sent")}
</Badge>
);
@@ -265,10 +256,9 @@ function BookingListItem(booking: BookingItemProps) {
},
});
};
const showRecordingsButtons =
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
const title = booking.title;
const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
return (
<>
<RescheduleDialog
@@ -518,7 +508,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
);
})}>
<div className="text-default">
<FiRefreshCcw
<RefreshCcw
strokeWidth="3"
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
/>
@@ -6,7 +6,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, TextArea } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
type Props = {
booking: {
@@ -48,7 +48,7 @@ export default function CancelBooking(props: Props) {
{error && (
<div className="mt-8">
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<FiX className="h-6 w-6 text-red-600" />
<X className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -194,6 +194,7 @@ export const SlotPicker = ({
bookingAttendees={bookingAttendees}
recurringCount={recurringEventCount}
ethSignature={ethSignature}
duration={parseInt(duration)}
/>
</>
);
@@ -1,4 +1,4 @@
import { FiGlobe } from "@calcom/ui/components/icon";
import { Globe } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -18,7 +18,7 @@ export function TimezoneDropdown({
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
</>
@@ -24,8 +24,8 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { HeadSeo, useCalcomTheme } from "@calcom/ui";
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon";
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -191,7 +191,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
"border-subtle md:rounded-md md:border",
"border-booker md:border-booker-width md:rounded-md",
isEmbed && "mx-auto"
)}>
<div className="md:flex">
@@ -210,7 +210,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
" text-default",
"ltr:mr-[10px] rtl:ml-[10px]"
)}>
<FiUser
<User
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"mt-[2px]"
@@ -221,21 +221,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)}
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<RefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<div>
<p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<input
type="number"
<NumberInput
defaultValue={eventType.recurringEvent.count}
min="1"
max={eventType.recurringEvent.count}
className="w-15 border-default bg-default dark:border-empthasis h-7 rounded-sm text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2"
defaultValue={eventType.recurringEvent.count}
isFullWidth={false}
className="me-2 inline w-16"
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
@@ -246,7 +248,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)}
{paymentAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>
{t("no_show_fee_amount", {
@@ -32,7 +32,7 @@ import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocati
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
@@ -41,8 +41,9 @@ import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -210,8 +211,11 @@ const BookingPage = ({
hashedLink,
...restProps
}: BookingPageProps) => {
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const { t, i18n } = useLocale();
const { duration: queryDuration } = useRouterQuery("duration");
const { date: queryDate } = useRouterQuery("date");
const isEmbed = useIsEmbed(restProps.isEmbed);
const embedUiConfig = useEmbedUiConfig();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
@@ -227,6 +231,15 @@ const BookingPage = ({
}),
{}
);
const reserveSlot = () => {
if (queryDuration) {
reserveSlotMutation.mutate({
eventTypeId: eventType.id,
slotUtcStartDate: dayjs(queryDate).utc().format(),
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
});
}
};
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (
@@ -246,6 +259,12 @@ const BookingPage = ({
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
);
}
reserveSlot();
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
return () => {
clearInterval(interval);
removeSelectedSlotMarkMutation.mutate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -528,8 +547,8 @@ const BookingPage = ({
<div
className={classNames(
"main",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-default dark:border",
"border-subtle rounded-md sm:border"
isBackgroundTransparent ? "" : "bg-default",
"border-booker sm:border-booker-width rounded-md"
)}>
<div className="sm:flex">
{showEventTypeDetails && (
@@ -537,8 +556,8 @@ const BookingPage = ({
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
<BookingDescriptionPayment eventType={eventType} t={t} />
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="text-default items-start text-sm font-medium">
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="dark:text-inverted text-default items-start text-sm font-medium">
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({
t,
@@ -549,7 +568,7 @@ const BookingPage = ({
</div>
)}
<div className="text-bookinghighlight flex items-start text-sm">
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<Calendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="text-sm font-medium">
{isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) &&
@@ -580,7 +599,7 @@ const BookingPage = ({
{t("former_time")}
</p>
<p className="line-through ">
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{isClientTimezoneAvailable &&
typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
@@ -589,7 +608,7 @@ const BookingPage = ({
)}
{!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm">
<FiUser
<User
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
@@ -649,9 +668,9 @@ const BookingPage = ({
</Button>
</div>
</Form>
{(mutation.isError || recurringMutation.isError) && (
{mutation.isError || recurringMutation.isError ? (
<ErrorMessage error={mutation.error || recurringMutation.error} />
)}
) : null}
</div>
</div>
</div>
@@ -672,7 +691,7 @@ function ErrorMessage({ error }: { error: unknown }) {
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
<AlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
</div>
<div className="ms-3">
<p className="text-sm text-blue-700">
@@ -14,7 +14,7 @@ import {
DialogHeader,
showToast,
} from "@calcom/ui";
import { FiCreditCard, FiAlertTriangle } from "@calcom/ui/components/icon";
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -45,7 +45,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<FiCreditCard className="m-auto h-6 w-6" />
<CreditCard className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("charge_card")} />
@@ -65,7 +65,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
{chargeError && (
<div className="mt-4 flex text-red-500">
<FiAlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
<p className="text-sm">{t("error_charging_card")}</p>
</div>
)}
@@ -19,7 +19,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Input } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
import { FiMapPin } from "@calcom/ui/components/icon";
import { MapPin } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -218,7 +218,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="bg-subtle 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">
<FiMapPin className="text-emphasis h-6 w-6" />
<MapPin className="text-emphasis h-6 w-6" />
</div>
<div className="w-full">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
@@ -13,7 +13,7 @@ import {
showToast,
TextArea,
} from "@calcom/ui";
import { FiClock } from "@calcom/ui/components/icon";
import { Clock } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -44,7 +44,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<FiClock className="m-auto h-6 w-6" />
<Clock className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} />
@@ -10,6 +10,7 @@ import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
@@ -17,7 +18,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
import { Edit, Copy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -80,11 +81,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
);
};
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const eventNamePlaceholder = getEventName({
...eventNameObject,
eventName: formMethods.watch("eventName"),
});
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
const closeEventNameTip = () => setShowEventNameTip(false);
const setEventName = (value: string) => formMethods.setValue("eventName", value);
@@ -128,19 +137,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<TextField
label={t("event_name_in_calendar")}
type="text"
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
addOnSuffix={
<Button
type="button"
StartIcon={FiEdit}
variant="icon"
color="minimal"
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 hover:bg-transparent"
onClick={() => setShowEventNameTip((old) => !old)}
size="sm"
aria-label="edit custom name"
/>
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
onClick={() => setShowEventNameTip((old) => !old)}>
<Edit className="h-4 w-4" />
</Button>
}
/>
</div>
@@ -150,6 +159,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
description={t("booking_questions_description")}
addFieldLabel={t("add_a_booking_question")}
formProp="bookingFields"
{...shouldLockDisableProps("bookingFields")}
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
@@ -158,6 +168,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>
<hr className="border-subtle" />
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
requiresConfirmation={requiresConfirmation}
@@ -171,6 +182,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("disable_notes")}
{...shouldLockDisableProps("hideCalendarNotes")}
description={t("disable_notes_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
@@ -185,6 +197,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<>
<SettingsToggle
title={t("redirect_success_booking")}
{...successRedirectUrlLocked}
description={t("redirect_url_description")}
checked={redirectUrlVisible}
onCheckedChange={(e) => {
@@ -197,6 +210,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
className="w-full"
label={t("redirect_success_booking")}
labelSrOnly
disabled={successRedirectUrlLocked.disabled}
placeholder={t("external_redirect_url")}
required={redirectUrlVisible}
type="text"
@@ -219,6 +233,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<SettingsToggle
data-testid="hashedLinkCheck"
title={t("private_link")}
{...shouldLockDisableProps("hashedLinkCheck")}
description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible}
onCheckedChange={(e) => {
@@ -240,6 +255,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
size="sm"
type="button"
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
@@ -247,10 +266,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}
className="hover:stroke-3 hover:text-emphasis hover:bg-transparent"
type="button">
<FiCopy />
}}>
<Copy className="h-4 w-4" />
</Button>
</Tooltip>
}
@@ -267,6 +284,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<SettingsToggle
data-testid="offer-seats-toggle"
title={t("offer_seats")}
{...seatsLocked}
description={t("offer_seats_description")}
checked={value}
disabled={noShowFeeEnabled}
@@ -295,8 +313,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
labelSrOnly
label={t("number_of_seats")}
type="number"
disabled={seatsLocked.disabled}
defaultValue={value || 2}
min={1}
className="w-24"
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
@@ -305,6 +325,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<div className="mt-2">
<Checkbox
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
/>
+44 -21
View File
@@ -5,10 +5,11 @@ import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppConte
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen } from "@calcom/ui";
import { FiGrid } from "@calcom/ui/components/icon";
import { Button, EmptyScreen, Alert } from "@calcom/ui";
import { Grid, Lock } from "@calcom/ui/components/icon";
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"];
@@ -55,19 +56,39 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
return (
<>
<div>
<div className="before:border-0">
{!installedApps?.length && isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_apps_description")}
/>
)}
{!isLoading && !installedApps?.length ? (
<EmptyScreen
Icon={FiGrid}
Icon={Grid}
headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")}
buttonRaw={
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
)
}
/>
) : null}
@@ -82,22 +103,24 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
))}
</div>
</div>
<div>
{!isLoading && notInstalledApps?.length ? (
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">Available Apps</h2>
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
app={app}
eventType={eventType}
/>
))}
{!shouldLockDisableProps("apps").disabled && (
<div>
{!isLoading && notInstalledApps?.length ? (
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">{t("available_apps")}</h2>
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
app={app}
eventType={eventType}
/>
))}
</div>
</div>
</div>
)}
</>
);
};
@@ -1,29 +1,29 @@
import type { FormValues } from "pages/event-types/[type]";
import { SchedulingType } from "@prisma/client";
import type { FormValues, EventTypeSetup } from "pages/event-types/[type]";
import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
import dayjs from "@calcom/dayjs";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { NewScheduleButton } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
type AvailabilityOption = {
label: string;
value: number;
isDefault: boolean;
isManaged?: boolean;
};
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale();
return (
<components.Option {...props}>
@@ -33,12 +33,17 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
{t("default")}
</Badge>
)}
{isManaged && (
<Badge variant="gray" className="ml-2">
{t("managed")}
</Badge>
)}
</components.Option>
);
};
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale();
return (
<components.SingleValue {...props}>
@@ -48,50 +53,39 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
{t("default")}
</Badge>
)}
{isManaged && (
<Badge variant="gray" className="ml-2">
{t("managed")}
</Badge>
)}
</components.SingleValue>
);
};
const AvailabilitySelect = ({
className = "",
isLoading,
schedules,
options,
value,
...props
}: {
className?: string;
name: string;
value: number;
isLoading: boolean;
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
value: AvailabilityOption | undefined;
options: AvailabilityOption[];
isDisabled?: boolean;
onBlur: () => void;
onChange: (value: AvailabilityOption | null) => void;
}) => {
const { t } = useLocale();
if (isLoading) {
return <SelectSkeletonLoader />;
}
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
}));
const value = options.find((option) =>
props.value
? option.value === props.value
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
return (
<Select
placeholder={t("select")}
options={options}
isDisabled={props.isDisabled}
isSearchable={false}
onChange={props.onChange}
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
value={value}
defaultValue={value}
components={{ Option, SingleValue }}
isMulti={false}
/>
@@ -103,17 +97,25 @@ const format = (date: Date, hour12: boolean) =>
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
);
const EventTypeScheduleDetails = () => {
const EventTypeScheduleDetails = ({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
{
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const filterDays = (dayNum: number) =>
@@ -157,15 +159,15 @@ const EventTypeScheduleDetails = () => {
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && (
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={FiExternalLink}
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
@@ -176,14 +178,23 @@ const EventTypeScheduleDetails = () => {
);
};
const EventTypeSchedule = () => {
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery();
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const { watch } = useFormContext<FormValues>();
const watchSchedule = watch("schedule");
if (!schedules?.schedules.length && !isLoading)
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
if (!data?.schedules.length && !isLoading)
return (
<EmptyScreen
Icon={FiClock}
Icon={Clock}
headline={t("new_schedule_heading")}
description={t("new_schedule_description")}
buttonRaw={<NewScheduleButton fromEventType />}
@@ -191,22 +202,65 @@ const EventTypeSchedule = () => {
/>
);
const schedules = data?.schedules || [];
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
isManaged: false,
}));
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
if (isManagedEventType) {
options.push({
value: 0,
label: t("members_default_schedule"),
isDefault: false,
isManaged: false,
});
}
// We are showing a managed event for a member and team owner selected their own schedule, so adding
// the managed schedule option
if (
isChildrenManagedEventType &&
watchSchedule &&
!schedules.find((schedule) => schedule.id === watchSchedule)
) {
options.push({
value: watchSchedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
isDefault: false,
isManaged: false,
});
}
const value = options.find((option) =>
watchSchedule
? option.value === watchSchedule
: isManagedEventType
? option.value === 0
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
return (
<div className="space-y-4">
<div>
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
{t("availability")}
{shouldLockIndicator("availability")}
</label>
<Controller
name="schedule"
render={({ field }) => (
<>
<AvailabilitySelect
value={field.value}
value={value}
options={options}
onBlur={field.onBlur}
isDisabled={shouldLockDisableProps("schedule").disabled}
name={field.name}
schedules={schedules?.schedules || []}
isLoading={isLoading}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
@@ -215,12 +269,21 @@ const EventTypeSchedule = () => {
)}
/>
</div>
<EventTypeScheduleDetails />
{value?.value !== 0 ? (
<EventTypeScheduleDetails
selectedScheduleValue={value}
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
/>
) : (
isManagedEventType && (
<p className="!mt-2 ml-1 text-sm text-gray-600">{t("members_default_schedule_description")}</p>
)
)}
</div>
);
};
const UseCommonScheduleSettingsToggle = () => {
const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const { resetField, setValue } = useFormContext<FormValues>();
return (
@@ -239,13 +302,23 @@ const UseCommonScheduleSettingsToggle = () => {
}}
title={t("choose_common_schedule_team_event")}
description={t("choose_common_schedule_team_event_description")}>
<EventTypeSchedule />
<EventTypeSchedule eventType={eventType} />
</SettingsToggle>
)}
/>
);
};
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
export const EventAvailabilityTab = ({
eventType,
isTeamEvent,
}: {
eventType: EventTypeSetup;
isTeamEvent: boolean;
}) => {
return isTeamEvent && eventType.schedulingType !== SchedulingType.MANAGED ? (
<UseCommonScheduleSettingsToggle eventType={eventType} />
) : (
<EventTypeSchedule eventType={eventType} />
);
};
@@ -7,6 +7,7 @@ import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { SingleValue } from "react-select";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { classNames } from "@calcom/lib";
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
@@ -14,17 +15,8 @@ import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { PeriodType } from "@calcom/prisma/client";
import type { IntervalLimit } from "@calcom/types/Calendar";
import {
Button,
DateRangePicker,
Input,
InputField,
Label,
Select,
SettingsToggle,
TextField,
} from "@calcom/ui";
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
import { Plus, Trash } from "@calcom/ui/components/icon";
const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement,
@@ -78,6 +70,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
<div className="w-1/2 md:w-full">
<InputField
required
disabled={passThroughProps.disabled}
defaultValue={minimumBookingNoticeDisplayValues.value}
onChange={(e) =>
setMinimumBookingNoticeDisplayValues({
@@ -95,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
</div>
<Select
isSearchable={false}
isDisabled={passThroughProps.disabled}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeDisplayValues.type
@@ -146,12 +140,30 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue: periodType?.type,
});
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
const periodTypeLocked = shouldLockDisableProps("periodType");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
];
return (
<div className="space-y-8">
<div className="space-y-4 lg:space-y-8">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
<Label htmlFor="beforeBufferTime">
{t("before_event")}
{shouldLockIndicator("bookingLimits")}
</Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
@@ -170,6 +182,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
onChange={(val) => {
if (val) onChange(val.value);
}}
@@ -183,7 +196,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Label htmlFor="afterBufferTime">
{t("after_event")}
{shouldLockIndicator("bookingLimits")}
</Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
@@ -202,6 +218,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
onChange={(val) => {
if (val) onChange(val.value);
}}
@@ -217,11 +234,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
<Label htmlFor="minimumBookingNotice">
{t("minimum_booking_notice")}
{shouldLockIndicator("minimumBookingNotice")}
</Label>
<MinimumBookingNoticeInput
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
{...formMethods.register("minimumBookingNotice")}
/>
</div>
<div className="w-full">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
<Label htmlFor="slotInterval">
{t("slot_interval")}
{shouldLockIndicator("slotInterval")}
</Label>
<Controller
name="slotInterval"
control={formMethods.control}
@@ -239,6 +265,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("slotInterval").disabled}
onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
@@ -261,6 +288,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
render={({ field: { value } }) => (
<SettingsToggle
title={t("limit_booking_frequency")}
{...bookingLimitsLocked}
description={t("limit_booking_frequency_description")}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
@@ -272,7 +300,12 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("bookingLimits", {});
}
}}>
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
<IntervalLimitsManager
disabled={bookingLimitsLocked.disabled}
propertyName="bookingLimits"
defaultLimit={1}
step={1}
/>
</SettingsToggle>
)}
/>
@@ -284,6 +317,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<SettingsToggle
title={t("limit_total_booking_duration")}
description={t("limit_total_booking_duration_description")}
{...durationLimitsLocked}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
if (active) {
@@ -297,6 +331,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<IntervalLimitsManager
propertyName="durationLimits"
defaultLimit={60}
disabled={durationLimitsLocked.disabled}
step={15}
textFieldSuffix={t("minutes")}
/>
@@ -311,13 +346,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<SettingsToggle
title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")}
{...periodTypeLocked}
checked={value && value !== "UNLIMITED"}
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{PERIOD_TYPES.map((period) => {
{PERIOD_TYPES.filter((opt) =>
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
).map((period) => {
if (period.type === "UNLIMITED") return null;
return (
<div
@@ -326,30 +364,42 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
)}
key={period.type}>
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 bg-default flex h-4 w-4 cursor-pointer items-center rounded-full border border-black focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
{!periodTypeLocked.disabled && (
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
</RadioGroup.Item>
)}
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
<div className="flex h-9">
<Input
<div className="flex items-center">
<TextField
labelSrOnly
type="number"
className="border-default block w-16 rounded-md py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
disabled={periodTypeLocked.disabled}
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<select
id=""
className="border-default bg-default text-default block h-9 w-full rounded-md py-2 pl-3 pr-10 text-sm focus:outline-none"
{...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>
<option value="0">{t("business_days")}</option>
</select>
<Select
options={optionsPeriod}
isSearchable={false}
isDisabled={periodTypeLocked.disabled}
onChange={(opt) => {
formMethods.setValue(
"periodCountCalendarDays",
opt?.value.toString() as "0" | "1"
);
}}
defaultValue={
optionsPeriod.find(
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
) ?? optionsPeriod[0]
}
/>
</div>
)}
{period.type === "RANGE" && (
@@ -362,6 +412,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
disabled={periodTypeLocked.disabled}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
startDate,
@@ -400,6 +451,7 @@ type IntervalLimitItemProps = {
step: number;
value: number;
textFieldSuffix?: string;
disabled?: boolean;
selectOptions: { value: keyof IntervalLimit; label: string }[];
hasDeleteButton?: boolean;
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
@@ -414,6 +466,7 @@ const IntervalLimitItem = ({
textFieldSuffix,
selectOptions,
hasDeleteButton,
disabled,
onDelete,
onLimitChange,
onIntervalSelect,
@@ -424,8 +477,9 @@ const IntervalLimitItem = ({
required
type="number"
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
className="mb-0"
className="mb-0 !h-auto"
placeholder={`${value}`}
disabled={disabled}
min={step}
step={step}
defaultValue={value}
@@ -435,11 +489,12 @@ const IntervalLimitItem = ({
<Select
options={selectOptions}
isSearchable={false}
isDisabled={disabled}
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
onChange={onIntervalSelect}
/>
{hasDeleteButton && (
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
{hasDeleteButton && !disabled && (
<Button variant="icon" StartIcon={Trash} color="destructive" onClick={() => onDelete(limitKey)} />
)}
</div>
);
@@ -450,6 +505,7 @@ type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> =
defaultLimit: number;
step: number;
textFieldSuffix?: string;
disabled?: boolean;
};
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
@@ -457,6 +513,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
defaultLimit,
step,
textFieldSuffix,
disabled,
}: IntervalLimitsManagerProps<K>) => {
const { watch, setValue, control } = useFormContext<FormValues>();
const watchIntervalLimits = watch(propertyName);
@@ -506,6 +563,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
limitKey={limitKey}
step={step}
value={value}
disabled={disabled}
textFieldSuffix={textFieldSuffix}
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
@@ -536,8 +594,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
/>
);
})}
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
{t("add_limit")}
</Button>
)}
@@ -11,7 +11,7 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
return (
<div className="">
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} />
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
</div>
);
};
+92 -25
View File
@@ -11,6 +11,7 @@ import { z } from "zod";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@@ -27,7 +28,7 @@ import {
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
import { Edit2, Check, X, Plus } from "@calcom/ui/components/icon";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
@@ -45,8 +46,33 @@ const getLocationFromType = (
}
};
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const locationAvailable =
props.eventType.locations &&
props.eventType.locations.length > 0 &&
props.locationOptions.some((op) =>
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
);
const locationDetails = props.eventType.locations &&
props.eventType.locations.length > 0 &&
!locationAvailable && {
slug: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", "-")
.replace("_video", ""),
name: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", " ")
.replace("_video", "")
.split(" ")
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" "),
};
return { locationAvailable, locationDetails };
};
interface DescriptionEditorProps {
description?: string | null;
editable?: boolean;
}
const DescriptionEditor = (props: DescriptionEditorProps) => {
@@ -64,6 +90,7 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={props.editable}
/>
) : (
<SkeletonContainer>
@@ -174,6 +201,13 @@ export const EventSetupTab = (
resolver: zodResolver(locationFormSchema),
});
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const Locations = () => {
const { t } = useLocale();
@@ -188,6 +222,12 @@ export const EventSetupTab = (
return true;
});
const defaultValue = isManagedEventType
? locationOptions.find((op) => op.label === t("default"))?.options[0]
: undefined;
const { locationDetails, locationAvailable } = getLocationInfo(props);
return (
<div className="w-full">
{validLocations.length === 0 && (
@@ -195,6 +235,8 @@ export const EventSetupTab = (
<LocationSelect
placeholder={t("select")}
options={locationOptions}
isDisabled={shouldLockDisableProps("locations").disabled}
defaultValue={defaultValue}
isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
menuPlacement="auto"
@@ -253,10 +295,10 @@ export const EventSetupTab = (
}}
aria-label={t("edit")}
className="hover:text-emphasis text-subtle mr-1 p-1">
<FiEdit2 className="h-4 w-4" />
<Edit2 className="h-4 w-4" />
</button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<FiX className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
<X className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
</button>
</div>
</div>
@@ -268,7 +310,7 @@ export const EventSetupTab = (
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
) && (
<div className="text-default flex text-sm">
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
<Check className="mt-0.5 mr-1.5 h-2 w-2.5" />
<Trans i18nKey="event_type_requres_google_cal">
<p>
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
@@ -282,11 +324,19 @@ export const EventSetupTab = (
</Trans>
</div>
)}
{validLocations.length > 0 && (
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
<p className="pl-1 text-sm leading-none text-red-600">
{t("app_not_connected", { appName: locationDetails.name })}{" "}
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
{t("connect_now")}
</a>
</p>
)}
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
<li>
<Button
data-testid="add-location"
StartIcon={FiPlus}
StartIcon={Plus}
color="minimal"
onClick={() => setShowLocationModal(true)}>
{t("add_location")}
@@ -299,22 +349,33 @@ export const EventSetupTab = (
);
};
const lengthLockedProps = shouldLockDisableProps("length");
const descriptionLockedProps = shouldLockDisableProps("description");
return (
<div>
<div className="space-y-8">
<TextField
required
label={t("title")}
{...shouldLockDisableProps("title")}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
<div>
<Label>{t("description")}</Label>
<DescriptionEditor description={eventType?.description} />
<Label>
{t("description")}
{shouldLockIndicator("description")}
</Label>
<DescriptionEditor
description={eventType?.description}
editable={!descriptionLockedProps.disabled}
/>
</div>
<TextField
required
label={t("URL")}
{...shouldLockDisableProps("slug")}
defaultValue={eventType.slug}
addOnLeading={
<>
@@ -367,12 +428,14 @@ export const EventSetupTab = (
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("default_duration")}
{shouldLockIndicator("length")}
</Skeleton>
<Select
value={defaultDuration}
isSearchable={false}
name="length"
className="text-sm"
isDisabled={lengthLockedProps.disabled}
noOptionsMessage={() => t("default_duration_no_options")}
options={selectedMultipleDuration}
onChange={(option) => {
@@ -388,32 +451,36 @@ export const EventSetupTab = (
<TextField
required
type="number"
{...lengthLockedProps}
label={t("duration")}
defaultValue={eventType.length ?? 15}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
/>
)}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
{!lengthLockedProps.disabled && (
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
)}
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("location")}
{shouldLockIndicator("locations")}
</Skeleton>
<Controller
+88 -5
View File
@@ -1,4 +1,4 @@
import type { SchedulingType } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
@@ -7,24 +7,46 @@ import type { Options } from "react-select";
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Select } from "@calcom/ui";
interface IMemberToValue {
interface IUserToValue {
id: number | null;
name: string | null;
username: string | null;
email: string;
}
const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
value: `${id || ""}`,
label: `${name || ""}`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
email,
});
export const mapMemberToChildrenOption = (
member: EventTypeSetupProps["teamMembers"][number],
slug: string
) => {
return {
slug,
hidden: false,
created: false,
owner: {
id: member.id,
name: member.name ?? "",
email: member.email,
username: member.username ?? "",
membership: member.membership,
eventTypeSlugs: member.eventTypes ?? [],
},
value: `${member.id ?? ""}`,
label: member.name ?? "",
};
};
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
if (a.label < b.label) {
return -1;
@@ -35,6 +57,40 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
return 0;
};
const ChildrenEventTypesList = ({
options = [],
value,
onChange,
...rest
}: {
value: ReturnType<typeof mapMemberToChildrenOption>[];
onChange?: (options: ReturnType<typeof mapMemberToChildrenOption>[]) => void;
options?: Options<ReturnType<typeof mapMemberToChildrenOption>>;
} & Omit<Partial<ComponentProps<typeof ChildrenEventTypeSelect>>, "onChange" | "value">) => {
const { t } = useLocale();
return (
<div className="flex flex-col space-y-5">
<div>
<Label>{t("assign_to")}</Label>
<ChildrenEventTypeSelect
onChange={(options) => {
onChange &&
onChange(
options.map((option) => ({
...option,
}))
);
}}
value={value}
options={options.filter((opt) => !value.find((val) => val.owner.id.toString() === opt.value))}
controlShouldRenderValue={false}
{...rest}
/>
</div>
</div>
);
};
const CheckedHostField = ({
labelText,
placeholder,
@@ -123,6 +179,21 @@ const RoundRobinHosts = ({
);
};
const ChildrenEventTypes = ({
childrenEventTypeOptions,
}: {
childrenEventTypeOptions: ReturnType<typeof mapMemberToChildrenOption>[];
}) => {
return (
<Controller<FormValues>
name="children"
render={({ field: { onChange, value } }) => (
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
)}
/>
);
};
const Hosts = ({
teamMembers,
}: {
@@ -189,6 +260,7 @@ const Hosts = ({
/>*/}
</>
),
MANAGED: <></>,
};
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
}}
@@ -196,7 +268,11 @@ const Hosts = ({
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
export const EventTeamTab = ({
team,
teamMembers,
eventType,
}: Pick<EventTypeSetupProps, "teamMembers" | "team" | "eventType">) => {
const { t } = useLocale();
const schedulingTypeOptions: {
@@ -216,9 +292,13 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
},
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
const childrenEventTypeOptions = teamMembers.map((member) => {
return mapMemberToChildrenOption(member, eventType.slug);
});
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
return (
<div>
{team && (
{team && !isManagedEventType && (
<div className="space-y-5">
<div className="flex flex-col">
<Label>{t("scheduling_type")}</Label>
@@ -239,6 +319,9 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
<Hosts teamMembers={teamMembersOptions} />
</div>
)}
{team && isManagedEventType && (
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
)}
</div>
);
};
@@ -1,16 +1,17 @@
import type { Webhook } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useState } from "react";
import { TbWebhook } from "react-icons/tb";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { WebhookForm } from "@calcom/features/webhooks/components";
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Webhook } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { Plus, Lock } from "@calcom/ui/components/icon";
export const EventTeamWebhooksTab = ({
eventType,
@@ -85,12 +86,20 @@ export const EventTeamWebhooksTab = ({
<Button
color="secondary"
data-testid="new_webhook"
StartIcon={FiPlus}
StartIcon={Plus}
onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")}
</Button>
);
};
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const webhookLockedStatus = shouldLockDisableProps("webhooks");
return (
<div>
{team && webhooks && !isLoading && (
@@ -98,15 +107,24 @@ export const EventTeamWebhooksTab = ({
<div>
<div>
<>
{isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_webhooks_description")}
/>
)}
{webhooks.length ? (
<>
<div className="mb-8 rounded-md border">
<div className="mb-2 rounded-md border">
{webhooks.map((webhook, index) => {
return (
<WebhookListItem
key={webhook.id}
webhook={webhook}
lastItem={webhooks.length === index + 1}
canEditWebhook={!webhookLockedStatus.disabled}
onEditWebhook={() => {
setEditModalOpen(true);
setWebhookToEdit(webhook);
@@ -122,7 +140,15 @@ export const EventTeamWebhooksTab = ({
Icon={TbWebhook}
headline={t("create_your_first_webhook")}
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
buttonRaw={<NewWebhookButton />}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<NewWebhookButton />
)
}
/>
)}
</>
@@ -1,10 +1,13 @@
import { SchedulingType } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useRouter } from "next/router";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form";
import { TbWebhook } from "react-icons/tb";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
@@ -32,19 +35,19 @@ import {
VerticalTabs,
} from "@calcom/ui";
import {
FiLink,
FiCalendar,
FiClock,
FiSliders,
FiRepeat,
FiGrid,
FiZap,
FiUsers,
FiExternalLink,
FiCode,
FiTrash,
FiMoreHorizontal,
FiLoader,
Link as LinkIcon,
Calendar,
Clock,
Sliders,
Repeat,
Grid,
Zap,
Users,
ExternalLink,
Code,
Trash,
MoreHorizontal,
Loader,
} from "@calcom/ui/components/icon";
import { EmbedButton, EmbedDialog } from "@components/Embed";
@@ -77,44 +80,38 @@ function getNavigation(props: {
{
name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`,
icon: FiLink,
icon: LinkIcon,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
icon: FiCalendar,
info: `default_schedule_name`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`,
icon: FiClock,
icon: Clock,
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`,
icon: FiSliders,
icon: Sliders,
info: `event_advanced_tab_description`,
},
{
name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`,
icon: FiRepeat,
icon: Repeat,
info: `recurring_event_tab_description`,
},
{
name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`,
icon: FiGrid,
icon: Grid,
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`,
icon: FiZap,
icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
];
@@ -137,7 +134,10 @@ function EventTypeSingleLayout({
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const hasPermsToDelete = currentUserMembership?.role !== "MEMBER" || !currentUserMembership;
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
@@ -157,23 +157,55 @@ function EventTypeSingleLayout({
},
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({
let navigation = getNavigation({
t,
eventType,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
});
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
icon: Calendar,
info:
isManagedEventType || isChildrenManagedEventType
? eventType.schedule === null
? "Member's default schedule"
: isChildrenManagedEventType
? `${
eventType.scheduleName
? `${eventType.scheduleName} - ${t("managed")}`
: `default_schedule_name`
}`
: eventType.scheduleName ?? `default_schedule_name`
: `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`,
icon: FiUsers,
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
icon: Users,
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
isManagedEventType
? ` - ${t("count_members", { count: formMethods.watch("children").length || 0 })}`
: ""
}`,
});
}
if (isManagedEventType || isChildrenManagedEventType) {
// Removing apps and workflows for manageg event types by admins v1
navigation = navigation.slice(0, -2);
} else {
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
@@ -189,6 +221,7 @@ function EventTypeSingleLayout({
}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
return (
<Shell
@@ -197,77 +230,87 @@ function EventTypeSingleLayout({
heading={eventType.title}
CTA={
<div className="flex items-center justify-end">
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
{t("hide_from_profile")}
</Skeleton>
<Switch
id="hiddenSwitch"
checked={formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", e);
}}
/>
</div>
<VerticalDivider className="hidden lg:block" />
{!eventType.metadata.managedEventConfig && (
<>
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
{t("hide_from_profile")}
</Skeleton>
<Switch
id="hiddenSwitch"
checked={formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", e);
}}
/>
</div>
<VerticalDivider className="hidden lg:block" />
</>
)}
{/* TODO: Figure out why combined isnt working - works in storybook */}
<ButtonGroup combined containerProps={{ className: "border-default hidden lg:flex" }}>
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
<Tooltip content={t("preview")}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon={FiExternalLink}
/>
</Tooltip>
{!isManagedEventType && (
<>
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
<Tooltip content={t("preview")}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon={ExternalLink}
/>
</Tooltip>
<Button
color="secondary"
variant="icon"
StartIcon={FiLink}
tooltip={t("copy_link")}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}
/>
<EmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={FiCode}
color="secondary"
variant="icon"
tooltip={t("embed")}
/>
<Button
color="destructive"
variant="icon"
StartIcon={FiTrash}
tooltip={t("delete")}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}
/>
<Button
color="secondary"
variant="icon"
StartIcon={LinkIcon}
tooltip={t("copy_link")}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}
/>
<EmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={Code}
color="secondary"
variant="icon"
tooltip={t("embed")}
/>
</>
)}
{!isChildrenManagedEventType && (
<Button
color="destructive"
variant="icon"
StartIcon={Trash}
tooltip={t("delete")}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}
/>
)}
</ButtonGroup>
<VerticalDivider className="hidden lg:block" />
<Dropdown>
<DropdownMenuTrigger asChild>
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent style={{ minWidth: "200px" }}>
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
target="_blank"
type="button"
StartIcon={FiExternalLink}
StartIcon={ExternalLink}
href={permalink}
rel="noreferrer">
{t("preview")}
@@ -276,7 +319,7 @@ function EventTypeSingleLayout({
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
type="button"
StartIcon={FiLink}
StartIcon={LinkIcon}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
@@ -288,7 +331,7 @@ function EventTypeSingleLayout({
<DropdownItem
type="button"
color="destructive"
StartIcon={FiTrash}
StartIcon={Trash}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}>
{t("delete")}
@@ -323,7 +366,7 @@ function EventTypeSingleLayout({
</Button>
</div>
}>
<Suspense fallback={<FiLoader />}>
<Suspense fallback={<Loader />}>
<div className="flex flex-col xl:flex-row xl:space-x-6">
<div className="hidden xl:block">
<VerticalTabs
@@ -351,14 +394,25 @@ function EventTypeSingleLayout({
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
loadingText={t("confirm_delete_event_type")}
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventType.id });
}}>
{t("delete_event_type_description")}
<p className="mt-5">
<Trans
i18nKey={`delete${isManagedEvent}_event_type_description`}
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
<ul>
<li>Members assigned to this event type will also have their event types deleted.</li>
<li>
Anyone who they&apos;ve shared their link with will no longer be able to book using it.
</li>
</ul>
</Trans>
</p>
</ConfirmationDialogContent>
</Dialog>
<EmbedDialog />
@@ -1,23 +1,26 @@
import type { FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Frequency } from "@calcom/prisma/zod-utils";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Alert, Select, SettingsToggle } from "@calcom/ui";
import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
type RecurringEventControllerProps = {
recurringEvent: RecurringEvent | null;
eventType: EventTypeSetup;
paymentEnabled: boolean;
};
export default function RecurringEventController({
recurringEvent,
eventType,
paymentEnabled,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
eventType.recurringEvent
);
const formMethods = useFormContext<FormValues>();
/* Just yearly-0, monthly-1 and weekly-2 */
@@ -28,6 +31,14 @@ export default function RecurringEventController({
value: value.toString(),
}));
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const recurringLocked = shouldLockDisableProps("recurringEvent");
return (
<div className="block items-start sm:flex">
<div className={!paymentEnabled ? "w-full" : ""}>
@@ -37,6 +48,7 @@ export default function RecurringEventController({
<>
<SettingsToggle
title={t("recurring_event")}
{...recurringLocked}
description={t("recurring_event_description")}
checked={recurringEventState !== null}
data-testid="recurring-event-check"
@@ -45,7 +57,7 @@ export default function RecurringEventController({
formMethods.setValue("recurringEvent", null);
setRecurringEventState(null);
} else {
const newVal = recurringEvent || {
const newVal = eventType.recurringEvent || {
interval: 1,
count: 12,
freq: Frequency.WEEKLY,
@@ -58,11 +70,12 @@ export default function RecurringEventController({
<div data-testid="recurring-event-collapsible" className="text-sm">
<div className="flex items-center">
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<input
<TextField
disabled={recurringLocked.disabled}
type="number"
min="1"
max="20"
className="border-default bg-default text-default block h-[36px] w-16 rounded-md text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
className="mb-0"
defaultValue={recurringEventState.interval}
onChange={(event) => {
const newVal = {
@@ -77,7 +90,8 @@ export default function RecurringEventController({
options={recurringEventFreqOptions}
value={recurringEventFreqOptions[recurringEventState.freq]}
isSearchable={false}
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
className="w-18 ml-2 block min-w-0 rounded-md text-sm"
isDisabled={recurringLocked.disabled}
onChange={(event) => {
const newVal = {
...recurringEventState,
@@ -90,12 +104,13 @@ export default function RecurringEventController({
</div>
<div className="mt-4 flex items-center">
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<input
<TextField
disabled={recurringLocked.disabled}
type="number"
min="1"
max="20"
className="border-default bg-default text-default block h-[36px] w-16 rounded-md text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
defaultValue={recurringEventState.count}
className="mb-0"
onChange={(event) => {
const newVal = {
...recurringEventState,
@@ -105,7 +120,7 @@ export default function RecurringEventController({
setRecurringEventState(newVal);
}}
/>
<p className="text-emphasis ltr:mr-2 rtl:ml-2">
<p className="text-emphasis ltr:ml-2 rtl:mr-2">
{t("events", {
count: recurringEventState.count,
})}
@@ -1,25 +1,29 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { UnitTypeLongPlural } from "dayjs";
import { Trans } from "next-i18next";
import type { FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type z from "zod";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Input, Label, SettingsToggle, RadioField } from "@calcom/ui";
import { Input, SettingsToggle, RadioField, Select } from "@calcom/ui";
type RequiresConfirmationControllerProps = {
metadata: z.infer<typeof EventTypeMetaDataSchema>;
requiresConfirmation: boolean;
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
seatsEnabled: boolean;
eventType: EventTypeSetup;
};
export default function RequiresConfirmationController({
metadata,
eventType,
requiresConfirmation,
onRequiresConfirmation,
seatsEnabled,
@@ -37,6 +41,23 @@ export default function RequiresConfirmationController({
}
}, [requiresConfirmation]);
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
const options = [
{ label: t("minute_timeUnit"), value: "minutes" },
{ label: t("hour_timeUnit"), value: "hours" },
];
const defaultValue = options.find(
(opt) =>
opt.value === (metadata?.requiresConfirmationThreshold?.unit ?? defaultRequiresConfirmationSetup.unit)
);
return (
<div className="block items-start sm:flex">
<div className="w-full">
@@ -46,10 +67,11 @@ export default function RequiresConfirmationController({
render={() => (
<SettingsToggle
title={t("requires_confirmation")}
disabled={seatsEnabled}
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
description={t("requires_confirmation_description")}
checked={requiresConfirmation}
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
onRequiresConfirmation(val);
@@ -77,70 +99,82 @@ export default function RequiresConfirmationController({
);
}
}}>
<div className="flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
<RadioField label={t("always_requires_confirmation")} id="always" value="always" />
<RadioField
label={
<>
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 flex">
<Input
type="number"
min={1}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
}}
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<select
onChange={(evt) => {
const val = evt.target.value as UnitTypeLongPlural;
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
}}
className="border-default text-default bg-default ml-2 block h-9 rounded-md py-2 pl-3 pr-10 text-sm focus:outline-none"
defaultValue={
metadata?.requiresConfirmationThreshold?.unit ||
defaultRequiresConfirmationSetup.unit
}>
<option value="minutes">{t("minute_timeUnit")}</option>
<option value="hours">{t("hour_timeUnit")}</option>
</select>
</div>
),
}}
/>
</>
}
id="notice"
value="notice"
/>
<div className="flex items-center">
<RadioGroup.Item
<div className="flex flex-col flex-wrap justify-start gap-y-2">
{(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && (
<RadioField
label={t("always_requires_confirmation")}
disabled={requiresConfirmationLockedProps.disabled}
id="always"
value="always"
/>
)}
{(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && (
<RadioField
disabled={requiresConfirmationLockedProps.disabled}
className="items-center"
label={
<>
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 inline-flex">
<Input
type="number"
min={1}
disabled={requiresConfirmationLockedProps.disabled}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.time",
val
);
}}
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<label
className={classNames(
requiresConfirmationLockedProps.disabled && "cursor-not-allowed"
)}>
<Select
inputId="notice"
options={options}
isSearchable={false}
isDisabled={requiresConfirmationLockedProps.disabled}
className="ml-2"
onChange={(opt) => {
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: opt?.value as UnitTypeLongPlural,
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.unit",
opt?.value as UnitTypeLongPlural
);
}}
defaultValue={defaultValue}
/>
</label>
</div>
),
}}
/>
</>
}
id="notice"
value="notice"
className="min-w-4 bg-default flex h-4 w-4 cursor-pointer items-center rounded-full border border-black focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="notice" className="!m-0 flex items-center" />
</div>
/>
)}
</div>
</RadioGroup.Root>
</SettingsToggle>
@@ -1,5 +1,5 @@
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
import { FiClock, FiUser } from "@calcom/ui/components/icon";
import { Clock, User } from "@calcom/ui/components/icon";
function SkeletonLoader() {
return (
@@ -32,11 +32,11 @@ function SkeletonItem() {
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<FiClock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
<Clock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
<SkeletonText className="h-4 w-12" />
</li>
<li className="flex items-center whitespace-nowrap">
<FiUser className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
<User className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
<SkeletonText className="h-4 w-12" />
</li>
</ul>
@@ -1,10 +1,10 @@
import { FiShield } from "@calcom/ui/components/icon";
import { Shield } from "@calcom/ui/components/icon";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<FiShield className="text-inverted h-6 w-6" />
<Shield className="text-inverted h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -3,7 +3,7 @@ import { useMutation } from "@tanstack/react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Badge, showToast, Switch } from "@calcom/ui";
import { FiArrowLeft } from "@calcom/ui/components/icon";
import { ArrowLeft } from "@calcom/ui/components/icon";
export function CalendarSwitch(props: {
type: string;
@@ -76,7 +76,7 @@ export function CalendarSwitch(props: {
{props.defaultSelected && (
<Badge variant="gray">
<div className="flex">
<FiArrowLeft className="text-default mr-1" /> {t("adding_events_to")}
<ArrowLeft className="text-default mr-1" /> {t("adding_events_to")}
</div>
</Badge>
)}
+2 -2
View File
@@ -9,7 +9,7 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
import { FiUserCheck } from "@calcom/ui/components/icon";
import { UserCheck } from "@calcom/ui/components/icon";
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
const { t } = useLocale();
@@ -24,7 +24,7 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
props.onSuccess();
}}>
<EmptyScreen
Icon={FiUserCheck}
Icon={UserCheck}
headline={t("admin_user_created")}
description={t("admin_user_created_description")}
/>
@@ -10,7 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Button, TextField } from "@calcom/ui";
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
import { Check, ExternalLink, Loader } from "@calcom/ui/components/icon";
type EnterpriseLicenseFormValues = {
licenseKey: string;
@@ -91,7 +91,7 @@ const EnterpriseLicense = (
<div>
<Button
className="w-full justify-center text-lg"
EndIcon={FiExternalLink}
EndIcon={ExternalLink}
href="https://console.cal.com"
target="_blank">
{t("purchase_license")}
@@ -121,9 +121,9 @@ const EnterpriseLicense = (
)}
addOnSuffix={
checkLicenseLoading ? (
<FiLoader className="h-5 w-5 animate-spin" />
<Loader className="h-5 w-5 animate-spin" />
) : errors.licenseKey === undefined && isDirty ? (
<FiCheck className="h-5 w-5 text-green-700" />
<Check className="h-5 w-5 text-green-700" />
) : undefined
}
color={errors.licenseKey ? "warn" : ""}
+2 -2
View File
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
import type { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { FiCheck } from "@calcom/ui/components/icon";
import { Check } from "@calcom/ui/components/icon";
const StepDone = (props: {
currentStep: number;
@@ -24,7 +24,7 @@ const StepDone = (props: {
}}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
<FiCheck className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
+2 -2
View File
@@ -2,7 +2,7 @@ import classNames from "classnames";
import { useState } from "react";
import type { ControllerRenderProps } from "react-hook-form";
import { FiEdit2 } from "@calcom/ui/components/icon";
import { Edit2 } from "@calcom/ui/components/icon";
const EditableHeading = function EditableHeading({
value,
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
<label className="min-w-8 relative inline-block">
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value}&nbsp;</span>
{!isEditing && isReady && (
<FiEdit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
<Edit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
)}
<input
{...passThroughProps}
+2 -2
View File
@@ -1,12 +1,12 @@
import { Tooltip } from "@calcom/ui";
import { FiInfo } from "@calcom/ui/components/icon";
import { Info } from "@calcom/ui/components/icon";
export default function InfoBadge({ content }: { content: string }) {
return (
<>
<Tooltip side="top" content={content}>
<span title={content}>
<FiInfo className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
<Info className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
</span>
</Tooltip>
</>
@@ -15,7 +15,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
import { FiCheck, FiEdit2, FiExternalLink } from "@calcom/ui/components/icon";
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
export enum UsernameChangeStatusEnum {
UPGRADE = "UPGRADE",
@@ -235,7 +235,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
usernameIsAvailable ? "" : ""
)}>
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 w-6" /> : <></>}
</span>
</div>
</div>
@@ -251,7 +251,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<Dialog open={openDialogSaveUsername}>
<DialogContent
Icon={FiEdit2}
Icon={Edit2}
title={t("confirm_username_change_dialog_title")}
description={
<>
@@ -288,7 +288,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
data-testid="go-to-billing"
href={paymentLink}>
<>
{t("go_to_stripe_billing")} <FiExternalLink className="ml-1 h-4 w-4" />
{t("go_to_stripe_billing")} <ExternalLink className="ml-1 h-4 w-4" />
</>
</Button>
)}
@@ -9,7 +9,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, TextField } from "@calcom/ui";
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
import { Check, Edit2 } from "@calcom/ui/components/icon";
interface ICustomUsernameProps {
currentUsername: string | undefined;
@@ -137,7 +137,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
{currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-6 flex flex-row">
<span className={classNames("mx-2 py-2")}>
{usernameIsAvailable ? <FiCheck className="w-6" /> : <></>}
{usernameIsAvailable ? <Check className="w-6" /> : <></>}
</span>
</div>
)}
@@ -154,7 +154,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
</div>
)}
<Dialog open={openDialogSaveUsername}>
<DialogContent type="confirmation" Icon={FiEdit2} title={t("confirm_username_change_dialog_title")}>
<DialogContent type="confirmation" Icon={Edit2} title={t("confirm_username_change_dialog_title")}>
<div className="flex flex-row">
<div className="mb-4 w-full pt-1">
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
@@ -3,7 +3,7 @@ import type { Props } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select";
@@ -49,7 +49,7 @@ export const CheckedSelect = ({
alt={option.label}
/>
{option.label}
<FiX
<X
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
/>
+2 -2
View File
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
import "react-date-picker/dist/DatePicker.css";
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
import { FiCalendar } from "@calcom/ui/components/icon";
import { Calendar } from "@calcom/ui/components/icon";
import classNames from "@lib/classNames";
@@ -23,7 +23,7 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
className
)}
clearIcon={null}
calendarIcon={<FiCalendar className="text-subtle h-5 w-5" />}
calendarIcon={<Calendar className="text-subtle h-5 w-5" />}
value={date}
minDate={minDate}
disabled={disabled}
+2 -2
View File
@@ -92,11 +92,11 @@ const nextConfig = {
"@calcom/prisma",
"@calcom/trpc",
"@calcom/ui",
"lucide-react",
],
modularizeImports: {
"@calcom/ui/components/icon": {
transform: "@react-icons/all-files/fi/{{member}}",
skipDefaultConversion: true,
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
preventFullImport: true,
},
"@calcom/features/insights/components": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.7.15",
"version": "2.8.0",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+12 -12
View File
@@ -6,7 +6,7 @@ import { useEffect, useState } from "react";
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
import { BookOpen, Check, ChevronRight, FileText } from "@calcom/ui/components/icon";
import { ssgInit } from "@server/lib/ssg";
@@ -20,13 +20,13 @@ export default function Custom404() {
{
title: t("documentation"),
description: t("documentation_description"),
icon: FiFileText,
icon: FileText,
href: DOCS_URL,
},
{
title: t("blog"),
description: t("blog_description"),
icon: FiBookOpen,
icon: BookOpen,
href: `${WEBSITE_URL}/blog`,
},
];
@@ -112,7 +112,7 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
<Check className="h-6 w-6 text-green-500" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
@@ -127,7 +127,7 @@ export default function Custom404() {
<p className="text-subtle text-base">{t("the_infrastructure_plan")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
@@ -140,7 +140,7 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<FiFileText className="text-default h-6 w-6" aria-hidden="true" />
<FileText className="text-default h-6 w-6" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
@@ -153,7 +153,7 @@ export default function Custom404() {
<p className="text-subtle text-base">{t("prisma_studio_tip_description")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</Link>
</li>
@@ -198,7 +198,7 @@ export default function Custom404() {
<p className="text-subtle text-base">{t("join_our_community")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
@@ -257,7 +257,7 @@ export default function Custom404() {
rel="noreferrer">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
<Check className="h-6 w-6 text-green-500" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
@@ -272,7 +272,7 @@ export default function Custom404() {
<p className="text-subtle text-base">{t("claim_username_and_schedule_events")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
@@ -300,7 +300,7 @@ export default function Custom404() {
<p className="text-subtle text-base">{link.description}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</Link>
</li>
@@ -346,7 +346,7 @@ export default function Custom404() {
<p className="text-subtle text-base">{t("join_our_community")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
+2 -2
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
import { FiCopy } from "@calcom/ui/components/icon";
import { Copy } from "@calcom/ui/components/icon";
export default function Error500() {
const { t } = useLocale();
@@ -34,7 +34,7 @@ export default function Error500() {
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon={FiCopy}
StartIcon={Copy}
onClick={() => {
navigator.clipboard.writeText(router.query.error as string);
showToast("Link copied!", "success");
+5 -5
View File
@@ -29,7 +29,7 @@ import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
import { FiArrowRight } from "@calcom/ui/components/icon";
import { ArrowRight } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -60,7 +60,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<li
key={index}
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<FiArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
<ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
<Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
className="flex justify-between px-6 py-4"
@@ -122,7 +122,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? " border-bookinglightest bg-default rounded-md border" : "",
isEmbed ? " border-booker border-booker-width bg-default rounded-md border" : "",
"max-w-3xl py-24 px-4"
)}>
{isSingleUser && ( // When we deal with a single user, not dynamic group
@@ -161,7 +161,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<FiArrowRight className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
<ArrowRight className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<Link
@@ -180,7 +180,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div className="flex flex-wrap items-center">
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription eventType={type} />
<EventTypeDescription eventType={type} isPublic={true} />
</Link>
</div>
</div>
+19
View File
@@ -5,6 +5,7 @@ import Head from "next/head";
import Script from "next/script";
import "@calcom/embed-core/src/embed-iframe";
import { useEmbedUiConfig } from "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { trpc } from "@calcom/trpc/react";
@@ -27,6 +28,18 @@ const calFont = localFont({
function MyApp(props: AppProps) {
const { Component, pageProps, err, router } = props;
let pageStatus = "200";
const { cssVarsPerTheme } = useEmbedUiConfig();
const cssVarsStyle = [];
if (cssVarsPerTheme) {
for (const [themeName, cssVars] of Object.entries(cssVarsPerTheme)) {
cssVarsStyle.push(`.${themeName} {`);
for (const [cssVarName, value] of Object.entries(cssVars)) {
cssVarsStyle.push(`--${cssVarName}: ${value};`);
}
cssVarsStyle.push(`}`);
}
}
if (router.pathname === "/404") {
pageStatus = "404";
} else if (router.pathname === "/500") {
@@ -62,6 +75,12 @@ function MyApp(props: AppProps) {
--font-cal: ${calFont.style.fontFamily};
}
`}</style>
<style jsx global>
{`
${cssVarsStyle.join("")}
`}
</style>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
-29
View File
@@ -1,29 +0,0 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import { defaultResponder } from "@calcom/lib/server";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
const availabilitySchema = z.object({
user: z.string(),
dateFrom: z.string(),
dateTo: z.string(),
eventTypeId: stringOrNumber.optional(),
});
/**
* @deprecated Use TRCP's viewer.availability.user
*/
async function handler(req: NextApiRequest) {
const { user: username, eventTypeId, dateTo, dateFrom } = availabilitySchema.parse(req.query);
return getUserAvailability({
username,
dateFrom,
dateTo,
eventTypeId,
});
}
export default defaultResponder(handler);
+147
View File
@@ -0,0 +1,147 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { DailyLocationType } from "@calcom/app-store/locations";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { defaultHandler } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
const schema = z.object({
recordingId: z.string(),
bookingUID: z.string(),
});
const downloadLinkSchema = z.object({
download_link: z.string(),
});
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
return res.status(405).json({ message: "No SendGrid API key or email" });
}
const response = schema.safeParse(JSON.parse(req.body));
if (!response.success) {
return res.status(400).send({
message: "Invalid Payload",
});
}
const { recordingId, bookingUID } = response.data;
const session = await getServerSession({ req, res });
if (!session?.user) {
return res.status(401).send({
message: "User not logged in",
});
}
try {
const booking = await prisma.booking.findFirst({
where: {
uid: bookingUID,
},
select: {
...bookingMinimalSelect,
uid: true,
location: true,
isRecorded: true,
user: {
select: {
id: true,
credentials: true,
timeZone: true,
email: true,
name: true,
locale: true,
destinationCalendar: true,
},
},
},
});
if (!booking || booking.location !== DailyLocationType) {
return res.status(404).send({
message: `Booking of uid ${bookingUID} does not exist or does not contain daily video as location`,
});
}
const t = await getTranslation(booking?.user?.locale ?? "en", "common");
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
id: attendee.id,
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
const isUserAttendeeOrOrganiser =
booking?.user?.id === session.user.id ||
attendeesList.find((attendee) => attendee.id === session.user.id);
if (!isUserAttendeeOrOrganiser) {
return res.status(403).send({
message: "Unauthorised",
});
}
await prisma.booking.update({
where: {
uid: booking.uid,
},
data: {
isRecorded: true,
},
});
const isSendingEmailsAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
// send emails to all attendees only when user has team plan
if (isSendingEmailsAllowed) {
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
const downloadLinkResponse = downloadLinkSchema.parse(response);
const downloadLink = downloadLinkResponse.download_link;
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
description: booking.description || undefined,
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
email: booking.user?.email || "Email-less",
name: booking.user?.name || "Nameless",
timeZone: booking.user?.timeZone || "Europe/London",
language: { translate: t, locale: booking?.user?.locale ?? "en" },
},
attendees: attendeesList,
uid: booking.uid,
};
await sendDailyVideoRecordingEmails(evt, downloadLink);
return res.status(200).json({ message: "Success" });
}
return res.status(403).json({ message: "User does not have team plan to send out emails" });
} catch (err) {
console.warn("something_went_wrong", err);
return res.status(500).json({ message: "something went wrong" });
}
}
export default defaultHandler({
POST: Promise.resolve({ default: handler }),
});
+3 -3
View File
@@ -5,7 +5,7 @@ import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SkeletonText } from "@calcom/ui";
import { FiArrowLeft, FiArrowRight } from "@calcom/ui/components/icon";
import { ArrowLeft, ArrowRight } from "@calcom/ui/components/icon";
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
const { t, isLocaleReady } = useLocale();
@@ -16,7 +16,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
<Link
href="/apps"
className="text-emphasis inline-flex items-center justify-start gap-1 rounded-sm py-2">
<FiArrowLeft className="h-4 w-4" />
<ArrowLeft className="h-4 w-4" />
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
</Link>
</div>
@@ -32,7 +32,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
<h3 className="font-medium capitalize">{category.name}</h3>
<p className="text-subtle text-sm">
{t("number_apps", { count: category.count })}{" "}
<FiArrowRight className="inline-block h-4 w-4" />
<ArrowRight className="inline-block h-4 w-4" />
</p>
</div>
</Link>
+2 -2
View File
@@ -10,7 +10,7 @@ import type { AppCategories } from "@calcom/prisma/client";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import type { HorizontalTabItemProps } from "@calcom/ui";
import { AllApps, AppStoreCategories, HorizontalTabs, TextField, PopularAppsSlider } from "@calcom/ui";
import { FiSearch } from "@calcom/ui/components/icon";
import { Search } from "@calcom/ui/components/icon";
import AppsLayout from "@components/apps/layouts/AppsLayout";
@@ -37,7 +37,7 @@ function AppsSearch({
return (
<TextField
className="bg-subtle !border-muted !pl-0 focus:!ring-offset-0"
addOnLeading={<FiSearch className="text-subtle h-4 w-4" />}
addOnLeading={<Search className="text-subtle h-4 w-4" />}
addOnClassname="!border-muted"
containerClassName={classNames("focus:!ring-offset-0", className)}
type="search"
+20 -20
View File
@@ -30,15 +30,15 @@ import {
showToast,
} from "@calcom/ui";
import {
FiBarChart,
FiCalendar,
FiCreditCard,
FiGrid,
FiMoreHorizontal,
FiPlus,
FiShare2,
FiTrash,
FiVideo,
BarChart,
Calendar,
CreditCard,
Grid,
MoreHorizontal,
Plus,
Share2,
Trash,
Video,
} from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -71,7 +71,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
color="destructive"
onClick={() => handleDisconnect(credentialId)}
disabled={isGlobal}
StartIcon={FiTrash}>
StartIcon={Trash}>
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
@@ -159,7 +159,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
<div className="flex justify-end">
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{!appIsDefault && variant === "conferencing" && (
@@ -167,7 +167,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
<DropdownItem
type="button"
color="secondary"
StartIcon={FiVideo}
StartIcon={Video}
onClick={() => {
const locationType = getEventLocationTypeFromApp(
item?.locationOption?.value ?? ""
@@ -225,13 +225,13 @@ const IntegrationsContainer = ({
const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
const emptyIcon = {
calendar: FiCalendar,
conferencing: FiVideo,
automation: FiShare2,
analytics: FiBarChart,
payment: FiCreditCard,
web3: FiBarChart,
other: FiGrid,
calendar: Calendar,
conferencing: Video,
automation: Share2,
analytics: BarChart,
payment: CreditCard,
web3: BarChart,
other: Grid,
};
return (
@@ -270,7 +270,7 @@ const IntegrationsContainer = ({
variant ? `/apps/categories/${variant === "conferencing" ? "video" : variant}` : "/apps"
}
color="secondary"
StartIcon={FiPlus}>
StartIcon={Plus}>
{t("add")}
</Button>
}
+2 -2
View File
@@ -5,7 +5,7 @@ import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, SkeletonText } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
import AuthContainer from "@components/ui/AuthContainer";
@@ -29,7 +29,7 @@ export default function Error() {
<AuthContainer title="" description="">
<div>
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<FiX className="h-6 w-6 text-red-600" />
<X className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
@@ -4,6 +4,7 @@ import type { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import type { CSSProperties } from "react";
import React, { useMemo } from "react";
import dayjs from "@calcom/dayjs";
@@ -109,6 +110,14 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
<>
<form
className="space-y-6"
style={
{
"--cal-brand": "#111827",
"--cal-brand-emphasis": "#101010",
"--cal-brand-text": "white",
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}
onSubmit={async (e) => {
e.preventDefault();
@@ -164,6 +173,8 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
);
}
Page.isThemeSupported = false;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const id = context.params?.id as string;
+15 -2
View File
@@ -4,7 +4,7 @@ import { getCsrfToken } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import Link from "next/link";
import { useRouter } from "next/router";
import type { SyntheticEvent } from "react";
import type { CSSProperties, SyntheticEvent } from "react";
import React from "react";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
@@ -105,7 +105,18 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
{!success && (
<>
<div className="space-y-6">{error && <p className="text-red-600">{error.message}</p>}</div>
<form className="space-y-6" onSubmit={handleSubmit} action="#">
<form
className="space-y-6"
onSubmit={handleSubmit}
action="#"
style={
{
"--cal-brand": "#111827",
"--cal-brand-emphasis": "#101010",
"--cal-brand-text": "white",
"--cal-brand-subtle": "#9CA3AF",
} as CSSProperties
}>
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<EmailField
onChange={handleChange}
@@ -132,6 +143,8 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
);
}
ForgotPassword.isThemeSupported = false;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
+2 -2
View File
@@ -19,7 +19,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
import { FiArrowLeft } from "@calcom/ui/components/icon";
import { ArrowLeft } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { WithNonceProps } from "@lib/withNonce";
@@ -91,7 +91,7 @@ export default function Login({
setTwoFactorRequired(false);
methods.setValue("totpCode", "");
}}
StartIcon={FiArrowLeft}
StartIcon={ArrowLeft}
color="minimal">
{t("go_back")}
</Button>
+2 -2
View File
@@ -6,7 +6,7 @@ import { useEffect } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { FiCheck } from "@calcom/ui/components/icon";
import { Check } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -32,7 +32,7 @@ export function Logout(props: Props) {
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
<div className="mb-4">
<div className="bg-success mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<FiCheck className="h-6 w-6 text-green-600" />
<Check className="h-6 w-6 text-green-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
+4 -4
View File
@@ -27,7 +27,7 @@ import {
ConfirmationDialogContent,
VerticalDivider,
} from "@calcom/ui";
import { FiInfo, FiPlus, FiTrash } from "@calcom/ui/components/icon";
import { Info, Plus, Trash } from "@calcom/ui/components/icon";
import { HttpError } from "@lib/core/http/error";
@@ -57,7 +57,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
{t("date_overrides")}{" "}
<Tooltip content={t("date_overrides_info")}>
<span className="inline-block">
<FiInfo />
<Info className="h-4 w-4" />
</span>
</Tooltip>
</h3>
@@ -75,7 +75,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
onChange={(ranges) => append({ ranges })}
Trigger={
<Button color="secondary" StartIcon={FiPlus} data-testid="add-override">
<Button color="secondary" StartIcon={Plus} data-testid="add-override">
Add an override
</Button>
}
@@ -202,7 +202,7 @@ export default function Availability() {
<Dialog>
<DialogTrigger asChild>
<Button
StartIcon={FiTrash}
StartIcon={Trash}
variant="icon"
color="destructive"
aria-label={t("delete")}
+2 -2
View File
@@ -6,7 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { EmptyScreen, showToast } from "@calcom/ui";
import { FiClock } from "@calcom/ui/components/icon";
import { Clock } from "@calcom/ui/components/icon";
import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
@@ -75,7 +75,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
{schedules.length === 0 ? (
<div className="flex justify-center">
<EmptyScreen
Icon={FiClock}
Icon={Clock}
headline={t("new_schedule_heading")}
description={t("new_schedule_description")}
buttonRaw={<NewScheduleButton />}
+8 -8
View File
@@ -49,7 +49,7 @@ import type { Prisma } from "@calcom/prisma/client";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, HeadSeo, Badge, useCalcomTheme } from "@calcom/ui";
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
import { X, ExternalLink, ChevronLeft, Check, Calendar } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -177,7 +177,7 @@ export default function Success(props: SuccessProps) {
// - Event Type has require confirmation option enabled always
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
// - It's a paid event and payment is pending.
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
const isCancelled =
@@ -306,7 +306,7 @@ export default function Success(props: SuccessProps) {
<Link
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
className="hover:bg-subtle text-subtle dark:hover:text-inverted hover:text-default mt-2 inline-flex px-1 py-2 text-sm dark:hover:bg-transparent">
<FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
<ChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
</Link>
</div>
)}
@@ -325,7 +325,7 @@ export default function Success(props: SuccessProps) {
<div
className={classNames(
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
isBackgroundTransparent ? "" : " bg-default dark:bg-muted border-subtle",
!isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width",
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
)}
role="dialog"
@@ -347,10 +347,10 @@ export default function Success(props: SuccessProps) {
<img src={giphyImage} alt="Gif from Giphy" />
)}
{!giphyImage && !needsConfirmation && !isCancelled && (
<FiCheck className="h-5 w-5 text-green-600" />
<Check className="h-5 w-5 text-green-600" />
)}
{needsConfirmation && !isCancelled && <FiCalendar className="text-emphasis h-5 w-5" />}
{isCancelled && <FiX className="h-5 w-5 text-red-600" />}
{needsConfirmation && !isCancelled && <Calendar className="text-emphasis h-5 w-5" />}
{isCancelled && <X className="h-5 w-5 text-red-600" />}
</div>
<div className="mt-6 mb-8 text-center last:mb-0">
<h3
@@ -458,7 +458,7 @@ export default function Success(props: SuccessProps) {
className="text-default flex items-center gap-2 underline"
rel="noreferrer">
{providerName || "Link"}
<FiExternalLink className="text-default inline h-4 w-4" />
<ExternalLink className="text-default inline h-4 w-4" />
</a>
) : (
locationToDisplay
+2 -2
View File
@@ -12,7 +12,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, EmptyScreen } from "@calcom/ui";
import { FiCalendar } from "@calcom/ui/components/icon";
import { Calendar } from "@calcom/ui/components/icon";
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
@@ -180,7 +180,7 @@ export default function Bookings() {
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={FiCalendar}
Icon={Calendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
+192 -127
View File
@@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import type { PeriodType } from "@prisma/client";
import type { SchedulingType } from "@prisma/client";
import type { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -11,26 +12,28 @@ import { z } from "zod";
import { validateCustomEventName } from "@calcom/core/event";
import type { EventLocationType } from "@calcom/core/location";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry";
import type { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
import { Form, showToast } from "@calcom/ui";
import { ConfirmationDialogContent, Dialog, Form, showToast } from "@calcom/ui";
import { asStringOrThrow } from "@lib/asStringOrNull";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
import { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab";
import { EventAppsTab } from "@components/eventtype/EventAppsTab";
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
@@ -87,6 +90,7 @@ export type FormValues = {
successRedirectUrl: string;
durationLimits?: IntervalLimit;
bookingLimits?: IntervalLimit;
children: ChildrenEventType[];
hosts: { userId: number; isFixed: boolean }[];
bookingFields: z.infer<typeof eventTypeBookingFields>;
};
@@ -111,10 +115,12 @@ const querySchema = z.object({
});
export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
const EventTypePage = (props: EventTypeSetupProps) => {
const { t } = useLocale();
const utils = trpc.useContext();
const telemetry = useTelemetry();
const {
data: { tabName },
} = useTypedQuery(querySchema);
@@ -126,6 +132,41 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
onSuccess: async () => {
showToast(
t("event_type_updated_successfully", {
eventTypeTitle: eventType.title,
}),
"success"
);
},
async onSettled() {
await utils.viewer.eventTypes.get.invalidate();
},
onError: (err) => {
let message = "";
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
if (err.data?.code === "UNAUTHORIZED") {
message = `${err.data.code}: You are not able to update this event`;
}
if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
message = `${err.data.code}: ${err.message}`;
}
if (message) {
showToast(message, "error");
} else {
showToast(err.message, "error");
}
},
});
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
startDate: new Date(eventType.periodStartDate || Date.now()),
endDate: new Date(eventType.periodEndDate || Date.now()),
@@ -173,6 +214,18 @@ const EventTypePage = (props: EventTypeSetupProps) => {
minimumBookingNotice: eventType.minimumBookingNotice,
metadata,
hosts: eventType.hosts,
children: eventType.children.map((ch) => ({
...ch,
created: true,
owner: {
...ch.owner,
eventTypeSlugs:
eventType.team?.members
.find((mem) => mem.user.id === ch.owner.id)
?.user.eventTypes.map((evTy) => evTy.slug)
.filter((slug) => slug !== eventType.slug) ?? [],
},
})),
} as const;
const formMethods = useForm<FormValues>({
@@ -200,48 +253,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
),
});
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
onSuccess: async () => {
showToast(
t("event_type_updated_successfully", {
eventTypeTitle: eventType.title,
}),
"success"
);
},
async onSettled() {
await utils.viewer.eventTypes.get.invalidate();
},
onError: (err) => {
let message = "";
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
if (err.data?.code === "UNAUTHORIZED") {
message = `${err.data.code}: You are not able to update this event`;
}
if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
message = `${err.data.code}: ${err.message}`;
}
if (message) {
showToast(message, "error");
} else {
showToast(err.message, "error");
}
console.log(formMethods.formState.isSubmitting);
// formMethods.setFormState((prevState) => ({
// ...prevState,
// isSubmitting: false,
// }));
},
});
useEffect(() => {
if (!formMethods.formState.isDirty) {
//TODO: What's the best way to sync the form with backend
@@ -273,8 +284,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
destinationCalendar={destinationCalendar}
/>
),
availability: <AvailabilityTab isTeamEvent={!!team} />,
team: <EventTeamTab teamMembers={teamMembers} team={team} />,
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
limits: <EventLimitsTab eventType={eventType} />,
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
recurring: <EventRecurringTab eventType={eventType} />,
@@ -288,89 +299,143 @@ const EventTypePage = (props: EventTypeSetupProps) => {
webhooks: <EventTeamWebhooksTab eventType={eventType} team={team} />,
} as const;
const handleSubmit = async (values: FormValues) => {
const {
periodDates,
periodCountCalendarDays,
beforeBufferTime,
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
bookingLimits,
durationLimits,
recurringEvent,
locations,
metadata,
customInputs,
children,
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
minimumBookingNoticeInDurationType,
...input
} = values;
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
if (durationLimits) {
const isValid = validateIntervalLimitOrder(durationLimits);
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
}
if (metadata?.multipleDuration !== undefined) {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
throw new Error(t("seats_and_no_show_fee_error"));
}
updateMutation.mutate({
...input,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
periodEndDate: periodDates.endDate,
periodCountCalendarDays: periodCountCalendarDays === "1",
id: eventType.id,
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,
bookingLimits,
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
metadata,
customInputs,
children,
});
};
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
const slug = formMethods.watch("slug") ?? eventType.slug;
return (
<EventTypeSingleLayout
enabledAppsNumber={numberOfActiveApps}
installedAppsNumber={numberOfInstalledApps}
enabledWorkflowsNumber={eventType.workflows.length}
eventType={eventType}
team={team}
isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
currentUserMembership={currentUserMembership}>
<Form
form={formMethods}
id="event-type-form"
handleSubmit={async (values) => {
const {
periodDates,
periodCountCalendarDays,
beforeBufferTime,
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
bookingLimits,
durationLimits,
recurringEvent,
locations,
metadata,
customInputs,
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
minimumBookingNoticeInDurationType,
...input
} = values;
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
if (durationLimits) {
const isValid = validateIntervalLimitOrder(durationLimits);
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
}
if (metadata?.multipleDuration !== undefined) {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
throw new Error(t("seats_and_no_show_fee_error"));
}
updateMutation.mutate({
...input,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
periodEndDate: periodDates.endDate,
periodCountCalendarDays: periodCountCalendarDays === "1",
id: eventType.id,
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,
bookingLimits,
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
metadata,
customInputs,
});
<>
<EventTypeSingleLayout
enabledAppsNumber={numberOfActiveApps}
installedAppsNumber={numberOfInstalledApps}
enabledWorkflowsNumber={eventType.workflows.length}
eventType={eventType}
team={team}
isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
currentUserMembership={currentUserMembership}>
<Form
form={formMethods}
id="event-type-form"
handleSubmit={async (values: FormValues) => {
if (!values.children.length) return handleSubmit(values);
const existingSlugEventTypes = values.children.filter((ch) =>
ch.owner.eventTypeSlugs.includes(slug)
);
if (!existingSlugEventTypes.length) return handleSubmit(values);
setSlugExistsChildrenDialogOpen(existingSlugEventTypes);
}}>
<div ref={animationParentRef}>{tabMap[tabName]}</div>
</Form>
</EventTypeSingleLayout>
<Dialog
open={slugExistsChildrenDialogOpen.length > 0}
onOpenChange={() => {
setSlugExistsChildrenDialogOpen([]);
}}>
<div ref={animationParentRef} className="space-y-6">
{tabMap[tabName]}
</div>
</Form>
</EventTypeSingleLayout>
<ConfirmationDialogContent
isLoading={formMethods.formState.isSubmitting}
variety="warning"
title={t("managed_event_dialog_title", {
slug,
count: slugExistsChildrenDialogOpen.length,
})}
confirmBtnText={t("managed_event_dialog_confirm_button", {
count: slugExistsChildrenDialogOpen.length,
})}
cancelBtnText={t("go_back")}
onConfirm={(e: { preventDefault: () => void }) => {
e.preventDefault();
handleSubmit(formMethods.getValues());
telemetry.event(telemetryEventTypes.slugReplacementAction);
setSlugExistsChildrenDialogOpen([]);
}}>
<p className="mt-5">
<Trans
i18nKey="managed_event_dialog_information"
values={{
names: `${slugExistsChildrenDialogOpen
.map((ch) => ch.owner.name)
.slice(0, -1)
.join(", ")} ${
slugExistsChildrenDialogOpen.length > 1 ? t("and") : ""
} ${slugExistsChildrenDialogOpen.map((ch) => ch.owner.name).slice(-1)}`,
slug,
}}
count={slugExistsChildrenDialogOpen.length}
/>
</p>{" "}
<p className="mt-5">{t("managed_event_dialog_clarification")}</p>
</ConfirmationDialogContent>
</Dialog>
</>
);
};
+204 -105
View File
@@ -1,4 +1,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { User } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import { Trans } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import type { FC } from "react";
@@ -39,19 +42,19 @@ import {
HeadSeo,
} from "@calcom/ui";
import {
FiArrowDown,
FiArrowUp,
FiClipboard,
FiCode,
FiCopy,
FiEdit,
FiEdit2,
FiExternalLink,
FiLink,
FiMoreHorizontal,
FiTrash,
FiUpload,
FiUsers,
ArrowDown,
ArrowUp,
Clipboard,
Code,
Copy,
Edit,
Edit2,
ExternalLink,
Link as LinkIcon,
MoreHorizontal,
Trash,
Upload,
Users,
} from "@calcom/ui/components/icon";
import { withQuery } from "@lib/QueryCell";
@@ -116,7 +119,40 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
const { t } = useLocale();
return (
const content = () => (
<div>
<span
className="text-default font-semibold ltr:mr-1 rtl:ml-1"
data-testid={"event-type-title-" + type.id}>
{type.title}
</span>
{group.profile.slug ? (
<small
className="text-subtle hidden font-normal leading-4 sm:inline"
data-testid={"event-type-slug-" + type.id}>
{`/${
type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder")
}/${type.slug}`}
</small>
) : null}
{readOnly && (
<Badge variant="gray" className="ml-2">
{t("readonly")}
</Badge>
)}
</div>
);
return readOnly ? (
<div className="flex-1 overflow-hidden pr-4 text-sm">
{content()}
<EventTypeDescription
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
eventType={type}
shortenDescription
/>
</div>
) : (
<Link
href={`/event-types/${type.id}?tabName=setup`}
className="flex-1 overflow-hidden pr-4 text-sm"
@@ -157,6 +193,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const [parent] = useAutoAnimate<HTMLUListElement>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
null
);
const utils = trpc.useContext();
const mutation = trpc.viewer.eventTypeOrder.useMutation({
onError: async (err) => {
@@ -317,6 +356,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${CAL_URL}/${embedLink}`;
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
const isChildrenManagedEventType =
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
return (
<li key={type.id}>
<div className="hover:bg-muted flex w-full items-center justify-between">
@@ -325,7 +367,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<button
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, -1)}>
<FiArrowUp className="h-5 w-5" />
<ArrowUp className="h-5 w-5" />
</button>
)}
@@ -333,116 +375,148 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<button
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
onClick={() => moveEventType(index, 1)}>
<FiArrowDown className="h-5 w-5" />
<ArrowDown className="h-5 w-5" />
</button>
)}
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden sm:mt-0 sm:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.team && (
{type.team && !isManagedEventType && (
<AvatarGroup
className="relative top-1 right-3"
size="sm"
truncateAfter={4}
items={type.users.map((organizer) => ({
items={type.users.map((organizer: { name: any; username: any }) => ({
alt: organizer.name || "",
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
title: organizer.name || "",
}))}
/>
)}
{isManagedEventType && (
<AvatarGroup
className="relative top-1 right-3"
size="sm"
truncateAfter={4}
items={type.children
.flatMap((ch) => ch.users)
.map((user: User) => ({
alt: user.name || "",
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
title: user.name || "",
}))}
/>
)}
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
<Tooltip content={t("show_eventtype_on_profile")}>
<div className="self-center rounded-md p-2">
<Switch
name="Hidden"
checked={!type.hidden}
onCheckedChange={() => {
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
}}
/>
</div>
</Tooltip>
{!isManagedEventType && (
<>
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
<Tooltip content={t("show_eventtype_on_profile")}>
<div className="self-center rounded-md p-2">
<Switch
name="Hidden"
checked={!type.hidden}
onCheckedChange={() => {
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
}}
/>
</div>
</Tooltip>
</>
)}
<ButtonGroup combined>
<Tooltip content={t("preview")}>
<Button
data-testid="preview-link-button"
color="secondary"
target="_blank"
variant="icon"
href={calLink}
StartIcon={FiExternalLink}
/>
</Tooltip>
{!isManagedEventType && (
<>
<Tooltip content={t("preview")}>
<Button
data-testid="preview-link-button"
color="secondary"
target="_blank"
variant="icon"
href={calLink}
StartIcon={ExternalLink}
/>
</Tooltip>
<Tooltip content={t("copy_link")}>
<Button
color="secondary"
variant="icon"
StartIcon={FiLink}
onClick={() => {
showToast(t("link_copied"), "success");
navigator.clipboard.writeText(calLink);
}}
/>
</Tooltip>
<Tooltip content={t("copy_link")}>
<Button
color="secondary"
variant="icon"
StartIcon={LinkIcon}
onClick={() => {
showToast(t("link_copied"), "success");
navigator.clipboard.writeText(calLink);
}}
/>
</Tooltip>
</>
)}
<Dropdown modal={false}>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button
type="button"
variant="icon"
color="secondary"
StartIcon={FiMoreHorizontal}
StartIcon={MoreHorizontal}
className="ltr:radix-state-open:rounded-r-md rtl:radix-state-open:rounded-l-md"
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
type="button"
data-testid={"event-type-edit-" + type.id}
StartIcon={FiEdit2}
onClick={() => router.push("/event-types/" + type.id)}>
{t("edit")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<DropdownItem
type="button"
data-testid={"event-type-duplicate-" + type.id}
StartIcon={FiCopy}
onClick={() => openDuplicateModal(type, group)}>
{t("duplicate")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<EmbedButton
as={DropdownItem}
type="button"
StartIcon={FiCode}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
<DropdownMenuSeparator />
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
{!readOnly && (
<DropdownMenuItem>
<DropdownItem
color="destructive"
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
}}
StartIcon={FiTrash}
className="w-full rounded-none">
{t("delete")}
type="button"
data-testid={"event-type-edit-" + type.id}
StartIcon={Edit2}
onClick={() => router.push("/event-types/" + type.id)}>
{t("edit")}
</DropdownItem>
</DropdownMenuItem>
)}
{!isManagedEventType && !isChildrenManagedEventType && (
<>
<DropdownMenuItem className="outline-none">
<DropdownItem
type="button"
data-testid={"event-type-duplicate-" + type.id}
StartIcon={Copy}
onClick={() => openDuplicateModal(type, group)}>
{t("duplicate")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<EmbedButton
as={DropdownItem}
type="button"
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
</>
)}
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
isManagedEventType && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem
color="destructive"
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
setDeleteDialogSchedulingType(type.schedulingType);
}}
StartIcon={Trash}
className="w-full rounded-none">
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</Dropdown>
</ButtonGroup>
@@ -453,7 +527,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="min-w-9 mx-5 flex sm:hidden">
<Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button type="button" variant="icon" color="secondary" StartIcon={FiMoreHorizontal} />
<Button type="button" variant="icon" color="secondary" StartIcon={MoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
@@ -461,7 +535,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownItem
href={calLink}
target="_blank"
StartIcon={FiExternalLink}
StartIcon={ExternalLink}
className="w-full rounded-none">
{t("preview")}
</DropdownItem>
@@ -473,7 +547,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
navigator.clipboard.writeText(calLink);
showToast(t("link_copied"), "success");
}}
StartIcon={FiClipboard}
StartIcon={Clipboard}
className="w-full rounded-none text-left">
{t("copy_link")}
</DropdownItem>
@@ -492,7 +566,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
.then(() => showToast(t("link_shared"), "success"))
.catch(() => showToast(t("failed"), "error"));
}}
StartIcon={FiUpload}
StartIcon={Upload}
className="w-full rounded-none">
{t("share")}
</DropdownItem>
@@ -501,7 +575,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownMenuItem className="outline-none">
<DropdownItem
onClick={() => router.push("/event-types/" + type.id)}
StartIcon={FiEdit}
StartIcon={Edit}
className="w-full rounded-none">
{t("edit")}
</DropdownItem>
@@ -509,7 +583,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownMenuItem className="outline-none">
<DropdownItem
onClick={() => openDuplicateModal(type, group)}
StartIcon={FiCopy}
StartIcon={Copy}
data-testid={"event-type-duplicate-" + type.id}>
{t("duplicate")}
</DropdownItem>
@@ -521,8 +595,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
setDeleteDialogSchedulingType(type.schedulingType);
}}
StartIcon={FiTrash}
StartIcon={Trash}
className="w-full rounded-none">
{t("delete")}
</DropdownItem>
@@ -539,14 +614,37 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
loadingText={t("confirm_delete_event_type")}
title={t(
`delete_${deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"}_event_type`
)}
confirmBtnText={t(
`confirm_delete_${
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
}_event_type`
)}
loadingText={t(
`confirm_delete_${
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
}_event_type`
)}
onConfirm={(e) => {
e.preventDefault();
deleteEventTypeHandler(deleteDialogTypeId);
}}>
{t("delete_event_type_description")}
<p className="mt-5">
{t(
`delete_${
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
}_event_type_description`
)}
</p>
<p className="mt-5">
<Trans
i18nKey={`delete_${
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
}_event_type_warning`}
/>
</p>
</ConfirmationDialogContent>
</Dialog>
</div>
@@ -589,7 +687,7 @@ const EventTypeListHeading = ({
<span className="text-subtle ms-2 me-2 relative -top-px text-xs">
<Link href={`/settings/teams/${teamId}/members`}>
<Badge variant="gray">
<FiUsers className="mr-1 -mt-px inline h-3 w-3" />
<Users className="mr-1 -mt-px inline h-3 w-3" />
{membershipCount}
</Badge>
</Link>
@@ -617,7 +715,7 @@ const CreateFirstEventTypeView = () => {
return (
<EmptyScreen
Icon={FiLink}
Icon={LinkIcon}
headline={t("new_event_type_heading")}
description={t("new_event_type_description")}
/>
@@ -638,6 +736,7 @@ const CTA = () => {
teamId: profile.teamId,
label: profile.name || profile.slug,
image: profile.image,
membershipRole: profile.membershipRole,
slug: profile.slug,
};
});
@@ -646,7 +745,7 @@ const CTA = () => {
<CreateButton
subtitle={t("create_event_on").toUpperCase()}
options={profileOptions}
createDialog={CreateEventTypeDialog}
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
/>
);
};
+4 -4
View File
@@ -15,7 +15,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import { Button, ButtonGroup } from "@calcom/ui";
import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
import { RefreshCcw, UserPlus, Users } from "@calcom/ui/components/icon";
const Heading = () => {
const { t } = useLocale();
@@ -35,17 +35,17 @@ export default function InsightsPage() {
const { data: user } = trpc.viewer.me.useQuery();
const features = [
{
icon: <FiUsers className="h-5 w-5" />,
icon: <Users className="h-5 w-5" />,
title: t("view_bookings_across"),
description: t("view_bookings_across_description"),
},
{
icon: <FiRefreshCcw className="h-5 w-5" />,
icon: <RefreshCcw className="h-5 w-5" />,
title: t("identify_booking_trends"),
description: t("identify_booking_trends_description"),
},
{
icon: <FiUserPlus className="h-5 w-5" />,
icon: <UserPlus className="h-5 w-5" />,
title: t("spot_popular_event_types"),
description: t("spot_popular_event_types_description"),
},
@@ -0,0 +1 @@
export { default } from "@calcom/features/ee/users/pages/users-edit-view";
@@ -0,0 +1 @@
export { default } from "@calcom/features/ee/users/pages/users-add-view";
@@ -0,0 +1 @@
export { default } from "@calcom/features/ee/users/pages/users-listing-view";
+2 -2
View File
@@ -6,7 +6,7 @@ import { classNames } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Meta } from "@calcom/ui";
import { FiExternalLink } from "@calcom/ui/components/icon";
import { ExternalLink } from "@calcom/ui/components/icon";
interface CtaRowProps {
title: string;
@@ -48,7 +48,7 @@ const BillingView = () => {
<CtaRow
title={t("billing_manage_details_title")}
description={t("billing_manage_details_description")}>
<Button color="primary" href={billingHref} target="_blank" EndIcon={FiExternalLink}>
<Button color="primary" href={billingHref} target="_blank" EndIcon={ExternalLink}>
{t("billing_portal")}
</Button>
</CtaRow>
@@ -16,7 +16,7 @@ import {
Meta,
AppSkeletonLoader as SkeletonLoader,
} from "@calcom/ui";
import { FiLink, FiPlus } from "@calcom/ui/components/icon";
import { Link as LinkIcon, Plus } from "@calcom/ui/components/icon";
const ApiKeysView = () => {
const { t } = useLocale();
@@ -32,7 +32,7 @@ const ApiKeysView = () => {
return (
<Button
color="secondary"
StartIcon={FiPlus}
StartIcon={Plus}
onClick={() => {
setApiKeyToEdit(undefined);
setApiKeyModal(true);
@@ -72,7 +72,7 @@ const ApiKeysView = () => {
</>
) : (
<EmptyScreen
Icon={FiLink}
Icon={LinkIcon}
headline={t("create_first_api_key")}
description={t("create_first_api_key_description", { appName: APP_NAME })}
buttonRaw={<NewApiKeyButton />}
@@ -181,7 +181,7 @@ const AppearanceView = () => {
{/* TODO future PR to preview brandColors */}
{/* <Button
color="secondary"
EndIcon={FiExternalLink}
EndIcon={ExternalLink}
className="mt-6"
onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}>
Preview
@@ -24,7 +24,7 @@ import {
SkeletonText,
showToast,
} from "@calcom/ui";
import { FiPlus, FiCalendar } from "@calcom/ui/components/icon";
import { Plus, Calendar } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -50,7 +50,7 @@ const AddCalendarButton = () => {
return (
<>
<Button color="secondary" StartIcon={FiPlus} href="/apps/categories/calendar">
<Button color="secondary" StartIcon={Plus} href="/apps/categories/calendar">
{t("add_calendar")}
</Button>
</>
@@ -87,7 +87,7 @@ const CalendarsView = () => {
<div>
<div className="bg-muted border-subtle mt-4 flex space-x-4 rounded-md p-2 sm:mx-0 sm:p-10 md:border md:p-6 xl:mt-0">
<div className=" bg-default border-subtle flex h-9 w-9 items-center justify-center rounded-md border-2 p-[6px]">
<FiCalendar className="text-default h-6 w-6" />
<Calendar className="text-default h-6 w-6" />
</div>
<div className="flex w-full flex-col space-y-3">
@@ -198,7 +198,7 @@ const CalendarsView = () => {
</div>
) : (
<EmptyScreen
Icon={FiCalendar}
Icon={Calendar}
headline={t("no_calendar_installed")}
description={t("no_calendar_installed_description")}
buttonText={t("add_a_calendar")}
@@ -24,7 +24,7 @@ import {
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { FiAlertCircle, FiMoreHorizontal, FiTrash, FiVideo } from "@calcom/ui/components/icon";
import { AlertCircle, MoreHorizontal, Trash, Video } from "@calcom/ui/components/icon";
import AppListCard from "@components/AppListCard";
@@ -111,7 +111,7 @@ const ConferencingLayout = () => {
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{!appIsDefault && (
@@ -119,7 +119,7 @@ const ConferencingLayout = () => {
<DropdownItem
type="button"
color="secondary"
StartIcon={FiVideo}
StartIcon={Video}
onClick={() => {
const locationType = getEventLocationTypeFromApp(
app?.locationOption?.value ?? ""
@@ -141,7 +141,7 @@ const ConferencingLayout = () => {
type="button"
color="destructive"
disabled={app.isGlobal}
StartIcon={FiTrash}
StartIcon={Trash}
onClick={() => {
setDeleteCredentialId(app.credentialIds[0]);
setDeleteAppModal(true);
@@ -163,7 +163,7 @@ const ConferencingLayout = () => {
title={t("Remove app")}
description={t("are_you_sure_you_want_to_remove_this_app")}
type="confirmation"
Icon={FiAlertCircle}>
Icon={AlertCircle}>
<DialogFooter>
<Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}>
{t("yes_remove_app")}
@@ -38,7 +38,7 @@ import {
TextField,
Editor,
} from "@calcom/ui";
import { FiAlertTriangle, FiTrash2 } from "@calcom/ui/components/icon";
import { AlertTriangle, Trash2 } from "@calcom/ui/components/icon";
import TwoFactor from "@components/auth/TwoFactor";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
@@ -232,7 +232,7 @@ const ProfileView = () => {
{/* Delete account Dialog */}
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
<DialogTrigger asChild>
<Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={FiTrash2}>
<Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={Trash2}>
{t("delete_account")}
</Button>
</DialogTrigger>
@@ -240,7 +240,7 @@ const ProfileView = () => {
title={t("delete_account_modal_title")}
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
type="creation"
Icon={FiAlertTriangle}>
Icon={AlertTriangle}>
<>
<p className="text-default mb-7">
{t("delete_account_confirmation_message", { appName: APP_NAME })}
@@ -283,7 +283,7 @@ const ProfileView = () => {
title={t("confirm_password")}
description={t("confirm_password_change_email")}
type="creation"
Icon={FiAlertTriangle}>
Icon={AlertTriangle}>
<>
<PasswordField
data-testid="password"
+2 -2
View File
@@ -15,7 +15,7 @@ import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
import { FiArrowRight } from "@calcom/ui/components/icon";
import { ArrowRight } from "@calcom/ui/components/icon";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -137,7 +137,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
<Button
color="minimal"
EndIcon={FiArrowRight}
EndIcon={ArrowRight}
className="dark:hover:bg-darkgray-200"
href={`/team/${team.slug}?members=1`}
shallow={true}>
+2 -2
View File
@@ -5,7 +5,7 @@ import Shell from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { Plus } from "@calcom/ui/components/icon";
function Teams() {
const { t } = useLocale();
@@ -16,7 +16,7 @@ function Teams() {
CTA={
<Button
variant="fab"
StartIcon={FiPlus}
StartIcon={Plus}
type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
{t("new")}
+31 -2
View File
@@ -1,8 +1,10 @@
import type { DailyEventObjectRecordingStarted } from "@daily-co/daily-js";
import DailyIframe from "@daily-co/daily-js";
import MarkdownIt from "markdown-it";
import type { GetServerSidePropsContext } from "next";
import Head from "next/head";
import { useState, useEffect, useRef } from "react";
import z from "zod";
import dayjs from "@calcom/dayjs";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
@@ -12,16 +14,23 @@ import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-f
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { FiChevronRight } from "@calcom/ui/components/icon";
import { ChevronRight } from "@calcom/ui/components/icon";
import { ssrInit } from "@server/lib/ssr";
const recordingStartedEventResponse = z
.object({
recordingId: z.string(),
})
.passthrough();
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
export default function JoinCall(props: JoinCallPageProps) {
const { t } = useLocale();
const { meetingUrl, meetingPassword, booking } = props;
const recordingId = useRef<string | null>(null);
useEffect(() => {
const callFrame = DailyIframe.createFrame({
@@ -49,11 +58,30 @@ export default function JoinCall(props: JoinCallPageProps) {
...(typeof meetingPassword === "string" && { token: meetingPassword }),
});
callFrame.join();
callFrame.on("recording-started", onRecordingStarted).on("recording-stopped", onRecordingStopped);
return () => {
callFrame.destroy();
};
}, []);
const onRecordingStopped = () => {
const data = { recordingId: recordingId.current, bookingUID: booking.uid };
fetch("/api/recorded-daily-video", {
method: "POST",
body: JSON.stringify(data),
}).catch((err) => {
console.log(err);
});
recordingId.current = null;
};
const onRecordingStarted = (event?: DailyEventObjectRecordingStarted | undefined) => {
const response = recordingStartedEventResponse.parse(event);
recordingId.current = response.recordingId;
};
const title = `${APP_NAME} Video`;
return (
<>
@@ -222,7 +250,7 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
aria-label={`${open ? "close" : "open"} booking description sidebar`}
className="h-20 w-6 rounded-r-md border border-l-0 border-gray-300/20 bg-black/60 text-white shadow-sm backdrop-blur-lg"
onClick={() => setOpen(!open)}>
<FiChevronRight
<ChevronRight
aria-hidden
className={classNames(open && "rotate-180", "w-5 transition-all duration-300 ease-in-out")}
/>
@@ -246,6 +274,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
...bookingMinimalSelect,
uid: true,
description: true,
isRecorded: true,
user: {
select: {
id: true,
+4 -4
View File
@@ -5,7 +5,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { Button, HeadSeo } from "@calcom/ui";
import { FiArrowRight, FiCalendar, FiX } from "@calcom/ui/components/icon";
import { ArrowRight, Calendar, X } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -29,7 +29,7 @@ export default function MeetingUnavailable(props: inferSSRProps<typeof getServer
aria-labelledby="modal-headline">
<div>
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<FiX className="h-6 w-6 text-red-600" />
<X className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-headline">
@@ -41,14 +41,14 @@ export default function MeetingUnavailable(props: inferSSRProps<typeof getServer
{props.booking.title}
</h2>
<p className="text-subtle text-center">
<FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
<Calendar className="mr-1 -mt-1 inline-block h-4 w-4" />
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
</p>
</div>
</div>
<div className="mt-5 text-center sm:mt-6">
<div className="mt-5">
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
<Button data-testid="return-home" href="/event-types" EndIcon={ArrowRight}>
{t("go_back")}
</Button>
</div>
@@ -6,7 +6,7 @@ import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button, HeadSeo, EmptyScreen } from "@calcom/ui";
import { FiArrowRight, FiCalendar, FiClock } from "@calcom/ui/components/icon";
import { ArrowRight, Calendar, Clock } from "@calcom/ui/components/icon";
export default function MeetingNotStarted(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
@@ -15,19 +15,19 @@ export default function MeetingNotStarted(props: inferSSRProps<typeof getServerS
<HeadSeo title={t("this_meeting_has_not_started_yet")} description={props.booking.title} />
<main className="mx-auto my-24 max-w-3xl">
<EmptyScreen
Icon={FiClock}
Icon={Clock}
headline={t("this_meeting_has_not_started_yet")}
description={
<>
<h2 className="mb-2 text-center font-medium">{props.booking.title}</h2>
<p className="text-subtle text-center">
<FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
<Calendar className="mr-1 -mt-1 inline-block h-4 w-4" />
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
</p>
</>
}
buttonRaw={
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
<Button data-testid="return-home" href="/event-types" EndIcon={ArrowRight}>
{t("go_back")}
</Button>
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, EmptyScreen, HeadSeo } from "@calcom/ui";
import { FiX, FiArrowRight } from "@calcom/ui/components/icon";
import { X, ArrowRight } from "@calcom/ui/components/icon";
export default function NoMeetingFound() {
const { t } = useLocale();
@@ -10,11 +10,11 @@ export default function NoMeetingFound() {
<HeadSeo title={t("no_meeting_found")} description={t("no_meeting_found")} />
<main className="mx-auto my-24 max-w-3xl">
<EmptyScreen
Icon={FiX}
Icon={X}
headline={t("no_meeting_found")}
description={t("no_meeting_found_description")}
buttonRaw={
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
<Button data-testid="return-home" href="/event-types" EndIcon={ArrowRight}>
{t("go_back_home")}
</Button>
}
+5 -3
View File
@@ -268,6 +268,9 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
routingForms: user.routingForms,
self,
login: async () => login({ ...(await self()), password: user.username }, store.page),
logout: async () => {
await page.goto("/auth/logout");
},
getPaymentCredential: async () => getPaymentCredential(store.page),
// ths is for developemnt only aimed to inject debugging messages in the metadata field of the user
debug: async (message: string | Record<string, JSONValue>) => {
@@ -333,9 +336,8 @@ export async function login(
await passwordLocator.fill(user.password ?? user.username!);
await signInLocator.click();
// 2 seconds of delay to give the session enough time for a clean load
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(2000);
// Moving away from waiting 2 seconds, as it is not a reliable way to expect session to be started
await page.waitForLoadState("networkidle");
}
export async function getPaymentCredential(page: Page) {
@@ -0,0 +1,94 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
test.afterEach(({ users }) => users.deleteAll());
test.describe("Managed Event Types tests", () => {
test("Can create managed event type", async ({ page, users }) => {
// Creating the owner user of the team
const adminUser = await users.create();
// Creating the member user of the team
const memberUser = await users.create();
// First we work with owner user, logging in
await adminUser.login();
// Making sure page loads completely
await page.waitForLoadState("networkidle");
// Let's create a team
await page.goto("/teams");
await test.step("Managed event option exists for team admin", async () => {
// Proceed to create a team
await page.locator("text=Create Team").click();
await page.waitForURL("/settings/teams/new");
// Filling team creation form wizard
await page.locator('input[name="name"]').fill(`${adminUser.username}'s Team`);
await page.locator("text=Continue").click();
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members$/i);
await page.locator('[data-testid="new-member-button"]').click();
await page.locator('[placeholder="email\\@example\\.com"]').fill(`${memberUser.username}@example.com`);
await page.locator('[data-testid="invite-new-member-button"]').click();
await page.waitForLoadState("networkidle");
await page.locator("text=Publish team").click();
await page.waitForLoadState("networkidle");
// Going to create an event type
await page.goto("/event-types");
await page.waitForLoadState("networkidle");
await page.click("[data-testid=new-event-type-dropdown]");
await page.click("[data-testid=option-team-1]");
// Expecting we can add a managed event type as team owner
await expect(page.locator('input[value="MANAGED"]')).toBeVisible();
// Actually creating a managed event type to test things further
await page.click('input[value="MANAGED"]');
await page.fill("[name=title]", "managed");
await page.click("[type=submit]");
});
await test.step("Managed event type has unlocked fields for admin", async () => {
await page.waitForSelector('[data-testid="update-eventtype"]');
await expect(page.locator('input[name="title"]')).toBeEditable();
await expect(page.locator('input[name="slug"]')).toBeEditable();
await expect(page.locator('input[name="length"]')).toBeEditable();
});
await test.step("Managed event type exists for added member", async () => {
// Now we need to accept the invitation as member and come back in as admin to
// assign the member in the managed event type
await adminUser.logout();
await memberUser.login();
await page.waitForSelector('[data-testid="event-types"]');
await page.goto("/teams");
await page.waitForLoadState("networkidle");
await page.locator('button[data-testid^="accept-invitation"]').click();
await page.waitForLoadState("networkidle");
await memberUser.logout();
// Coming back as team owner to assign member user to managed event
await adminUser.login();
await page.waitForLoadState("networkidle");
await page.locator('[data-testid="event-types"] a[title="managed"]').click();
await page.locator('[data-testid="vertical-tab-assignment"]').click();
await page.locator('[class$="control"]').filter({ hasText: "Select..." }).click();
await page.locator("#react-select-5-option-1").click();
await page.locator('[type="submit"]').click();
await page.waitForLoadState("networkidle");
await adminUser.logout();
// Coming back as member user to see if there is a managed event present after assignment
await memberUser.login();
await page.waitForLoadState("networkidle");
await expect(page.locator('[data-testid="event-types"] a[title="managed"]')).toBeVisible();
});
await test.step("Managed event type has locked fields for added member", async () => {
page.locator('[data-testid="event-types"] a[title="managed"]').click();
await page.waitForLoadState("networkidle");
await expect(page.locator('input[name="title"]')).not.toBeEditable();
await expect(page.locator('input[name="slug"]')).not.toBeEditable();
await expect(page.locator('input[name="length"]')).not.toBeEditable();
});
});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>

After

Width:  |  Height:  |  Size: 307 B

@@ -736,9 +736,6 @@
"minimum_booking_notice": "الحد الأدنى من الوقت للحجز",
"slot_interval": "الفترات الزمنية بين عمليات الحجز",
"slot_interval_default": "استخدام طول الحدث (الوضع الافتراضي)",
"delete_event_type_description": "هل تريد بالتأكيد حذف نوع الحدث هذا؟ أي شخص شاركت هذا الرابط معه لن يستطيع بعد الآن الحجز باستخدامه.",
"delete_event_type": "حذف نوع الحدث",
"confirm_delete_event_type": "نعم، احذف نوع الحدث",
"delete_account": "حذف الحساب",
"confirm_delete_account": "نعم، احذف الحساب",
"delete_account_confirmation_message": "هل تريد بالتأكيد حذف حسابك على {{appName}}؟ أي شخص شاركت رابط حسابك معه لن يستطيع بعد الآن الحجز باستخدامه، وستفقد أي تفضيلات حفظتها.",
@@ -1652,8 +1649,8 @@
"verification_code": "رمز التحقق",
"can_you_try_again": "هلّا جربت المحاولة ثانيةً في وقت مختلف؟",
"verify": "التحقق",
"invalid_event_name_variables": "ثمة متغيّر غير غير صالح في اسم فعاليتك",
"select_all": "اختيار الكل",
"default_conferencing_bulk_title": "التحديث الجماعي لمجموعة أنواع الفعاليات الحالية",
"invalid_event_name_variables": "ثمة متغيّر غير غير صالح في اسم فعاليتك",
"default_conferencing_bulk_description": "تحديث المواقع لأنواع الفعاليات المحددة"
}
@@ -736,9 +736,6 @@
"minimum_booking_notice": "Minimální potřebná doba",
"slot_interval": "Časové intervaly",
"slot_interval_default": "Použít délku události (výchozí)",
"delete_event_type_description": "Opravdu chcete smazat tento typ události? Všichni, se kterými jsme tento odkaz sdíleli, ho nebudou moct používat pro rezervování.",
"delete_event_type": "Smazat typ události",
"confirm_delete_event_type": "Ano, smazat typ události",
"delete_account": "Smazat účet",
"confirm_delete_account": "Ano, smazat účet",
"delete_account_confirmation_message": "Opravdu chcete odstranit svůj účet na {{appName}}? Všichni, se kterými jste sdíleli odkaz na účet, přijdou o možnost vytvářet rezervace a vy přijdete o uložené předvolby.",
@@ -1652,8 +1649,8 @@
"verification_code": "Ověřovací kód",
"can_you_try_again": "Můžete to zkusit znovu a použít jiný čas?",
"verify": "Ověřit",
"invalid_event_name_variables": "V názvu události je neplatná proměnná",
"select_all": "Vybrat vše",
"default_conferencing_bulk_title": "Hromadná aktualizace stávajících typů událostí",
"invalid_event_name_variables": "V názvu události je neplatná proměnná",
"default_conferencing_bulk_description": "Aktualizujte místa pro vybrané typy událostí"
}
@@ -733,9 +733,6 @@
"minimum_booking_notice": "Minimum Varsel",
"slot_interval": "Tidsrums intervaller",
"slot_interval_default": "Brug begivenhedslængde (standard)",
"delete_event_type_description": "Er du sikker på, at du vil slette denne begivenhedstype? Alle, som du har delt dette link med, vil ikke længere kunne booke ved hjælp af det.",
"delete_event_type": "Slet Begivenhedstype",
"confirm_delete_event_type": "Ja, slet begivenhedstype",
"delete_account": "Slet konto",
"confirm_delete_account": "Ja, slet konto",
"delete_account_confirmation_message": "Er du sikker på, at du vil slette din {{appName}} konto? Alle, som du har delt dit konto-link med, vil ikke længere kunne booke ved hjælp af det, og eventuelle præferencer du har gemt, vil gå tabt.",
@@ -736,9 +736,6 @@
"minimum_booking_notice": "Mindesvorlaufzeit für eine Buchung",
"slot_interval": "Zeit-Slot-Intervalle",
"slot_interval_default": "Verwende Terminlänge (Standard)",
"delete_event_type_description": "Sind Sie sicher, dass Sie diesen Termintypen löschen möchten? Empfänger dieses Links können dann nicht mehr buchen.",
"delete_event_type": "Termintyp löschen",
"confirm_delete_event_type": "Ja, Termintyp löschen",
"delete_account": "Account löschen",
"confirm_delete_account": "Ja, Account löschen",
"delete_account_confirmation_message": "Sind Sie sicher, dass Sie Ihr {{appName}}-Konto löschen möchten? Sie können dann nicht mehr über Ihre geteilten Links gebucht werden und alle Einstellungen werden gelöscht.",
@@ -1652,8 +1649,8 @@
"verification_code": "Bestätigungscode",
"can_you_try_again": "Können Sie es zu einem anderen Zeitpunkt erneut versuchen?",
"verify": "Verifizieren",
"invalid_event_name_variables": "Es gibt eine ungültige Variable in Ihrem Terminnamen",
"select_all": "Alle auswählen",
"default_conferencing_bulk_title": "Massenaktualisierung vorhandener Termintypen",
"invalid_event_name_variables": "Es gibt eine ungültige Variable in Ihrem Terminnamen",
"default_conferencing_bulk_description": "Orte für die ausgewählten Termintypen aktualisieren"
}
+53 -4
View File
@@ -1,4 +1,5 @@
{
"identity_provider": "Identity provider",
"trial_days_left": "You have $t(day, {\"count\": {{days}} }) left on your PRO trial",
"day_one": "{{count}} day",
"day_other": "{{count}} days",
@@ -69,6 +70,8 @@
"event_awaiting_approval_subject": "Awaiting Approval: {{title}} at {{date}}",
"event_still_awaiting_approval": "An event is still waiting for your approval",
"booking_submitted_subject": "Booking Submitted: {{title}} at {{date}}",
"download_recording_subject": "Download Recording: {{title}} at {{date}}",
"download_your_recording": "Download your recording",
"your_meeting_has_been_booked": "Your meeting has been booked",
"event_type_has_been_rescheduled_on_time_date": "Your {{title}} has been rescheduled to {{date}}.",
"event_has_been_rescheduled": "Updated - Your event has been rescheduled",
@@ -224,6 +227,7 @@
"done": "Done",
"all_done": "All done!",
"all_apps": "All",
"available_apps": "Available Apps",
"check_email_reset_password": "Check your email. We sent you a link to reset your password.",
"finish": "Finish",
"few_sentences_about_yourself": "A few sentences about yourself. This will appear on your personal url page.",
@@ -586,6 +590,20 @@
"minutes": "Minutes",
"round_robin": "Round Robin",
"round_robin_description": "Cycle meetings between multiple team members.",
"managed_event": "Managed Event",
"username_placeholder": "username",
"managed_event_description": "Create & distribute event types in bulk to team members",
"managed": "Managed",
"managed_event_url_clarification": "\"username\" will be filled by the username of the members assigned",
"assign_to": "Assign to",
"add_members": "Add members...",
"count_members_one": "{{count}} member",
"count_members_other": "{{count}} members",
"no_assigned_members": "No assigned members",
"assigned_to": "Assigned to",
"start_assigning_members_above": "Start assigning members above",
"locked_fields_admin_description": "Members will not be able to edit this",
"locked_fields_member_description": "This option was locked by the team admin",
"url": "URL",
"hidden": "Hidden",
"readonly": "Readonly",
@@ -737,9 +755,11 @@
"minimum_booking_notice": "Minimum Notice",
"slot_interval": "Time-slot intervals",
"slot_interval_default": "Use event length (default)",
"delete_event_type_description": "Are you sure you want to delete this event type? Anyone who you've shared this link with will no longer be able to book using it.",
"delete_event_type": "Delete Event Type",
"confirm_delete_event_type": "Yes, delete event type",
"delete_event_type": "Delete event type?",
"delete_managed_event_type": "Delete managed event type?",
"delete_event_type_description": "Anyone who you've shared this link with will no longer be able to book using it.",
"delete_managed_event_type_description": "<ul><li>Members assigned to this event type will also have their event types deleted.</li><li>Anyone who they've shared their link with will no longer be able to book using it.</li></ul>",
"confirm_delete_event_type": "Yes, delete",
"delete_account": "Delete account",
"confirm_delete_account": "Yes, delete account",
"delete_account_confirmation_message": "Are you sure you want to delete your {{appName}} account? Anyone who you've shared your account link with will no longer be able to book using it and any preferences you have saved will be lost.",
@@ -1182,6 +1202,7 @@
"start_of_week": "Start of week",
"recordings_title": "Recordings",
"recording": "Recording",
"happy_scheduling": "Happy Scheduling",
"select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
"check_for_conflicts": "Check for conflicts",
"view_recordings": "View recordings",
@@ -1260,6 +1281,8 @@
"download_responses": "Download responses",
"download_responses_description": "Download all responses to your form in CSV format.",
"download": "Download",
"download_recording": "Download Recording",
"recording_from_your_recent_call":"A recording from your recent call on Cal.com is ready for download",
"create_your_first_form": "Create your first form",
"create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.",
"create_your_first_webhook": "Create your first Webhook",
@@ -1448,11 +1471,17 @@
"fixed_round_robin": "Fixed round robin",
"add_one_fixed_attendee": "Add one fixed attendee and round robin through a number of attendees.",
"calcom_is_better_with_team": "Cal.com is better with teams",
"the_calcom_team":"The Cal.com team",
"add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.",
"booking_limit_reached": "Booking Limit for this event type has been reached",
"duration_limit_reached": "Duration Limit for this event type has been reached",
"admin_has_disabled": "An admin has disabled {{appName}}",
"disabled_app_affects_event_type": "An admin has disabled {{appName}} which affects your event type {{eventType}}",
"event_replaced_notice": "An admin has replaced one of your event types",
"email_subject_slug_replacement": "A team administrator has replaced your event /{{slug}}",
"email_body_slug_replacement_notice": "An administrator on the <strong>{{teamName}}</strong> team has replaced your event type <strong>/{{slug}}</strong> with a managed event type that they control.",
"email_body_slug_replacement_info": "Your link will continue to work but some settings for it may have changed. You can review it in event types.",
"email_body_slug_replacement_suggestion": "If you have any questions about the event type, please reach out to your administrator.<br /><br />Happy scheduling, <br />The Cal.com team",
"disable_payment_app": "The admin has disabled {{appName}} which affects your event type {{title}}. Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin renables your payment method.",
"payment_disabled_still_able_to_book": "Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin reenables your payment method.",
"app_disabled_with_event_type": "The admin has disabled {{appName}} which affects your event type {{title}}.",
@@ -1667,11 +1696,31 @@
"verification_code": "Verification code",
"can_you_try_again": "Can you try again with a different time?",
"verify": "Verify",
"invalid_event_name_variables": "There is an invalid variable in your event name",
"select_all": "Select All",
"default_conferencing_bulk_title": "Bulk update existing event types",
"invalid_event_name_variables": "There is an invalid variable in your event name",
"members_default_schedule": "Member's default schedule",
"set_by_admin": "Set by team admin",
"members_default_location": "Member's default location",
"members_default_schedule_description": "We will use each members default availability schedule. They will be able to edit or change it.",
"requires_at_least_one_schedule": "You are required to have at least one schedule",
"default_conferencing_bulk_description": "Update the locations for the selected event types",
"locked_for_members": "Locked for members",
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
"locked_by_admin": "Locked by admin",
"app_not_connected": "You have not connected a {{appName}} account.",
"connect_now": "Connect now",
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
"managed_event_dialog_confirm_button_other": "Replace & notify {{count}} members",
"managed_event_dialog_title_one": "The url /{{slug}} already exists for {{count}} member. Do you want to replace it?",
"managed_event_dialog_title_other": "The url /{{slug}} already exists for {{count}} members. Do you want to replace it?",
"managed_event_dialog_information_one": "<strong>{{names}}</strong> is already using the <strong>/{{slug}}</strong> url.",
"managed_event_dialog_information_other": "<strong>{{names}}</strong> are already using the <string>/{{slug}}</strong> url.",
"managed_event_dialog_clarification": "If you choose to replace it, we will notify them. Go back and remove them if you don't want to overwrite it.",
"review_event_type": "Review Event Type",
"looking_for_more_analytics": "Looking for more analytics?",
"looking_for_more_insights": "Looking for more Insights?",
"add_filter": "Add filter",
"select_user": "Select User",
@@ -736,9 +736,6 @@
"minimum_booking_notice": "Aviso de Reserva",
"slot_interval": "Intervalos de tiempo",
"slot_interval_default": "Usar duración del evento (por defecto)",
"delete_event_type_description": "¿Está seguro de que desea eliminar este tipo de evento? Cualquiera con quien hayas compartido este enlace ya no podrá reservar con él.",
"delete_event_type": "Eliminar Tipo de Evento",
"confirm_delete_event_type": "Sí, Eliminar Tipo de Evento",
"delete_account": "Eliminar cuenta",
"confirm_delete_account": "Sí, eliminar cuenta",
"delete_account_confirmation_message": "¿Estás seguro de que quieres eliminar tu cuenta de {{appName}}? Cualquier persona con la que hayas compartido el enlace de tu cuenta ya no podrá agendar con ella y cualquier preferencia que hayas guardado se perderá.",
@@ -1652,8 +1649,8 @@
"verification_code": "Código de verificación",
"can_you_try_again": "¿Puede intentarlo de nuevo con una hora diferente?",
"verify": "Verificar",
"invalid_event_name_variables": "Hay una variable no válida en el nombre de su evento",
"select_all": "Seleccionar todo",
"default_conferencing_bulk_title": "Actualización masiva de tipos de eventos existentes",
"invalid_event_name_variables": "Hay una variable no válida en el nombre de su evento",
"default_conferencing_bulk_description": "Actualizar las ubicaciones para los tipos de eventos seleccionados"
}
+55 -7
View File
@@ -69,6 +69,8 @@
"event_awaiting_approval_subject": "En attente de validation : {{title}} le {{date}}",
"event_still_awaiting_approval": "Un événement est toujours en attente de votre validation",
"booking_submitted_subject": "Réservation confirmée : {{title}} le {{date}}",
"download_recording_subject": "Télécharger l'enregistrement : {{title}} le {{date}}",
"download_your_recording": "Télécharger votre enregistrement",
"your_meeting_has_been_booked": "Votre rendez-vous a été réservé",
"event_type_has_been_rescheduled_on_time_date": "Votre {{title}} a été replanifié le {{date}}",
"event_has_been_rescheduled": "Mise à jour - Votre événement a été replanifié",
@@ -224,6 +226,7 @@
"done": "Terminé",
"all_done": "Terminé !",
"all_apps": "Toutes",
"available_apps": "Applications disponibles",
"check_email_reset_password": "Vérifiez vos e-mails. Nous vous avons envoyé un lien pour réinitialiser votre mot de passe.",
"finish": "Terminer",
"few_sentences_about_yourself": "Quelques mots à propos de vous. Ces informations apparaîtront sur votre page publique.",
@@ -394,7 +397,7 @@
"our_support_team": "notre équipe d'assistance",
"contact_our_support_team": "Contactez notre équipe d'assistance",
"uh_oh": "Oups !",
"no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de types d'événement.",
"no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de type d'événement.",
"edit_logo": "Modifier le logo",
"upload_a_logo": "Télécharger un logo",
"remove_logo": "Supprimer le logo",
@@ -586,6 +589,20 @@
"minutes": "minutes",
"round_robin": "Round-Robin",
"round_robin_description": "Alternez vos rendez-vous entre plusieurs membres d'équipe.",
"managed_event": "Événement géré",
"username_placeholder": "nom d'utilisateur",
"managed_event_description": "Créez et distribuez plusieurs types d'événements aux membres de l'équipe.",
"managed": "Géré",
"managed_event_url_clarification": "« nom d'utilisateur » sera rempli par le nom d'utilisateur des membres assignés",
"assign_to": "Assigner à",
"add_members": "Ajouter des membres...",
"count_members_one": "{{count}} membre",
"count_members_other": "{{count}} membres",
"no_assigned_members": "Aucun membre assigné",
"assigned_to": "Assigné à",
"start_assigning_members_above": "Commencez à assigner des membres ci-dessus",
"locked_fields_admin_description": "Les membres ne pourront pas modifier ceci.",
"locked_fields_member_description": "Cette option a été verrouillée par l'administrateur de l'équipe.",
"url": "Lien",
"hidden": "Masqué",
"readonly": "Lecture seule",
@@ -595,7 +612,7 @@
"plan_upgrade": "Vous devez mettre à niveau votre plan pour avoir plus d'un type d'événement actif.",
"plan_upgrade_teams": "Vous devez mettre à niveau votre plan pour créer une équipe.",
"plan_upgrade_instructions": "Vous pouvez <1>mettre à niveau ici</1>.",
"event_types_page_title": "Types d’événement",
"event_types_page_title": "Types d'évènements",
"event_types_page_subtitle": "Créez des événements à partager pour que les personnes puissent réserver des créneaux sur votre calendrier.",
"new": "Nouveau",
"new_event_type_btn": "Nouveau type d'événement",
@@ -737,9 +754,11 @@
"minimum_booking_notice": "Préavis minimum",
"slot_interval": "Fréquence de créneaux horaires",
"slot_interval_default": "Utiliser la durée de l'événement (par défaut)",
"delete_event_type_description": "Voulez-vous vraiment supprimer ce type d'événement ? Toute personne avec qui vous avez partagé ce lien ne pourra plus faire de réservation en l'utilisant.",
"delete_event_type": "Supprimer le type d'événement",
"confirm_delete_event_type": "Oui, supprimer le type d'événement",
"delete_event_type": "Supprimer le type d'événement ?",
"delete_managed_event_type": "Supprimer le type d'événement géré ?",
"delete_event_type_description": "Toute personne avec qui vous avez partagé ce lien ne pourra plus réserver en l'utilisant.",
"delete_managed_event_type_description": "<ul><li>Les membres affectés à ce type d'événement verront également leurs types d'événement supprimés.</li><li>Toute personne avec qui ils ont partagé leur lien ne pourra plus réserver en l'utilisant.</li></ul>",
"confirm_delete_event_type": "Oui, supprimer",
"delete_account": "Supprimer le compte",
"confirm_delete_account": "Oui, supprimer le compte",
"delete_account_confirmation_message": "Voulez-vous vraiment supprimer votre compte {{appName}} ? Toute personne avec qui vous avez partagé le lien de votre compte ne pourra plus réserver en utilisant ce lien et toutes les préférences que vous avez sauvegardées seront perdues.",
@@ -1182,6 +1201,7 @@
"start_of_week": "Début de la semaine",
"recordings_title": "Enregistrements",
"recording": "Enregistrement",
"happy_scheduling": "Bonne planification",
"select_calendars": "Sélectionnez les calendriers dont vous souhaitez vérifier les conflits afin d'éviter les réservations en double.",
"check_for_conflicts": "Vérifier les conflits",
"view_recordings": "Voir les enregistrements",
@@ -1260,6 +1280,8 @@
"download_responses": "Télécharger les réponses",
"download_responses_description": "Téléchargez toutes les réponses à votre formulaire au format CSV.",
"download": "Télécharger",
"download_recording": "Télécharger l'enregistrement",
"recording_from_your_recent_call": "Un enregistrement de votre appel récent sur Cal.com est prêt à être téléchargé",
"create_your_first_form": "Créez votre premier formulaire",
"create_your_first_form_description": "Avec les formulaires de routage, vous pouvez poser des questions de qualification et rediriger vers la bonne personne ou le bon type d'événement.",
"create_your_first_webhook": "Créez votre premier webhook",
@@ -1448,11 +1470,17 @@
"fixed_round_robin": "Round-Robin fixe",
"add_one_fixed_attendee": "Ajoutez un participant fixe et le Round-Robin via un certain nombre de participants.",
"calcom_is_better_with_team": "Cal.com est meilleur en équipe",
"the_calcom_team": "L'équipe Cal.com",
"add_your_team_members": "Ajoutez les membres de votre équipe à vos types d'événements. Utilisez la planification collective pour inclure tout le monde ou trouvez la personne la plus appropriée avec la planification Round-Robin.",
"booking_limit_reached": "La limite de réservation pour ce type d'événement a été atteinte",
"duration_limit_reached": "La limite de durée pour ce type d'événement a été atteinte",
"admin_has_disabled": "Un administrateur a désactivé {{appName}}",
"disabled_app_affects_event_type": "Un administrateur a désactivé {{appName}} qui affecte votre type d'événement {{eventType}}",
"event_replaced_notice": "Un administrateur a remplacé l'un de vos types d'événements",
"email_subject_slug_replacement": "Un administrateur d'équipe a remplacé votre événement /{{slug}}",
"email_body_slug_replacement_notice": "Un administrateur de l'équipe <strong>{{teamName}}</strong> a remplacé votre type d'événement <strong>/{{slug}}</strong> par un type d'événement géré qu'il contrôle.",
"email_body_slug_replacement_info": "Votre lien continuera à fonctionner, mais certains de ses paramètres peuvent avoir changé. Vous pouvez l'examiner dans les types d'événements.",
"email_body_slug_replacement_suggestion": "Si vous avez des questions sur le type d'événement, veuillez contacter votre administrateur.<br /><br />Bonne planification, <br />L'équipe Cal.com",
"disable_payment_app": "L'administrateur a désactivé {{appName}} qui affecte votre type d'événement {{title}}. Les participants sont toujours en mesure de réserver ce type d'événement, mais ne seront pas invités à payer. Vous pouvez masquer le type d'événement pour l'empêcher jusqu'à ce que votre administrateur réactive votre moyen de paiement.",
"payment_disabled_still_able_to_book": "Les participants sont toujours en mesure de réserver ce type d'événement, mais ne seront pas invités à payer. Vous pouvez masquer le type d'événement pour l'empêcher jusqu'à ce que votre administrateur réactive votre mode de paiement.",
"app_disabled_with_event_type": "L'administrateur a désactivé {{appName}} qui affecte votre type d'événement {{title}}.",
@@ -1650,7 +1678,7 @@
"select_a_router": "Sélectionner un routeur",
"add_a_new_route": "Ajouter une nouvelle route",
"make_informed_decisions": "Prenez des décisions éclairées grâce aux statistiques",
"make_informed_decisions_description": "Notre tableau de bord de statistiques analyse toute l'activité de votre équipe et vous révèle les tendances qui permettent d'améliorer la planification et la prise de décision au sein de l'équipe.",
"make_informed_decisions_description": "Notre tableau de bord de statistiques analyse l'activité de votre équipe et vous révèle les tendances qui permettent d'améliorer la planification et la prise de décision au sein de l'équipe.",
"view_bookings_across": "Visualisez les réservations de tous les membres",
"view_bookings_across_description": "Découvrez qui reçoit le plus de réservations et assurez la meilleure répartition au sein de votre équipe.",
"identify_booking_trends": "Identifiez les tendances de réservation",
@@ -1667,11 +1695,31 @@
"verification_code": "Code de vérification",
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
"verify": "Vérifier",
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
"select_all": "Tout sélectionner",
"default_conferencing_bulk_title": "Mise à jour multiple des types d'événements existants",
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
"members_default_schedule": "Planning par défaut du membre",
"set_by_admin": "Défini par l'administrateur de l'équipe",
"members_default_location": "Lieu par défaut du membre",
"members_default_schedule_description": "Nous utiliserons le calendrier de disponibilité par défaut de chaque membre. Ils pourront le modifier ou le changer.",
"requires_at_least_one_schedule": "Vous devez avoir au moins un planning",
"default_conferencing_bulk_description": "Mettez à jour les emplacements pour les types d'événements sélectionnés.",
"locked_for_members": "Verrouillé pour les membres",
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
"locked_by_admin": "Verrouillé par l'administrateur",
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
"connect_now": "Connecter maintenant",
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
"managed_event_dialog_confirm_button_other": "Remplacer et notifier {{count}} membres",
"managed_event_dialog_title_one": "Le lien /{{slug}} existe déjà pour {{count}} membre. Voulez-vous le remplacer ?",
"managed_event_dialog_title_other": "Le lien /{{slug}} existe déjà pour {{count}} membres. Voulez-vous le remplacer ?",
"managed_event_dialog_information_one": "<strong>{{names}}</strong> utilise déjà le lien <strong>/{{slug}}</strong>.",
"managed_event_dialog_information_other": "<strong>{{names}}</strong> utilisent déjà le lien <strong>/{{slug}}</strong>.",
"managed_event_dialog_clarification": "Si vous choisissez de le remplacer, nous les en informerons. Revenez en arrière et supprimez-les si vous ne souhaitez pas l'écraser.",
"review_event_type": "Examiner le type d'événement",
"looking_for_more_analytics": "Vous cherchez plus d'analyses ?",
"looking_for_more_insights": "Vous cherchez plus de statistiques ?",
"add_filter": "Ajouter un filtre",
"select_user": "Sélectionner un utilisateur",

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