Compare commits

...
36 Commits
Author SHA1 Message Date
Alex van Andel 72629ffee0 v2.7.7
release.yml / release (push) Canceled after 0s
2023-04-03 19:47:10 +02:00
Nafees Nazik 5b6f007d8f fix: flickering of tips card rendering while changing routes (#8065)
* feat: optimize tips component

* fix: use dynamic rendering

* fix: remove unnecessary memo

* fix: prefetch warning

* fix: use reverse list
2023-04-03 19:25:45 +02:00
Joe Au-YeungandAlex van Andel 2e0951d4dc Set GCal & Outlook iCalUID in .ics file (#8010)
* Add calendar UID to calendar event

* Add iCalUID to booking event

* On reschedule write to evt the iCalUID

* Add uid to ics file

* Remove console logs

* Pass iCalUID if available

* Remove generated app store files

* Rename product id to calcom

* Add UID to ics on reschedule

* Type fixes

* Type fixes

* Type fixes

* Remove comment

* Remove console.log

* Removed serverConfig block from this branch

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-03 17:13:57 +00:00
Richard Poelderl fd84b5754d chore: update robots.txt to allow api/app-store (#8023) 2023-04-03 16:43:21 +02:00
Udit TakkarandAlex van Andel 73afb03308 fix: remove validateSlug Query (#8015)
* fix: remove validateSlug Query

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

* chore

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: Alex van Andel <me@alexvanandel.com>
2023-04-03 16:37:58 +02:00
Jeroen ReumkensandAlex van Andel 99dba73d0b Updated Vercel OG to latest + added fallback img (#8057)
* Updated Vercel OG to latest so we can add a fallback image — and added this fallback image to the screenshot og image.

* Updated vercel/og in web package as well.

* Updated yarn.lock with auth dependencies

* Upgraded next to latest to fix next image throwing weird bad response errors.

* Revert "Upgraded next to latest to fix next image throwing weird bad response errors."

This reverts commit 4e9d97dbe5ebe682b905ce41f88197915e19647d.

* Added content type headers to og image api response.

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-03 16:06:02 +02:00
Carina WollendorferandCarinaWolli db4449d849 fix double new button (#8058)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-03 14:34:53 +02:00
Nafees Nazik 53e52a0510 fix: flash of grey in event types single page (#8056)
* feat: prefetch query

* fix: hydration error

* fix: add a skeleton loader
2023-04-03 14:33:36 +02:00
Peer Richelsen cf4bd8a77d yarn format 2023-04-03 14:08:17 +02:00
Nafees Nazik 9d61dc2f7c fix: flash of grey when navigating to workflow page (#8038)
* fix: flash of grey

* fix: remove rendering null
2023-04-03 11:50:39 +02:00
Carina WollendorferandCarinaWolli caf5918b72 Delete canceled workflow emails from scheduled_sends (SendGrid endpoint) (#8026)
* show already verified numbers as verified

* delete old batch_ids from scheduled sends

* only delete cancelled ones

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-03 11:25:20 +02:00
Hariom Balhara 84f64f5d06 Show correct label in email and make sure that rechedule reason is shown on booking success page (#8053) 2023-04-03 11:18:12 +02:00
GitStart-Cal.comandgitstart-calcom 13a53c020b [CAL-1094] Embed modal / Inline - UI/Layout/Spacing issues (#7350)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-04-03 10:50:44 +05:30
GermaVinsmoke a72f90fe02 fix: misaligned booking frequency toggle (#8006) 2023-04-02 18:35:10 +02:00
Sai Deepesh 283d5b4a65 refactor: replace TextWidget component with EmailField (#8047) 2023-04-02 18:34:11 +02:00
Nafees Nazik 81b2e37efd fix: add calendar button layout shift (#7930) 2023-04-02 10:34:48 +00:00
Nafees NazikandPeer Richelsen 469967b320 fix: disable 2FA switch for non CAL auth identity providers (#8021)
* fix: disable switch on non cal providers

* feat: extract to an i18n key

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

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-02 10:31:40 +00:00
d708625da0 Update packages/features/ee/teams/components/MemberListItem.tsx (#8030)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-02 10:26:22 +00:00
Udit Takkar acf2487eaa fix: use ssrinit in /apps (#7807)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-04-02 12:23:30 +02:00
Peer Richelsen b81ca0e2ab renamed banner folder to prevent adblock from blocking (#8020) 2023-04-02 12:22:31 +02:00
GitStart-Cal.comandgitstart-calcom 2357f76cc8 CALCOM-8011 - [CAL-1372] /workflows: Filters avatars not showing (#8036)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-04-02 10:21:58 +00:00
Nafees Nazik 304e8aaa68 fix: modal padding on mobile (#8002)
* fix: width full

* fix: max width
2023-04-02 12:10:30 +02:00
Nafees Nazik 9fe912c392 feat: soft delete rescheduled booking (#7970)
* feat: add rescheduled status

* feat: add new reservation status

* fix: add rescheduled to trpc procedure

* feat: use soft delete

* feat: add new i18n key

* feat: add data display for rescheduled booking

* chore: fix comment

* Revert "feat: add new i18n key"

This reverts commit 4cecc00bd3351af6fbdff05d728c438f68fbfc4e.

* Revert "feat: add data display for rescheduled booking"

This reverts commit 81b4f7e1ab24763f630428b6c338926cf1958be0.

* Revert "fix: add rescheduled to trpc procedure"

This reverts commit 82b26582d2188e2b18f2f733760c741e7127b1d2.

* Revert "feat: add new reservation status"

This reverts commit b1dafab774d566dfcbe1c979035a89c729150d59.

* Revert "feat: add rescheduled status"

This reverts commit c8f24aec0e054b6be7186a631eee2b8a36ced40f.

* fix: use cancelled

* fix: e2e

* fix: e2e

* fix: e2e
2023-04-02 12:09:57 +02:00
Nafees Nazik ab7ecff580 fix: use dropdown item (#8024) 2023-04-02 12:03:40 +02:00
GitStart-Cal.comandgitstart-calcom 19e830acad CALCOM-7983 - [CAL-1359] /teams: Members should not see "Next steps" since they can't take any (#8029)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-04-02 12:03:28 +02:00
Nafees Nazik 458300bc83 fix: use the correct step (#8037) 2023-04-01 22:14:08 +02:00
Gurkiran Singh 352a467104 feat: Throwing a toast when adding a new team member (#8046)
Signed-off-by: GurkiranSingh <gurkiransinghk@gmail.com>
2023-04-01 22:12:48 +02:00
c052c66abc chore: removed duplicate destination calendar selector components (#7880)
* Deleted DestinationCalendarSelector

* Renamed to DestinationCalendarSelectorComp

* Delete DestinationCalendarSelectorComp instead of rename

* Add DestinationCalendarSelect to CreateEventsOnCalendar

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-03-31 21:21:00 +00:00
Joe Au-YeungandOmar López 8f8ed4d80d Show proper default destination calendar (#7644)
* Make destination calendar immutable

* Add comment

---------

Co-authored-by: Omar López <zomars@me.com>
2023-03-31 23:12:28 +02:00
Udit TakkarandPeer Richelsen 04fb7e07b4 fix: change ui of booking fields in booking page (#7776)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-03-31 17:15:09 +00:00
Nafees Nazik 20d2129f81 hotfix: locale not changeable (#7981)
* fix: export type

* feat: extract into a utility

* feat: use the new utility

* fix: comment out authedRateLimitedProcedure

* fix: type error

* fix: remove spread
2023-03-31 11:38:46 -03:00
Joe Au-Yeung ae5425f908 Fix showing seat attendees in emails (#8033)
* Pass seats info to location change

* Fix cloning emails

* Add cloning of event in AttendeeScheduledEmail class
2023-03-30 19:45:48 -04:00
Peer Richelsen 762810684e Update common.json 2023-03-30 21:20:37 +02:00
Omar López 4720dc410f Fixes embeds tsconfig (#7995)
* Update tsconfig.json

* Fixes?

* Fixes
2023-03-30 16:20:39 +00:00
Joe Au-Yeung 715aea5733 Refactor Prisma call creating attendees and seat reference (#8014)
* Refactor prisma calls

* Reverted change to pass test
2023-03-29 23:13:35 +02:00
GitStart-Cal.comandgitstart-calcom 6cbb9bf967 CALCOM-7857 - [CAL-1303] UI issues webhook list (#7968)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-03-29 07:44:45 +02:00
71 changed files with 727 additions and 830 deletions

No files matched your search

@@ -1,86 +0,0 @@
import React from "react";
import Select from "react-select";
import type { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
interface AdditionalCalendarSelectorProps {
isLoading?: boolean;
}
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { data } = optionProps;
return (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="w-full" color="minimal">
{/* eslint-disable @next/next/no-img-element */}
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p>{data.label}</p>
</Button>
);
}}
/>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
return (
<QueryCell
query={query}
success={({ data }) => {
const options = data.items.map((item) => ({
label: item.name,
slug: item.slug,
image: item.logo,
type: item.type,
}));
return (
<Select
name="additionalCalendar"
placeholder={t("connect_additional_calendar")}
options={options}
styles={{
placeholder: (defaultStyles) => {
return {
...defaultStyles,
color: "#3E3E3E",
marginLeft: "3px",
};
},
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "2px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
maxWidth: "320px",
},
};
},
}}
isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
isLoading={isLoading}
components={{ Option: ImageOption }}
/>
);
}}
/>
);
};
export default AdditionalCalendarSelector;
@@ -1,137 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: selected.name || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: cal.name || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
+70 -83
View File
@@ -1,4 +1,4 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import classNames from "classnames"; import classNames from "classnames";
import type { NextRouter } from "next/router"; import type { NextRouter } from "next/router";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -22,7 +22,7 @@ import {
TextField, TextField,
ColorPicker, ColorPicker,
} from "@calcom/ui"; } from "@calcom/ui";
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon"; import { FiCode, FiTrello, FiSun, FiArrowLeft } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
@@ -313,7 +313,7 @@ const embeds: {
}[] = [ }[] = [
{ {
title: "Inline Embed", title: "Inline Embed",
subtitle: "Loads your Cal scheduling page directly inline with your other website content", subtitle: "Loads your event type directly inline with your other website content.",
type: "inline", type: "inline",
illustration: ( illustration: (
<svg <svg
@@ -798,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
} }
const ThemeOptions = [ const ThemeOptions = [
{ value: Theme.auto, label: "Auto Theme" }, { value: Theme.auto, label: "Auto" },
{ value: Theme.dark, label: "Dark Theme" }, { value: Theme.dark, label: "Dark Theme" },
{ value: Theme.light, label: "Light Theme" }, { value: Theme.light, label: "Light Theme" },
]; ];
@@ -815,11 +815,18 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
]; ];
return ( return (
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation"> <DialogContent
ref={dialogContentRef}
className="rounded-lg p-0.5 sm:max-w-[80rem]"
enableOverflow
type="creation">
<div className="flex"> <div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8"> <div className="flex w-1/3 flex-col bg-gray-50 p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title"> <h3
className="mb-2.5 flex items-center text-xl font-semibold leading-5 text-gray-900"
id="modal-title">
<button <button
className="h-6 w-6"
onClick={() => { onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]); removeQueryParams(router, ["embedType", "embedTabName"]);
}}> }}>
@@ -827,55 +834,43 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</button> </button>
{embed.title} {embed.title}
</h3> </h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} /> <h4 className="mb-6 text-sm font-normal text-gray-500">{embed.subtitle}</h4>
<div className="flex flex-col overflow-y-auto"> <div className="flex flex-col">
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}> <div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible <Collapsible
open={isEmbedCustomizationOpen} open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}> onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger
type="button"
className="flex w-full items-center text-base font-medium text-gray-900">
<div>
{embedType === "inline"
? "Inline Embed Customization"
: embedType === "floating-popup"
? "Floating Popup Customization"
: "Element Click Customization"}
</div>
<FiChevronRight
className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent className="text-sm"> <CollapsibleContent className="text-sm">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}> <div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */} {/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div> <div className="mb-[9px] text-sm">Window sizing</div>
<div className="justify-left flex items-center"> <div className="justify-left flex items-center !font-normal">
<TextField <div className="mr-[9px]">
labelProps={{ className: "hidden" }} <TextField
required labelProps={{ className: "hidden" }}
value={previewState.inline.width} className="focus:ring-offset-0"
onChange={(e) => { required
setPreviewState((previewState) => { value={previewState.inline.width}
const width = e.target.value || "100%"; onChange={(e) => {
setPreviewState((previewState) => {
const width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
</div>
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<>W</>}
/>
<span className="p-2">×</span>
<TextField <TextField
labelProps={{ className: "hidden" }} labelProps={{ className: "hidden" }}
className="focus:ring-offset-0"
value={previewState.inline.height} value={previewState.inline.height}
required required
onChange={(e) => { onChange={(e) => {
@@ -1007,22 +1002,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>
<hr className="mt-4" /> <div className="font-medium">
<div className="mt-4 font-medium">
<Collapsible <Collapsible
open={isBookingCustomizationOpen} open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}> onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
<FiChevronRight
className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-gray-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="mt-6 text-sm"> <div className="mt-6 text-sm">
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse"> <div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch <Switch
checked={previewState.hideEventTypeDetails} checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
@@ -1036,13 +1022,38 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/> />
<div className="text-sm">{t("hide_eventtype_details")}</div> <div className="text-sm">{t("hide_eventtype_details")}</div>
</div> </div>
<Label className=""> {[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="mb-6">
<div className="mb-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
<Label>
<div className="mb-2">Theme</div> <div className="mb-2">Theme</div>
<Select <Select
className="w-full" className="w-full"
defaultValue={ThemeOptions[0]} defaultValue={ThemeOptions[0]}
components={{ components={{
Control: ThemeSelectControl, Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}} }}
onChange={(option) => { onChange={(option) => {
if (!option) { if (!option) {
@@ -1058,30 +1069,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={ThemeOptions} options={ThemeOptions}
/> />
</Label> </Label>
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="pb-4">
<div className="mb-2 pt-2">{palette.title}</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
addToPalette({
[palette.name as keyof (typeof previewState)["palette"]]: color,
});
}}
/>
</div>
</Label>
))}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
@@ -1,10 +1,14 @@
import type { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App"; import {
import { Button, Select } from "@calcom/ui"; Button,
Dropdown,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon"; import { FiPlus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
isLoading?: boolean; isLoading?: boolean;
} }
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { t } = useLocale();
const { data } = optionProps;
return data.slug !== "add-new" ? (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="flex w-full align-top" color="minimal">
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p className="text-left">{`${t("add")} ${data.label}`}</p>
</Button>
);
}}
/>
) : (
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
<p>{t("install_new_calendar_app")}</p>
</Button>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => { const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale(); const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true }); const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
type: "new_other", type: "new_other",
}); });
return ( return (
<Select <Dropdown modal={false}>
name="additionalCalendar" <DropdownMenuTrigger asChild>
placeholder={ <Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={FiPlus} color="secondary">
{t("add")} {t("add")}
</Button> </Button>
} </DropdownMenuTrigger>
options={options} <DropdownMenuContent>
isSearchable={false} {options.map((data) => (
isLoading={isLoading} <DropdownMenuItem key={data.slug} className="focus:outline-none">
components={{ Option: ImageOption }} {data.slug === "add-new" ? (
styles={{ <DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
menu: (defaultStyles) => ({ {t("install_new_calendar_app")}
...defaultStyles, </DropdownItem>
"@media only screen and (max-width: 640px)": { ) : (
left: "0", <InstallAppButton
}, type={data.type}
width: "max-content", render={(installProps) => {
right: "0", const props = { ...installProps } as any;
}), return (
indicatorSeparator: (defaultStyles) => ({ <DropdownItem {...props} color="minimal" type="button">
...defaultStyles, <span className="flex items-center gap-x-2">
display: "none", {data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
}), {`${t("add")} ${data.label}`}
control: (defaultStyles) => ({ </span>
...defaultStyles, </DropdownItem>
padding: "0", );
border: "0", }}
borderRadius: "6px", />
minHeight: "auto", )}
}), </DropdownMenuItem>
dropdownIndicator: (defaultStyles) => ({ ))}
...defaultStyles, </DropdownMenuContent>
display: "none", </Dropdown>
}),
valueContainer: (defaultStyles) => ({
...defaultStyles,
padding: "0",
}),
placeholder: (defaultStyles) => ({
...defaultStyles,
margin: "0",
}),
}}
/>
); );
}} }}
/> />
@@ -1,139 +0,0 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { DestinationCalendar } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
hidePlaceholder?: boolean;
/** The external Id of the connected calendar */
value: string | undefined;
destinationCalendar?: DestinationCalendar | null;
}
const DestinationCalendarSelector = ({
onChange,
isLoading,
value,
hidePlaceholder,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: `${selected.name} (${selected.externalId})` || "",
});
}
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
}
const options =
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: `${cal.name} (${cal.externalId})` || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div
className="relative flex items-center"
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name="primarySelectedCalendar"
placeholder={
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
control: (defaultStyles) => {
return {
...defaultStyles,
borderRadius: "6px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
width: "100%",
},
};
},
}}
isSearchable={false}
className={classNames(
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
return;
}
/* Split only the first `:`, since Apple uses the full URL as externalId */
const [integration, externalId] = option.value.split(/:(.+)/);
onChange({
integration,
externalId,
});
}}
isLoading={isLoading}
value={selectedOption}
components={{
IndicatorSeparator: () => null,
}}
/>
</div>
);
};
export default DestinationCalendarSelector;
@@ -426,6 +426,7 @@ const IntervalLimitItem = ({
required required
type="number" type="number"
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"} containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
className="mb-0"
placeholder={`${value}`} placeholder={`${value}`}
min={step} min={step}
step={step} step={step}
@@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form"; import { Controller, useForm, useFormContext } from "react-hook-form";
import type { MultiValue } from "react-select"; import type { MultiValue } from "react-select";
import { z } from "zod"; import { z } from "zod";
@@ -16,7 +16,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt"; import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify"; import { slugify } from "@calcom/lib/slugify";
import turndown from "@calcom/lib/turndownService"; import turndown from "@calcom/lib/turndownService";
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui"; import {
Button,
Label,
Select,
SettingsToggle,
Skeleton,
TextField,
Editor,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon"; import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
@@ -35,6 +45,33 @@ const getLocationFromType = (
} }
}; };
interface DescriptionEditorProps {
description?: string | null;
}
const DescriptionEditor = (props: DescriptionEditorProps) => {
const formMethods = useFormContext<FormValues>();
const [mounted, setIsMounted] = useState(false);
const { t } = useLocale();
const { description } = props;
useEffect(() => {
setIsMounted(true);
}, []);
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
/>
) : (
<SkeletonContainer>
<SkeletonText className="block h-24 w-full" />
</SkeletonContainer>
);
};
export const EventSetupTab = ( export const EventSetupTab = (
props: Pick< props: Pick<
EventTypeSetupProps, EventTypeSetupProps,
@@ -281,12 +318,7 @@ export const EventSetupTab = (
/> />
<div> <div>
<Label>{t("description")}</Label> <Label>{t("description")}</Label>
<Editor <DescriptionEditor description={eventType?.description} />
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
/>
</div> </div>
<TextField <TextField
required required
@@ -1,9 +1,8 @@
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs } from "@calcom/trpc/react"; import type { RouterInputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
interface ICreateEventsOnCalendarSelectProps { interface ICreateEventsOnCalendarSelectProps {
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null; calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.7.6", "version": "2.7.7",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -65,7 +65,7 @@
"@types/turndown": "^5.0.1", "@types/turndown": "^5.0.1",
"@vercel/edge-config": "^0.1.1", "@vercel/edge-config": "^0.1.1",
"@vercel/edge-functions-ui": "^0.2.1", "@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.21", "@vercel/og": "^0.5.0",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
"async": "^3.2.4", "async": "^3.2.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
+3 -3
View File
@@ -88,7 +88,7 @@ export default async function handler(req: NextApiRequest) {
ogConfig ogConfig
) as { body: Buffer }; ) as { body: Buffer };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
case "app": { case "app": {
const { name, description, slug } = appSchema.parse({ const { name, description, slug } = appSchema.parse({
@@ -101,7 +101,7 @@ export default async function handler(req: NextApiRequest) {
body: Buffer; body: Buffer;
}; };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
case "generic": { case "generic": {
@@ -115,7 +115,7 @@ export default async function handler(req: NextApiRequest) {
body: Buffer; body: Buffer;
}; };
return new Response(img.body, { status: 200 }); return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
} }
default: default:
+3 -3
View File
@@ -14,7 +14,7 @@ import { FiSearch } from "@calcom/ui/components/icon";
import AppsLayout from "@components/apps/layouts/AppsLayout"; import AppsLayout from "@components/apps/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg"; import { ssrInit } from "@server/lib/ssr";
const tabs: HorizontalTabItemProps[] = [ const tabs: HorizontalTabItemProps[] = [
{ {
@@ -88,7 +88,7 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context; const { req, res } = context;
const ssg = await ssgInit(context); const ssr = await ssrInit(context);
const session = await getServerSession({ req, res }); const session = await getServerSession({ req, res });
@@ -119,7 +119,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return b.count - a.count; return b.count - a.count;
}), }),
appStore, appStore,
trpcState: ssg.dehydrate(), trpcState: ssr.dehydrate(),
}, },
}; };
}; };
+6 -10
View File
@@ -569,7 +569,8 @@ export default function Success(props: SuccessProps) {
</div> </div>
</> </>
)} )}
</div>
<div className="text-bookingdark dark:border-darkgray-200 mt-8 text-left dark:text-gray-300">
{Object.entries(bookingInfo.responses).map(([name, response]) => { {Object.entries(bookingInfo.responses).map(([name, response]) => {
const field = eventType.bookingFields.find((field) => field.name === name); const field = eventType.bookingFields.find((field) => field.name === name);
// We show location in the "where" section // We show location in the "where" section
@@ -584,15 +585,10 @@ export default function Success(props: SuccessProps) {
return ( return (
<> <>
<div className="mt-9 font-medium">{label}</div> <div className="mt-4 font-medium">{label}</div>
<div className="col-span-2 mb-2 mt-9"> <p className="break-words" data-testid="field-response" data-fob-field={field.name}>
<p {response.toString()}
className="break-words" </p>
data-testid="field-response"
data-fob-field={field.name}>
{response.toString()}
</p>
</div>
</> </>
); );
})} })}
+10 -25
View File
@@ -13,11 +13,9 @@ import type { EventLocationType } from "@calcom/core/location";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { validateIntervalLimitOrder } from "@calcom/lib"; import { validateIntervalLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -366,9 +364,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}; };
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => { const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery({ id: props.type }); const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
if (isLoading || !data) return null;
return <EventTypePage {...data} />; return <EventTypePage {...(data as EventTypeSetupProps)} />;
}; };
export const getServerSideProps = async (context: GetServerSidePropsContext) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
@@ -394,26 +392,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}; };
} }
try { await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma }); return {
props: {
return { type: typeParam,
props: { trpcState: ssr.dehydrate(),
// session, },
type: typeParam, };
trpcState: ssr.dehydrate(),
initialData: {
eventType: res.eventType,
locationOptions: res.locationOptions,
team: res.team,
teamMembers: res.teamMembers,
currentUserMembership: res.currentUserMembership,
},
},
};
} catch (err) {
throw err;
}
}; };
export default EventTypePageWrapper; export default EventTypePageWrapper;
+1 -1
View File
@@ -63,7 +63,7 @@ export default function InsightsPage() {
title={t("make_informed_decisions")} title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")} description={t("make_informed_decisions_description")}
features={features} features={features}
background="/banners/insights.jpg" background="/tips/insights.jpg"
buttons={ buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2"> <div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup> <ButtonGroup>
@@ -3,7 +3,7 @@ import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui"; import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText, Alert } from "@calcom/ui";
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal"; import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal"; import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
@@ -32,11 +32,14 @@ const TwoFactorAuthView = () => {
if (isLoading) return <SkeletonLoader />; if (isLoading) return <SkeletonLoader />;
const isCalProvider = user?.identityProvider === "CAL";
return ( return (
<> <>
<Meta title={t("2fa")} description={t("2fa_description")} /> <Meta title={t("2fa")} description={t("2fa_description")} />
{!isCalProvider && <Alert severity="neutral" message={t("2fa_disabled")} />}
<div className="mt-6 flex items-start space-x-4"> <div className="mt-6 flex items-start space-x-4">
<Switch <Switch
disabled={!isCalProvider}
checked={user?.twoFactorEnabled} checked={user?.twoFactorEnabled}
onCheckedChange={() => onCheckedChange={() =>
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true) user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
@@ -19,7 +19,7 @@ const OnboardTeamMembersPage = () => {
}; };
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => ( OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
<WizardLayout currentStep={1} maxSteps={2}> <WizardLayout currentStep={2} maxSteps={2}>
{page} {page}
</WizardLayout> </WizardLayout>
); );
+9 -2
View File
@@ -3,7 +3,7 @@ import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components"; import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getLayout } from "@components/layouts/WizardLayout"; import WizardLayout from "@components/layouts/WizardLayout";
const CreateNewTeamPage = () => { const CreateNewTeamPage = () => {
const { t } = useLocale(); const { t } = useLocale();
@@ -17,7 +17,14 @@ const CreateNewTeamPage = () => {
</> </>
); );
}; };
const LayoutWrapper = (page: React.ReactElement) => {
return (
<WizardLayout currentStep={1} maxSteps={2}>
{page}
</WizardLayout>
);
};
CreateNewTeamPage.getLayout = getLayout; CreateNewTeamPage.getLayout = LayoutWrapper;
export default CreateNewTeamPage; export default CreateNewTeamPage;
+1 -1
View File
@@ -205,7 +205,7 @@ test.describe("Booking with Seats", () => {
}, },
}); });
expect(oldBooking?.attendees).toBeFalsy(); expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);
}); });
test("Should cancel with seats and have no attendees and cancelled", async ({ test("Should cancel with seats and have no attendees and cancelled", async ({
+10 -4
View File
@@ -97,13 +97,19 @@ test.describe("Reschedule Tests", async () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } }); const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking.uid } });
const rescheduledBooking = await prisma.booking.findFirst({ where: { uid: booking.uid } }); const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: booking.uid } });
expect(newBooking).not.toBeNull(); expect(newBooking).not.toBeNull();
expect(rescheduledBooking).toBeNull(); expect(rescheduledBooking.status).toBe(BookingStatus.CANCELLED);
await prisma.booking.delete({ where: { id: newBooking?.id } }); await prisma.booking.deleteMany({
where: {
id: {
in: [newBooking.id, rescheduledBooking.id],
},
},
});
}); });
test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => { test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => {
+2
View File
@@ -5,3 +5,5 @@ Disallow: /static/locales
# Specifically allow access to OG Image api, otherwise eg Twitter won't render these images. # Specifically allow access to OG Image api, otherwise eg Twitter won't render these images.
Allow: /api/social/og/image Allow: /api/social/og/image
# Specifically allow access to api/app-store to allow search engines to fetch assets
Allow: /api/app-store/
@@ -404,6 +404,7 @@
"add_time_availability": "Add new time slot", "add_time_availability": "Add new time slot",
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.", "add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
"2fa": "Two-Factor Authentication", "2fa": "Two-Factor Authentication",
"2fa_disabled":"Two-Factor authentication can only be enabled for email and password authentication",
"enable_2fa": "Enable two-factor authentication", "enable_2fa": "Enable two-factor authentication",
"disable_2fa": "Disable two-factor authentication", "disable_2fa": "Disable two-factor authentication",
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.", "disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
@@ -1500,7 +1501,7 @@
"date_overrides_update_btn": "Update Override", "date_overrides_update_btn": "Update Override",
"event_type_duplicate_copy_text": "{{slug}}-copy", "event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default", "set_as_default": "Set as default",
"hide_eventtype_details": "Hide EventType Details", "hide_eventtype_details": "Hide event type details",
"show_navigation": "Show navigation", "show_navigation": "Show navigation",
"hide_navigation": "Hide navigation", "hide_navigation": "Hide navigation",
"verification_code_sent": "Verification code sent", "verification_code_sent": "Verification code sent",
@@ -1514,7 +1515,7 @@
"create_your_first_team_webhook_description": "Create your first webhook for this team event type", "create_your_first_team_webhook_description": "Create your first webhook for this team event type",
"create_webhook_team_event_type": "Create a webhook for this team event type", "create_webhook_team_event_type": "Create a webhook for this team event type",
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)", "disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters", "invalid_admin_password": "You are admin but you do not have a password length of at least 15 characters or no 2FA yet",
"change_password_admin": "Change Password to gain admin access", "change_password_admin": "Change Password to gain admin access",
"username_already_taken": "Username is already taken", "username_already_taken": "Username is already taken",
"assignment": "Assignment", "assignment": "Assignment",
@@ -1692,5 +1693,6 @@
"subtitle_analytics": "Learn more about your team's activity", "subtitle_analytics": "Learn more about your team's activity",
"event_trends": "Event Trends", "event_trends": "Event Trends",
"clear_filters": "Clear Filters", "clear_filters": "Clear Filters",
"insights": "Insights" "insights": "Insights",
"email_invite_team": "{{email}} has been invited"
} }
File renamed without changes.
File renamed without changes.
@@ -3,6 +3,7 @@
Don't modify this file manually. Don't modify this file manually.
**/ **/
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
export const InstallAppButtonMap = { export const InstallAppButtonMap = {
applecalendar: dynamic(() => import("./applecalendar/components/InstallAppButton")), applecalendar: dynamic(() => import("./applecalendar/components/InstallAppButton")),
around: dynamic(() => import("./around/components/InstallAppButton")), around: dynamic(() => import("./around/components/InstallAppButton")),
@@ -24,6 +24,7 @@ import { appKeysSchema as vital_zod_ts } from "./vital/zod";
import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod"; import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod";
import { appKeysSchema as zapier_zod_ts } from "./zapier/zod"; import { appKeysSchema as zapier_zod_ts } from "./zapier/zod";
import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
export const appKeysSchemas = { export const appKeysSchemas = {
dailyvideo: dailyvideo_zod_ts, dailyvideo: dailyvideo_zod_ts,
fathom: fathom_zod_ts, fathom: fathom_zod_ts,
@@ -58,6 +58,7 @@ import wordpress_config_json from "./wordpress/config.json";
import { metadata as zapier__metadata_ts } from "./zapier/_metadata"; import { metadata as zapier__metadata_ts } from "./zapier/_metadata";
import zohocrm_config_json from "./zohocrm/config.json"; import zohocrm_config_json from "./zohocrm/config.json";
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata"; import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
export const appStoreMetadata = { export const appStoreMetadata = {
amie: amie_config_json, amie: amie_config_json,
applecalendar: applecalendar__metadata_ts, applecalendar: applecalendar__metadata_ts,
@@ -24,6 +24,7 @@ import { appDataSchema as vital_zod_ts } from "./vital/zod";
import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod"; import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod";
import { appDataSchema as zapier_zod_ts } from "./zapier/zod"; import { appDataSchema as zapier_zod_ts } from "./zapier/zod";
import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
export const appDataSchemas = { export const appDataSchemas = {
dailyvideo: dailyvideo_zod_ts, dailyvideo: dailyvideo_zod_ts,
fathom: fathom_zod_ts, fathom: fathom_zod_ts,
@@ -165,6 +165,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar", type: "google_calendar",
password: "", password: "",
url: "", url: "",
iCalUID: event.data.iCalUID,
}); });
} }
); );
@@ -201,6 +202,9 @@ export default class GoogleCalendarService implements Calendar {
id: String(event.organizer.id), id: String(event.organizer.id),
organizer: true, organizer: true,
responseStatus: "accepted", responseStatus: "accepted",
email: event.destinationCalendar?.externalId
? event.destinationCalendar.externalId
: event.organizer.email,
}, },
// eslint-disable-next-line // eslint-disable-next-line
...eventAttendees, ...eventAttendees,
@@ -268,6 +272,7 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar", type: "google_calendar",
password: "", password: "",
url: "", url: "",
iCalUID: evt.data.iCalUID,
}); });
} }
return resolve(evt?.data); return resolve(evt?.data);
@@ -73,7 +73,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)), body: JSON.stringify(this.translateEvent(event)),
}); });
return handleErrorsJson(response); const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) { } catch (error) {
this.log.error(error); this.log.error(error);
@@ -88,7 +90,9 @@ export default class Office365CalendarService implements Calendar {
body: JSON.stringify(this.translateEvent(event)), body: JSON.stringify(this.translateEvent(event)),
}); });
return handleErrorsRaw(response); const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
return { ...responseJson, iCalUID: responseJson.iCalUId };
} catch (error) { } catch (error) {
this.log.error(error); this.log.error(error);
@@ -4,6 +4,8 @@ import type { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-b
//@ts-ignore //@ts-ignore
import BasicConfig from "react-awesome-query-builder/lib/config/basic"; import BasicConfig from "react-awesome-query-builder/lib/config/basic";
import { EmailField } from "@calcom/ui";
import widgetsComponents from "../widgets"; import widgetsComponents from "../widgets";
const { const {
@@ -92,8 +94,7 @@ const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string
if (!props) { if (!props) {
return <div />; return <div />;
} }
// TODO: Use EmailField component for Routing Form Email field return <EmailField type="email" {...props} />;
return <TextWidget type="email" {...props} />;
}, },
}, },
}; };
@@ -102,7 +102,7 @@ export default function RoutingForms({
title={t("teams_plan_required")} title={t("teams_plan_required")}
description={t("routing_forms_are_a_great_way")} description={t("routing_forms_are_a_great_way")}
features={features} features={features}
background="/banners/routing-forms.jpg" background="/tips/routing-forms.jpg"
isParentLoading={isLoading && <SkeletonLoaderTeamList />} isParentLoading={isLoading && <SkeletonLoaderTeamList />}
buttons={ buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2"> <div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
+4 -1
View File
@@ -77,9 +77,11 @@ export const getConnectedCalendars = async (
}, },
}; };
} }
if (destinationCalendar) { // HACK https://github.com/calcom/cal.com/pull/7644/files#r1131508414
if (destinationCalendar && !Object.isFrozen(destinationCalendar)) {
destinationCalendar.primaryEmail = primary.email; destinationCalendar.primaryEmail = primary.email;
destinationCalendar.integrationTitle = integration.title; destinationCalendar.integrationTitle = integration.title;
destinationCalendar = Object.freeze(destinationCalendar);
} }
return { return {
@@ -263,6 +265,7 @@ export const createEvent = async (
type: credential.type, type: credential.type,
success, success,
uid, uid,
iCalUID: creationResult?.iCalUID || undefined,
createdEvent: creationResult, createdEvent: creationResult,
originalEvent: calEvent, originalEvent: calEvent,
calError, calError,
+16 -7
View File
@@ -10,15 +10,12 @@ import { MeetLocationType } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils"; import getApps from "@calcom/app-store/utils";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { createdEventSchema } from "@calcom/prisma/zod-utils"; import { createdEventSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar"; import type { NewCalendarEventType } from "@calcom/types/Calendar";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential"; import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { Event } from "@calcom/types/Event"; import type { Event } from "@calcom/types/Event";
import type { import type { EventResult } from "@calcom/types/EventManager";
CreateUpdateResult, import type { CreateUpdateResult, PartialBooking, PartialReference } from "@calcom/types/EventManager";
EventResult,
PartialBooking,
PartialReference,
} from "@calcom/types/EventManager";
import { createEvent, updateEvent } from "./CalendarManager"; import { createEvent, updateEvent } from "./CalendarManager";
import { createMeeting, updateMeeting } from "./videoClient"; import { createMeeting, updateMeeting } from "./videoClient";
@@ -125,12 +122,24 @@ export default class EventManager {
// Create the calendar event with the proper video call data // Create the calendar event with the proper video call data
results.push(...(await this.createAllCalendarEvents(clonedCalEvent))); results.push(...(await this.createAllCalendarEvents(clonedCalEvent)));
// Since the result can be a new calendar event or video event, we have to create a type guard
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
const isCalendarResult = (
result: (typeof results)[number]
): result is EventResult<NewCalendarEventType> => {
return result.type.includes("_calendar");
};
const referencesToCreate = results.map((result) => { const referencesToCreate = results.map((result) => {
let createdEventObj: createdEventSchema | null = null; let createdEventObj: createdEventSchema | null = null;
if (typeof result?.createdEvent === "string") { if (typeof result?.createdEvent === "string") {
createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent)); createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent));
} }
if (isCalendarResult(result)) {
evt.iCalUID = result.iCalUID || undefined;
}
return { return {
type: result.type, type: result.type,
uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "", uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "",
@@ -16,20 +16,18 @@ export default class AttendeeScheduledEmail extends BaseEmail {
attendee: Person; attendee: Person;
showAttendees: boolean | undefined; showAttendees: boolean | undefined;
t: TFunction; t: TFunction;
attendees: Person[];
constructor(calEvent: CalendarEvent, attendee: Person, showAttendees?: boolean | undefined) { constructor(calEvent: CalendarEvent, attendee: Person, showAttendees?: boolean | undefined) {
super(); super();
this.name = "SEND_BOOKING_CONFIRMATION"; if (!showAttendees && calEvent.seatsPerTimeSlot) {
this.calEvent = calEvent; this.calEvent = cloneDeep(calEvent);
this.attendee = attendee; this.calEvent.attendees = [attendee];
this.showAttendees = showAttendees; } else {
this.t = attendee.language.translate; this.calEvent = calEvent;
this.attendees = [...this.calEvent.attendees];
if (!this.showAttendees && this.calEvent.seatsPerTimeSlot) {
this.attendees = [this.attendee];
this.calEvent.attendees = [this.attendee];
} }
this.name = "SEND_BOOKING_CONFIRMATION";
this.attendee = attendee;
this.t = attendee.language.translate;
} }
protected getiCalEventAsString(): string | undefined { protected getiCalEventAsString(): string | undefined {
@@ -39,21 +37,21 @@ export default class AttendeeScheduledEmail extends BaseEmail {
// ics appends "RRULE:" already, so removing it from RRule generated string // ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", ""); recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
} }
const partstat: ParticipationStatus = "NEEDS-ACTION"; const partstat: ParticipationStatus = "ACCEPTED";
const role: ParticipationRole = "REQ-PARTICIPANT"; const role: ParticipationRole = "REQ-PARTICIPANT";
const icsEvent = createEvent({ const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
start: dayjs(this.calEvent.startTime) start: dayjs(this.calEvent.startTime)
.utc() .utc()
.toArray() .toArray()
.slice(0, 6) .slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc", startInputType: "utc",
productId: "calendso/ics", productId: "calcom/ics",
title: this.calEvent.title, title: this.calEvent.title,
description: this.getTextBody(), description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: [ attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({ ...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name, name: attendee.name,
@@ -47,7 +47,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
.slice(0, 6) .slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc", startInputType: "utc",
productId: "calendso/ics", productId: "calcom/ics",
title: this.t("ics_event_title", { title: this.t("ics_event_title", {
eventType: this.calEvent.type, eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name, name: this.calEvent.attendees[0].name,
@@ -54,7 +54,7 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
.slice(0, 6) .slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc", startInputType: "utc",
productId: "calendso/ics", productId: "calcom/ics",
title: this.t("ics_event_title", { title: this.t("ics_event_title", {
eventType: this.calEvent.type, eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name, name: this.calEvent.attendees[0].name,
@@ -35,13 +35,14 @@ export default class OrganizerScheduledEmail extends BaseEmail {
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", ""); recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
} }
const icsEvent = createEvent({ const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
start: dayjs(this.calEvent.startTime) start: dayjs(this.calEvent.startTime)
.utc() .utc()
.toArray() .toArray()
.slice(0, 6) .slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc", startInputType: "utc",
productId: "calendso/ics", productId: "calcom/ics",
title: this.calEvent.title, title: this.calEvent.title,
description: this.getTextBody(), description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
+6 -4
View File
@@ -3,11 +3,13 @@
"compilerOptions": { "compilerOptions": {
"target": "ES2015", "target": "ES2015",
"module": "esnext", "module": "esnext",
"paths": {
"@lib/*": ["../../../apps/web/lib/*"]
},
"moduleResolution": "Node", "moduleResolution": "Node",
"baseUrl": "." "baseUrl": ".",
"outDir": "dist",
"paths": {
"@calcom/embed-react": ["../embed-react/src"],
"@calcom/embed-snippet": ["../embed-snippet/src"]
}
}, },
"include": ["src", "env.d.ts", "index.ts"], "include": ["src", "env.d.ts", "index.ts"],
"exclude": ["dist", "build", "node_modules"] "exclude": ["dist", "build", "node_modules"]
+3 -1
View File
@@ -7,8 +7,10 @@
"baseUrl": ".", "baseUrl": ".",
"declaration": true, "declaration": true,
"jsx": "preserve", "jsx": "preserve",
"outDir": "dist",
"paths": { "paths": {
"@lib/*": ["../../../apps/web/lib/*"] "@calcom/embed-core": ["../embed-core/src"],
"@calcom/embed-snippet": ["../embed-snippet/src"]
} }
}, },
"include": ["src", "env.d.ts"], "include": ["src", "env.d.ts"],
+7 -2
View File
@@ -3,8 +3,13 @@
"compilerOptions": { "compilerOptions": {
"target": "ES2015", "target": "ES2015",
"baseUrl": ".", "baseUrl": ".",
"module": "esnext", "module": "ESNext",
"declaration": true "declaration": true,
"outDir": "dist",
"paths": {
"@calcom/embed-core": ["../embed-core/src"],
"@calcom/embed-react": ["../embed-react/src"]
}
}, },
"include": ["."], "include": ["."],
"exclude": ["dist", "build", "node_modules"] "exclude": ["dist", "build", "node_modules"]
@@ -67,6 +67,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
currency: true, currency: true,
length: true, length: true,
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAttendees: true,
hosts: { hosts: {
select: { select: {
user: true, user: true,
@@ -190,6 +191,8 @@ async function handler(req: CustomRequest) {
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar, destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
cancellationReason: cancellationReason, cancellationReason: cancellationReason,
...(teamMembers && { team: { name: "", members: teamMembers } }), ...(teamMembers && { team: { name: "", members: teamMembers } }),
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
}; };
// If it's just an attendee of a booking then just remove them from that booking // If it's just an attendee of a booking then just remove them from that booking
@@ -895,10 +895,13 @@ async function handler(
} }
await Promise.all(integrationsToDelete).then(async () => { await Promise.all(integrationsToDelete).then(async () => {
await prisma.booking.delete({ await prisma.booking.update({
where: { where: {
id: originalRescheduledBooking.id, id: originalRescheduledBooking.id,
}, },
data: {
status: BookingStatus.CANCELLED,
},
}); });
}); });
deletedReferences = true; deletedReferences = true;
@@ -1010,6 +1013,16 @@ async function handler(
throw new HttpError({ statusCode: 401 }); throw new HttpError({ statusCode: 401 });
} }
// Moving forward in this block is the owner making changes to the booking. All attendees should be affected
evt.attendees = originalRescheduledBooking.attendees.map((attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: { translate: tAttendees, locale: attendee.locale ?? "en" },
};
});
// If owner reschedules the event we want to update the entire booking // If owner reschedules the event we want to update the entire booking
// Also if owner is rescheduling there should be no bookingSeat // Also if owner is rescheduling there should be no bookingSeat
@@ -1044,6 +1057,10 @@ async function handler(
const results = updateManager.results; const results = updateManager.results;
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = calendarResult.updatedEvent.iCalUID || undefined;
if (results.length > 0 && results.some((res) => !res.success)) { if (results.length > 0 && results.some((res) => !res.success)) {
const error = { const error = {
errorCode: "BookingReschedulingMeetingFailed", errorCode: "BookingReschedulingMeetingFailed",
@@ -1071,7 +1088,7 @@ async function handler(
await sendRescheduledEmails({ await sendRescheduledEmails({
...copyEvent, ...copyEvent,
additionalNotes, // Resets back to the additionalNote input and not the override value additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: "$RCH$" + rescheduleReason ? rescheduleReason : "", // Removable code prefix to differentiate cancellation from rescheduling for email cancellationReason: "$RCH$" + (rescheduleReason ? rescheduleReason : ""), // Removable code prefix to differentiate cancellation from rescheduling for email
}); });
} }
const resultBooking = await resultBookingQuery(newBooking.id); const resultBooking = await resultBookingQuery(newBooking.id);
@@ -1170,20 +1187,31 @@ async function handler(
const copyEvent = cloneDeep(evt); const copyEvent = cloneDeep(evt);
await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id); const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
const results = updateManager.results;
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
? calendarResult?.updatedEvent[0]?.iCalUID
: calendarResult?.updatedEvent?.iCalUID || undefined;
// TODO send reschedule emails to attendees of the old booking // TODO send reschedule emails to attendees of the old booking
await sendRescheduledEmails({ await sendRescheduledEmails({
...copyEvent, ...copyEvent,
additionalNotes, // Resets back to the additionalNote input and not the override value additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: "$RCH$" + rescheduleReason ? rescheduleReason : "", // Removable code prefix to differentiate cancellation from rescheduling for email cancellationReason: "$RCH$" + (rescheduleReason ? rescheduleReason : ""), // Removable code prefix to differentiate cancellation from rescheduling for email
}); });
// Delete the old booking // Update the old booking with the cancelled status
await prisma.booking.delete({ await prisma.booking.update({
where: { where: {
id: booking.id, id: booking.id,
}, },
data: {
status: BookingStatus.CANCELLED,
},
}); });
const resultBooking = await resultBookingQuery(newTimeSlotBooking.id); const resultBooking = await resultBookingQuery(newTimeSlotBooking.id);
@@ -1250,7 +1278,15 @@ async function handler(
const copyEvent = cloneDeep(evt); const copyEvent = cloneDeep(evt);
await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id); const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
const results = updateManager.results;
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
? calendarResult?.updatedEvent[0]?.iCalUID
: calendarResult?.updatedEvent?.iCalUID || undefined;
await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person); await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person);
const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => { const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => {
@@ -1284,6 +1320,8 @@ async function handler(
}; };
} }
const attendeeUniqueId = uuid();
const bookingUpdated = await prisma.booking.update({ const bookingUpdated = await prisma.booking.update({
where: { where: {
uid: reqBody.bookingUid, uid: reqBody.bookingUid,
@@ -1298,32 +1336,25 @@ async function handler(
name: invitee[0].name, name: invitee[0].name,
timeZone: invitee[0].timeZone, timeZone: invitee[0].timeZone,
locale: invitee[0].language.locale, locale: invitee[0].language.locale,
bookingSeat: {
create: {
referenceUid: attendeeUniqueId,
data: {
description: additionalNotes,
},
booking: {
connect: {
id: booking.id,
},
},
},
},
}, },
}, },
...(booking.status === BookingStatus.CANCELLED && { status: BookingStatus.ACCEPTED }), ...(booking.status === BookingStatus.CANCELLED && { status: BookingStatus.ACCEPTED }),
}, },
}); });
// Add entry to bookingSeat table
const attendeeUniqueId = uuid();
await prisma.bookingSeat.create({
data: {
data: {
description: additionalNotes,
},
booking: {
connect: {
id: booking.id,
},
},
referenceUid: attendeeUniqueId,
attendee: {
connect: {
id: bookingUpdated.attendees[bookingUpdated.attendees.length - 1].id,
},
},
},
});
evt.attendeeSeatId = attendeeUniqueId; evt.attendeeSeatId = attendeeUniqueId;
const newSeat = booking.attendees.length !== 0; const newSeat = booking.attendees.length !== 0;
@@ -1508,6 +1539,9 @@ async function handler(
}; };
newBookingData["paid"] = originalRescheduledBooking.paid; newBookingData["paid"] = originalRescheduledBooking.paid;
newBookingData["fromReschedule"] = originalRescheduledBooking.uid; newBookingData["fromReschedule"] = originalRescheduledBooking.uid;
if (originalRescheduledBooking.uid) {
newBookingData.cancellationReason = rescheduleReason;
}
if (newBookingData.attendees?.createMany?.data) { if (newBookingData.attendees?.createMany?.data) {
// Reschedule logic with booking with seats // Reschedule logic with booking with seats
if (eventType?.seatsPerTimeSlot && bookerEmail) { if (eventType?.seatsPerTimeSlot && bookerEmail) {
@@ -1561,7 +1595,7 @@ async function handler(
return prisma.booking.create(createBookingObj); return prisma.booking.create(createBookingObj);
} }
let results: EventResult<AdditionalInformation & { url?: string }>[] = []; let results: EventResult<AdditionalInformation & { url?: string; iCalUID?: string }>[] = [];
let referencesToCreate: PartialReference[] = []; let referencesToCreate: PartialReference[] = [];
type Booking = Prisma.PromiseReturnType<typeof createBooking>; type Booking = Prisma.PromiseReturnType<typeof createBooking>;
@@ -1675,12 +1709,15 @@ async function handler(
addVideoCallDataToEvt(originalRescheduledBooking.references); addVideoCallDataToEvt(originalRescheduledBooking.references);
const updateManager = await eventManager.reschedule(evt, originalRescheduledBooking.uid); const updateManager = await eventManager.reschedule(evt, originalRescheduledBooking.uid);
//delete original rescheduled booking (no seats event) //update original rescheduled booking (no seats event)
if (!eventType.seatsPerTimeSlot) { if (!eventType.seatsPerTimeSlot) {
await prisma.booking.delete({ await prisma.booking.update({
where: { where: {
id: originalRescheduledBooking.id, id: originalRescheduledBooking.id,
}, },
data: {
status: BookingStatus.CANCELLED,
},
}); });
} }
@@ -1699,6 +1736,11 @@ async function handler(
log.error(`Booking ${organizerUser.name} failed`, error, results); log.error(`Booking ${organizerUser.name} failed`, error, results);
} else { } else {
const metadata: AdditionalInformation = {}; const metadata: AdditionalInformation = {};
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
? calendarResult?.updatedEvent[0]?.iCalUID
: calendarResult?.updatedEvent?.iCalUID || undefined;
if (results.length) { if (results.length) {
// TODO: Handle created event metadata more elegantly // TODO: Handle created event metadata more elegantly
@@ -1719,7 +1761,7 @@ async function handler(
...copyEvent, ...copyEvent,
additionalInformation: metadata, additionalInformation: metadata,
additionalNotes, // Resets back to the additionalNote input and not the override value additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: "$RCH$" + rescheduleReason ? rescheduleReason : "", // Removable code prefix to differentiate cancellation from rescheduling for email cancellationReason: "$RCH$" + (rescheduleReason ? rescheduleReason : ""), // Removable code prefix to differentiate cancellation from rescheduling for email
}); });
} }
} }
@@ -7,7 +7,8 @@ import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberIn
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { WEBAPP_URL, APP_NAME } from "@calcom/lib/constants"; import { WEBAPP_URL, APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Avatar, Badge, Button, showToast, SkeletonContainer, SkeletonText } from "@calcom/ui"; import { Avatar, Badge, Button, showToast, SkeletonContainer, SkeletonText } from "@calcom/ui";
import { FiPlus, FiArrowRight, FiTrash2 } from "@calcom/ui/components/icon"; import { FiPlus, FiArrowRight, FiTrash2 } from "@calcom/ui/components/icon";
@@ -43,9 +44,17 @@ export const AddNewTeamMembersForm = ({
const utils = trpc.useContext(); const utils = trpc.useContext();
const router = useRouter(); const router = useRouter();
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({ const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
async onSuccess() { async onSuccess(data) {
await utils.viewer.teams.get.invalidate(); await utils.viewer.teams.get.invalidate();
setMemberInviteModal(false); setMemberInviteModal(false);
if (data.sendEmailInvitation) {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
}, },
onError: (error) => { onError: (error) => {
showToast(error.message, "error"); showToast(error.message, "error");
@@ -1,4 +1,5 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -7,7 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Avatar, Button, Form, ImageUploader, TextField } from "@calcom/ui"; import { Avatar, Button, Form, ImageUploader, TextField, Alert } from "@calcom/ui";
import { FiArrowRight } from "@calcom/ui/components/icon"; import { FiArrowRight } from "@calcom/ui/components/icon";
import type { NewTeamFormValues } from "../lib/types"; import type { NewTeamFormValues } from "../lib/types";
@@ -21,6 +22,7 @@ export const CreateANewTeamForm = () => {
const router = useRouter(); const router = useRouter();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const returnToParsed = querySchema.safeParse(router.query); const returnToParsed = querySchema.safeParse(router.query);
const [serverErrorMessage, setServerErrorMessage] = useState<string | null>(null);
const returnToParam = const returnToParam =
(returnToParsed.success ? getSafeRedirectUrl(returnToParsed.data.returnTo) : "/settings/teams") || (returnToParsed.success ? getSafeRedirectUrl(returnToParsed.data.returnTo) : "/settings/teams") ||
@@ -33,29 +35,32 @@ export const CreateANewTeamForm = () => {
telemetry.event(telemetryEventTypes.team_created); telemetry.event(telemetryEventTypes.team_created);
router.push(`/settings/teams/${data.id}/onboard-members`); router.push(`/settings/teams/${data.id}/onboard-members`);
}, },
onError: (err) => {
if (err.message === "team_url_taken") {
newTeamFormMethods.setError("slug", { type: "custom", message: t("team_url_taken") });
} else {
setServerErrorMessage(err.message);
}
},
}); });
const validateTeamSlugQuery = trpc.viewer.teams.validateTeamSlug.useQuery(
{ slug: newTeamFormMethods.watch("slug") },
{
enabled: false,
refetchOnWindowFocus: false,
}
);
const validateTeamSlug = async () => {
await validateTeamSlugQuery.refetch();
if (validateTeamSlugQuery.isFetched) return validateTeamSlugQuery.data || t("team_url_taken");
};
return ( return (
<> <>
<Form <Form
form={newTeamFormMethods} form={newTeamFormMethods}
handleSubmit={(v) => { handleSubmit={(v) => {
if (!createTeamMutation.isLoading) createTeamMutation.mutate(v); if (!createTeamMutation.isLoading) {
setServerErrorMessage(null);
createTeamMutation.mutate(v);
}
}}> }}>
<div className="mb-8"> <div className="mb-8">
{serverErrorMessage && (
<div className="mb-4">
<Alert severity="error" message={serverErrorMessage} />
</div>
)}
<Controller <Controller
name="name" name="name"
control={newTeamFormMethods.control} control={newTeamFormMethods.control}
@@ -75,7 +80,6 @@ export const CreateANewTeamForm = () => {
if (newTeamFormMethods.formState.touchedFields["slug"] === undefined) { if (newTeamFormMethods.formState.touchedFields["slug"] === undefined) {
newTeamFormMethods.setValue("slug", slugify(e?.target.value)); newTeamFormMethods.setValue("slug", slugify(e?.target.value));
} }
createTeamMutation.reset();
}} }}
autoComplete="off" autoComplete="off"
/> />
@@ -88,7 +92,7 @@ export const CreateANewTeamForm = () => {
<Controller <Controller
name="slug" name="slug"
control={newTeamFormMethods.control} control={newTeamFormMethods.control}
rules={{ required: t("team_url_required"), validate: async () => await validateTeamSlug() }} rules={{ required: t("team_url_required") }}
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<TextField <TextField
className="mt-2" className="mt-2"
@@ -103,7 +107,7 @@ export const CreateANewTeamForm = () => {
newTeamFormMethods.setValue("slug", slugify(e?.target.value), { newTeamFormMethods.setValue("slug", slugify(e?.target.value), {
shouldTouch: true, shouldTouch: true,
}); });
createTeamMutation.reset(); newTeamFormMethods.clearErrors("slug");
}} }}
/> />
)} )}
@@ -143,11 +147,7 @@ export const CreateANewTeamForm = () => {
{t("cancel")} {t("cancel")}
</Button> </Button>
<Button <Button
disabled={ disabled={newTeamFormMethods.formState.isSubmitting || createTeamMutation.isLoading}
newTeamFormMethods.formState.isSubmitting ||
createTeamMutation.isError ||
createTeamMutation.isLoading
}
color="primary" color="primary"
EndIcon={FiArrowRight} EndIcon={FiArrowRight}
type="submit" type="submit"
@@ -155,9 +155,6 @@ export const CreateANewTeamForm = () => {
{t("continue")} {t("continue")}
</Button> </Button>
</div> </div>
{createTeamMutation.isError && (
<p className="mt-4 text-red-700">{createTeamMutation.error.message}</p>
)}
</Form> </Form>
</> </>
); );
@@ -93,6 +93,8 @@ export default function MemberListItem(props: Props) {
props.member.accepted && props.member.accepted &&
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true"; process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true";
const bookingLink = `${WEBAPP_URL.split("http://")[1]}/${props.member.username}`;
return ( return (
<li className="divide-y px-5"> <li className="divide-y px-5">
<div className="my-4 flex justify-between"> <div className="my-4 flex justify-between">
@@ -112,12 +114,20 @@ export default function MemberListItem(props: Props) {
{!props.member.accepted && <TeamPill color="orange" text={t("pending")} />} {!props.member.accepted && <TeamPill color="orange" text={t("pending")} />}
{props.member.role && <TeamRole role={props.member.role} />} {props.member.role && <TeamRole role={props.member.role} />}
</div> </div>
<span <div className="flex items-center">
className="block text-sm text-gray-600" <span
data-testid="member-email" className="block text-sm text-gray-600"
data-email={props.member.email}> data-testid="member-email"
{props.member.email} data-email={props.member.email}>
</span> {props.member.email}
</span>
{bookingLink && (
<>
<span className="mx-2 block text-gray-600">•</span>
<span className="block text-sm text-gray-600">{bookingLink}</span>
</>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -60,6 +60,7 @@ export default function TeamList(props: Props) {
<> <>
{props.teams.map( {props.teams.map(
(team, i) => (team, i) =>
team.role !== "MEMBER" &&
i === 0 && ( i === 0 && (
<div className="bg-gray-100 p-6"> <div className="bg-gray-100 p-6">
<h3 className="mb-4 text-sm font-semibold text-gray-900">{t("recommended_next_steps")}</h3> <h3 className="mb-4 text-sm font-semibold text-gray-900">{t("recommended_next_steps")}</h3>
@@ -56,9 +56,17 @@ export default function TeamListItem(props: Props) {
const [openMemberInvitationModal, setOpenMemberInvitationModal] = useState(false); const [openMemberInvitationModal, setOpenMemberInvitationModal] = useState(false);
const teamQuery = trpc.viewer.teams.get.useQuery({ teamId: team?.id }); const teamQuery = trpc.viewer.teams.get.useQuery({ teamId: team?.id });
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({ const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
async onSuccess() { async onSuccess(data) {
await utils.viewer.teams.get.invalidate(); await utils.viewer.teams.get.invalidate();
setOpenMemberInvitationModal(false); setOpenMemberInvitationModal(false);
if (data.sendEmailInvitation) {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
}, },
onError: (error) => { onError: (error) => {
showToast(error.message, "error"); showToast(error.message, "error");
@@ -270,17 +278,15 @@ export default function TeamListItem(props: Props) {
<DropdownMenuItem> <DropdownMenuItem>
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <DropdownItem
type="button"
color="destructive" color="destructive"
size="lg" type="button"
StartIcon={FiLogOut} StartIcon={FiLogOut}
className="w-full rounded-none"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}}> }}>
{t("leave_team")} {t("leave_team")}
</Button> </DropdownItem>
</DialogTrigger> </DialogTrigger>
<ConfirmationDialogContent <ConfirmationDialogContent
variety="danger" variety="danger"
@@ -71,7 +71,7 @@ export function TeamsListing() {
emptyTitle="no_teams" emptyTitle="no_teams"
emptyDescription="no_teams_description" emptyDescription="no_teams_description"
features={features} features={features}
background="/banners/teams.jpg" background="/tips/teams.jpg"
buttons={ buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2"> <div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup> <ButtonGroup>
@@ -32,9 +32,17 @@ const MembersView = () => {
); );
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({ const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
async onSuccess() { async onSuccess(data) {
await utils.viewer.teams.get.invalidate(); await utils.viewer.teams.get.invalidate();
setShowMemberInvitationModal(false); setShowMemberInvitationModal(false);
if (data.sendEmailInvitation) {
showToast(
t("email_invite_team", {
email: data.usernameOrEmail,
}),
"success"
);
}
}, },
onError: (error) => { onError: (error) => {
showToast(error.message, "error"); showToast(error.message, "error");
@@ -7,7 +7,6 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { defaultHandler } from "@calcom/lib/server"; import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import type { Prisma, WorkflowReminder } from "@calcom/prisma/client";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { VariablesType } from "../lib/reminders/templates/customTemplate"; import type { VariablesType } from "../lib/reminders/templates/customTemplate";
@@ -31,7 +30,28 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return; return;
} }
//delete all scheduled email reminders where scheduled is past current date //delete batch_ids with already past scheduled date from scheduled_sends
const remindersToDelete = await prisma.workflowReminder.findMany({
where: {
method: WorkflowMethods.EMAIL,
cancelled: true,
scheduledDate: {
lte: dayjs().toISOString(),
},
},
});
for (const reminder of remindersToDelete) {
try {
await client.request({
url: `/v3/user/scheduled_sends/${reminder.referenceId}`,
method: "DELETE",
});
} catch (error) {
console.log(`Error deleting batch id from scheduled_sends: ${error}`);
}
}
await prisma.workflowReminder.deleteMany({ await prisma.workflowReminder.deleteMany({
where: { where: {
method: WorkflowMethods.EMAIL, method: WorkflowMethods.EMAIL,
@@ -45,16 +65,15 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const remindersToCancel = await prisma.workflowReminder.findMany({ const remindersToCancel = await prisma.workflowReminder.findMany({
where: { where: {
cancelled: true, cancelled: true,
scheduled: true, //if it is false then they are already cancelled
scheduledDate: { scheduledDate: {
lte: dayjs().add(1, "hour").toISOString(), lte: dayjs().add(1, "hour").toISOString(),
}, },
}, },
}); });
try { for (const reminder of remindersToCancel) {
const workflowRemindersToDelete: Prisma.Prisma__WorkflowReminderClient<WorkflowReminder, never>[] = []; try {
for (const reminder of remindersToCancel) {
await client.request({ await client.request({
url: "/v3/user/scheduled_sends", url: "/v3/user/scheduled_sends",
method: "POST", method: "POST",
@@ -64,17 +83,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}, },
}); });
const workflowReminderToDelete = prisma.workflowReminder.delete({ await prisma.workflowReminder.update({
where: { where: {
id: reminder.id, id: reminder.id,
}, },
data: {
scheduled: false, // to know which reminder already got cancelled (to avoid error from cancelling the same reminders again)
},
}); });
} catch (error) {
workflowRemindersToDelete.push(workflowReminderToDelete); console.log(`Error cancelling scheduled Emails: ${error}`);
} }
await Promise.all(workflowRemindersToDelete);
} catch (error) {
console.log(`Error cancelling scheduled Emails: ${error}`);
} }
//find all unscheduled Email reminders //find all unscheduled Email reminders
@@ -409,9 +409,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
onChange={(val) => { onChange={(val) => {
const isAlreadyVerified = !!verifiedNumbers const isAlreadyVerified = !!verifiedNumbers
?.concat([]) ?.concat([])
.find( .find((number) => number.replace(/\s/g, "") === val?.replace(/\s/g, ""));
(number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
);
setNumberVerified(isAlreadyVerified); setNumberVerified(isAlreadyVerified);
onChange(val); onChange(val);
}} }}
+41 -36
View File
@@ -5,6 +5,7 @@ import { useState, useEffect } from "react";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
@@ -78,9 +79,7 @@ function WorkflowsPage() {
} }
}, [session.status, query.isLoading, allWorkflowsData]); }, [session.status, query.isLoading, allWorkflowsData]);
if (!query.data) return null; const profileOptions = query?.data?.profiles
const profileOptions = query.data.profiles
.filter((profile) => !profile.readOnly) .filter((profile) => !profile.readOnly)
.map((profile) => { .map((profile) => {
return { return {
@@ -97,10 +96,11 @@ function WorkflowsPage() {
title={t("workflows")} title={t("workflows")}
subtitle={t("workflows_to_automate_notifications")} subtitle={t("workflows_to_automate_notifications")}
CTA={ CTA={
query.data.profiles.length === 1 && query?.data?.profiles.length === 1 &&
session.data?.hasValidLicense && session.data?.hasValidLicense &&
allWorkflowsData?.workflows && allWorkflowsData?.workflows &&
allWorkflowsData?.workflows.length > 0 ? ( allWorkflowsData?.workflows.length &&
profileOptions ? (
<CreateButton <CreateButton
subtitle={t("new_workflow_subtitle").toUpperCase()} subtitle={t("new_workflow_subtitle").toUpperCase()}
options={profileOptions} options={profileOptions}
@@ -110,40 +110,42 @@ function WorkflowsPage() {
isLoading={createMutation.isLoading} isLoading={createMutation.isLoading}
disableMobileButton={true} disableMobileButton={true}
/> />
) : ( ) : null
<></>
)
}> }>
<LicenseRequired> <LicenseRequired>
{isLoading ? ( {isLoading ? (
<SkeletonLoader /> <SkeletonLoader />
) : ( ) : (
<> <>
{query.data.profiles.length > 1 && {query?.data?.profiles &&
allWorkflowsData?.workflows && query?.data?.profiles.length > 1 &&
allWorkflowsData.workflows.length > 0 && ( allWorkflowsData?.workflows &&
<div className="mb-4 flex"> allWorkflowsData.workflows.length &&
<Filter profileOptions ? (
profiles={query.data.profiles} <div className="mb-4 flex">
checked={checkedFilterItems} <Filter
setChecked={setCheckedFilterItems} profiles={query.data.profiles}
checked={checkedFilterItems}
setChecked={setCheckedFilterItems}
/>
<div className="ml-auto">
<CreateButton
subtitle={t("new_workflow_subtitle").toUpperCase()}
options={profileOptions}
createFunction={(teamId?: number) => createMutation.mutate({ teamId })}
isLoading={createMutation.isLoading}
disableMobileButton={true}
/> />
<div className="ml-auto">
<CreateButton
subtitle={t("new_workflow_subtitle").toUpperCase()}
options={profileOptions}
createFunction={(teamId?: number) => createMutation.mutate({ teamId })}
isLoading={createMutation.isLoading}
disableMobileButton={true}
/>
</div>
</div> </div>
)} </div>
<WorkflowList ) : null}
workflows={filteredWorkflows} {profileOptions && profileOptions?.length ? (
profileOptions={profileOptions} <WorkflowList
hasNoWorkflows={!allWorkflowsData?.workflows || allWorkflowsData?.workflows.length === 0} workflows={filteredWorkflows}
/> profileOptions={profileOptions}
hasNoWorkflows={!allWorkflowsData?.workflows || allWorkflowsData?.workflows.length === 0}
/>
) : null}
</> </>
)} )}
</LicenseRequired> </LicenseRequired>
@@ -157,6 +159,7 @@ const Filter = (props: {
slug: string | null; slug: string | null;
name: string | null; name: string | null;
teamId: number | null | undefined; teamId: number | null | undefined;
image?: string | undefined | null;
}[]; }[];
checked: { checked: {
userId: number | null; userId: number | null;
@@ -171,7 +174,9 @@ const Filter = (props: {
}) => { }) => {
const session = useSession(); const session = useSession();
const userId = session.data?.user.id || 0; const userId = session.data?.user.id || 0;
const userName = session.data?.user.name || ""; const user = session.data?.user.name || "";
const userName = session.data?.user.username;
const userAvatar = WEBAPP_URL + "/" + userName + "/avatar.png";
const teams = props.profiles.filter((profile) => !!profile.teamId); const teams = props.profiles.filter((profile) => !!profile.teamId);
const { checked, setChecked } = props; const { checked, setChecked } = props;
@@ -183,9 +188,9 @@ const Filter = (props: {
<AnimatedPopover text={noFilter ? "All" : "Filtered"}> <AnimatedPopover text={noFilter ? "All" : "Filtered"}>
<div className="item-center flex px-4 py-[6px] focus-within:bg-gray-100 hover:cursor-pointer hover:bg-gray-50"> <div className="item-center flex px-4 py-[6px] focus-within:bg-gray-100 hover:cursor-pointer hover:bg-gray-50">
<Avatar <Avatar
imageSrc="" imageSrc={userAvatar || ""}
size="sm" size="sm"
alt={`${userName} Avatar`} alt={`${user} Avatar`}
gravatarFallbackMd5="fallback" gravatarFallbackMd5="fallback"
className="self-center" className="self-center"
asChild asChild
@@ -193,7 +198,7 @@ const Filter = (props: {
<label <label
htmlFor="yourWorkflows" htmlFor="yourWorkflows"
className="ml-2 mr-auto self-center truncate text-sm font-medium text-gray-700"> className="ml-2 mr-auto self-center truncate text-sm font-medium text-gray-700">
{userName} {user}
</label> </label>
<input <input
@@ -220,7 +225,7 @@ const Filter = (props: {
className="item-center flex px-4 py-[6px] focus-within:bg-gray-100 hover:cursor-pointer hover:bg-gray-50" className="item-center flex px-4 py-[6px] focus-within:bg-gray-100 hover:cursor-pointer hover:bg-gray-50"
key={`${profile.teamId || 0}`}> key={`${profile.teamId || 0}`}>
<Avatar <Avatar
imageSrc="" imageSrc={profile.image || ""}
size="sm" size="sm"
alt={`${profile.slug} Avatar`} alt={`${profile.slug} Avatar`}
gravatarFallbackMd5="fallback" gravatarFallbackMd5="fallback"
@@ -14,4 +14,3 @@ export const CardInsights = (props: ICardProps) => {
</Card> </Card>
); );
}; };
+6 -1
View File
@@ -1,5 +1,6 @@
import type { User } from "@prisma/client"; import type { User } from "@prisma/client";
import { signOut, useSession } from "next-auth/react"; import { signOut, useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
import type { NextRouter } from "next/router"; import type { NextRouter } from "next/router";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -16,7 +17,6 @@ import { TeamsUpgradeBanner } from "@calcom/features/ee/teams/components";
import { useFlagMap } from "@calcom/features/flags/context/provider"; import { useFlagMap } from "@calcom/features/flags/context/provider";
import { KBarContent, KBarRoot, KBarTrigger } from "@calcom/features/kbar/Kbar"; import { KBarContent, KBarRoot, KBarTrigger } from "@calcom/features/kbar/Kbar";
import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog"; import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog";
import { Tips } from "@calcom/features/tips";
import AdminPasswordBanner from "@calcom/features/users/components/AdminPasswordBanner"; import AdminPasswordBanner from "@calcom/features/users/components/AdminPasswordBanner";
import CustomBranding from "@calcom/lib/CustomBranding"; import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
@@ -69,6 +69,11 @@ import {
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider"; import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
import { TeamInviteBadge } from "./TeamInviteBadge"; import { TeamInviteBadge } from "./TeamInviteBadge";
// need to import without ssr to prevent hydration errors
const Tips = dynamic(() => import("@calcom/features/tips").then((mod) => mod.Tips), {
ssr: false,
});
/* TODO: Migate this */ /* TODO: Migate this */
export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString(); export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString();
+28 -9
View File
@@ -1,5 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useEffect, useState } from "react"; import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage"; import { localStorage } from "@calcom/lib/webstorage";
@@ -86,14 +86,40 @@ export const tips = [
description: "Free video conferencing with recording", description: "Free video conferencing with recording",
href: "https://go.cal.com/video", href: "https://go.cal.com/video",
}, },
{
id: 11,
thumbnailUrl: "https://img.youtube.com/vi/KTg_qzA9NEc/0.jpg",
mediaLink: "https://go.cal.com/insights",
title: "Insights",
description: "Get a better understanding of your business",
href: "https://go.cal.com/insights",
},
]; ];
const reversedTips = tips.slice(0).reverse();
export default function Tips() { export default function Tips() {
const [animationRef] = useAutoAnimate<HTMLDivElement>(); const [animationRef] = useAutoAnimate<HTMLDivElement>();
const { t } = useLocale(); const { t } = useLocale();
const [list, setList] = useState<typeof tips>([]); const [list, setList] = useState<typeof tips>(() => {
if (typeof window === "undefined") {
return reversedTips;
}
try {
const removedTipsString = localStorage.getItem("removedTipsIds");
if (removedTipsString !== null) {
const removedTipsIds = removedTipsString.split(",").map((id) => parseInt(id, 10));
const filteredTips = reversedTips.filter((tip) => removedTipsIds.indexOf(tip.id) === -1);
return filteredTips;
} else {
return reversedTips;
}
} catch {
return reversedTips;
}
});
const handleRemoveItem = (id: number) => { const handleRemoveItem = (id: number) => {
setList((currentItems) => { setList((currentItems) => {
@@ -109,13 +135,6 @@ export default function Tips() {
}); });
}; };
useEffect(() => {
const reversedTips = tips.slice(0).reverse();
const removedTipsString = localStorage.getItem("removedTipsIds") || "";
const removedTipsIds = removedTipsString.split(",").map((id) => parseInt(id, 10));
const filteredTips = reversedTips.filter((tip) => removedTipsIds.indexOf(tip.id) === -1);
setList(() => [...filteredTips]);
}, []);
const baseOriginalList = list.slice(0).reverse(); const baseOriginalList = list.slice(0).reverse();
return ( return (
<div <div
@@ -3,8 +3,20 @@ import type { WebhookTriggerEvents } from "@prisma/client";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Badge, Button, showToast, Switch, Tooltip } from "@calcom/ui"; import {
import { FiAlertCircle, FiTrash } from "@calcom/ui/components/icon"; Badge,
Button,
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
showToast,
Switch,
Tooltip,
} from "@calcom/ui";
import { FiAlertCircle, FiEdit, FiMoreHorizontal, FiTrash } from "@calcom/ui/components/icon";
type WebhookProps = { type WebhookProps = {
id: string; id: string;
@@ -39,21 +51,30 @@ export default function WebhookListItem(props: {
}, },
}); });
const onDeleteWebhook = () => {
// TODO: Confimation dialog before deleting
deleteWebhook.mutate({ id: webhook.id, eventTypeId: webhook.eventTypeId || undefined });
};
return ( return (
<div className={classNames("flex w-full justify-between p-4", props.lastItem ? "" : "border-b")}> <div className={classNames("flex w-full justify-between p-4", props.lastItem ? "" : "border-b")}>
<div> <div className="w-full truncate">
<p className="text-sm font-medium text-gray-900">{webhook.subscriberUrl}</p> <p className="truncate text-sm font-medium text-gray-900">{webhook.subscriberUrl}</p>
<Tooltip content={t("triggers_when")}> <Tooltip content={t("triggers_when")}>
<div className="mt-2.5 w-4/5"> <div className="flex w-4/5 flex-wrap">
{webhook.eventTriggers.map((trigger) => ( {webhook.eventTriggers.map((trigger) => (
<Badge key={trigger} className="ltr:mr-2 rtl:ml-2" variant="gray" startIcon={FiAlertCircle}> <Badge
key={trigger}
className="mt-2.5 basis-1/5 ltr:mr-2 rtl:ml-2"
variant="gray"
startIcon={FiAlertCircle}>
{t(`${trigger.toLowerCase()}`)} {t(`${trigger.toLowerCase()}`)}
</Badge> </Badge>
))} ))}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>
<div className="flex items-center space-x-4"> <div className="ml-2 flex items-center space-x-4">
<Switch <Switch
defaultChecked={webhook.active} defaultChecked={webhook.active}
onCheckedChange={() => onCheckedChange={() =>
@@ -65,18 +86,35 @@ export default function WebhookListItem(props: {
}) })
} }
/> />
<Button color="secondary" onClick={props.onEditWebhook}> <Button className="hidden lg:flex" color="secondary" onClick={props.onEditWebhook}>
{t("edit")} {t("edit")}
</Button> </Button>
<Button <Button
className="hidden lg:flex"
color="destructive" color="destructive"
StartIcon={FiTrash} StartIcon={FiTrash}
variant="icon" variant="icon"
onClick={() => { onClick={onDeleteWebhook}
// TODO: Confimation dialog before deleting
deleteWebhook.mutate({ id: webhook.id, eventTypeId: webhook.eventTypeId || undefined });
}}
/> />
<Dropdown>
<DropdownMenuTrigger asChild>
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem StartIcon={FiEdit} color="secondary" onClick={props.onEditWebhook}>
{t("edit")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem StartIcon={FiTrash} color="destructive" onClick={onDeleteWebhook}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div> </div>
</div> </div>
); );
+16 -10
View File
@@ -28,6 +28,10 @@ export interface GenericImageProps {
export interface ScreenshotImageProps { export interface ScreenshotImageProps {
image: string; image: string;
/**
* Fallback image to use if the image prop fails to load.
*/
fallbackImage: string;
} }
interface WrapperProps { interface WrapperProps {
@@ -242,17 +246,19 @@ export const Generic = ({ title, description }: GenericImageProps) => (
</Wrapper> </Wrapper>
); );
export const ScreenShot = ({ image }: ScreenshotImageProps) => ( export const ScreenShot = ({ image, fallbackImage }: ScreenshotImageProps) => (
<Wrapper rotateBackground> <Wrapper rotateBackground>
<div tw="h-full w-full flex flex-col justify-center items-center"> <div tw="relative h-full w-full flex flex-col justify-center items-center">
<img <div tw="relative mt-[140px] flex rounded-2xl" style={{ boxShadow: "0 0 45px -3px rgba(0,0,0,.3)" }}>
src={image} <img
width="1024" src={fallbackImage}
height="576" tw="absolute inset-0 rounded-2xl"
alt="screenshot" width="1024"
tw="rounded-2xl mt-[140px] object-cover" height="576"
style={{ boxShadow: "0 0 45px -3px rgba(0,0,0,.3)" }} alt="screenshot"
/> />
<img src={image} width="1024" height="576" tw="rounded-2xl" alt="screenshot" />
</div>
</div> </div>
</Wrapper> </Wrapper>
); );
+1
View File
@@ -235,6 +235,7 @@ export const createdEventSchema = z
id: z.string(), id: z.string(),
password: z.union([z.string(), z.undefined()]), password: z.union([z.string(), z.undefined()]),
onlineMeetingUrl: z.string().nullable(), onlineMeetingUrl: z.string().nullable(),
iCalUID: z.string().optional(),
}) })
.passthrough(); .passthrough();
+1 -1
View File
@@ -11,7 +11,7 @@ import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
type CreateContextOptions = CreateNextContextOptions | GetServerSidePropsContext; type CreateContextOptions = CreateNextContextOptions | GetServerSidePropsContext;
type CreateInnerContextOptions = { export type CreateInnerContextOptions = {
session: Session | null; session: Session | null;
locale: string; locale: string;
user?: Omit< user?: Omit<
+7 -2
View File
@@ -46,7 +46,7 @@ import { EventTypeMetaDataSchema, userMetadata } from "@calcom/prisma/zod-utils"
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { authedProcedure, mergeRouters, publicProcedure, router } from "../trpc"; import { authedProcedure, getLocale, mergeRouters, publicProcedure, router } from "../trpc";
import { apiKeysRouter } from "./viewer/apiKeys"; import { apiKeysRouter } from "./viewer/apiKeys";
import { appsRouter } from "./viewer/apps"; import { appsRouter } from "./viewer/apps";
import { authRouter } from "./viewer/auth"; import { authRouter } from "./viewer/auth";
@@ -66,7 +66,8 @@ const publicViewerRouter = router({
return ctx.session; return ctx.session;
}), }),
i18n: publicProcedure.query(async ({ ctx }) => { i18n: publicProcedure.query(async ({ ctx }) => {
const { locale, i18n } = ctx; const { locale, i18n } = await getLocale(ctx);
return { return {
i18n, i18n,
locale, locale,
@@ -1032,6 +1033,8 @@ const loggedInViewerRouter = router({
select: { select: {
recurringEvent: true, recurringEvent: true,
title: true, title: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
}, },
}, },
uid: true, uid: true,
@@ -1110,6 +1113,8 @@ const loggedInViewerRouter = router({
location: booking.location, location: booking.location,
destinationCalendar: booking.destinationCalendar || booking.user?.destinationCalendar, destinationCalendar: booking.destinationCalendar || booking.user?.destinationCalendar,
cancellationReason: "Payment method removed by organizer", cancellationReason: "Payment method removed by organizer",
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
}); });
} }
}); });
@@ -672,6 +672,8 @@ export const bookingsRouter = router({
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent), recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
location, location,
destinationCalendar: booking?.destinationCalendar || booking?.user?.destinationCalendar, destinationCalendar: booking?.destinationCalendar || booking?.user?.destinationCalendar,
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
}; };
const eventManager = new EventManager(ctx.user); const eventManager = new EventManager(ctx.user);
+3 -17
View File
@@ -85,13 +85,13 @@ export const viewerTeamsRouter = router({
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { slug, name, logo } = input; const { slug, name, logo } = input;
const nameCollisions = await ctx.prisma.team.findFirst({ const slugCollisions = await ctx.prisma.team.findFirst({
where: { where: {
slug: slug, slug: slug,
}, },
}); });
if (nameCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "Team name already taken." }); if (slugCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "team_url_taken" });
// Ensure that the user is not duplicating a requested team // Ensure that the user is not duplicating a requested team
const duplicatedRequest = await ctx.prisma.team.findFirst({ const duplicatedRequest = await ctx.prisma.team.findFirst({
@@ -386,6 +386,7 @@ export const viewerTeamsRouter = router({
} }
} }
if (IS_TEAM_BILLING_ENABLED) await updateQuantitySubscriptionFromStripe(input.teamId); if (IS_TEAM_BILLING_ENABLED) await updateQuantitySubscriptionFromStripe(input.teamId);
return input;
}), }),
acceptOrLeave: authedProcedure acceptOrLeave: authedProcedure
.input( .input(
@@ -586,21 +587,6 @@ export const viewerTeamsRouter = router({
}, },
}); });
}), }),
validateTeamSlug: authedProcedure
.input(
z.object({
slug: z.string(),
})
)
.query(async ({ ctx, input }) => {
const team = await ctx.prisma.team.findFirst({
where: {
slug: input.slug,
},
});
return !team;
}),
publish: authedProcedure publish: authedProcedure
.input( .input(
z.object({ z.object({
+16 -17
View File
@@ -10,7 +10,7 @@ import prisma from "@calcom/prisma";
import type { Maybe } from "@trpc/server"; import type { Maybe } from "@trpc/server";
import { initTRPC, TRPCError } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import type { createContextInner } from "./createContext"; import type { createContextInner, CreateInnerContextOptions } from "./createContext";
async function getUserFromSession({ session }: { session: Maybe<Session> }) { async function getUserFromSession({ session }: { session: Maybe<Session> }) {
if (!session?.user?.id) { if (!session?.user?.id) {
@@ -104,26 +104,25 @@ const perfMiddleware = t.middleware(async ({ path, type, next }) => {
return result; return result;
}); });
export const isAuthed = t.middleware(async ({ ctx: { session, locale, ...ctx }, next }) => { export const getLocale = async (ctx: CreateInnerContextOptions) => {
const user = await getUserFromSession({ session }); const user = await getUserFromSession({ session: ctx.session });
const i18n =
user?.locale && user?.locale !== ctx.locale
? await serverSideTranslations(user.locale, ["common", "vital"])
: ctx.i18n;
const locale = user?.locale || ctx.locale;
return { user, i18n, session: ctx.session, locale };
};
export const isAuthed = t.middleware(async ({ ctx, next }) => {
const { user, session, locale, i18n } = await getLocale(ctx);
if (!user || !session) { if (!user || !session) {
throw new TRPCError({ code: "UNAUTHORIZED" }); throw new TRPCError({ code: "UNAUTHORIZED" });
} }
const i18n =
user.locale && user.locale !== locale
? await serverSideTranslations(user.locale, ["common", "vital"])
: ctx.i18n;
locale = user.locale || locale;
return next({ return next({
ctx: { ctx: { locale, i18n, user: { ...user, locale }, session },
i18n,
// infers that `user` and `session` are non-nullable to downstream procedures
session,
user: {
...user,
locale,
},
},
}); });
}); });
+2
View File
@@ -61,6 +61,7 @@ export type NewCalendarEventType = {
password: string; password: string;
url: string; url: string;
additionalInfo: AdditionalInfo; additionalInfo: AdditionalInfo;
iCalUID?: string | null;
}; };
export type CalendarEventType = { export type CalendarEventType = {
@@ -172,6 +173,7 @@ export interface CalendarEvent {
seatsShowAttendees?: boolean | null; seatsShowAttendees?: boolean | null;
attendeeSeatId?: string; attendeeSeatId?: string;
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
iCalUID?: string | null;
// It has responses to all the fields(system + user) // It has responses to all the fields(system + user)
responses?: CalEventResponses | null; responses?: CalEventResponses | null;
+1 -3
View File
@@ -1,7 +1,4 @@
import { DestinationCalendar } from "@prisma/client";
import type { CalendarEvent } from "./Calendar"; import type { CalendarEvent } from "./Calendar";
import type { Event } from "./Event";
export interface PartialReference { export interface PartialReference {
id?: number; id?: number;
@@ -19,6 +16,7 @@ export interface EventResult<T> {
appName: string; appName: string;
success: boolean; success: boolean;
uid: string; uid: string;
iCalUID?: string | null;
createdEvent?: T; createdEvent?: T;
updatedEvent?: T | T[]; updatedEvent?: T | T[];
originalEvent: CalendarEvent; originalEvent: CalendarEvent;
+4 -6
View File
@@ -65,6 +65,7 @@ export function Card({
thumbnailUrl, thumbnailUrl,
learnMore, learnMore,
}: BaseCardProps) { }: BaseCardProps) {
const LinkComponent = learnMore && learnMore.href.startsWith("https") ? "a" : Link;
return ( return (
<div <div
className={classNames( className={classNames(
@@ -142,17 +143,14 @@ export function Card({
{variant === "SidebarCard" && ( {variant === "SidebarCard" && (
<div className="mt-2 flex items-center justify-between"> <div className="mt-2 flex items-center justify-between">
{learnMore && ( {learnMore && (
<Link <LinkComponent
href={learnMore.href} href={learnMore.href}
onClick={actionButton?.onClick} onClick={actionButton?.onClick}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-xs font-medium" className="text-xs font-medium">
// NextJS thinks this is an internal link and tries to prefetch it (also on hover), which fails
// TODO: Extract this into a constant to handle /docs & /developer as well
prefetch={!learnMore.href.startsWith("https://cal.com/blog")}>
{learnMore.text} {learnMore.text}
</Link> </LinkComponent>
)} )}
<button <button
className="p-0 text-xs font-normal text-gray-600 hover:text-gray-800" className="p-0 text-xs font-normal text-gray-600 hover:text-gray-800"
+1 -1
View File
@@ -77,7 +77,7 @@ export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps
<DialogPrimitive.Content <DialogPrimitive.Content
{...props} {...props}
className={classNames( className={classNames(
"fadeIn fixed left-1/2 top-1/2 z-50 min-w-[270px] -translate-x-1/2 -translate-y-1/2 rounded bg-white text-left shadow-xl focus-visible:outline-none sm:w-full sm:align-middle", "fadeIn fixed left-1/2 top-1/2 z-50 w-full max-w-[22rem] -translate-x-1/2 -translate-y-1/2 rounded bg-white text-left shadow-xl focus-visible:outline-none sm:align-middle",
props.size == "xl" props.size == "xl"
? "p-8 sm:max-w-[90rem]" ? "p-8 sm:max-w-[90rem]"
: props.size == "lg" : props.size == "lg"
@@ -18,12 +18,12 @@ const ColorPicker = (props: ColorPickerProps) => {
const [color, setColor] = useState(init); const [color, setColor] = useState(init);
return ( return (
<div className="mt-1 flex items-center justify-center"> <div className="mt-1 flex h-[38px] items-center justify-center">
<Popover.Root> <Popover.Root>
<div className="flex items-center rounded-l-md border border-r-0 border-gray-300 p-1.5"> <div className="flex h-full w-10 items-center items-center justify-center rounded-l-md border border-r-0 border-gray-300">
<Popover.Trigger asChild> <Popover.Trigger asChild>
<button <button
className="h-6 w-6 rounded-sm border-2 border-gray-900" className="h-5 w-5 rounded-sm"
aria-label="pick colors" aria-label="pick colors"
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
/> />
@@ -44,7 +44,7 @@ const ColorPicker = (props: ColorPickerProps) => {
</Popover.Root> </Popover.Root>
<HexColorInput <HexColorInput
className="block w-full border border-gray-300 px-3 py-2 ltr:rounded-r-md rtl:rounded-l-md sm:text-sm" className="block h-full w-full border border-gray-300 px-3 py-2 ltr:rounded-r-md rtl:rounded-l-md sm:text-sm"
color={color} color={color}
onChange={(val) => { onChange={(val) => {
setColor(val); setColor(val);
@@ -135,12 +135,12 @@ export const DropdownItem = (props: DropdownItemProps) => {
<ButtonOrLink <ButtonOrLink
{...rest} {...rest}
className={classNames( className={classNames(
"inline-flex w-full items-center px-3 py-2 text-gray-700 hover:text-gray-900 disabled:cursor-not-allowed", "inline-flex w-full items-center gap-x-3 px-3 py-2 text-gray-700 hover:text-gray-900 disabled:cursor-not-allowed",
color === "destructive" ? "hover:bg-red-100 hover:text-red-700" : "hover:bg-gray-100" color === "destructive" ? "hover:bg-red-100 hover:text-red-700" : "hover:bg-gray-100"
)}> )}>
<> <>
{StartIcon && <StartIcon className="h-4 w-4" />} {StartIcon && <StartIcon className="h-4 w-4" />}
<div className="mx-3 text-sm font-medium leading-5">{children}</div> <div className="text-sm font-medium leading-5">{children}</div>
{EndIcon && <EndIcon className="h-4 w-4" />} {EndIcon && <EndIcon className="h-4 w-4" />}
</> </>
</ButtonOrLink> </ButtonOrLink>
+83 -1
View File
@@ -4888,7 +4888,7 @@ __metadata:
"@types/uuid": 8.3.1 "@types/uuid": 8.3.1
"@vercel/edge-config": ^0.1.1 "@vercel/edge-config": ^0.1.1
"@vercel/edge-functions-ui": ^0.2.1 "@vercel/edge-functions-ui": ^0.2.1
"@vercel/og": ^0.0.21 "@vercel/og": ^0.5.0
accept-language-parser: ^1.5.0 accept-language-parser: ^1.5.0
async: ^3.2.4 async: ^3.2.4
autoprefixer: ^10.4.12 autoprefixer: ^10.4.12
@@ -9747,6 +9747,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@resvg/resvg-wasm@npm:2.4.1":
version: 2.4.1
resolution: "@resvg/resvg-wasm@npm:2.4.1"
checksum: 855aa1e5f1b228b9d2fe87e5d12b628c64b43a513fb67cd78b2eb447684d6b0861543a77ff8b878b21bf89b048ffeff73267685967d9f20cb8ef2c350dac5c81
languageName: node
linkType: hard
"@rollup/plugin-inject@npm:^4.0.0": "@rollup/plugin-inject@npm:^4.0.0":
version: 4.0.4 version: 4.0.4
resolution: "@rollup/plugin-inject@npm:4.0.4" resolution: "@rollup/plugin-inject@npm:4.0.4"
@@ -13535,6 +13542,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@vercel/og@npm:^0.5.0":
version: 0.5.0
resolution: "@vercel/og@npm:0.5.0"
dependencies:
"@resvg/resvg-wasm": 2.4.1
satori: 0.4.4
yoga-wasm-web: 0.3.3
checksum: bf6a361ad7f29fc32c04692fd19162f7b6c1dea9fa0f30bd601d810c853997c27f95e8e4e376dbf217bcf510e67e9f745db945426404e58f1806ae776604431d
languageName: node
linkType: hard
"@vitejs/plugin-react@npm:1.3.2": "@vitejs/plugin-react@npm:1.3.2":
version: 1.3.2 version: 1.3.2
resolution: "@vitejs/plugin-react@npm:1.3.2" resolution: "@vitejs/plugin-react@npm:1.3.2"
@@ -15699,6 +15717,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"base64-js@npm:0.0.8":
version: 0.0.8
resolution: "base64-js@npm:0.0.8"
checksum: e95d2fa4b9000789fedd1e1ebac7830a70f5b1c06c72b2cf7862a48335af5d42229c0deda5085b2c8e403e479b612a55cd682bd4438838980d07cfc21c91e750
languageName: node
linkType: hard
"base64-js@npm:^1.0.2, base64-js@npm:^1.1.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": "base64-js@npm:^1.0.2, base64-js@npm:^1.1.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1":
version: 1.5.1 version: 1.5.1
resolution: "base64-js@npm:1.5.1" resolution: "base64-js@npm:1.5.1"
@@ -26739,6 +26764,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"linebreak@npm:^1.1.0":
version: 1.1.0
resolution: "linebreak@npm:1.1.0"
dependencies:
base64-js: 0.0.8
unicode-trie: ^2.0.0
checksum: 65cb66900b4b60d99bb761fb4143d8673ad5dd57dd850fcd921fe425a5a8e3c4416ebee76a68058a90d88b5033a50b210f522558a7e0c7d95ca907b5a2b52520
languageName: node
linkType: hard
"lines-and-columns@npm:^1.1.6": "lines-and-columns@npm:^1.1.6":
version: 1.2.4 version: 1.2.4
resolution: "lines-and-columns@npm:1.2.4" resolution: "lines-and-columns@npm:1.2.4"
@@ -30618,6 +30653,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pako@npm:^0.2.5":
version: 0.2.9
resolution: "pako@npm:0.2.9"
checksum: 055f9487cd57fbb78df84315873bbdd089ba286f3499daed47d2effdc6253e981f5db6898c23486de76d4a781559f890d643bd3a49f70f1b4a18019c98aa5125
languageName: node
linkType: hard
"pako@npm:^1.0.5, pako@npm:~1.0.5": "pako@npm:^1.0.5, pako@npm:~1.0.5":
version: 1.0.11 version: 1.0.11
resolution: "pako@npm:1.0.11" resolution: "pako@npm:1.0.11"
@@ -34581,6 +34623,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"satori@npm:0.4.4":
version: 0.4.4
resolution: "satori@npm:0.4.4"
dependencies:
"@shuding/opentype.js": 1.4.0-beta.0
css-background-parser: ^0.1.0
css-box-shadow: 1.0.0-3
css-to-react-native: ^3.0.0
emoji-regex: ^10.2.1
linebreak: ^1.1.0
postcss-value-parser: ^4.2.0
yoga-wasm-web: ^0.3.3
checksum: 749f3afd10e21d40a720d24186042f741072e8f7ed567541672da35302eac63f31e53da4f7d6db7da87e82dbb79146ac03fc9d29b5fd1ec7b199bd5d4701cb57
languageName: node
linkType: hard
"sax@npm:>=0.6.0, sax@npm:^1.2.4": "sax@npm:>=0.6.0, sax@npm:^1.2.4":
version: 1.2.4 version: 1.2.4
resolution: "sax@npm:1.2.4" resolution: "sax@npm:1.2.4"
@@ -36990,6 +37048,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tiny-inflate@npm:^1.0.0":
version: 1.0.3
resolution: "tiny-inflate@npm:1.0.3"
checksum: 4086a1f8938dafa4a20c63b099aeb47bf8fef5aca991bf4ea4b35dd2684fa52363b2c19b3e76660311e7613cb7c4f063bc48751b9bdf9555e498d997c30bc2d6
languageName: node
linkType: hard
"tiny-invariant@npm:^1.2.0": "tiny-invariant@npm:^1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "tiny-invariant@npm:1.2.0" resolution: "tiny-invariant@npm:1.2.0"
@@ -38030,6 +38095,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"unicode-trie@npm:^2.0.0":
version: 2.0.0
resolution: "unicode-trie@npm:2.0.0"
dependencies:
pako: ^0.2.5
tiny-inflate: ^1.0.0
checksum: 19e637ce20953ec1fbfa9087abef4746a50352679b833be27924e4ba7ad753cc4073b74263747ccfccb5e38b30b17468cbb96f361eb49903ff8602396280b5a4
languageName: node
linkType: hard
"unified@npm:9.2.0": "unified@npm:9.2.0":
version: 9.2.0 version: 9.2.0
resolution: "unified@npm:9.2.0" resolution: "unified@npm:9.2.0"
@@ -40414,6 +40489,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"yoga-wasm-web@npm:0.3.3, yoga-wasm-web@npm:^0.3.3":
version: 0.3.3
resolution: "yoga-wasm-web@npm:0.3.3"
checksum: ff65192a832975ff531a1b6eae160c2da859c250feaa58b6389b684f9b48f53fda849a7ea49d12d241198309e671e6bd230a44e7155af9573d7843ac48831c98
languageName: node
linkType: hard
"yup@npm:^0.32.9": "yup@npm:^0.32.9":
version: 0.32.11 version: 0.32.11
resolution: "yup@npm:0.32.11" resolution: "yup@npm:0.32.11"