Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72629ffee0 | ||
|
|
5b6f007d8f | ||
|
|
2e0951d4dc | ||
|
|
fd84b5754d | ||
|
|
73afb03308 | ||
|
|
99dba73d0b | ||
|
|
db4449d849 | ||
|
|
53e52a0510 | ||
|
|
cf4bd8a77d | ||
|
|
9d61dc2f7c | ||
|
|
caf5918b72 | ||
|
|
84f64f5d06 | ||
|
|
13a53c020b | ||
|
|
a72f90fe02 | ||
|
|
283d5b4a65 | ||
|
|
81b2e37efd | ||
|
|
469967b320 | ||
|
|
d708625da0 | ||
|
|
acf2487eaa | ||
|
|
b81ca0e2ab | ||
|
|
2357f76cc8 | ||
|
|
304e8aaa68 | ||
|
|
9fe912c392 | ||
|
|
ab7ecff580 | ||
|
|
19e830acad | ||
|
|
458300bc83 | ||
|
|
352a467104 | ||
|
|
c052c66abc | ||
|
|
8f8ed4d80d | ||
|
|
04fb7e07b4 | ||
|
|
20d2129f81 | ||
|
|
ae5425f908 | ||
|
|
762810684e | ||
|
|
4720dc410f | ||
|
|
715aea5733 | ||
|
|
6cbb9bf967 |
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;
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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 ({
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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.
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);
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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") },
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
);
|
);
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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<
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -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;
|
||||||
|
|||||||
Vendored
+1
-3
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in new issue
Block a user