Compare commits

...
66 Commits
Author SHA1 Message Date
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
Alex van Andel 1d18a09cc8 v2.7.15
release.yml / release (push) Canceled after 0s
2023-04-12 13:58:42 +02:00
Peer Richelsen 887d63ceef app store fixes (#8224) 2023-04-12 11:40:53 +00:00
Hariom Balhara 555fcc21f2 Revert color-scheme for embed (#8229) 2023-04-12 11:16:59 +00:00
Peer Richelsen f45fb03838 wip: replaced dark border with border-subtle (#8211) 2023-04-12 10:42:41 +02:00
Peer Richelsen 46611fdf77 make routing form full width (#8215) 2023-04-12 07:39:04 +05:30
Crowdin Bot c0f7c40bec New Crowdin translations by Github Action 2023-04-12 01:57:27 +00:00
Joe Au-Yeungandzomars f6f257c705 Refactor app store & fix admin apps list bugs (#7812)
* If omni install do not show app successfully installed toast

* Clean up. Up to hubspot

* Fix double click to enable apps

* Clean up rest of apps

* Create cli app add dirName

* Type fix

* Only enable apps if keys are valid

* Save dirName as slug

* Remove dirname from metadata

---------

Co-authored-by: zomars <zomars@me.com>
2023-04-11 18:56:43 -07:00
Crowdin Bot d61f50724b New Crowdin translations by Github Action 2023-04-11 22:14:21 +00:00
Alex van Andel 960aa0e00b v2.7.14
release.yml / release (push) Canceled after 0s
2023-04-12 00:10:27 +02:00
Joe Au-YeungandAlex van Andel 817e20f11e Stripe add the ability to place hold on cards (#8022)
* Add payment option to schema

* Add payment option to Stripe zod

* Set payment option on event type

* Create manual payment intent in Stripe

* Set payment option from Stripe app

* Add payment option to DB

* Pass React.ReactNode to checkbox

* Create uncaptured payment intent

* WIP

* Capture card in setup intent

* Show charge card option

* Charge card from booking page

* Bug fixes

* Clean up

* Clean up app card

* Add no-show fee messaging on booking page

* Send payment email on payment & add price

* Fix messaging

* Create no show fee charged email

* Send charge fee collected email

* Disable submit on card failure

* Clean up

* Serverside prevent charging card again if already charged

* Only confirm booking if paid for

* Type fixes

* More type fixes

* More type fixes

* Type fix

* Type fixes

* UI changes

* Payment component rework

* Update apps/web/public/static/locales/en/common.json

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update apps/web/public/static/locales/en/common.json

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update apps/web/components/dialog/ChargeCardDialog.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update packages/trpc/server/routers/viewer/payments.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Revert GTM config

* Adjust payment option dropdown

* Show alert when seats are set

* Small bug fixes

* Create collect card method

* clean up

* Prevent seats & charge no-show fee to be enabled together

* Do not charge no-show fee on unconfirmed bookings

* Add check to collect card method

* Webhook send request emails

* Fix some dark mode colours

* Change awaiting payment language

* Type fixes

* Set height of Select and TextField both to 38px to fix alignment

* Fix message seats & payment error message

* Type fix

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-11 23:44:14 +02:00
Keith Williams af0123a606 Updated issue/PR staleness checks (#8209)
* Updated issue/PR staleness checks

* Changed order of settings to group them better
2023-04-11 22:39:53 +02:00
zomars 709bbf6626 Update yarn.lock 2023-04-11 11:41:48 -07:00
zomars 53097ee3bc Delete lockfile.yml 2023-04-11 11:40:13 -07:00
zomars 31f2002654 Update lockfile.yml 2023-04-11 11:38:20 -07:00
zomars@users.noreply.github.com ee155c3643 Update yarn.lock 2023-04-11 18:23:33 +00:00
zomars 8ab20c3c9f Update lockfile.yml 2023-04-11 11:21:17 -07:00
zomars 4f6d035d29 Create lockfile.yml 2023-04-11 11:07:55 -07:00
zomars 2e5b42ef47 Update yarn.lock 2023-04-11 10:56:30 -07:00
Alex van Andel b055672ea0 v2.7.13
release.yml / release (push) Canceled after 0s
2023-04-11 19:51:38 +02:00
Alex van Andel e4fbca388c Fix yarn.lock file 2023-04-11 19:50:36 +02:00
Joe Au-Yeung 2b8bba09c9 Prevent updating app to enabled on seed app store (#8092) 2023-04-11 21:51:21 +05:30
Alex van Andel 98cf29f655 Wrap useSuspense with NoSSR (#8079) 2023-04-11 21:49:27 +05:30
GitStart-Cal.com ae8706316f CALCOM-7895 - [CAL-1320] remove "application_fee_amount" for all regions (#7933) 2023-04-11 16:16:43 +00:00
Jeroen ReumkensandAlex van Andel ec8e08d442 Added serverside translations to teams page. (#7999)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-11 21:39:02 +05:30
sean-brydonandPeer Richelsen c33f424333 Fixes /more page + fixes multiple duration badge on BookingPage (#8200)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-11 15:21:24 +00:00
Hariom Balhara 6f2312d1e2 Fix app not visible in app-store (#8202) 2023-04-11 14:58:57 +00:00
Peer Richelsen c2b6070b7e Update package.json
release.yml / release (push) Canceled after 0s
2023-04-11 16:54:49 +02:00
Efraín Rochín a5bebe2b48 bugfix/sync-not-working-properly (#8168) 2023-04-11 16:51:27 +02:00
sean-brydon e3a1f20cc8 Darkmode Fixes (#8191) 2023-04-11 12:15:10 +00:00
sean-brydon ab639591da Fix forcing hard refresh across forced themes (#8188) 2023-04-11 17:20:25 +05:30
Hariom BalharaandPeer Richelsen 21a9b64eb0 Fix missing sylaps icon in admin/apps (#8190)
* Fi missing sylaps icon in admin/apps and remove usage of imageSrc

* Remove imageSrc from googleCalendar and verify that logo is still shown

* Use logo in video apps location select:

* Remove imageSrc from templates

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-11 11:38:37 +00:00
Omar López 636ee7b8a1 Prioritizes published teams over unpublished ones (#8177) 2023-04-11 06:16:57 +00:00
Crowdin Bot 543e526568 New Crowdin translations by Github Action 2023-04-11 02:21:03 +00:00
Hariom Balhara e465c1d71d Redirect on top iframe always - This is to make sure embed redirects on top frame (#8164) 2023-04-11 07:50:15 +05:30
Peer Richelsen cdab9037ce added dark mode upgrade banner (#8172) 2023-04-10 21:14:28 +00:00
Crowdin Bot 21e454d0e2 New Crowdin translations by Github Action 2023-04-10 20:27:50 +00:00
zomars 7384f29a1c Update crowdin.yml 2023-04-10 13:27:00 -07:00
419 changed files with 9884 additions and 3638 deletions
+2
View File
@@ -177,3 +177,5 @@ CSP_POLICY=
# Vercel Edge Config
EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
+6 -3
View File
@@ -15,6 +15,9 @@ jobs:
steps:
- uses: actions/stale@v7
with:
days-before-stale: 60
include-only-assigned: true
days-before-close: -1
days-before-issue-stale: 60
days-before-issue-close: -1
days-before-pr-stale: 14
days-before-pr-close: 7
stale-pr-message: "This PR is being marked as stale due to inactivity."
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued.
+2
View File
@@ -15,6 +15,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: crowdin action
uses: crowdin/github-action@1.5.1
+1
View File
@@ -2,6 +2,7 @@ name: PR Update
on:
pull_request_target:
types: [review_requested]
branches:
- main
paths-ignore:
+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>
) : (
+10 -19
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.
@@ -247,7 +238,7 @@ const Component = ({
</span>
)}
<div className="prose-sm prose prose-headings:text-emphasis prose-code:text-default text-default mt-8">
<div className="prose-sm prose prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body}
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
@@ -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]"
)}
/>
@@ -132,9 +132,9 @@ const BookingDescription: FC<Props> = (props) => {
variant="gray"
className={classNames(
duration === dur.toString()
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-emphasis text-emphasis dark:text-inverted",
"cursor-pointer"
? "bg-emphasis border-emphasis text-emphasis "
: "bg-subtle text-default border-transparent ",
"cursor-pointer border"
)}
onClick={() => {
setDuration(dur);
@@ -1,22 +1,35 @@
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] }) => {
const BookingDescriptionPayment = (props: {
eventType: Parameters<typeof getPaymentAppData>[0];
t: TFunction;
}) => {
const paymentAppData = getPaymentAppData(props.eventType);
if (!paymentAppData || paymentAppData.price <= 0) return null;
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]" />
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
<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", {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
})}
</>
) : (
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
)}
</p>
);
};
+53 -21
View File
@@ -28,10 +28,11 @@ import {
TableActions,
TextAreaField,
} from "@calcom/ui";
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } 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";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
@@ -56,7 +57,9 @@ function BookingListItem(booking: BookingItemProps) {
const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const cardCharged = booking?.payment[0]?.success;
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data?.status === BookingStatus.REJECTED) {
@@ -113,19 +116,23 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setRejectionDialogIsOpen(true);
},
icon: FiSlash,
icon: Slash,
disabled: mutation.isLoading,
},
{
id: "confirm",
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
onClick: () => {
bookingConfirm(true);
},
icon: FiCheck,
disabled: mutation.isLoading,
color: "primary",
},
// For bookings with payment, only confirm if the booking is paid for
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
? [
{
id: "confirm",
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
onClick: () => {
bookingConfirm(true);
},
icon: Check,
disabled: mutation.isLoading,
},
]
: []),
];
const showRecordingActions: ActionType[] = [
@@ -149,7 +156,7 @@ function BookingListItem(booking: BookingItemProps) {
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
`,
icon: FiX,
icon: X,
},
{
id: "edit_booking",
@@ -157,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()}` : ""
@@ -165,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: () => {
@@ -178,12 +185,24 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setIsOpenLocationDialog(true);
},
icon: FiMapPin,
icon: MapPin,
},
],
},
];
const chargeCardActions: ActionType[] = [
{
id: "charge_card",
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
disabled: cardCharged,
onClick: () => {
setChargeCardDialogIsOpen(true);
},
icon: CreditCard,
},
];
if (isTabRecurring && isRecurring) {
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
}
@@ -194,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>
);
@@ -237,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
@@ -254,6 +272,15 @@ function BookingListItem(booking: BookingItemProps) {
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
/>
{booking.paid && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
setIsOpenDialog={setChargeCardDialogIsOpen}
bookingId={booking.id}
paymentAmount={booking?.payment[0].amount}
paymentCurrency={booking?.payment[0].currency}
/>
)}
{showRecordingsButtons && (
<ViewRecordingsDialog
booking={booking}
@@ -321,7 +348,7 @@ function BookingListItem(booking: BookingItemProps) {
)}
{booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{t("paid")}
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
</Badge>
)}
{recurringDates !== undefined && (
@@ -423,6 +450,11 @@ function BookingListItem(booking: BookingItemProps) {
<RequestSentMessage />
</div>
)}
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
<div className="ml-2">
<TableActions actions={chargeCardActions} />
</div>
)}
</td>
</tr>
</>
@@ -476,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";
@@ -124,6 +124,16 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
[timeZone]
);
const paymentAppData = getPaymentAppData(eventType);
const paymentAmount = () => {
return;
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>;
};
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
@@ -181,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">
@@ -200,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]"
@@ -211,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,
@@ -236,14 +248,23 @@ 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]" />
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
<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", {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
})}
</>
) : (
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
)}
</p>
)}
{timezoneDropdown}
@@ -30,8 +30,9 @@ import getBookingResponsesSchema, {
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
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";
@@ -40,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";
@@ -209,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";
@@ -226,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 (
@@ -245,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
}, []);
@@ -264,15 +284,19 @@ const BookingPage = ({
);
}
return router.push({
pathname: `/booking/${uid}`,
query: {
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
},
const query = {
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
@@ -280,16 +304,18 @@ const BookingPage = ({
const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => {
const { uid } = responseData[0] || {};
return router.push({
pathname: `/booking/${uid}`,
query: {
isSuccessBookingPage: true,
allRemainingBookings: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
},
const query = {
isSuccessBookingPage: true,
allRemainingBookings: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
@@ -521,17 +547,17 @@ const BookingPage = ({
<div
className={classNames(
"main",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-default dark:border",
"dark:border-darkgray-300 rounded-md sm:border"
isBackgroundTransparent ? "" : "bg-default",
"border-booker sm:border-booker-width rounded-md"
)}>
<div className="sm:flex">
{showEventTypeDetails && (
<div className="sm:dark:border-darkgray-300 text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
<BookingDescriptionPayment eventType={eventType} />
<BookingDescriptionPayment eventType={eventType} t={t} />
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="dark:text-inverted text-default items-start text-sm font-medium">
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<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,
@@ -542,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) &&
@@ -573,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)}
@@ -582,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
@@ -642,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>
@@ -665,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">
@@ -0,0 +1,100 @@
import { Trans } from "next-i18next";
import { useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { IntlProvider, FormattedNumber } from "react-intl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
showToast,
} from "@calcom/ui";
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingId: number;
paymentAmount: number;
paymentCurrency: string;
}
export const ChargeCardDialog = (props: IRescheduleDialog) => {
const { t } = useLocale();
const utils = trpc.useContext();
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const [chargeError, setChargeError] = useState(false);
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
onSuccess: () => {
utils.viewer.bookings.invalidate();
setIsOpenDialog(false);
showToast("Charge successful", "success");
},
onError: () => {
setChargeError(true);
},
});
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<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]">
<CreditCard className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("charge_card")} />
<Trans i18nKey="charge_card_dialog_body">
<p className="text-sm text-gray-500">
You are about to charge the attendee{" "}
<IntlProvider locale="en">
<FormattedNumber
value={props.paymentAmount / 100.0}
style="currency"
currency={props.paymentCurrency?.toUpperCase()}
/>
</IntlProvider>
. Are you sure you want to continue?
</p>
</Trans>
{chargeError && (
<div className="mt-4 flex text-red-500">
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
<p className="text-sm">{t("error_charging_card")}</p>
</div>
)}
<DialogFooter>
<DialogClose />
<Button
data-testid="send_request"
disabled={chargeCardMutation.isLoading || chargeError}
onClick={() =>
chargeCardMutation.mutate({
bookingId,
})
}>
<Trans i18nKey="charge_card_confirm">
Charge attendee{" "}
<IntlProvider locale="en">
<FormattedNumber
value={props.paymentAmount / 100.0}
style="currency"
currency={props.paymentCurrency?.toUpperCase()}
/>
</IntlProvider>
</Trans>
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
};
@@ -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,14 +10,15 @@ 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";
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 } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
import { Edit, Copy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -58,6 +59,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
useEffect(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
@@ -79,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);
@@ -127,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>
@@ -149,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),
@@ -157,6 +168,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>
<hr className="border-subtle" />
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
requiresConfirmation={requiresConfirmation}
@@ -170,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)}
@@ -184,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) => {
@@ -196,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"
@@ -218,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) => {
@@ -239,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) {
@@ -246,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>
}
@@ -262,54 +280,62 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<SettingsToggle
data-testid="offer-seats-toggle"
title={t("offer_seats")}
description={t("offer_seats_description")}
checked={value}
onCheckedChange={(e) => {
// Enabling seats will disable guests and requiring confirmation until fully supported
if (e) {
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
}
onChange(e);
}}>
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="lg:-ml-2">
<TextField
required
name="seatsPerTimeSlot"
labelSrOnly
label={t("number_of_seats")}
type="number"
defaultValue={value || 2}
min={1}
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
}}
/>
<div className="mt-2">
<Checkbox
description={t("show_attendees")}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
<>
<SettingsToggle
data-testid="offer-seats-toggle"
title={t("offer_seats")}
{...seatsLocked}
description={t("offer_seats_description")}
checked={value}
disabled={noShowFeeEnabled}
onCheckedChange={(e) => {
// Enabling seats will disable guests and requiring confirmation until fully supported
if (e) {
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
}
onChange(e);
}}>
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="lg:-ml-2">
<TextField
required
name="seatsPerTimeSlot"
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)));
}}
/>
<div className="mt-2">
<Checkbox
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
/>
</div>
</div>
</div>
)}
/>
</SettingsToggle>
)}
/>
</SettingsToggle>
{noShowFeeEnabled && <Alert severity="warning" title={t("seats_and_no_show_fee_error")} />}
</>
)}
/>
+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>
@@ -6,17 +6,17 @@ import { Button } from "@calcom/ui";
interface ICalendarItem {
title: string;
description?: string;
imageSrc: string;
logo: string;
type: App["type"];
}
const CalendarItem = (props: ICalendarItem) => {
const { title, imageSrc, type } = props;
const { title, logo, type } = props;
const { t } = useLocale();
return (
<div className="flex flex-row items-center justify-between p-5">
<div className="flex items-center space-x-3">
<img src={imageSrc} alt={title} className="h-8 w-8" />
<img src={logo} alt={title} className="h-8 w-8" />
<p className="text-sm font-bold">{title}</p>
</div>
<InstallAppButtonWithoutPlanCheck
@@ -31,13 +31,13 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
firstCalendar &&
firstCalendar.integration &&
firstCalendar.integration.title &&
firstCalendar.integration.imageSrc && (
firstCalendar.integration.logo && (
<>
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black">
<ConnectedCalendarItem
key={firstCalendar.integration.title}
name={firstCalendar.integration.title}
logo={firstCalendar.integration.imageSrc}
logo={firstCalendar.integration.logo}
externalId={
firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0
? firstCalendar.calendars[0].externalId
@@ -59,12 +59,12 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
{queryIntegrations.data &&
queryIntegrations.data.items.map((item) => (
<li key={item.title}>
{item.title && item.imageSrc && (
{item.title && item.logo && (
<CalendarItem
type={item.type}
title={item.title}
description={item.description}
imageSrc={item.imageSrc}
logo={item.logo}
/>
)}
</li>
@@ -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>
</>
+1 -4
View File
@@ -9,10 +9,7 @@ const PoweredByCal = () => {
const isEmbed = useIsEmbed();
return (
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
<Link
href={POWERED_BY_URL}
target="_blank"
className="text-bookinglight dark:text-inverted opacity-50 hover:opacity-100">
<Link href={POWERED_BY_URL} target="_blank" className="text-subtle opacity-50 hover:opacity-100">
{t("powered_by")}{" "}
<img
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
@@ -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}
+19 -2
View File
@@ -84,7 +84,14 @@ const CalcomThemeProvider = (
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
const storageKey = typeof embedNamespace === "string" ? `embed-theme-${embedNamespace}` : "theme";
const isEmbedMode = typeof embedNamespace === "string";
// If embedNamespace is not defined, we use the default storageKey -> The default storage key changs based on if we force light mode or not
// This is done to ensure that the default theme is light when we force light mode and as soon as you navigate to a page that is dark we dont need a hard refresh to change
const storageKey = isEmbedMode
? `embed-theme-${embedNamespace}`
: !isThemeSupported
? "cal-light"
: "theme";
return (
<ThemeProvider
@@ -94,6 +101,17 @@ const CalcomThemeProvider = (
forcedTheme={forcedTheme}
storageKey={storageKey}
attribute="class">
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
<style jsx global>
{`
.dark {
color-scheme: dark;
}
`}
</style>
)}
{props.children}
</ThemeProvider>
);
@@ -114,7 +132,6 @@ const AppProviders = (props: AppPropsWithChildren) => {
<SessionProvider session={session || undefined}>
<CustomI18nextProvider {...props}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider
nonce={props.pageProps.nonce}
isThemeSupported={props.Component.isThemeSupported}>
+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.11",
"version": "2.7.17",
"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>
@@ -47,7 +47,7 @@ export const getStaticProps: GetStaticProps<
revalidate: 1,
};
} catch (error) {
let message = "Unknown error while fetching calendarƒ";
let message = "Unknown error while fetching calendar";
if (error instanceof Error) message = error.message;
console.error(error, message);
return {
+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>
+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 />}
+7 -16
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";
@@ -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
@@ -1013,15 +1013,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
if (eventType.successRedirectUrl && isSuccessBookingPage && !cancel) {
return {
redirect: {
destination: eventType.successRedirectUrl,
permanent: false,
},
};
}
const profile = {
name: eventType.team?.name || eventType.users[0]?.name || null,
email: eventType.team ? null : eventType.users[0].email || null,
+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(),
+157 -81
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);
@@ -208,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>({
@@ -266,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} />,
@@ -281,85 +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"));
}
}
}
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} />}
/>
);
};
+6 -6
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"),
},
@@ -58,14 +58,14 @@ export default function InsightsPage() {
title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")}
features={features}
background="/tips/insights.jpg"
background="/tips/insights"
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="secondary" href="https://go.cal.com/insights" target="_blank">
<Button color="minimal" href="https://go.cal.com/insights" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
+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"
+18 -18
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";
@@ -55,12 +55,12 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
}
const EventTypes = () => (
<ul className="dark:border-darkgray-300 border-subtle rounded-md border">
<ul className="border-subtle border-subtle rounded-md border">
{team.eventTypes.map((type, index) => (
<li
key={index}
className={classNames(
"dark:bg-darkgray-100 dark:border-darkgray-300 bg-default hover:bg-muted border-subtle group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0",
"dark:bg-darkgray-100 border-subtle bg-default hover:bg-muted border-subtle group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0",
!isEmbed && "bg-default"
)}>
<div className="px-6 py-4 ">
@@ -125,7 +125,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="dark:border-darkgray-300 border-subtle w-full border-t" />
<div className="border-subtle border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
@@ -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}>
@@ -157,16 +157,20 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const ssr = await ssrInit(context);
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
const unpublishedTeam = await prisma.team.findFirst({
where: {
metadata: {
path: ["requestedSlug"],
equals: slug,
},
},
});
const team = await getTeamWithMembers(undefined, slug);
if (!team) {
const unpublishedTeam = await prisma.team.findFirst({
where: {
metadata: {
path: ["requestedSlug"],
equals: slug,
},
},
});
if (!unpublishedTeam) return { notFound: true } as const;
if (unpublishedTeam) {
return {
props: {
isUnpublished: true,
@@ -176,10 +180,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
} as const;
}
const team = await getTeamWithMembers(undefined, slug);
if (!team) return { notFound: true } as { notFound: true };
team.eventTypes = team.eventTypes.map((type) => ({
...type,
users: type.users.map((user) => ({
+1
View File
@@ -65,6 +65,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
seatsPerTimeSlot: true,
schedulingType: true,
bookingFields: true,
successRedirectUrl: true,
workflows: {
include: {
workflow: {
+12 -2
View File
@@ -1,9 +1,11 @@
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { TeamsListing } from "@calcom/features/ee/teams/components";
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();
@@ -14,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")}
@@ -25,6 +27,14 @@ function Teams() {
);
}
export const getStaticProps = async () => {
return {
props: {
...(await serverSideTranslations("en", ["common"])),
},
};
};
Teams.requiresLicense = false;
export default Teams;
+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();
});
});
});
-41
View File
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 427 97.5" style="enable-background:new 0 0 427 97.5;" xml:space="preserve">
<style type="text/css">
.st0{fillRule:evenodd;clipRule:evenodd;fill:#fff;}
</style>
<path class="st0" d="M27.5,88.2c-4.9,0-9.7-1.2-14-3.6c-4.2-2.4-7.6-5.8-9.9-10c-4.8-8.8-4.8-19.4,0-28.2c2.3-4.2,5.8-7.7,10-10
c4.3-2.4,9.1-3.7,14-3.6c6-0.1,11.8,1.7,16.5,5.3s8,8.7,9.9,15.4H42.8c-1.3-3-3.4-5.5-6.2-7.2c-2.6-1.6-5.6-2.5-8.7-2.5
c-2.8,0-5.6,0.7-8.1,2s-4.7,3.3-6.1,5.8c-3,5.3-3.1,11.9-0.2,17.3c1.4,2.5,3.4,4.5,5.8,6s5.2,2.3,8,2.2c7.2,0,12.4-3.3,15.4-9.9
h11.4c-1.1,4.2-3.1,8.1-5.8,11.5c-2.5,3-5.6,5.4-9.2,7C35.5,87.4,31.5,88.1,27.5,88.2L27.5,88.2z M99.6,82.1
c-4.6,4-10.6,6.2-16.8,6.1c-4.9,0-9.7-1.2-14-3.6c-4.1-2.4-7.5-5.8-9.8-10c-2.5-4.5-3.7-9.5-3.6-14.6c-0.1-15,11.9-27.2,26.9-27.3
c0.1,0,0.2,0,0.3,0c6.4-0.2,12.6,2.1,17.5,6.2v-5.2h11.5V87h-12V82.1z M83.5,43.7c-3,0-5.9,0.7-8.4,2.2c-2.5,1.4-4.6,3.5-6,6
c-1.5,2.5-2.2,5.4-2.2,8.4c-0.2,4.5,1.5,8.9,4.7,12.2c3.2,3.2,7.6,4.9,12.1,4.8c2.9,0,5.8-0.7,8.3-2.2c2.5-1.4,4.5-3.5,5.9-6
c2.9-5.3,2.9-11.7,0-17c-1.4-2.5-3.5-4.6-6-6C89.3,44.5,86.4,43.8,83.5,43.7L83.5,43.7z M122,14.8h9.7V87H122
C122,87.1,122,14.8,122,14.8z M149.8,65.2c0.5,2.3,1.5,4.4,3,6.3c1.4,1.8,3.3,3.2,5.4,4.1c2.2,1,4.6,1.5,7,1.5
c2.8,0.1,5.6-0.5,8.1-1.8c2.4-1.4,4.3-3.4,5.5-5.9h11.9c-2.7,6.5-6.2,11.2-10.4,14.2c-4.4,3.1-9.8,4.7-15.2,4.5
c-4.8,0.1-9.6-1.2-13.8-3.6c-4.1-2.4-7.5-5.8-9.7-10c-2.4-4.3-3.6-9.2-3.5-14.1c-0.1-4.9,1.1-9.8,3.5-14.1c2.3-4.2,5.7-7.6,9.8-10
c8.6-4.8,19.2-4.8,27.7,0.1c4.1,2.4,7.4,6,9.6,10.2c2.2,4.3,3.3,9.1,3.3,14c0,1-0.1,2.5-0.3,4.5h-41.9V65.2z M165.2,43.6
c-3.4-0.1-6.8,0.9-9.6,2.9c-2.7,2-4.7,4.8-5.6,8h30.2c-0.9-3.2-2.9-6-5.6-8C171.9,44.5,168.6,43.5,165.2,43.6z M234.8,57.3
c0-4.7-1-8.2-3-10.5s-4.9-3.5-8.7-3.5c-2.3,0-4.6,0.7-6.5,2c-2,1.3-3.7,3.2-4.8,5.4c-1.2,2.3-1.8,4.8-1.8,7.4V87h-11.2V33.8h10.5
v4.8c3.9-3.9,9.3-6,14.9-5.8c3.9,0,7.7,1,11,3s6,4.8,7.9,8.2c1.9,3.5,2.9,7.5,2.9,11.5v31.6h-11.2L234.8,57.3L234.8,57.3z
M296.7,82.2c-2.4,1.9-5,3.4-7.9,4.4c-3,1-6.2,1.5-9.4,1.5c-4.8,0.1-9.6-1.2-13.8-3.7c-4.1-2.4-7.5-5.9-9.8-10.1
c-2.4-4.3-3.6-9.1-3.6-14.1c-0.1-5.1,1.1-10.1,3.5-14.7c2.3-4.2,5.7-7.7,9.8-10.1c4.3-2.5,9.2-3.8,14.1-3.7c3.1,0,6.1,0.5,9,1.4
c2.8,0.9,5.4,2.3,7.8,4.1V14.9h11.3v72.2h-11.1L296.7,82.2L296.7,82.2z M280.4,43.1c-3,0-5.9,0.7-8.4,2.2c-2.5,1.4-4.6,3.5-6,6
c-1.5,2.6-2.2,5.5-2.2,8.5c-0.2,4.6,1.5,9.1,4.7,12.4c3.2,3.2,7.5,5,12.1,4.8c2.9,0,5.8-0.7,8.3-2.3c2.5-1.5,4.5-3.6,5.9-6.1
c2.9-5.4,2.9-11.8,0-17.2C291.8,46.2,286.3,43,280.4,43.1z M335.8,88.2c-3.8,0.1-7.5-0.8-10.8-2.4c-3.1-1.6-5.7-4-7.5-7
c-1.9-3.3-2.9-7.1-3-10.9h11c0,2.7,1.1,5.3,3.1,7.1c2.1,1.7,4.8,2.6,7.5,2.5c1.8,0,3.6-0.2,5.3-0.8c1.3-0.4,2.4-1.2,3.3-2.2
c0.7-0.8,1.1-1.9,1.1-3c0.1-1.2-0.4-2.4-1.2-3.2c-0.5-0.5-1.1-0.8-1.7-1.2c-0.7-0.3-1.4-0.6-2.2-0.8c-1.8-0.4-3.9-0.9-6.1-1.3
c-1.8-0.2-3.5-0.6-5.1-1s-3.5-1-5-1.6c-0.8-0.2-1.6-0.6-2.4-1l-1-0.7c-0.4-0.4-0.8-0.7-1.2-1c-0.7-0.5-1.3-1.1-1.8-1.8
c-0.6-0.8-1.1-1.7-1.5-2.6c-0.9-2-1.3-4.2-1.2-6.4c0-3,0.9-6,2.7-8.5c1.8-2.5,4.3-4.5,7.2-5.7c3.3-1.4,6.8-2.1,10.3-2
c3.6-0.1,7.1,0.7,10.3,2.4c3,1.5,5.4,3.8,7.1,6.7c1.8,3.2,2.7,6.8,2.7,10.4H345c-0.1-3-1-5.2-2.7-6.7s-4-2.3-7-2.3s-5.2,0.5-6.8,1.5
c-1.4,0.8-2.3,2.2-2.3,3.8c-0.1,1,0.3,2,1,2.6c0.6,0.5,1.2,0.9,1.8,1.2c0.6,0.3,1.3,0.5,2,0.7c0.8,0.2,1.7,0.4,2.7,0.6l3.1,0.5
c3.4,0.5,6.7,1.2,10,2.2c2.6,0.8,5,2.3,6.7,4.5c1.8,2.4,2.9,5.3,3,8.4l0.1,1.8c0.1,3.2-0.9,6.4-2.8,9.1s-4.5,4.8-7.5,6
C343.2,87.6,339.5,88.3,335.8,88.2L335.8,88.2z M412.4,74.6c-2.4,4.2-5.9,7.7-10.2,10c-4.5,2.4-9.6,3.7-14.8,3.6
c-4.9,0.1-9.7-1.2-14-3.6c-4.2-2.4-7.7-5.8-10.1-9.9c-2.5-4.2-3.8-9-3.7-13.8c-0.1-5.1,1.1-10.1,3.6-14.6c2.4-4.2,6-7.7,10.2-10
c4.4-2.4,9.3-3.6,14.3-3.6s9.9,1.2,14.3,3.6c4.2,2.3,7.8,5.7,10.2,9.9c2.4,4.3,3.7,9.2,3.6,14.2C416.1,65.4,414.8,70.3,412.4,74.6
L412.4,74.6z M387.8,43.3c-3,0-6,0.8-8.6,2.3s-4.8,3.6-6.3,6.2c-1.5,2.6-2.3,5.5-2.3,8.5c0,4.6,1.8,9,5.2,12.2
c3.2,3.4,7.6,5.3,12.3,5.3c3,0,6-0.8,8.5-2.4c2.6-1.5,4.7-3.7,6.2-6.3c1.5-2.6,2.3-5.6,2.3-8.6s-0.8-6-2.3-8.6s-3.7-4.7-6.3-6.2
C393.9,44.1,390.9,43.3,387.8,43.3z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

-41
View File
@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 427 97.5" style="enable-background:new 0 0 427 97.5;" xml:space="preserve">
<style type="text/css">
.st0{fillRule:evenodd;clipRule:evenodd;fill:#104D86;}
</style>
<path class="st0" d="M27.5,88.2c-4.9,0-9.7-1.2-14-3.6c-4.2-2.4-7.6-5.8-9.9-10c-4.8-8.8-4.8-19.4,0-28.2c2.3-4.2,5.8-7.7,10-10
c4.3-2.4,9.1-3.7,14-3.6c6-0.1,11.8,1.7,16.5,5.3s8,8.7,9.9,15.4H42.8c-1.3-3-3.4-5.5-6.2-7.2c-2.6-1.6-5.6-2.5-8.7-2.5
c-2.8,0-5.6,0.7-8.1,2s-4.7,3.3-6.1,5.8c-3,5.3-3.1,11.9-0.2,17.3c1.4,2.5,3.4,4.5,5.8,6s5.2,2.3,8,2.2c7.2,0,12.4-3.3,15.4-9.9
h11.4c-1.1,4.2-3.1,8.1-5.8,11.5c-2.5,3-5.6,5.4-9.2,7C35.5,87.4,31.5,88.1,27.5,88.2L27.5,88.2z M99.6,82.1
c-4.6,4-10.6,6.2-16.8,6.1c-4.9,0-9.7-1.2-14-3.6c-4.1-2.4-7.5-5.8-9.8-10c-2.5-4.5-3.7-9.5-3.6-14.6c-0.1-15,11.9-27.2,26.9-27.3
c0.1,0,0.2,0,0.3,0c6.4-0.2,12.6,2.1,17.5,6.2v-5.2h11.5V87h-12V82.1z M83.5,43.7c-3,0-5.9,0.7-8.4,2.2c-2.5,1.4-4.6,3.5-6,6
c-1.5,2.5-2.2,5.4-2.2,8.4c-0.2,4.5,1.5,8.9,4.7,12.2c3.2,3.2,7.6,4.9,12.1,4.8c2.9,0,5.8-0.7,8.3-2.2c2.5-1.4,4.5-3.5,5.9-6
c2.9-5.3,2.9-11.7,0-17c-1.4-2.5-3.5-4.6-6-6C89.3,44.5,86.4,43.8,83.5,43.7L83.5,43.7z M122,14.8h9.7V87H122
C122,87.1,122,14.8,122,14.8z M149.8,65.2c0.5,2.3,1.5,4.4,3,6.3c1.4,1.8,3.3,3.2,5.4,4.1c2.2,1,4.6,1.5,7,1.5
c2.8,0.1,5.6-0.5,8.1-1.8c2.4-1.4,4.3-3.4,5.5-5.9h11.9c-2.7,6.5-6.2,11.2-10.4,14.2c-4.4,3.1-9.8,4.7-15.2,4.5
c-4.8,0.1-9.6-1.2-13.8-3.6c-4.1-2.4-7.5-5.8-9.7-10c-2.4-4.3-3.6-9.2-3.5-14.1c-0.1-4.9,1.1-9.8,3.5-14.1c2.3-4.2,5.7-7.6,9.8-10
c8.6-4.8,19.2-4.8,27.7,0.1c4.1,2.4,7.4,6,9.6,10.2c2.2,4.3,3.3,9.1,3.3,14c0,1-0.1,2.5-0.3,4.5h-41.9V65.2z M165.2,43.6
c-3.4-0.1-6.8,0.9-9.6,2.9c-2.7,2-4.7,4.8-5.6,8h30.2c-0.9-3.2-2.9-6-5.6-8C171.9,44.5,168.6,43.5,165.2,43.6z M234.8,57.3
c0-4.7-1-8.2-3-10.5s-4.9-3.5-8.7-3.5c-2.3,0-4.6,0.7-6.5,2c-2,1.3-3.7,3.2-4.8,5.4c-1.2,2.3-1.8,4.8-1.8,7.4V87h-11.2V33.8h10.5
v4.8c3.9-3.9,9.3-6,14.9-5.8c3.9,0,7.7,1,11,3s6,4.8,7.9,8.2c1.9,3.5,2.9,7.5,2.9,11.5v31.6h-11.2L234.8,57.3L234.8,57.3z
M296.7,82.2c-2.4,1.9-5,3.4-7.9,4.4c-3,1-6.2,1.5-9.4,1.5c-4.8,0.1-9.6-1.2-13.8-3.7c-4.1-2.4-7.5-5.9-9.8-10.1
c-2.4-4.3-3.6-9.1-3.6-14.1c-0.1-5.1,1.1-10.1,3.5-14.7c2.3-4.2,5.7-7.7,9.8-10.1c4.3-2.5,9.2-3.8,14.1-3.7c3.1,0,6.1,0.5,9,1.4
c2.8,0.9,5.4,2.3,7.8,4.1V14.9h11.3v72.2h-11.1L296.7,82.2L296.7,82.2z M280.4,43.1c-3,0-5.9,0.7-8.4,2.2c-2.5,1.4-4.6,3.5-6,6
c-1.5,2.6-2.2,5.5-2.2,8.5c-0.2,4.6,1.5,9.1,4.7,12.4c3.2,3.2,7.5,5,12.1,4.8c2.9,0,5.8-0.7,8.3-2.3c2.5-1.5,4.5-3.6,5.9-6.1
c2.9-5.4,2.9-11.8,0-17.2C291.8,46.2,286.3,43,280.4,43.1z M335.8,88.2c-3.8,0.1-7.5-0.8-10.8-2.4c-3.1-1.6-5.7-4-7.5-7
c-1.9-3.3-2.9-7.1-3-10.9h11c0,2.7,1.1,5.3,3.1,7.1c2.1,1.7,4.8,2.6,7.5,2.5c1.8,0,3.6-0.2,5.3-0.8c1.3-0.4,2.4-1.2,3.3-2.2
c0.7-0.8,1.1-1.9,1.1-3c0.1-1.2-0.4-2.4-1.2-3.2c-0.5-0.5-1.1-0.8-1.7-1.2c-0.7-0.3-1.4-0.6-2.2-0.8c-1.8-0.4-3.9-0.9-6.1-1.3
c-1.8-0.2-3.5-0.6-5.1-1s-3.5-1-5-1.6c-0.8-0.2-1.6-0.6-2.4-1l-1-0.7c-0.4-0.4-0.8-0.7-1.2-1c-0.7-0.5-1.3-1.1-1.8-1.8
c-0.6-0.8-1.1-1.7-1.5-2.6c-0.9-2-1.3-4.2-1.2-6.4c0-3,0.9-6,2.7-8.5c1.8-2.5,4.3-4.5,7.2-5.7c3.3-1.4,6.8-2.1,10.3-2
c3.6-0.1,7.1,0.7,10.3,2.4c3,1.5,5.4,3.8,7.1,6.7c1.8,3.2,2.7,6.8,2.7,10.4H345c-0.1-3-1-5.2-2.7-6.7s-4-2.3-7-2.3s-5.2,0.5-6.8,1.5
c-1.4,0.8-2.3,2.2-2.3,3.8c-0.1,1,0.3,2,1,2.6c0.6,0.5,1.2,0.9,1.8,1.2c0.6,0.3,1.3,0.5,2,0.7c0.8,0.2,1.7,0.4,2.7,0.6l3.1,0.5
c3.4,0.5,6.7,1.2,10,2.2c2.6,0.8,5,2.3,6.7,4.5c1.8,2.4,2.9,5.3,3,8.4l0.1,1.8c0.1,3.2-0.9,6.4-2.8,9.1s-4.5,4.8-7.5,6
C343.2,87.6,339.5,88.3,335.8,88.2L335.8,88.2z M412.4,74.6c-2.4,4.2-5.9,7.7-10.2,10c-4.5,2.4-9.6,3.7-14.8,3.6
c-4.9,0.1-9.7-1.2-14-3.6c-4.2-2.4-7.7-5.8-10.1-9.9c-2.5-4.2-3.8-9-3.7-13.8c-0.1-5.1,1.1-10.1,3.6-14.6c2.4-4.2,6-7.7,10.2-10
c4.4-2.4,9.3-3.6,14.3-3.6s9.9,1.2,14.3,3.6c4.2,2.3,7.8,5.7,10.2,9.9c2.4,4.3,3.7,9.2,3.6,14.2C416.1,65.4,414.8,70.3,412.4,74.6
L412.4,74.6z M387.8,43.3c-3,0-6,0.8-8.6,2.3s-4.8,3.6-6.3,6.2c-1.5,2.6-2.3,5.5-2.3,8.5c0,4.6,1.8,9,5.2,12.2
c3.2,3.4,7.6,5.3,12.3,5.3c3,0,6-0.8,8.5-2.4c2.6-1.5,4.7-3.7,6.2-6.3c1.5-2.6,2.3-5.6,2.3-8.6s-0.8-6-2.3-8.6s-3.7-4.7-6.3-6.2
C393.9,44.1,390.9,43.3,387.8,43.3z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

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

+95 -60
View File
@@ -7,7 +7,7 @@
"upgrade_now": "تحديث الآن",
"accept_invitation": "اقبل الدعوة",
"calcom_explained": "يوفر {{appName}} بنية أساسية للجدولة للجميع.",
"calcom_explained_new_user": "إنهاء إعداد حسابك في {{appName}}! تبقت لك فقط بضع خطوات فقط لحل جميع مشاكل الجدولة لديك.",
"calcom_explained_new_user": "قم بإنهاء إعداد حسابك في {{appName}}! متبقي بضع خطوات فقط لحل جميع مشاكل الجدولة لديك.",
"have_any_questions": "هل لديك أسئلة؟ نحن هنا للمساعدة.",
"reset_password_subject": "{{appName}}: إرشادات إعادة تعيين كلمة المرور",
"event_declined_subject": "تم الرفض: {{title}} في {{date}}",
@@ -25,7 +25,7 @@
"rejection_confirmation": "رفض الحجز",
"manage_this_event": "قم بإدارة هذا الحدث",
"invite_team_member": "دعوة عضو في الفريق",
"invite_team_notifcation_badge": "استثمر.",
"invite_team_notifcation_badge": "دعوة",
"your_event_has_been_scheduled": "تم جدولة الحدث الخاص بك",
"your_event_has_been_scheduled_recurring": "تم جدولة الحدث المتكرر الخاص بك",
"accept_our_license": "اقبل ترخيصنا من خلال تغيير متغير ‎.env المسمى <1>NEXT_PUBLIC_LICENSE_CONSENT</1> إلى '{{agree}}'.",
@@ -90,6 +90,7 @@
"new_event_scheduled_recurring": "تم جدولة الحدث المتكرر الخاص بك.",
"invitee_email": "البريد الإلكتروني للمدعو",
"invitee_timezone": "المنطقة الزمنية للمدعو",
"time_left": "الوقت المتبقي",
"event_type": "نوع الحدث",
"enter_meeting": "ادخل الاجتماع",
"video_call_provider": "مزود مكالمة الفيديو",
@@ -110,6 +111,7 @@
"hidden_team_owner_message": "تحتاج إلى حساب Pro لاستخدام خدمة الفرق، وأنت حاليًا في الوضع الخفي حتى تقوم بالترقية.",
"link_expires": "ملاحظة: تنتهي الصلاحية في غضون {{expiresIn}} من الساعات.",
"upgrade_to_per_seat": "الترقية إلى \"كل مقعد بشكل منفرد\"",
"seat_options_doesnt_support_confirmation": "لا يدعم خيار المقاعد متطلبات التأكيد",
"team_upgrade_seats_details": "من بين {{memberCount}} من الأعضاء في فريقك، لم يتم الدفع مقابل {{unpaidCount}} من المقاعد. بما أن سعر المقعد ${{seatPrice}} شهريًا، تبلغ التكلفة الإجمالية المقدرة لعضويتك ${{totalCost}} شهريًا.",
"team_upgrade_banner_description": "شكراً لك على تجربة خطة الفريق الجديدة التي نقدمها. لقد لاحظنا أن فريقك \"{{teamName}}\" بحاجة إلى الترقية.",
"team_upgrade_banner_action": "قم بالترقية هنا",
@@ -413,7 +415,7 @@
"password_updated_successfully": "تم تحديث كلمة المرور بنجاح",
"password_has_been_changed": "تم تغيير كلمة المرور بنجاح.",
"error_changing_password": "خطأ في تغيير كلمة المرور",
"session_timeout_changed": "تم تحديث ملفك الشخصي بنجاح.",
"session_timeout_changed": "تم تحديث تكوين الجلسة بنجاح.",
"session_timeout_change_error": "خطأ في تحديث تكوين الجلسة",
"something_went_wrong": "حدث خطأ ما.",
"something_doesnt_look_right": "هل يوجد شيء ما لا يبدو صحيحًا؟",
@@ -431,6 +433,7 @@
"password_hint_min": "7 أحرف كحد أدنى",
"password_hint_admin_min": "15 محرفًا كحد أدنى",
"password_hint_num": "يحتوي على رقم واحد على الأقل",
"max_limit_allowed_hint": "يجب أن يكون بعدد محارف هو {{limit}} أو أقل",
"invalid_password_hint": "يجب أن تتألف كلمة المرور على الأقل من {{passwordLength}} أحرف، ورقم واحد على الأقل، ومزيج من الحروف الكبيرة والصغيرة",
"incorrect_password": "كلمة المرور غير صحيحة.",
"incorrect_username_password": "اسم المستخدم أو كلمة المرور غير صحيحة.",
@@ -678,7 +681,7 @@
"add_attendees": "إضافة الحضور",
"show_advanced_settings": "عرض الإعدادات المتقدمة",
"event_name": "اسم الحدث",
"event_name_in_calendar": "اسم الحدث في الرزنامة",
"event_name_in_calendar": "اسم الحدث في التقويم",
"event_name_tooltip": "الاسم الذي سيظهر في التقاويم",
"meeting_with_user": "الاجتماع مع {{attendeeName}}",
"additional_inputs": "مدخلات إضافية",
@@ -733,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}}؟ أي شخص شاركت رابط حسابك معه لن يستطيع بعد الآن الحجز باستخدامه، وستفقد أي تفضيلات حفظتها.",
@@ -860,6 +860,7 @@
"add_new_calendar": "إضافة تقويم جديد",
"set_calendar": "حدد أين سيتم إضافة الأحداث الجديدة عند الحجز.",
"delete_schedule": "حذف الجدول",
"delete_schedule_description": "سيؤدي حذف الجدول الزمني إلى إزالته من جميع أنواع الفعاليات، ولا يمكن التراجع عن هذا الإجراء.",
"schedule_created_successfully": "تم إنشاء جدول {{scheduleName}} بنجاح",
"availability_updated_successfully": "تم تحديث جدول {{scheduleName}} بنجاح",
"schedule_deleted_successfully": "تم حذف الجدول بنجاح",
@@ -947,7 +948,7 @@
"session_description": "التحكم في جلسة حسابك",
"session_timeout_after": "مهلة الجلسة بعد",
"session_timeout": "مهلة الجلسة",
"session_timeout_description": "أبطل جلسة عملك بعد مدة معينة من الوقت.",
"session_timeout_description": "إبطال جلستك بعد مدة معينة من الوقت.",
"no_location": "لم يتم تحديد موقع",
"set_location": "تعيين الموقع",
"update_location": "تحديث الموقع",
@@ -1155,6 +1156,7 @@
"event_setup_multiple_duration_error": "إعداد الحدث: تستلزم فترات متعددة خيارًا واحدًا على الأقل.",
"event_setup_multiple_duration_default_error": "إعداد الحدث: الرجاء تحديد مدة افتراضية صالحة.",
"event_setup_booking_limits_error": "يجب أن تكون حدود الحجز بترتيب تصاعدي. [يوم،أسبوع،شهر،سنة]",
"event_setup_duration_limits_error": "يجب أن تكون حدود الحجز بترتيب تصاعدي. [يوم، أسبوع، شهر، سنة]",
"select_which_cal": "اختر أي رزنامة تريد إضافة الحجز إليها",
"custom_event_name": "اسم حدث مخصص",
"custom_event_name_description": "إنشاء أسماء أحداث مخصصة لعرضها في حدث الرزنامة",
@@ -1251,7 +1253,7 @@
"to": "إلى",
"workflow_turned_on_successfully": "تم تشغيل {{offOn}} سير العمل {{workflowName}} بنجاح",
"download_responses": "تنزيل الردود",
"download_responses_description": "تحميل جميع الردود على نموذجك بتنسيق CSV.",
"download_responses_description": "تنزيل جميع الردود إلى النموذج بتنسيق CSV.",
"download": "التنزيل",
"create_your_first_form": "إنشاء أول استمارة",
"create_your_first_form_description": "باستخدام نماذج المسارات، يمكنك طرح أسئلة التأهيل والتوجيه إلى الشخص/نوع الحدث المناسب.",
@@ -1293,8 +1295,8 @@
"routing_forms_send_email_owner": "إرسال رسالة إلكترونية إلى المالك",
"routing_forms_send_email_owner_description": "يرسل رسالة بريد إلكتروني إلى المالك عند إرسال النموذج",
"add_new_form": "إضافة استمارة جديدة",
"create_your_first_route": "إنشاء مسارك الأول",
"route_to_the_right_person": "التوجه إلى الشخص المناسب بناء على الإجابات على النموذج لديك",
"create_your_first_route": "أنشئ مسارك الأول",
"route_to_the_right_person": "التوجيه إلى الشخص المناسب بناءً على إجابات النموذج الخاص بك",
"form_description": "قم بإنشاء النموذج الخاص بك لتوجيه صاحب حجز",
"copy_link_to_form": "نسخ الرابط إلى النموذج",
"theme": "السمة",
@@ -1338,6 +1340,8 @@
"report_app": "الإبلاغ عن تطبيق",
"limit_booking_frequency": "الحد من تواتر الحجز",
"limit_booking_frequency_description": "الحد من عدد مرات حجز هذا الحدث",
"limit_total_booking_duration": "الحد من مدة الحجز الإجمالية",
"limit_total_booking_duration_description": "الحد من الوقت المخصص الذي يمكن حجزه لهذه الفعالية",
"add_limit": "إضافة حد",
"team_name_required": "اسم الفريق مطلوب",
"show_attendees": "مشاركة معلومات الحضور مع الضيوف",
@@ -1441,6 +1445,7 @@
"calcom_is_better_with_team": "يعمل Cal.com أفضل مع الفرق",
"add_your_team_members": "أضف أعضاء فريقك إلى أنواع الأحداث الخاصة بك. استخدم الجدولة الجماعية لإدراج الجميع أو العثور على أنسب شخص باستخدام الجدولة الدوارة.",
"booking_limit_reached": "تم الوصول إلى الحد الأقصى للحجز لهذا النوع من الأحداث",
"duration_limit_reached": "تم الوصول إلى الحد الأقصى للحجز لهذا النوع من الفعاليات",
"admin_has_disabled": "قام مشرفًا بتعطيل {{appName}}",
"disabled_app_affects_event_type": "قام مشرفًا بتعطيل {{appName}} مما يؤثر على نوع الحدث {{eventType}} لديك",
"disable_payment_app": "قام المشرف بتعطيل {{appName}} الذي يؤثر على نوع الحدث {{title}} لديك. لا يزال بإمكان الحاضرين حجز هذا النوع من الأحداث، ولكن لن يُطالبوا بالدفع. يمكنك إخفاء نوع الحدث لمنع حدوث هذا إلى أن يقوم المشرف بتغيير طريقة الدفع لك.",
@@ -1453,7 +1458,7 @@
"enable_apps": "تمكين التطبيقات",
"enable_apps_description": "تمكين التطبيقات التي يمكن للمستخدمين دمجها مع Cal.com",
"purchase_license": "شراء ترخيص",
"already_have_key": "لدي بالفعل مفتاح:",
"already_have_key": "لدي مفتاح بالفعل:",
"already_have_key_suggestion": "يرجى نسخ متغير البيئة الحالي CALCOM_LICENSE_KEY هنا.",
"app_is_enabled": "تم تمكين {{appName}}",
"app_is_disabled": "تم تعطيل {{appName}}",
@@ -1475,7 +1480,7 @@
"individual": "فرد",
"all_bookings_filter_label": "جميع الحجوزات",
"all_users_filter_label": "جميع المستخدمين",
"your_bookings_filter_label": "الحجوزات لديك",
"your_bookings_filter_label": "الحجوزات",
"meeting_url_variable": "رابط الاجتماع",
"meeting_url_info": "رابط فعالية اجتماع المؤتمر",
"date_overrides": "تجاوز التاريخ",
@@ -1491,7 +1496,6 @@
"date_overrides_update_btn": "تحديث التجاوز",
"event_type_duplicate_copy_text": "{{slug}}-نسخ",
"set_as_default": "تعيين كافتراضي",
"hide_eventtype_details": "إخفاء تفاصيل EventType",
"show_navigation": "إظهار التنقل",
"hide_navigation": "إخفاء التنقل",
"verification_code_sent": "تم إرسال رمز التحقق",
@@ -1505,7 +1509,6 @@
"create_your_first_team_webhook_description": "قم بإنشاء أول شبكة ويب هوك لهذا النوع من فعاليات الفريق",
"create_webhook_team_event_type": "قم بإنشاء ويب هوك لهذا النوع من فعاليات الفريق",
"disable_success_page": "تعطيل صفحة النجاح (يعمل فقط إذا كان لديك رابط إعادة توجيه)",
"invalid_admin_password": "تم تعيينك كمشرف ولكن ليس لديك كلمة مرور تتكون من 15 حرفاً على الأقل",
"change_password_admin": "قم بتغيير كلمة المرور للحصول على وصول المشرف",
"username_already_taken": "اسم المستخدم هذا مستخدم مسبقًا",
"assignment": "تعيين",
@@ -1526,96 +1529,128 @@
"sender_name": "اسم المرسل",
"already_invited": "تمت دعوة هذا الحاضر بالفعل",
"no_recordings_found": "لم يتم العثور على تسجيلات",
"new_workflow_subtitle": "سير عمل جديد من أجل...",
"reporting": "تقديم التقارير",
"reporting_feature": "عرض كل ما هو وارد من بيانات وتحميلها بصيغة CSV",
"teams_plan_required": "خطة الفرق مطلوبة",
"routing_forms_are_a_great_way": "نماذج التوجيه هي طريقة رائعة لتوجيه الوارد إليك إلى الشخص الصحيح. قم بالترقية إلى خطة \"الفرق\" للوصول إلى هذه الميزة.",
"choose_a_license": "اختر ترخيص",
"choose_license_description": "يأتي Cal.com مع رخصة AGPLv3 متاحة ومجانية والتي يمكن ترقيتها إلى رخصة \"المؤسسة\" في أي وقت. يمكنك الترقية في أي وقت لاحق.",
"reporting_feature": "عرض جميع البيانات الواردة وتنزيلها كملف CSV",
"teams_plan_required": "خطة Teams مطلوبة",
"routing_forms_are_a_great_way": "تعد نماذج التوجيه طريقة رائعة لتوجيه العملاء الحتملين إلى الشخص المناسب. قم بالترقية إلى خطة Teams للوصول إلى هذه الميزة.",
"choose_a_license": "اختر ترخيصًا",
"choose_license_description": "يأتي Cal.com مع ترخيص AGPLv3 مجاني يمكن الوصول إليه ويمكن ترقيته إلى ترخيص Enterprise في أي وقت. يمكنك الترقية في أي وقت لاحق.",
"license": "الترخيص",
"agplv3_license": "ترخيص AGPLv3",
"ee_enterprise_license": "رخصة المؤسسة \"/ee\"",
"enterprise_booking_fee": "يبدأ في {{enterprise_booking_fee}}/ الشهر",
"enterprise_booking_fee": "بدءًا من {{enterprise_booking_fee}}/ الشهر",
"enterprise_license_includes": "كل شيء لأغراض الاستخدام التجاري",
"no_need_to_keep_your_code_open_source": "لا حاجة للحفاظ على تعليماتك البرمجية مفتوحة المصدر",
"repackage_rebrand_resell": "إعادة التعبئة وإعادة الترويج وإعادة البيع بسهولة",
"a_vast_suite_of_enterprise_features": "مجموعة كبيرة من سمات المشاريع",
"free_license_fee": "$0.00/شهر",
"repackage_rebrand_resell": "يمكنك إعادة التعبئة وإعادة الترويج وإعادة البيع بسهولة",
"a_vast_suite_of_enterprise_features": "مجموعة كبيرة من ميزات المؤسسة",
"free_license_fee": "0.00 دولار أمريكي/شهر",
"forever_open_and_free": "مفتوح و مجاني إلى الأبد",
"required_to_keep_your_code_open_source": "مطلوب للإبقاء على تعليماتك البرمجية مفتوحة المصدر",
"cannot_repackage_and_resell": "لا يمكن إعادة التعبئة وإعادة العلامة التجارية وإعادة البيع بسهولة",
"no_enterprise_features": "ميزات \"المؤسسة\" غير موجودة",
"required_to_keep_your_code_open_source": "مطلوب للحفاظ على التعليمات البرمجية مفتوحة المصدر",
"cannot_repackage_and_resell": "لا يمكن إعادة التعبئة والتغليف وإعادة العلامة التجارية وإعادة البيع بسهولة",
"no_enterprise_features": "لا توجد ميزات للمؤسسة",
"step_enterprise_license": "ترخيص المؤسسة",
"step_enterprise_license_description": "كل شيء موجود ضمن الرخصة التجارية مع الاستضافة الخاصة وإعادة التغليف وإعادة التوسيم وإعادة البيع والوصول إلى مكونات المؤسسة الحصرية.",
"step_enterprise_license_description": "كل شيء لحالة الاستخدام التجاري مع الاستضافة الخاصة، وإعادة التغليف، وإعادة تصميم العلامة التجارية، وإعادة البيع، والوصول هي مكونات المؤسسة الحصرية.",
"setup": "الإعداد",
"setup_description": "إعداد مثال Cal.com",
"configure": "تهيئة",
"sso_configuration": "تسجيل الدخول لمرة واحدة",
"sso_configuration_description": "إعداد SAML/OIDC SSO والسماح لأعضاء الفريق بتسجيل الدخول باستخدام \"موفر الهوية\"",
"sso_configuration_description": "قم بتهيئة SAML/OIDC SSO والسماح لأعضاء الفريق بتسجيل الدخول باستخدام موفر الهوية",
"sso_oidc_heading": "SSO مع OIDC",
"sso_oidc_description": "تكوين OIDC SSO مع موفر هوية من اختيارك.",
"sso_oidc_description": "قم بتكوين OIDC SSO مع موفر الهوية الذي تختاره.",
"sso_oidc_configuration_title": "تكوين OIDC",
"sso_oidc_configuration_description": "قم بتكوين اتصال OIDC مع موفر هويتك الشخصية. يمكنك العثور على المعلومات المطلوبة في موفر هويتك الشخصية.",
"sso_oidc_callback_copied": "تم نسخ رابط رد اتصال URL",
"sso_oidc_configuration_description": "قم بتكوين اتصال OIDC بموفر الهوية الخاص بك. يمكنك العثور على المعلومات المطلوبة في موفر هويتك.",
"sso_oidc_callback_copied": "تم نسخ رابط رد الاتصال",
"sso_saml_heading": "SSO مع SAML",
"sso_saml_description": "تكوين SAML SSO مع موفر هوية من اختيارك.",
"sso_saml_description": "قم بتكوين SAML SSO مع موفر الهوية الذي تختاره.",
"sso_saml_configuration_title": "تكوين SAML",
"sso_saml_configuration_description": "تكوين اتصال SAML مع مزود هويتك الشخصية. يمكنك العثور على المعلومات المطلوبة في مزود هويتك الشخصية.",
"sso_saml_configuration_description": "قم بتكوين اتصال SAML بموفر الهوية الخاص بك. يمكنك العثور على المعلومات المطلوبة في موفر هويتك.",
"sso_saml_acsurl_copied": "تم نسخ رابط ACS",
"sso_saml_entityid_copied": "تم نسخ معرف الكيان",
"sso_connection_created_successfully": "تم إنشاء تكوين {{connectionType}} بنجاح",
"sso_connection_deleted_successfully": "تم حذف تكوين {{connectionType}} بنجاح",
"delete_sso_configuration": "حذف اعدادات {{connectionType}}",
"delete_sso_configuration_confirmation": "نعم، احذف اعدادات {{connectionType}}",
"delete_sso_configuration_confirmation_description": "هل أنت متأكد من أنك تريد حذف تكوين {{connectionType}}؟ لن يكون لأعضاء فريقك الذين يستخدمون {{connectionType}} لتسجيل الدخول القدرة بعد الآن على الوصول إلى Cal.com.",
"delete_sso_configuration": "حذف تكوين {{connectionType}}",
"delete_sso_configuration_confirmation": "نعم، حذف تكوين {{connectionType}}",
"delete_sso_configuration_confirmation_description": "هل تريد بالتأكيد حذف تكوين {{connectionType}}؟ لن يتمكن أعضاء فريقك الذين يستخدمون معلومات تسجيل الدخول إلى {{connectionType}} من الوصول إلى Cal.com بعد الآن.",
"organizer_timezone": "منظم المناطق الزمنية",
"email_no_user_cta": "إنشاء حسابك",
"email_user_cta": "عرض الدعوة",
"email_no_user_invite_heading": "لقد تمت دعوتك للانضمام إلى فريق في {{appName}}",
"email_no_user_invite_subheading": "قام {{invitedBy}} بدعوتك للانضمام إلى فريقهم على {{appName}}. {{appName}} هو أداة لجدولة الأحداث تعطيك أنت وفريقك القدرة على جدولة اجتماعات من دون الحاجة إلى البريد الإلكتروني.",
"email_no_user_invite_steps_intro": "سنقطع معك بضع خطوات قصيرة وستستمتع بالجدولة المجانية مع فريقك في أي وقت.",
"email_no_user_invite_subheading": "دعاك {{invitedBy}} للانضمام إلى فريقه على {{appName}}. إن {{appName}} هو برنامج جدولة الأحداث الذي يمكّنك أنت وفريقك من جدولة الاجتماعات دون الحاجة إلى المراسلة عبر البريد الإلكتروني.",
"email_no_user_invite_steps_intro": "سنرشدك خلال بضع خطوات قصيرة وستستمتع بجدولة خالية من الإجهاد مع فريقك وسريعة.",
"email_no_user_step_one": "اختر اسم المستخدم الخاص بك",
"email_no_user_step_two": "ربط حساب التقويم الخاص بك",
"email_no_user_step_three": "تعيين توافرك",
"email_no_user_step_three": "تعيين أوقاتك المتاحة",
"email_no_user_step_four": "انضم إلى {{teamName}}",
"email_no_user_signoff": "جدولة سعيدة من فريق {{appName}}",
"impersonation_user_tip": "أنت على وشك انتحال شخصية مستخدم، مما يعني أنه يمكنك إجراء تغييرات نيابة عنهم. يرجى توخي الحذر.",
"email_no_user_signoff": "نتمنى لك جدولة سعيدة، من فريق {{appName}}",
"impersonation_user_tip": "أنت على وشك تمثيل شخصية مستخدم، مما يعني أنه يمكنك إجراء تغييرات نيابة عنه. يرجى توخي الحذر.",
"available_variables": "المتغيرات المتوفرة",
"scheduler": "{Scheduler}",
"no_workflows": "لا يوجد أي سير عمل",
"change_filter": "قم بتغيير عامل الفرز لمشاهدة سير عملك وفريقك.",
"recommended_next_steps": "الخطوات التالية الموصى بها",
"create_a_managed_event": "إنشاء نوع من الأحداث المُدارة",
"meetings_are_better_with_the_right": "الاجتماعات أفضل مع أعضاء الفريق الصحيحين. ادعوهم الآن.",
"collective_or_roundrobin": "جماعية أو ذهاب وإياب",
"book_your_team_members": "احجز أعضاء فريقك مع الأحداث الجماعية أو الدورة للحصول على الشخص المناسب مع خيار الذهاب والإياب.",
"meetings_are_better_with_the_right": "الاجتماعات أفضل مع وجود أعضاء الفريق المناسبين. قم بدعوتهم الآن.",
"create_a_one_one_template": "إنشاء قالب فردي واحد لنوع الفعالية وتوزيعه على عدة أعضاء.",
"collective_or_roundrobin": "جماعية أو دوارة",
"book_your_team_members": "احجز أعضاء فريقك والأحداث الجماعية أو تمكن من الوصول إلى الشخص المناسب باستخدام الجدولة الدوارة.",
"event_no_longer_attending_subject": "لن يتم حضور {{title}} في {{date}}",
"no_longer_attending": "لن تحضر هذا الحدث بعد الآن",
"attendee_no_longer_attending_subject": "أحد الحضور لن يحضر {{title}} في {{date}}",
"attendee_no_longer_attending": "أحد الحضور لن يحضر فعاليتك بعد الآن",
"attendee_no_longer_attending_subtitle": "تم إلغاء الفعالية {{name}}، وهو ما يعني توافر معقد شاغر في هذا التوقيت",
"create_event_on": "إنشاء أحداث في",
"default_app_link_title": "تعيين رابط تطبيق افتراضي",
"default_app_link_description": "إعداد رابط التطبيق الافتراضي يسمح لجميع أنواع الأحداث التي تم إنشاؤها حديثا باستخدام رابط التطبيق الذي قمت بتعيينه.",
"default_app_link_description": "يسمح تعيين رابط التطبيق الافتراضي لجميع أنواع الأحداث التي تم إنشاؤها حديثًا باستخدام رابط التطبيق الذي عينته.",
"change_default_conferencing_app": "تعيين كافتراضي",
"under_maintenance": "معطل للصيانة",
"under_maintenance_description": "فريق {{appName}} يقوم بأعمال الصيانة المجدولة. إذا كان لديك أي أسئلة، يرجى الاتصال بالدعم.",
"event_type_seats": "{{numberOfSeats}} مقعد",
"booking_with_payment_cancelled": "الدفع لهذا الحدث لم يعد ممكناً",
"booking_with_payment_cancelled_already_paid": "جاري العمل على استرداد دفعة الحجز هذه.",
"booking_with_payment_cancelled_refunded": "تم استرداد هذه الدفعة للحجز.",
"under_maintenance_description": "يجري فريق {{appName}} صيانة مجدولة. إذا كان لديك أي أسئلة، يرجى الاتصال بالدعم.",
"event_type_seats": "{{numberOfSeats}} من المقاعد",
"booking_questions_title": "أسئلة حول الحجز",
"booking_questions_description": "اعمل على تخصيص الأسئلة المطروحة في صفحة الحجز",
"add_a_booking_question": "اطرح سؤالاً",
"duplicate_email": "البريد الإلكتروني مكرر",
"booking_with_payment_cancelled": "لم يعد الدفع لهذا الحدث ممكنًا",
"booking_with_payment_cancelled_already_paid": "استرداد المبلغ المدفوع لهذا الحجز قيد التنفيذ.",
"booking_with_payment_cancelled_refunded": "تم استرداد مبلغ هذا الحجز.",
"booking_confirmation_failed": "فشل تأكيد الحجز",
"get_started_zapier_templates": "ابدأ مع قوالب Zapier",
"not_enough_seats": "لا توجد مقاعد كافية",
"form_builder_field_already_exists": "ثمة حقل بهذا الاسم موجود بالفعل",
"form_builder_field_add_subtitle": "اعمل على تخصيص الأسئلة المطروحة في صفحة الحجز",
"get_started_zapier_templates": "البدء في استخدام قوالب Zapier",
"team_is_unpublished": "لم يُنشر {{team}}",
"team_is_unpublished_description": "رابط هذا الفريق غير متوفر حالياً. يرجى الاتصال بمالك الفريق أو الطلب منهم نشره.",
"team_member": "عضو الفريق",
"a_routing_form": "نموذج توجيه",
"form_description_placeholder": "وصف النموذج",
"keep_me_connected_with_form": "ابقني متصلاً بالنموذج",
"fields_in_form_duplicated": "سيعكس التكرار أية تغييرات تطرأ على التدوير والحقول المكررة في النموذج.",
"fields_in_form_duplicated": "أي تغييرات في جهاز التوجيه والحقول الخاصة بالنموذج المكرر، ستنعكس في النسخة المكررة.",
"form_deleted": "تم حذف النموذج",
"delete_form": "حذف النموذج",
"delete_form_action": "نعم، احذف النموذج",
"delete_form_action": "نعم، حذف النموذج",
"delete_form_confirmation": "هل أنت متأكد من أنك تريد حذف هذا النموذج؟ لن يتمكن أي شخص تشارك الرابط معه من الحجز بعد الآن. كما ستحذف جميع الردود المرتبطة بذلك.",
"typeform_redirect_url_copied": "تم نسخ عنوان URL في Typeform Redirect! يمكنك الذهاب وتعيين عنوان URL في نموذج Typeform.",
"modifications_in_fields_warning": "سيعكس هذا الشكل التعديلات التي ستطرأ على الحقول والطرق التي تسلكها النماذج اللاحقة.",
"delete_form_confirmation_2": "سيتم حذف جميع الردود ذات الصلة.",
"typeform_redirect_url_copied": "تم نسخ رابط Typeform Redirect! يمكنك الانتقال وتعيين الرابط في نموذج Typeform.",
"modifications_in_fields_warning": "ستنعكس التعديلات في الحقول والمسارات للنماذج التالية في هذا النموذج.",
"connected_forms": "النماذج المتصلة",
"form_modifications_warning": "ستتأثر النماذج التالية عند تعديل الحقول أو المسارات هنا.",
"responses_collection_waiting_description": "انتظر لبعض الوقت لجمع الردود. يمكنك الذهاب وتقديم النموذج بنفسك أيضاً.",
"responses_collection_waiting_description": "انتظر بعض الوقت حتى يتم جمع الردود. يمكنك إرسال النموذج بنفسك أيضًا.",
"this_is_what_your_users_would_see": "هذا ما سيراه المستخدمون لديك",
"identifies_name_field": "تحديد الحقل بهذا الاسم.",
"identifies_name_field": "يتعرف على الحقل بهذا الاسم.",
"add_1_option_per_line": "إضافة خيار واحد لكل سطر",
"select_a_router": "حدد مساراً",
"add_a_new_route": "إضافة مسار جديد",
"no_responses_yet": "لا توجد ردود بعد",
"this_will_be_the_placeholder": "هذا سيكون العنصر النائب"
"this_will_be_the_placeholder": "سيكون هذا هو العنصر النائب",
"this_meeting_has_not_started_yet": "لم يبدأ هذا الاجتماع بعد",
"this_app_requires_connected_account": "{{appName}} يتطلب حساب {{dependencyName}} متصل",
"connect_app": "الاتصال {{dependencyName}}",
"app_is_connected": "{{dependencyName}} متصل",
"requires_app": "يتطلب {{dependencyName}}",
"verification_code": "رمز التحقق",
"can_you_try_again": "هلّا جربت المحاولة ثانيةً في وقت مختلف؟",
"verify": "التحقق",
"select_all": "اختيار الكل",
"default_conferencing_bulk_title": "التحديث الجماعي لمجموعة أنواع الفعاليات الحالية",
"invalid_event_name_variables": "ثمة متغيّر غير غير صالح في اسم فعاليتك",
"default_conferencing_bulk_description": "تحديث المواقع لأنواع الفعاليات المحددة"
}
+46 -9
View File
@@ -90,6 +90,7 @@
"new_event_scheduled_recurring": "Byla naplánována nová opakovaná událost.",
"invitee_email": "E-mail pozvaného",
"invitee_timezone": "Časová zóna pozvaného",
"time_left": "Zbývající čas",
"event_type": "Typ události",
"enter_meeting": "Připojit se ke schůzce",
"video_call_provider": "Řešení pro video cally",
@@ -110,6 +111,7 @@
"hidden_team_owner_message": "Pro využívání týmů potřebujete tarif Pro, budete skrytí, než přejdete na vyšší verzi.",
"link_expires": "P.S. Platnost vyprší za {{expiresIn}} hodin.",
"upgrade_to_per_seat": "Přejít na placení za místo",
"seat_options_doesnt_support_confirmation": "Možnost Místa nevyžaduje potvrzení",
"team_upgrade_seats_details": "Z {{memberCount}} členů ve vašem týmů je {{unpaidCount}} míst neplacených. Při ${{seatPrice}} měsíčně za místo je odhad celkové částky za členství ${{totalCost}} měsíčně.",
"team_upgrade_banner_description": "Děkujeme vám za vyzkoušení našeho nového týmového plánu. Všimli jsme si, že váš tým „{{teamName}}“ je nutné upgradovat.",
"team_upgrade_banner_action": "Upgradovat zde",
@@ -431,6 +433,7 @@
"password_hint_min": "Minimální délka 7 znaků",
"password_hint_admin_min": "Minimální délka 15 znaků",
"password_hint_num": "Obsahuje alespoň 1 číslo",
"max_limit_allowed_hint": "Maximální povolená délka je {{limit}} znaků",
"invalid_password_hint": "Heslo musí být minimálně {{passwordLength}} znaků dlouhé, obsahovat alespoň jedno číslo a obsahovat kombinaci velkých a malých písmen",
"incorrect_password": "Heslo není správné.",
"incorrect_username_password": "Uživatelské jméno nebo heslo je nesprávné.",
@@ -733,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.",
@@ -860,6 +860,7 @@
"add_new_calendar": "Přidat nový kalendář",
"set_calendar": "Nastavte, kam se mají při rezervaci přidávat nové události.",
"delete_schedule": "Odstranit plán",
"delete_schedule_description": "Odstranění plánu má za následek, že bude odstraněn ze všech typů událostí. Tuto akci nelze vrátit zpět.",
"schedule_created_successfully": "Plán {{scheduleName}} byl úspěšně vytvořen",
"availability_updated_successfully": "Plán {{scheduleName}} byl úspěšně aktualizován",
"schedule_deleted_successfully": "Plán byl úspěšně odstraněn",
@@ -1155,6 +1156,7 @@
"event_setup_multiple_duration_error": "Nastavení události: více dob trvání vyžaduje alespoň 1 možnost.",
"event_setup_multiple_duration_default_error": "Nastavení události: vyberte prosím platnou výchozí dobu trvání.",
"event_setup_booking_limits_error": "Limity rezervací musí být seřazeny vzestupně. [den,týden,měsíc,rok]",
"event_setup_duration_limits_error": "Limity doby trvání musí být seřazeny vzestupně. [den,týden,měsíc,rok]",
"select_which_cal": "Vyberte kalendář, do kterého se mají přidávat rezervace",
"custom_event_name": "Vlastní název události",
"custom_event_name_description": "Vytvářejte vlastní názvy událostí, které se budou zobrazovat v kalendáři událostí",
@@ -1338,6 +1340,8 @@
"report_app": "Nahlásit aplikaci",
"limit_booking_frequency": "Omezit četnost rezervací",
"limit_booking_frequency_description": "Omezit, kolikrát lze tuto událost rezervovat",
"limit_total_booking_duration": "Omezení celkové doby trvání rezervace",
"limit_total_booking_duration_description": "Omezení celkové doby, po kterou je možné tuto událost rezervovat",
"add_limit": "Přidat limit",
"team_name_required": "Je vyžadován název týmu",
"show_attendees": "Sdílet informace o účastnících",
@@ -1441,6 +1445,7 @@
"calcom_is_better_with_team": "Cal.com je s týmy lepší",
"add_your_team_members": "Přidejte členy týmu k typům událostí. Použijte kolektivní plánování, abyste zahrnuli všechny, případně vyhledejte tu nejvhodnější osobu s plánováním „round robin“.",
"booking_limit_reached": "Byl dosažen limit rezervací pro tento typ události",
"duration_limit_reached": "Byl dosažen limit doby trvání pro tento typ události",
"admin_has_disabled": "Administrátor zakázal {{appName}}",
"disabled_app_affects_event_type": "Administrátor zakázal {{appName}}, což ovlivňuje váš typ události {{eventType}}",
"disable_payment_app": "Administrátor zakázal {{appName}}, což ovlivňuje váš typ události {{title}}. Účastníci si stále mohou rezervovat tento typ události, ale nebudou vyzváni k platbě. Abyste tomu zabránili, můžete tento typ události skrýt, dokud správce tento způsob platby znovu nepovolí.",
@@ -1491,7 +1496,6 @@
"date_overrides_update_btn": "Aktualizovat změnu",
"event_type_duplicate_copy_text": "{{slug}} – kopie",
"set_as_default": "Nastavit jako výchozí",
"hide_eventtype_details": "Skrýt podrobnosti o typu události",
"show_navigation": "Zobrazit navigaci",
"hide_navigation": "Skrýt navigaci",
"verification_code_sent": "Ověřovací kód odeslán",
@@ -1505,7 +1509,6 @@
"create_your_first_team_webhook_description": "Vytvořte svůj první webhook pro tento typ týmové události",
"create_webhook_team_event_type": "Vytvořte webhook pro tento typ týmové události",
"disable_success_page": "Zakázat stránku s informací o úspěšném provedení (funguje pouze v případě, že máte přesměrovanou adresu URL)",
"invalid_admin_password": "Jste nastaveni jako administrátor, ale vaše heslo nemá délku alespoň 15 znaků.",
"change_password_admin": "Změňte heslo, abyste získali administrátorský přístup",
"username_already_taken": "Uživatelské jméno je již obsazeno",
"assignment": "Přiřazení",
@@ -1526,6 +1529,7 @@
"sender_name": "Jméno odesílatele",
"already_invited": "Účastník je již pozvaný",
"no_recordings_found": "Nenalezeny žádné nahrávky",
"new_workflow_subtitle": "Nový pracovní postup pro...",
"reporting": "Hlášení",
"reporting_feature": "Zobrazení všech příchozích dat a jejich stažení ve formátu CSV",
"teams_plan_required": "Potřebujete tarif Teams",
@@ -1537,7 +1541,6 @@
"ee_enterprise_license": "„/ee“ Licence Enterprise",
"enterprise_booking_fee": "Od {{{enterprise_booking_fee}}/měsíc",
"enterprise_license_includes": "Vše pro komerční použití",
"no_need_to_keep_your_code_open_source": "Kód nemusíte nechávat jako open-source",
"repackage_rebrand_resell": "Snadné přebalení, rebranding a přeprodej",
"a_vast_suite_of_enterprise_features": "Rozsáhlá sada firemních funkcí",
"free_license_fee": "0,00 $/měsíc",
@@ -1582,38 +1585,72 @@
"impersonation_user_tip": "Chystáte se zosobnit uživatele, což znamená, že můžete provádět změny jeho jménem. Počínejte si obezřetně, prosím.",
"available_variables": "Dostupné proměnné",
"scheduler": "{Scheduler}",
"no_workflows": "Žádné pracovní postupy",
"change_filter": "Změňte filtr, abyste viděli osobní i týmové pracovní postupy.",
"recommended_next_steps": "Doporučené další kroky",
"create_a_managed_event": "Vytvořte typ spravované události",
"meetings_are_better_with_the_right": "Schůzky jsou lepší, když jsou na nich přítomni ti správní členové týmu. Pozvěte je.",
"create_a_one_one_template": "Vytvořte pro daný typ události šablonu jeden na jednoho a rozešlete ji více členům.",
"collective_or_roundrobin": "Kolektivní nebo round-robin",
"book_your_team_members": "Rezervujte si členy týmu všechny spolu pomocí kolektivních událostí nebo postupně pomocí round-robinu.",
"event_no_longer_attending_subject": "Již nejste účastníky události {{title}} v den {{date}}",
"no_longer_attending": "Této události se již neúčastníte",
"attendee_no_longer_attending_subject": "Účastník se již neúčastní události {{title}} v den {{date}}",
"attendee_no_longer_attending": "Účastník se již neúčastní vaší události",
"attendee_no_longer_attending_subtitle": "Uživatel {{name}} zrušil svou účast. V tomto časovém intervalu se tedy uvolnilo místo",
"create_event_on": "Vytvořit událost v:",
"default_app_link_title": "Nastavte výchozí odkaz na aplikaci",
"default_app_link_description": "Nastavení výchozího odkazu na aplikaci umožní, aby všechny nově vytvořené typy událostí používaly vámi nastavený odkaz na aplikaci.",
"change_default_conferencing_app": "Nastavit jako výchozí",
"under_maintenance": "Odstávka z důvodu údržby",
"under_maintenance_description": "Tým {{appName}} provádí plánovanou údržbu. V případě dotazů se obraťte na podporu.",
"event_type_seats": "Počet míst: {{numberOfSeats}}",
"booking_questions_title": "Otázky k rezervaci",
"booking_questions_description": "Přizpůsobte otázky položené na stránce rezervace",
"add_a_booking_question": "Přidat otázku",
"duplicate_email": "Duplicitní e-mail",
"booking_with_payment_cancelled": "Platba za tuto událost již není možná",
"booking_with_payment_cancelled_already_paid": "Vrácení peněz za tuto rezervační platbu je na cestě.",
"booking_with_payment_cancelled_refunded": "Tato rezervační platba byla vrácena.",
"booking_confirmation_failed": "Potvrzení rezervace se nezdařilo",
"not_enough_seats": "Nedostatek míst",
"form_builder_field_already_exists": "Pole s tímto názvem již existuje",
"form_builder_field_add_subtitle": "Přizpůsobte otázky položené na stránce rezervace",
"get_started_zapier_templates": "Začněte používat šablony Zapier",
"team_is_unpublished": "Tým {{team}} není zveřejněn",
"team_is_unpublished_description": "Tento týmový odkaz není v současné době k dispozici. Kontaktujte prosím vlastníka týmu nebo ho požádejte o jeho zveřejnění.",
"team_member": "Člen týmu",
"a_routing_form": "Směrovací formulář",
"form_description_placeholder": "Popis formuláře",
"keep_me_connected_with_form": "Udržujte se mnou spojení pomocí formuláře",
"fields_in_form_duplicated": "Veškeré změny ve směrování a polích duplikovaného formuláře s promítnou duplicitě.",
"fields_in_form_duplicated": "Veškeré změny ve směrování a polích duplikovaného formuláře se promítnou v duplicitě.",
"form_deleted": "Formulář odstraněn",
"delete_form": "Opravdu chcete tento formulář odstranit?",
"delete_form_action": "Ano, odstranit formulář",
"delete_form_confirmation": "Každý, komu jste odkaz nasdíleli, k němu ztratí přístup.",
"delete_form_confirmation_2": "Všechny související odpovědi budou odstraněny.",
"typeform_redirect_url_copied": "Adresa URL přesměrování Typeform zkopírována! Můžete jít nastavit adresu URL ve formuláři Typeform.",
"modifications_in_fields_warning": "Úpravy v polích a směrování následujících formulářů se projeví v tomto formuláři.",
"connected_forms": "Připojené formuláře",
"form_modifications_warning": "Úprava polí a tras na tomto místě by se dotkla následujících formulářů.",
"responses_collection_waiting_description": "Počkejte chvíli na shromáždění odpovědí. Formulář můžete odeslat také sami.",
"responses_collection_waiting_description": "Počkejte chvíli na shromáždění odpovědí. Formulář ale můžete odeslat i sami.",
"this_is_what_your_users_would_see": "Toto uvidí vaši uživatelé",
"identifies_name_field": "Identifikuje pole podle tohoto názvu.",
"add_1_option_per_line": "Přidejte 1 možnost na řádek",
"select_a_router": "Vyberte směrování",
"add_a_new_route": "Přidejte nové směrování",
"no_responses_yet": "Zatím žádné odpovědi",
"this_will_be_the_placeholder": "Toto bude zástupný znak"
"this_will_be_the_placeholder": "Toto bude zástupný znak",
"this_meeting_has_not_started_yet": "Tato schůzka ještě nezačala",
"this_app_requires_connected_account": "{{appName}} vyžaduje připojený účet {{dependencyName}}",
"connect_app": "Připojit aplikaci {{dependencyName}}",
"app_is_connected": "Aplikace {{dependencyName}} je připojena",
"requires_app": "Vyžaduje {{dependencyName}}",
"verification_code": "Ověřovací kód",
"can_you_try_again": "Můžete to zkusit znovu a použít jiný čas?",
"verify": "Ověřit",
"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.",
@@ -1491,7 +1488,6 @@
"date_overrides_update_btn": "Opdatér Tilsidesæt",
"event_type_duplicate_copy_text": "{{slug}}-kopi",
"set_as_default": "Angiv som standard",
"hide_eventtype_details": "Skjul EventType Detaljer",
"show_navigation": "Vis navigation",
"hide_navigation": "Skjul navigation",
"verification_code_sent": "Bekræftelseskode sendt",
@@ -1505,7 +1501,6 @@
"create_your_first_team_webhook_description": "Opret din første webhook for denne teambegivenhedstype",
"create_webhook_team_event_type": "Opret en webhook for denne teambegivenhedstype",
"disable_success_page": "Deaktivér Successide (virker kun, hvis du har en omdirigerings URL)",
"invalid_admin_password": "Du er indstillet som administrator, men du har ikke en adgangskodelængde på mindst 15 tegn",
"change_password_admin": "Skift adgangskode for at få admin adgang",
"username_already_taken": "Brugernavn er allerede i brug",
"assignment": "Tildeling",
@@ -1538,7 +1533,6 @@
"ee_enterprise_license": "“/ee” Virksomhedslicens",
"enterprise_booking_fee": "Fra {{enterprise_booking_fee}}/måned",
"enterprise_license_includes": "Alt til en kommerciel brugssag",
"no_need_to_keep_your_code_open_source": "Ingen grund til at holde din kode open source",
"repackage_rebrand_resell": "Ompak, rebrand og videresælg nemt",
"a_vast_suite_of_enterprise_features": "En bred vifte af virksomhedsfunktioner",
"free_license_fee": "0,00kr./måned",

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