merge with remote main
This commit is contained in:
commit
4652b3ee01
|
@ -9,12 +9,11 @@
|
||||||
"langchain": "^0.0.131",
|
"langchain": "^0.0.131",
|
||||||
"mailparser": "^3.6.5",
|
"mailparser": "^3.6.5",
|
||||||
"next": "^13.4.6",
|
"next": "^13.4.6",
|
||||||
"zod": "^3.20.2"
|
"supports-color": "8.1.1",
|
||||||
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mailparser": "^3.4.0",
|
"@types/mailparser": "^3.4.0"
|
||||||
"@types/node": "^20.5.1",
|
|
||||||
"typescript": "^4.9.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|
|
@ -23,6 +23,7 @@ export const env = createEnv({
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||||
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
||||||
|
DATABASE_URL: process.env.DATABASE_URL,
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,5 +38,6 @@ export const env = createEnv({
|
||||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
OPENAI_API_KEY: z.string().min(1),
|
OPENAI_API_KEY: z.string().min(1),
|
||||||
SENDGRID_API_KEY: z.string().min(1),
|
SENDGRID_API_KEY: z.string().min(1),
|
||||||
|
DATABASE_URL: z.string().url(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,6 +40,6 @@
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"tzdata": "^1.0.30",
|
"tzdata": "^1.0.30",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.22.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,10 +248,10 @@
|
||||||
--cal-bg-inverted: #f3f4f6;
|
--cal-bg-inverted: #f3f4f6;
|
||||||
|
|
||||||
/* background -> components*/
|
/* background -> components*/
|
||||||
--cal-bg-info: #dee9fc;
|
--cal-bg-info: #263fa9;
|
||||||
--cal-bg-success: #e2fbe8;
|
--cal-bg-success: #306339;
|
||||||
--cal-bg-attention: #fceed8;
|
--cal-bg-attention: #8e3b1f;
|
||||||
--cal-bg-error: #f9e3e2;
|
--cal-bg-error: #8c2822;
|
||||||
--cal-bg-dark-error: #752522;
|
--cal-bg-dark-error: #752522;
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
|
@ -269,10 +269,10 @@
|
||||||
--cal-text-inverted: #101010;
|
--cal-text-inverted: #101010;
|
||||||
|
|
||||||
/* Content/Text -> components */
|
/* Content/Text -> components */
|
||||||
--cal-text-info: #253985;
|
--cal-text-info: #dee9fc;
|
||||||
--cal-text-success: #285231;
|
--cal-text-success: #e2fbe8;
|
||||||
--cal-text-attention: #73321b;
|
--cal-text-attention: #fceed8;
|
||||||
--cal-text-error: #752522;
|
--cal-text-error: #f9e3e2;
|
||||||
|
|
||||||
/* Brand shenanigans
|
/* Brand shenanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
|
|
|
@ -14,7 +14,8 @@ const {
|
||||||
|
|
||||||
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
|
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
|
||||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
|
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
|
||||||
|
const isOrganizationsEnabled =
|
||||||
|
process.env.ORGANIZATIONS_ENABLED === "1" || process.env.ORGANIZATIONS_ENABLED === "true";
|
||||||
// To be able to use the version in the app without having to import package.json
|
// To be able to use the version in the app without having to import package.json
|
||||||
process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
|
process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
|
||||||
|
|
||||||
|
@ -226,7 +227,7 @@ const nextConfig = {
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
const beforeFiles = [
|
const beforeFiles = [
|
||||||
// These rewrites are other than booking pages rewrites and so that they aren't redirected to org pages ensure that they happen in beforeFiles
|
// These rewrites are other than booking pages rewrites and so that they aren't redirected to org pages ensure that they happen in beforeFiles
|
||||||
...(process.env.ORGANIZATIONS_ENABLED
|
...(isOrganizationsEnabled
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
...matcherConfigRootPath,
|
...matcherConfigRootPath,
|
||||||
|
@ -333,44 +334,46 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...[
|
...(isOrganizationsEnabled
|
||||||
{
|
? [
|
||||||
...matcherConfigRootPath,
|
|
||||||
headers: [
|
|
||||||
{
|
{
|
||||||
key: "X-Cal-Org-path",
|
...matcherConfigRootPath,
|
||||||
value: "/team/:orgSlug",
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-Cal-Org-path",
|
||||||
|
value: "/team/:orgSlug",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...matcherConfigUserRoute,
|
|
||||||
headers: [
|
|
||||||
{
|
{
|
||||||
key: "X-Cal-Org-path",
|
...matcherConfigUserRoute,
|
||||||
value: "/org/:orgSlug/:user",
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-Cal-Org-path",
|
||||||
|
value: "/org/:orgSlug/:user",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...matcherConfigUserTypeRoute,
|
|
||||||
headers: [
|
|
||||||
{
|
{
|
||||||
key: "X-Cal-Org-path",
|
...matcherConfigUserTypeRoute,
|
||||||
value: "/org/:orgSlug/:user/:type",
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-Cal-Org-path",
|
||||||
|
value: "/org/:orgSlug/:user/:type",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...matcherConfigUserTypeEmbedRoute,
|
|
||||||
headers: [
|
|
||||||
{
|
{
|
||||||
key: "X-Cal-Org-path",
|
...matcherConfigUserTypeEmbedRoute,
|
||||||
value: "/org/:orgSlug/:user/:type/embed",
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-Cal-Org-path",
|
||||||
|
value: "/org/:orgSlug/:user/:type/embed",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
},
|
: []),
|
||||||
],
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
|
@ -447,6 +450,13 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/support",
|
source: "/support",
|
||||||
|
missing: [
|
||||||
|
{
|
||||||
|
type: "header",
|
||||||
|
key: "host",
|
||||||
|
value: orgHostPath,
|
||||||
|
},
|
||||||
|
],
|
||||||
destination: "/event-types?openIntercom=true",
|
destination: "/event-types?openIntercom=true",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
|
@ -463,7 +473,7 @@ const nextConfig = {
|
||||||
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
|
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
|
||||||
...(process.env.NODE_ENV === "development" &&
|
...(process.env.NODE_ENV === "development" &&
|
||||||
// Safer to enable the redirect only when the user is opting to test out organizations
|
// Safer to enable the redirect only when the user is opting to test out organizations
|
||||||
process.env.ORGANIZATIONS_ENABLED &&
|
isOrganizationsEnabled &&
|
||||||
// Prevent infinite redirect by checking that we aren't already on localhost
|
// Prevent infinite redirect by checking that we aren't already on localhost
|
||||||
process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000"
|
process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000"
|
||||||
? [
|
? [
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "3.2.6",
|
"version": "3.2.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
"turndown": "^7.1.1",
|
"turndown": "^7.1.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web3": "^1.7.5",
|
"web3": "^1.7.5",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.19.6",
|
||||||
|
|
|
@ -104,7 +104,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
});
|
});
|
||||||
|
|
||||||
const attendeesList = await Promise.all(attendeesListPromises);
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
|
||||||
const evt: CalendarEvent = {
|
const evt: CalendarEvent = {
|
||||||
type: booking.title,
|
type: booking.title,
|
||||||
title: booking.title,
|
title: booking.title,
|
||||||
|
@ -127,7 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
attendees: attendeesList,
|
attendees: attendeesList,
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
|
||||||
};
|
};
|
||||||
|
|
||||||
await sendOrganizerRequestReminderEmail(evt);
|
await sendOrganizerRequestReminderEmail(evt);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs";
|
||||||
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||||
import Schedule from "@calcom/features/schedules/components/Schedule";
|
import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
import { availabilityAsString } from "@calcom/lib/availability";
|
import { availabilityAsString } from "@calcom/lib/availability";
|
||||||
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";
|
||||||
|
@ -17,11 +18,6 @@ import {
|
||||||
ConfirmationDialogContent,
|
ConfirmationDialogContent,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
Form,
|
Form,
|
||||||
Label,
|
Label,
|
||||||
showToast,
|
showToast,
|
||||||
|
@ -32,7 +28,7 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
VerticalDivider,
|
VerticalDivider,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Info, MoreHorizontal, Plus, Trash } from "@calcom/ui/components/icon";
|
import { Info, MoreVertical, ArrowLeft, Plus, Trash } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
@ -95,7 +91,7 @@ export default function Availability() {
|
||||||
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
|
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
|
||||||
const fromEventType = searchParams?.get("fromEventType");
|
const fromEventType = searchParams?.get("fromEventType");
|
||||||
const { timeFormat } = me.data || { timeFormat: null };
|
const { timeFormat } = me.data || { timeFormat: null };
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [openSidebar, setOpenSidebar] = useState(false);
|
||||||
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
{ scheduleId },
|
{ scheduleId },
|
||||||
{
|
{
|
||||||
|
@ -225,33 +221,60 @@ export default function Availability() {
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<VerticalDivider className="hidden sm:inline" />
|
<VerticalDivider className="hidden sm:inline" />
|
||||||
<Dropdown>
|
<div
|
||||||
<DropdownMenuTrigger asChild>
|
className={classNames(
|
||||||
<Button className="sm:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
openSidebar
|
||||||
</DropdownMenuTrigger>
|
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
|
||||||
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
: ""
|
||||||
<DropdownItem
|
)}>
|
||||||
type="button"
|
<div
|
||||||
color="destructive"
|
className={classNames(
|
||||||
StartIcon={Trash}
|
"bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform",
|
||||||
onClick={() => setDeleteDialogOpen(true)}>
|
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||||
{t("delete")}
|
)}>
|
||||||
</DropdownItem>
|
<div className="flex flex-row items-center pt-5">
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Button StartIcon={ArrowLeft} color="minimal" onClick={() => setOpenSidebar(false)} />
|
||||||
<ConfirmationDialogContent
|
<p className="-ml-2">{t("availability_settings")}</p>
|
||||||
isLoading={deleteMutation.isLoading}
|
<Dialog>
|
||||||
variety="danger"
|
<DialogTrigger asChild>
|
||||||
title={t("delete_schedule")}
|
<Button
|
||||||
confirmBtnText={t("delete")}
|
StartIcon={Trash}
|
||||||
loadingText={t("delete")}
|
variant="icon"
|
||||||
onConfirm={() => {
|
color="destructive"
|
||||||
schedule !== undefined && deleteMutation.mutate({ scheduleId: schedule.id });
|
aria-label={t("delete")}
|
||||||
}}>
|
className="ml-16 inline"
|
||||||
{t("delete_schedule_description")}
|
disabled={schedule?.isLastSchedule}
|
||||||
</ConfirmationDialogContent>
|
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
|
||||||
</Dialog>
|
/>
|
||||||
<DropdownMenuSeparator />
|
</DialogTrigger>
|
||||||
<div className="flex h-9 flex-row items-center justify-between px-4 py-2 hover:bg-gray-100">
|
<ConfirmationDialogContent
|
||||||
|
isLoading={deleteMutation.isLoading}
|
||||||
|
variety="danger"
|
||||||
|
title={t("delete_schedule")}
|
||||||
|
confirmBtnText={t("delete")}
|
||||||
|
loadingText={t("delete")}
|
||||||
|
onConfirm={() => {
|
||||||
|
scheduleId && deleteMutation.mutate({ scheduleId });
|
||||||
|
setOpenSidebar(false);
|
||||||
|
}}>
|
||||||
|
{t("delete_schedule_description")}
|
||||||
|
</ConfirmationDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col px-2 py-2">
|
||||||
|
<Skeleton as={Label}>{t("name")}</Skeleton>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<input
|
||||||
|
className="hover:border-emphasis dark:focus:border-emphasis border-default bg-default placeholder:text-muted text-emphasis focus:ring-brand-default disabled:bg-subtle disabled:hover:border-subtle mb-2 block h-9 w-full rounded-md border px-3 py-2 text-sm leading-4 focus:border-neutral-300 focus:outline-none focus:ring-2 disabled:cursor-not-allowed"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-9 flex-row-reverse items-center justify-end gap-3 px-2">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
|
@ -267,9 +290,44 @@ export default function Availability() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
|
<div className="min-w-40 col-span-3 space-y-2 px-2 py-4 lg:col-span-1">
|
||||||
|
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||||
|
<div>
|
||||||
|
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
|
||||||
|
{t("timezone")}
|
||||||
|
</Skeleton>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="timeZone"
|
||||||
|
render={({ field: { onChange, value } }) =>
|
||||||
|
value ? (
|
||||||
|
<TimezoneSelect
|
||||||
|
value={value}
|
||||||
|
className="focus:border-brand-default border-default mt-1 block w-72 rounded-md text-sm"
|
||||||
|
onChange={(timezone) => onChange(timezone.value)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SelectSkeletonLoader className="mt-1 w-72" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr className="border-subtle my-7" />
|
||||||
|
<div className="rounded-md md:block">
|
||||||
|
<Skeleton as="h3" className="mb-0 inline-block text-sm font-medium">
|
||||||
|
{t("something_doesnt_look_right")}
|
||||||
|
</Skeleton>
|
||||||
|
<div className="mt-3 flex">
|
||||||
|
<Skeleton as={Button} href="/availability/troubleshoot" color="secondary">
|
||||||
|
{t("launch_troubleshooter")}
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="border-default border-l-2" />
|
<div className="border-default border-l-2" />
|
||||||
<Button
|
<Button
|
||||||
className="ml-4 lg:ml-0"
|
className="ml-4 lg:ml-0"
|
||||||
|
@ -278,6 +336,13 @@ export default function Availability() {
|
||||||
loading={updateMutation.isLoading}>
|
loading={updateMutation.isLoading}>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="ml-3 sm:hidden"
|
||||||
|
StartIcon={MoreVertical}
|
||||||
|
variant="icon"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => setOpenSidebar(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<div className="mt-4 w-full md:mt-0">
|
<div className="mt-4 w-full md:mt-0">
|
||||||
|
@ -313,7 +378,7 @@ export default function Availability() {
|
||||||
{schedule?.workingHours && <DateOverride workingHours={schedule.workingHours} />}
|
{schedule?.workingHours && <DateOverride workingHours={schedule.workingHours} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1">
|
||||||
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
|
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
|
||||||
|
@ -335,7 +400,7 @@ export default function Availability() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr className="border-subtle my-6 mr-8" />
|
<hr className="border-subtle my-6 mr-8" />
|
||||||
<div className="hidden rounded-md md:block">
|
<div className="rounded-md">
|
||||||
<Skeleton as="h3" className="mb-0 inline-block text-sm font-medium">
|
<Skeleton as="h3" className="mb-0 inline-block text-sm font-medium">
|
||||||
{t("something_doesnt_look_right")}
|
{t("something_doesnt_look_right")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
|
@ -55,10 +55,9 @@ import {
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Switch,
|
Switch,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
ArrowButton,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
ArrowDown,
|
|
||||||
ArrowUp,
|
|
||||||
Clipboard,
|
Clipboard,
|
||||||
Code,
|
Code,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -393,18 +392,10 @@ export const EventTypeList = ({ data }: EventTypeListProps): JSX.Element => {
|
||||||
<div className="hover:bg-muted flex w-full items-center justify-between">
|
<div className="hover:bg-muted flex w-full items-center justify-between">
|
||||||
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
|
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
|
||||||
{!(firstItem && firstItem.id === eventType.id) && (
|
{!(firstItem && firstItem.id === eventType.id) && (
|
||||||
<button
|
<ArrowButton onClick={() => moveEventType(index, -1)} arrowDirection="up" />
|
||||||
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -ml-4 -mt-4 mb-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
|
||||||
onClick={() => moveEventType(index, -1)}>
|
|
||||||
<ArrowUp className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
{!(lastItem && lastItem.id === eventType.id) && (
|
{!(lastItem && lastItem.id === eventType.id) && (
|
||||||
<button
|
<ArrowButton onClick={() => moveEventType(index, 1)} arrowDirection="down" />
|
||||||
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] -ml-4 mt-8 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
|
||||||
onClick={() => moveEventType(index, 1)}>
|
|
||||||
<ArrowDown className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
<MemoizedItem eventType={eventType} />
|
<MemoizedItem eventType={eventType} />
|
||||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||||
|
@ -887,7 +878,7 @@ const Main = ({
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<MobileTeamsTab eventTypeGroups={data} />
|
<MobileTeamsTab eventTypeGroups={data} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="mt-4 flex flex-col" key={group.profile.slug}>
|
||||||
<EventTypeListHeading
|
<EventTypeListHeading
|
||||||
profile={data[0].users[0] || data[0].team}
|
profile={data[0].users[0] || data[0].team}
|
||||||
membershipCount={data[0].team?.members.length || 0}
|
membershipCount={data[0].team?.members.length || 0}
|
||||||
|
|
|
@ -25,7 +25,7 @@ const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
|
||||||
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
|
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
|
||||||
process.env.NEXT_PUBLIC_WEBAPP_URL || "https://" + process.env.VERCEL_URL
|
process.env.NEXT_PUBLIC_WEBAPP_URL || "https://" + process.env.VERCEL_URL
|
||||||
));
|
));
|
||||||
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\..*`;
|
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`;
|
||||||
|
|
||||||
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes);
|
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes);
|
||||||
exports.orgUserRoutePath = `/:user((?!${beforeRewriteExcludePages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`;
|
exports.orgUserRoutePath = `/:user((?!${beforeRewriteExcludePages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`;
|
||||||
|
|
|
@ -268,3 +268,60 @@ test.describe("prefill", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("Booking on different layouts", () => {
|
||||||
|
test.beforeEach(async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await page.goto(`/${user.username}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Book on week layout", async ({ page }) => {
|
||||||
|
// Click first event type
|
||||||
|
await page.click('[data-testid="event-type-link"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="toggle-group-item-week_view"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="incrementMonth"]');
|
||||||
|
|
||||||
|
await page.locator('[data-testid="calendar-empty-cell"]').nth(0).click();
|
||||||
|
|
||||||
|
// Fill what is this meeting about? name email and notes
|
||||||
|
await page.locator('[name="name"]').fill("Test name");
|
||||||
|
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
|
||||||
|
await page.locator('[name="notes"]').fill("Test notes");
|
||||||
|
|
||||||
|
await page.click('[data-testid="confirm-book-button"]');
|
||||||
|
|
||||||
|
await page.waitForURL((url) => {
|
||||||
|
return url.pathname.startsWith("/booking");
|
||||||
|
});
|
||||||
|
|
||||||
|
// expect page to be booking page
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Book on column layout", async ({ page }) => {
|
||||||
|
// Click first event type
|
||||||
|
await page.click('[data-testid="event-type-link"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="toggle-group-item-column_view"]');
|
||||||
|
|
||||||
|
await page.click('[data-testid="incrementMonth"]');
|
||||||
|
|
||||||
|
await page.locator('[data-testid="time"]').nth(0).click();
|
||||||
|
|
||||||
|
// Fill what is this meeting about? name email and notes
|
||||||
|
await page.locator('[name="name"]').fill("Test name");
|
||||||
|
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
|
||||||
|
await page.locator('[name="notes"]').fill("Test notes");
|
||||||
|
|
||||||
|
await page.click('[data-testid="confirm-book-button"]');
|
||||||
|
|
||||||
|
await page.waitForURL((url) => {
|
||||||
|
return url.pathname.startsWith("/booking");
|
||||||
|
});
|
||||||
|
|
||||||
|
// expect page to be booking page
|
||||||
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -246,7 +246,7 @@ test.describe("BOOKING_REJECTED", async () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
location: "[redacted/dynamic]",
|
location: "[redacted/dynamic]",
|
||||||
destinationCalendar: null,
|
destinationCalendar: [],
|
||||||
// hideCalendarNotes: false,
|
// hideCalendarNotes: false,
|
||||||
requiresConfirmation: "[redacted/dynamic]",
|
requiresConfirmation: "[redacted/dynamic]",
|
||||||
eventTypeId: "[redacted/dynamic]",
|
eventTypeId: "[redacted/dynamic]",
|
||||||
|
|
|
@ -529,7 +529,7 @@
|
||||||
"location": "Ort",
|
"location": "Ort",
|
||||||
"address": "Adresse",
|
"address": "Adresse",
|
||||||
"enter_address": "Geben Sie eine Adresse ein",
|
"enter_address": "Geben Sie eine Adresse ein",
|
||||||
"in_person_attendee_address": "In Person (Adresse von Ihnen)",
|
"in_person_attendee_address": "Vor Ort (Adresse von Ihnen)",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
"additional_notes": "Zusätzliche Notizen",
|
"additional_notes": "Zusätzliche Notizen",
|
||||||
|
@ -539,7 +539,7 @@
|
||||||
"booking_confirmation": "Bestätigen Sie {{eventTypeTitle}} mit {{profileName}}",
|
"booking_confirmation": "Bestätigen Sie {{eventTypeTitle}} mit {{profileName}}",
|
||||||
"booking_reschedule_confirmation": "Planen Sie Ihr {{eventTypeTitle}} mit {{profileName}} um",
|
"booking_reschedule_confirmation": "Planen Sie Ihr {{eventTypeTitle}} mit {{profileName}} um",
|
||||||
"in_person_meeting": "Vor-Ort-Termin",
|
"in_person_meeting": "Vor-Ort-Termin",
|
||||||
"in_person": "Persönlich (Organisator-Adresse)",
|
"in_person": "Vor Ort (Organisator-Adresse)",
|
||||||
"link_meeting": "Termin verknüpfen",
|
"link_meeting": "Termin verknüpfen",
|
||||||
"phone_number": "Telefonnummer",
|
"phone_number": "Telefonnummer",
|
||||||
"attendee_phone_number": "Telefonnummer",
|
"attendee_phone_number": "Telefonnummer",
|
||||||
|
|
|
@ -266,6 +266,7 @@
|
||||||
"nearly_there_instructions": "Last thing, a brief description about you and a photo really helps you get bookings and let people know who they’re booking with.",
|
"nearly_there_instructions": "Last thing, a brief description about you and a photo really helps you get bookings and let people know who they’re booking with.",
|
||||||
"set_availability_instructions": "Define ranges of time when you are available on a recurring basis. You can create more of these later and assign them to different calendars.",
|
"set_availability_instructions": "Define ranges of time when you are available on a recurring basis. You can create more of these later and assign them to different calendars.",
|
||||||
"set_availability": "Set your availability",
|
"set_availability": "Set your availability",
|
||||||
|
"availability_settings": "Availability Settings",
|
||||||
"continue_without_calendar": "Continue without calendar",
|
"continue_without_calendar": "Continue without calendar",
|
||||||
"connect_your_calendar": "Connect your calendar",
|
"connect_your_calendar": "Connect your calendar",
|
||||||
"connect_your_video_app": "Connect your video apps",
|
"connect_your_video_app": "Connect your video apps",
|
||||||
|
@ -1107,6 +1108,7 @@
|
||||||
"email_attendee_action": "send email to attendees",
|
"email_attendee_action": "send email to attendees",
|
||||||
"sms_attendee_action": "Send SMS to attendee",
|
"sms_attendee_action": "Send SMS to attendee",
|
||||||
"sms_number_action": "send SMS to a specific number",
|
"sms_number_action": "send SMS to a specific number",
|
||||||
|
"send_reminder_sms": "Easily send meeting reminders via SMS to your attendees",
|
||||||
"whatsapp_number_action": "send WhatsApp message to a specific number",
|
"whatsapp_number_action": "send WhatsApp message to a specific number",
|
||||||
"whatsapp_attendee_action": "send WhatsApp message to attendee",
|
"whatsapp_attendee_action": "send WhatsApp message to attendee",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
|
|
@ -266,6 +266,7 @@
|
||||||
"nearly_there_instructions": "Pour finir, une brève description de vous et une photo vous aideront vraiment à obtenir des réservations et à faire savoir aux gens avec qui ils prennent rendez-vous.",
|
"nearly_there_instructions": "Pour finir, une brève description de vous et une photo vous aideront vraiment à obtenir des réservations et à faire savoir aux gens avec qui ils prennent rendez-vous.",
|
||||||
"set_availability_instructions": "Définissez des plages de temps pendant lesquelles vous êtes disponible de manière récurrente. Vous pourrez en créer d'autres ultérieurement et les assigner à différents calendriers.",
|
"set_availability_instructions": "Définissez des plages de temps pendant lesquelles vous êtes disponible de manière récurrente. Vous pourrez en créer d'autres ultérieurement et les assigner à différents calendriers.",
|
||||||
"set_availability": "Définissez vos disponibilités",
|
"set_availability": "Définissez vos disponibilités",
|
||||||
|
"availability_settings": "Paramètres de disponibilité",
|
||||||
"continue_without_calendar": "Continuer sans calendrier",
|
"continue_without_calendar": "Continuer sans calendrier",
|
||||||
"connect_your_calendar": "Connectez votre calendrier",
|
"connect_your_calendar": "Connectez votre calendrier",
|
||||||
"connect_your_video_app": "Connectez vos applications vidéo",
|
"connect_your_video_app": "Connectez vos applications vidéo",
|
||||||
|
@ -1107,6 +1108,7 @@
|
||||||
"email_attendee_action": "envoyer un e-mail aux participants",
|
"email_attendee_action": "envoyer un e-mail aux participants",
|
||||||
"sms_attendee_action": "Envoyer un SMS au participant",
|
"sms_attendee_action": "Envoyer un SMS au participant",
|
||||||
"sms_number_action": "envoyer un SMS à un numéro spécifique",
|
"sms_number_action": "envoyer un SMS à un numéro spécifique",
|
||||||
|
"send_reminder_sms": "Envoyez facilement des rappels de rendez-vous par SMS à vos participants",
|
||||||
"whatsapp_number_action": "envoyer un message WhatsApp à un numéro spécifique",
|
"whatsapp_number_action": "envoyer un message WhatsApp à un numéro spécifique",
|
||||||
"whatsapp_attendee_action": "envoyer un message WhatsApp au participant",
|
"whatsapp_attendee_action": "envoyer un message WhatsApp au participant",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
|
|
@ -58,10 +58,10 @@
|
||||||
--cal-bg-inverted: #f3f4f6;
|
--cal-bg-inverted: #f3f4f6;
|
||||||
|
|
||||||
/* background -> components*/
|
/* background -> components*/
|
||||||
--cal-bg-info: #dee9fc;
|
--cal-bg-info: #263fa9;
|
||||||
--cal-bg-success: #e2fbe8;
|
--cal-bg-success: #306339;
|
||||||
--cal-bg-attention: #fceed8;
|
--cal-bg-attention: #8e3b1f;
|
||||||
--cal-bg-error: #f9e3e2;
|
--cal-bg-error: #8c2822;
|
||||||
--cal-bg-dark-error: #752522;
|
--cal-bg-dark-error: #752522;
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
|
@ -80,10 +80,10 @@
|
||||||
--cal-text-inverted: #101010;
|
--cal-text-inverted: #101010;
|
||||||
|
|
||||||
/* Content/Text -> components */
|
/* Content/Text -> components */
|
||||||
--cal-text-info: #253985;
|
--cal-text-info: #dee9fc;
|
||||||
--cal-text-success: #285231;
|
--cal-text-success: #e2fbe8;
|
||||||
--cal-text-attention: #73321b;
|
--cal-text-attention: #fceed8;
|
||||||
--cal-text-error: #752522;
|
--cal-text-error: #f9e3e2;
|
||||||
|
|
||||||
/* Brand shenanigans
|
/* Brand shenanigans
|
||||||
-> These will be computed for the users theme at runtime.
|
-> These will be computed for the users theme at runtime.
|
||||||
|
|
|
@ -31,7 +31,7 @@ beforeAll(async () => {
|
||||||
describe("next.config.js - Org Rewrite", () => {
|
describe("next.config.js - Org Rewrite", () => {
|
||||||
const orgHostRegExp = (subdomainRegExp: string) =>
|
const orgHostRegExp = (subdomainRegExp: string) =>
|
||||||
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.
|
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.
|
||||||
new RegExp(`^(?<orgSlug>${subdomainRegExp})\\..*`);
|
new RegExp(`^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`);
|
||||||
|
|
||||||
describe("Host matching based on NEXT_PUBLIC_WEBAPP_URL", () => {
|
describe("Host matching based on NEXT_PUBLIC_WEBAPP_URL", () => {
|
||||||
it("https://app.cal.com", () => {
|
it("https://app.cal.com", () => {
|
||||||
|
@ -87,6 +87,11 @@ describe("next.config.js - Org Rewrite", () => {
|
||||||
?.orgSlug
|
?.orgSlug
|
||||||
).toEqual("some-other");
|
).toEqual("some-other");
|
||||||
});
|
});
|
||||||
|
it("Should ignore Vercel preview URLs", () => {
|
||||||
|
const subdomainRegExp = getSubdomainRegExp("https://cal-xxxxxxxx-cal.vercel.app");
|
||||||
|
expect(orgHostRegExp(subdomainRegExp).exec("https://cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null')
|
||||||
|
expect(orgHostRegExp(subdomainRegExp).exec("cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null')
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Rewrite", () => {
|
describe("Rewrite", () => {
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async createEvent(calEventRaw: CalendarEvent): Promise<NewCalendarEventType> {
|
async createEvent(calEventRaw: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {
|
||||||
const eventAttendees = calEventRaw.attendees.map(({ id: _id, ...rest }) => ({
|
const eventAttendees = calEventRaw.attendees.map(({ id: _id, ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
responseStatus: "accepted",
|
responseStatus: "accepted",
|
||||||
|
@ -97,6 +97,10 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
responseStatus: "accepted",
|
responseStatus: "accepted",
|
||||||
})) || [];
|
})) || [];
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const [mainHostDestinationCalendar] =
|
||||||
|
calEventRaw?.destinationCalendar && calEventRaw?.destinationCalendar.length > 0
|
||||||
|
? calEventRaw.destinationCalendar
|
||||||
|
: [];
|
||||||
const myGoogleAuth = await this.auth.getToken();
|
const myGoogleAuth = await this.auth.getToken();
|
||||||
const payload: calendar_v3.Schema$Event = {
|
const payload: calendar_v3.Schema$Event = {
|
||||||
summary: calEventRaw.title,
|
summary: calEventRaw.title,
|
||||||
|
@ -115,8 +119,8 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
id: String(calEventRaw.organizer.id),
|
id: String(calEventRaw.organizer.id),
|
||||||
responseStatus: "accepted",
|
responseStatus: "accepted",
|
||||||
organizer: true,
|
organizer: true,
|
||||||
email: calEventRaw.destinationCalendar?.externalId
|
email: mainHostDestinationCalendar?.externalId
|
||||||
? calEventRaw.destinationCalendar.externalId
|
? mainHostDestinationCalendar.externalId
|
||||||
: calEventRaw.organizer.email,
|
: calEventRaw.organizer.email,
|
||||||
},
|
},
|
||||||
...eventAttendees,
|
...eventAttendees,
|
||||||
|
@ -138,13 +142,16 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
const calendar = google.calendar({
|
const calendar = google.calendar({
|
||||||
version: "v3",
|
version: "v3",
|
||||||
});
|
});
|
||||||
const selectedCalendar = calEventRaw.destinationCalendar?.externalId
|
// Find in calEventRaw.destinationCalendar the one with the same credentialId
|
||||||
? calEventRaw.destinationCalendar.externalId
|
|
||||||
: "primary";
|
const selectedCalendar = calEventRaw.destinationCalendar?.find(
|
||||||
|
(cal) => cal.credentialId === credentialId
|
||||||
|
)?.externalId;
|
||||||
|
|
||||||
calendar.events.insert(
|
calendar.events.insert(
|
||||||
{
|
{
|
||||||
auth: myGoogleAuth,
|
auth: myGoogleAuth,
|
||||||
calendarId: selectedCalendar,
|
calendarId: selectedCalendar || "primary",
|
||||||
requestBody: payload,
|
requestBody: payload,
|
||||||
conferenceDataVersion: 1,
|
conferenceDataVersion: 1,
|
||||||
sendUpdates: "none",
|
sendUpdates: "none",
|
||||||
|
@ -188,6 +195,8 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
|
|
||||||
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
|
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const [mainHostDestinationCalendar] =
|
||||||
|
event?.destinationCalendar && event?.destinationCalendar.length > 0 ? event.destinationCalendar : [];
|
||||||
const myGoogleAuth = await this.auth.getToken();
|
const myGoogleAuth = await this.auth.getToken();
|
||||||
const eventAttendees = event.attendees.map(({ ...rest }) => ({
|
const eventAttendees = event.attendees.map(({ ...rest }) => ({
|
||||||
...rest,
|
...rest,
|
||||||
|
@ -216,8 +225,8 @@ 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
|
email: mainHostDestinationCalendar?.externalId
|
||||||
? event.destinationCalendar.externalId
|
? mainHostDestinationCalendar.externalId
|
||||||
: event.organizer.email,
|
: event.organizer.email,
|
||||||
},
|
},
|
||||||
...(eventAttendees as any),
|
...(eventAttendees as any),
|
||||||
|
@ -244,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
|
|
||||||
const selectedCalendar = externalCalendarId
|
const selectedCalendar = externalCalendarId
|
||||||
? externalCalendarId
|
? externalCalendarId
|
||||||
: event.destinationCalendar?.externalId;
|
: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
|
||||||
|
|
||||||
calendar.events.update(
|
calendar.events.update(
|
||||||
{
|
{
|
||||||
|
@ -303,7 +312,9 @@ export default class GoogleCalendarService implements Calendar {
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultCalendarId = "primary";
|
const defaultCalendarId = "primary";
|
||||||
const calendarId = externalCalendarId ? externalCalendarId : event.destinationCalendar?.externalId;
|
const calendarId = externalCalendarId
|
||||||
|
? externalCalendarId
|
||||||
|
: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
|
||||||
|
|
||||||
calendar.events.delete(
|
calendar.events.delete(
|
||||||
{
|
{
|
||||||
|
|
|
@ -125,7 +125,8 @@ export default class LarkCalendarService implements Calendar {
|
||||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||||
let eventId = "";
|
let eventId = "";
|
||||||
let eventRespData;
|
let eventRespData;
|
||||||
const calendarId = event.destinationCalendar?.externalId;
|
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||||
|
const calendarId = mainHostDestinationCalendar?.externalId;
|
||||||
if (!calendarId) {
|
if (!calendarId) {
|
||||||
throw new Error("no calendar id");
|
throw new Error("no calendar id");
|
||||||
}
|
}
|
||||||
|
@ -160,7 +161,8 @@ export default class LarkCalendarService implements Calendar {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createAttendees = async (event: CalendarEvent, eventId: string) => {
|
private createAttendees = async (event: CalendarEvent, eventId: string) => {
|
||||||
const calendarId = event.destinationCalendar?.externalId;
|
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||||
|
const calendarId = mainHostDestinationCalendar?.externalId;
|
||||||
if (!calendarId) {
|
if (!calendarId) {
|
||||||
this.log.error("no calendar id provided in createAttendees");
|
this.log.error("no calendar id provided in createAttendees");
|
||||||
throw new Error("no calendar id provided in createAttendees");
|
throw new Error("no calendar id provided in createAttendees");
|
||||||
|
@ -187,7 +189,8 @@ export default class LarkCalendarService implements Calendar {
|
||||||
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
|
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
|
||||||
const eventId = uid;
|
const eventId = uid;
|
||||||
let eventRespData;
|
let eventRespData;
|
||||||
const calendarId = externalCalendarId || event.destinationCalendar?.externalId;
|
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||||
|
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
|
||||||
if (!calendarId) {
|
if (!calendarId) {
|
||||||
this.log.error("no calendar id provided in updateEvent");
|
this.log.error("no calendar id provided in updateEvent");
|
||||||
throw new Error("no calendar id provided in updateEvent");
|
throw new Error("no calendar id provided in updateEvent");
|
||||||
|
@ -231,7 +234,8 @@ export default class LarkCalendarService implements Calendar {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
|
async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
|
||||||
const calendarId = externalCalendarId || event.destinationCalendar?.externalId;
|
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||||
|
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
|
||||||
if (!calendarId) {
|
if (!calendarId) {
|
||||||
this.log.error("no calendar id provided in deleteEvent");
|
this.log.error("no calendar id provided in deleteEvent");
|
||||||
throw new Error("no calendar id provided in deleteEvent");
|
throw new Error("no calendar id provided in deleteEvent");
|
||||||
|
|
|
@ -70,9 +70,10 @@ export default class Office365CalendarService implements Calendar {
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||||
|
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||||
try {
|
try {
|
||||||
const eventsUrl = event.destinationCalendar?.externalId
|
const eventsUrl = mainHostDestinationCalendar?.externalId
|
||||||
? `/me/calendars/${event.destinationCalendar?.externalId}/events`
|
? `/me/calendars/${mainHostDestinationCalendar?.externalId}/events`
|
||||||
: "/me/calendar/events";
|
: "/me/calendar/events";
|
||||||
|
|
||||||
const response = await this.fetcher(eventsUrl, {
|
const response = await this.fetcher(eventsUrl, {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import z from "zod";
|
||||||
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
|
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
|
||||||
import { findPaymentCredentials } from "@calcom/features/ee/payments/api/paypal-webhook";
|
import { findPaymentCredentials } from "@calcom/features/ee/payments/api/paypal-webhook";
|
||||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
@ -78,12 +77,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
const err = getErrorFromUnknown(_err);
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
message: err.message,
|
|
||||||
stack: IS_PRODUCTION ? undefined : err.stack,
|
|
||||||
});
|
|
||||||
res.redirect(`/booking/${req.query.bookingUid}?paypalPaymentStatus=failed`);
|
res.redirect(`/booking/${req.query.bookingUid}?paypalPaymentStatus=failed`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ class Paypal {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
||||||
this.getAccessToken();
|
await this.getAccessToken();
|
||||||
return fetch(`${this.url}${endpoint}`, {
|
return fetch(`${this.url}${endpoint}`, {
|
||||||
method: "get",
|
method: "get",
|
||||||
...init,
|
...init,
|
||||||
|
@ -173,7 +173,7 @@ class Paypal {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return false;
|
throw error;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,25 +41,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
|
||||||
name="Plausible URL"
|
|
||||||
defaultValue="https://plausible.io/js/script.js"
|
|
||||||
placeholder="https://plausible.io/js/script.js"
|
|
||||||
value={plausibleUrl}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => {
|
|
||||||
setAppData("PLAUSIBLE_URL", e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
disabled={disabled}
|
|
||||||
name="Tracked Domain"
|
|
||||||
placeholder="yourdomain.com"
|
|
||||||
value={trackingId}
|
|
||||||
onChange={(e) => {
|
|
||||||
setAppData("trackingId", e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</AppCard>
|
</AppCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user ",
|
"description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user ",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.3.1",
|
||||||
"json-logic-js": "^2.0.2",
|
"json-logic-js": "^2.0.2",
|
||||||
"react-awesome-query-builder": "^5.1.2"
|
"react-awesome-query-builder": "^5.1.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// TODO: i18n
|
// TODO: i18n
|
||||||
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
@ -30,6 +31,7 @@ import {
|
||||||
List,
|
List,
|
||||||
ListLinkItem,
|
ListLinkItem,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
ArrowButton,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
|
@ -83,6 +85,20 @@ export default function RoutingForms({
|
||||||
const { hasPaidPlan } = useHasPaidPlan();
|
const { hasPaidPlan } = useHasPaidPlan();
|
||||||
const routerQuery = useRouterQuery();
|
const routerQuery = useRouterQuery();
|
||||||
const hookForm = useFormContext<RoutingFormWithResponseCount>();
|
const hookForm = useFormContext<RoutingFormWithResponseCount>();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||||
|
|
||||||
|
const mutation = trpc.viewer.routingFormOrder.useMutation({
|
||||||
|
onError: async (err) => {
|
||||||
|
console.error(err.message);
|
||||||
|
await utils.viewer.appRoutingForms.forms.cancel();
|
||||||
|
await utils.viewer.appRoutingForms.invalidate();
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
utils.viewer.appRoutingForms.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hookForm.reset({});
|
hookForm.reset({});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
@ -128,6 +144,29 @@ export default function RoutingForms({
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
async function moveRoutingForm(index: number, increment: 1 | -1) {
|
||||||
|
const types = forms?.map((type) => {
|
||||||
|
return type.form;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (types?.length) {
|
||||||
|
const newList = [...types];
|
||||||
|
|
||||||
|
const type = types[index];
|
||||||
|
const tmp = types[index + increment];
|
||||||
|
if (tmp) {
|
||||||
|
newList[index] = tmp;
|
||||||
|
newList[index + increment] = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.viewer.appRoutingForms.forms.cancel();
|
||||||
|
|
||||||
|
mutation.mutate({
|
||||||
|
ids: newList?.map((type) => type.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
<ShellMain
|
<ShellMain
|
||||||
|
@ -177,8 +216,8 @@ export default function RoutingForms({
|
||||||
}
|
}
|
||||||
SkeletonLoader={SkeletonLoaderTeamList}>
|
SkeletonLoader={SkeletonLoaderTeamList}>
|
||||||
<div className="bg-default mb-16 overflow-hidden">
|
<div className="bg-default mb-16 overflow-hidden">
|
||||||
<List data-testid="routing-forms-list">
|
<List data-testid="routing-forms-list" ref={parent}>
|
||||||
{forms?.map(({ form, readOnly }) => {
|
{forms?.map(({ form, readOnly }, index) => {
|
||||||
if (!form) {
|
if (!form) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -187,116 +226,129 @@ export default function RoutingForms({
|
||||||
form.routes = form.routes || [];
|
form.routes = form.routes || [];
|
||||||
const fields = form.fields || [];
|
const fields = form.fields || [];
|
||||||
const userRoutes = form.routes.filter((route) => !isFallbackRoute(route));
|
const userRoutes = form.routes.filter((route) => !isFallbackRoute(route));
|
||||||
|
const firstItem = forms[0].form;
|
||||||
|
const lastItem = forms[forms.length - 1].form;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListLinkItem
|
<div
|
||||||
key={form.id}
|
className="group flex w-full max-w-full items-center justify-between overflow-hidden"
|
||||||
href={appUrl + "/form-edit/" + form.id}
|
key={form.id}>
|
||||||
heading={form.name}
|
{!(firstItem && firstItem.id === form.id) && (
|
||||||
disabled={readOnly}
|
<ArrowButton onClick={() => moveRoutingForm(index, -1)} arrowDirection="up" />
|
||||||
subHeading={description}
|
)}
|
||||||
className="space-x-2 rtl:space-x-reverse"
|
|
||||||
actions={
|
{!(lastItem && lastItem.id === form.id) && (
|
||||||
<>
|
<ArrowButton onClick={() => moveRoutingForm(index, 1)} arrowDirection="down" />
|
||||||
{form.team?.name && (
|
)}
|
||||||
<div className="border-r-2 border-neutral-300">
|
<ListLinkItem
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
href={appUrl + "/form-edit/" + form.id}
|
||||||
{form.team.name}
|
heading={form.name}
|
||||||
</Badge>
|
disabled={readOnly}
|
||||||
</div>
|
subHeading={description}
|
||||||
)}
|
className="space-x-2 rtl:space-x-reverse"
|
||||||
<FormAction
|
actions={
|
||||||
disabled={readOnly}
|
<>
|
||||||
className="self-center"
|
{form.team?.name && (
|
||||||
action="toggle"
|
<div className="border-r-2 border-neutral-300">
|
||||||
routingForm={form}
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||||
/>
|
{form.team.name}
|
||||||
<ButtonGroup combined>
|
</Badge>
|
||||||
<Tooltip content={t("preview")}>
|
</div>
|
||||||
|
)}
|
||||||
|
<FormAction
|
||||||
|
disabled={readOnly}
|
||||||
|
className="self-center"
|
||||||
|
action="toggle"
|
||||||
|
routingForm={form}
|
||||||
|
/>
|
||||||
|
<ButtonGroup combined>
|
||||||
|
<Tooltip content={t("preview")}>
|
||||||
|
<FormAction
|
||||||
|
action="preview"
|
||||||
|
routingForm={form}
|
||||||
|
target="_blank"
|
||||||
|
StartIcon={ExternalLink}
|
||||||
|
color="secondary"
|
||||||
|
variant="icon"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
<FormAction
|
<FormAction
|
||||||
action="preview"
|
|
||||||
routingForm={form}
|
routingForm={form}
|
||||||
target="_blank"
|
action="copyLink"
|
||||||
StartIcon={ExternalLink}
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
|
StartIcon={LinkIcon}
|
||||||
|
tooltip={t("copy_link_to_form")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
<FormAction
|
|
||||||
routingForm={form}
|
|
||||||
action="copyLink"
|
|
||||||
color="secondary"
|
|
||||||
variant="icon"
|
|
||||||
StartIcon={LinkIcon}
|
|
||||||
tooltip={t("copy_link_to_form")}
|
|
||||||
/>
|
|
||||||
<FormAction
|
|
||||||
routingForm={form}
|
|
||||||
action="embed"
|
|
||||||
color="secondary"
|
|
||||||
variant="icon"
|
|
||||||
StartIcon={Code}
|
|
||||||
tooltip={t("embed")}
|
|
||||||
/>
|
|
||||||
<FormActionsDropdown disabled={readOnly}>
|
|
||||||
<FormAction
|
<FormAction
|
||||||
action="edit"
|
|
||||||
routingForm={form}
|
routingForm={form}
|
||||||
color="minimal"
|
action="embed"
|
||||||
className="!flex"
|
color="secondary"
|
||||||
StartIcon={Edit}>
|
variant="icon"
|
||||||
{t("edit")}
|
StartIcon={Code}
|
||||||
</FormAction>
|
tooltip={t("embed")}
|
||||||
<FormAction
|
/>
|
||||||
action="download"
|
<FormActionsDropdown disabled={readOnly}>
|
||||||
routingForm={form}
|
|
||||||
color="minimal"
|
|
||||||
StartIcon={Download}>
|
|
||||||
{t("download_responses")}
|
|
||||||
</FormAction>
|
|
||||||
<FormAction
|
|
||||||
action="duplicate"
|
|
||||||
routingForm={form}
|
|
||||||
color="minimal"
|
|
||||||
className="w-full"
|
|
||||||
StartIcon={Copy}>
|
|
||||||
{t("duplicate")}
|
|
||||||
</FormAction>
|
|
||||||
{typeformApp?.isInstalled ? (
|
|
||||||
<FormAction
|
<FormAction
|
||||||
data-testid="copy-redirect-url"
|
action="edit"
|
||||||
routingForm={form}
|
routingForm={form}
|
||||||
action="copyRedirectUrl"
|
|
||||||
color="minimal"
|
color="minimal"
|
||||||
type="button"
|
className="!flex"
|
||||||
StartIcon={LinkIcon}>
|
StartIcon={Edit}>
|
||||||
{t("Copy Typeform Redirect Url")}
|
{t("edit")}
|
||||||
</FormAction>
|
</FormAction>
|
||||||
) : null}
|
<FormAction
|
||||||
<FormAction
|
action="download"
|
||||||
action="_delete"
|
routingForm={form}
|
||||||
routingForm={form}
|
color="minimal"
|
||||||
color="destructive"
|
StartIcon={Download}>
|
||||||
className="w-full"
|
{t("download_responses")}
|
||||||
StartIcon={Trash}>
|
</FormAction>
|
||||||
{t("delete")}
|
<FormAction
|
||||||
</FormAction>
|
action="duplicate"
|
||||||
</FormActionsDropdown>
|
routingForm={form}
|
||||||
</ButtonGroup>
|
color="minimal"
|
||||||
</>
|
className="w-full"
|
||||||
}>
|
StartIcon={Copy}>
|
||||||
<div className="flex flex-wrap gap-1">
|
{t("duplicate")}
|
||||||
<Badge variant="gray" startIcon={Menu}>
|
</FormAction>
|
||||||
{fields.length} {fields.length === 1 ? "field" : "fields"}
|
{typeformApp?.isInstalled ? (
|
||||||
</Badge>
|
<FormAction
|
||||||
<Badge variant="gray" startIcon={GitMerge}>
|
data-testid="copy-redirect-url"
|
||||||
{userRoutes.length} {userRoutes.length === 1 ? "route" : "routes"}
|
routingForm={form}
|
||||||
</Badge>
|
action="copyRedirectUrl"
|
||||||
<Badge variant="gray" startIcon={MessageCircle}>
|
color="minimal"
|
||||||
{form._count.responses}{" "}
|
type="button"
|
||||||
{form._count.responses === 1 ? "response" : "responses"}
|
StartIcon={LinkIcon}>
|
||||||
</Badge>
|
{t("Copy Typeform Redirect Url")}
|
||||||
</div>
|
</FormAction>
|
||||||
</ListLinkItem>
|
) : null}
|
||||||
|
<FormAction
|
||||||
|
action="_delete"
|
||||||
|
routingForm={form}
|
||||||
|
color="destructive"
|
||||||
|
className="w-full"
|
||||||
|
StartIcon={Trash}>
|
||||||
|
{t("delete")}
|
||||||
|
</FormAction>
|
||||||
|
</FormActionsDropdown>
|
||||||
|
</ButtonGroup>
|
||||||
|
</>
|
||||||
|
}>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
<Badge variant="gray" startIcon={Menu}>
|
||||||
|
{fields.length} {fields.length === 1 ? "field" : "fields"}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="gray" startIcon={GitMerge}>
|
||||||
|
{userRoutes.length} {userRoutes.length === 1 ? "route" : "routes"}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="gray" startIcon={MessageCircle}>
|
||||||
|
{form._count.responses}{" "}
|
||||||
|
{form._count.responses === 1 ? "response" : "responses"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</ListLinkItem>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</List>
|
</List>
|
||||||
|
|
|
@ -21,7 +21,7 @@ test.describe("Routing Forms", () => {
|
||||||
|
|
||||||
await page.waitForSelector('[data-testid="routing-forms-list"]');
|
await page.waitForSelector('[data-testid="routing-forms-list"]');
|
||||||
// Ensure that it's visible in forms list
|
// Ensure that it's visible in forms list
|
||||||
expect(await page.locator('[data-testid="routing-forms-list"] > li').count()).toBe(1);
|
expect(await page.locator('[data-testid="routing-forms-list"] > div').count()).toBe(1);
|
||||||
|
|
||||||
await gotoRoutingLink({ page, formId });
|
await gotoRoutingLink({ page, formId });
|
||||||
await expect(page.locator("text=Test Form Name")).toBeVisible();
|
await expect(page.locator("text=Test Form Name")).toBeVisible();
|
||||||
|
|
|
@ -59,6 +59,7 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt
|
||||||
fields: true,
|
fields: true,
|
||||||
settings: true,
|
settings: true,
|
||||||
teamId: true,
|
teamId: true,
|
||||||
|
position: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -26,9 +26,14 @@ export const formsHandler = async ({ ctx, input }: FormsHandlerOptions) => {
|
||||||
|
|
||||||
const forms = await prisma.app_RoutingForms_Form.findMany({
|
const forms = await prisma.app_RoutingForms_Form.findMany({
|
||||||
where,
|
where,
|
||||||
orderBy: {
|
orderBy: [
|
||||||
createdAt: "desc",
|
{
|
||||||
},
|
position: "desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: "asc",
|
||||||
|
},
|
||||||
|
],
|
||||||
include: {
|
include: {
|
||||||
team: {
|
team: {
|
||||||
include: {
|
include: {
|
||||||
|
|
|
@ -50,7 +50,8 @@ export class PaymentService implements IAbstractPaymentService {
|
||||||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||||
bookingId: Booking["id"],
|
bookingId: Booking["id"],
|
||||||
bookerEmail: string,
|
bookerEmail: string,
|
||||||
paymentOption: PaymentOption
|
paymentOption: PaymentOption,
|
||||||
|
eventTitle?: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Ensure that the payment service can support the passed payment option
|
// Ensure that the payment service can support the passed payment option
|
||||||
|
@ -78,6 +79,12 @@ export class PaymentService implements IAbstractPaymentService {
|
||||||
currency: this.credentials.default_currency,
|
currency: this.credentials.default_currency,
|
||||||
payment_method_types: ["card"],
|
payment_method_types: ["card"],
|
||||||
customer: customer.id,
|
customer: customer.id,
|
||||||
|
metadata: {
|
||||||
|
identifier: "cal.com",
|
||||||
|
bookingId,
|
||||||
|
bookerEmail,
|
||||||
|
eventName: eventTitle || "",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const paymentIntent = await this.stripe.paymentIntents.create(params, {
|
const paymentIntent = await this.stripe.paymentIntents.create(params, {
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"stripe": "^9.16.0",
|
"stripe": "^9.16.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*",
|
"@calcom/types": "*",
|
||||||
|
|
|
@ -217,7 +217,8 @@ export const getBusyCalendarTimes = async (
|
||||||
|
|
||||||
export const createEvent = async (
|
export const createEvent = async (
|
||||||
credential: CredentialWithAppName,
|
credential: CredentialWithAppName,
|
||||||
calEvent: CalendarEvent
|
calEvent: CalendarEvent,
|
||||||
|
externalId?: string
|
||||||
): Promise<EventResult<NewCalendarEventType>> => {
|
): Promise<EventResult<NewCalendarEventType>> => {
|
||||||
const uid: string = getUid(calEvent);
|
const uid: string = getUid(calEvent);
|
||||||
const calendar = await getCalendar(credential);
|
const calendar = await getCalendar(credential);
|
||||||
|
@ -226,29 +227,31 @@ export const createEvent = async (
|
||||||
|
|
||||||
// Check if the disabledNotes flag is set to true
|
// Check if the disabledNotes flag is set to true
|
||||||
if (calEvent.hideCalendarNotes) {
|
if (calEvent.hideCalendarNotes) {
|
||||||
calEvent.additionalNotes = "Notes have been hidden by the organiser"; // TODO: i18n this string?
|
calEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string?
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Surface success/error messages coming from apps to improve end user visibility
|
// TODO: Surface success/error messages coming from apps to improve end user visibility
|
||||||
const creationResult = calendar
|
const creationResult = calendar
|
||||||
? await calendar.createEvent(calEvent).catch(async (error: { code: number; calError: string }) => {
|
? await calendar
|
||||||
success = false;
|
.createEvent(calEvent, credential.id)
|
||||||
/**
|
.catch(async (error: { code: number; calError: string }) => {
|
||||||
* There is a time when selectedCalendar externalId doesn't match witch certain credential
|
success = false;
|
||||||
* so google returns 404.
|
/**
|
||||||
* */
|
* There is a time when selectedCalendar externalId doesn't match witch certain credential
|
||||||
if (error?.code === 404) {
|
* so google returns 404.
|
||||||
|
* */
|
||||||
|
if (error?.code === 404) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (error?.calError) {
|
||||||
|
calError = error.calError;
|
||||||
|
}
|
||||||
|
log.error("createEvent failed", JSON.stringify(error), calEvent);
|
||||||
|
// @TODO: This code will be off till we can investigate an error with it
|
||||||
|
//https://github.com/calcom/cal.com/issues/3949
|
||||||
|
// await sendBrokenIntegrationEmail(calEvent, "calendar");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
})
|
||||||
if (error?.calError) {
|
|
||||||
calError = error.calError;
|
|
||||||
}
|
|
||||||
log.error("createEvent failed", JSON.stringify(error), calEvent);
|
|
||||||
// @TODO: This code will be off till we can investigate an error with it
|
|
||||||
//https://github.com/calcom/cal.com/issues/3949
|
|
||||||
// await sendBrokenIntegrationEmail(calEvent, "calendar");
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -261,6 +264,8 @@ export const createEvent = async (
|
||||||
originalEvent: calEvent,
|
originalEvent: calEvent,
|
||||||
calError,
|
calError,
|
||||||
calWarnings: creationResult?.additionalInfo?.calWarnings || [],
|
calWarnings: creationResult?.additionalInfo?.calWarnings || [],
|
||||||
|
externalId,
|
||||||
|
credentialId: credential.id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,9 @@ export default class EventManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to Cal Video if Google Meet is selected w/o a Google Cal
|
// Fallback to Cal Video if Google Meet is selected w/o a Google Cal
|
||||||
if (evt.location === MeetLocationType && evt.destinationCalendar?.integration !== "google_calendar") {
|
// @NOTE: destinationCalendar it's an array now so as a fallback we will only check the first one
|
||||||
|
const [mainHostDestinationCalendar] = evt.destinationCalendar ?? [];
|
||||||
|
if (evt.location === MeetLocationType && mainHostDestinationCalendar.integration !== "google_calendar") {
|
||||||
evt["location"] = "integrations:daily";
|
evt["location"] = "integrations:daily";
|
||||||
}
|
}
|
||||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
||||||
|
@ -164,8 +166,8 @@ export default class EventManager {
|
||||||
meetingId: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString(),
|
meetingId: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString(),
|
||||||
meetingPassword: createdEventObj ? createdEventObj.password : result.createdEvent?.password,
|
meetingPassword: createdEventObj ? createdEventObj.password : result.createdEvent?.password,
|
||||||
meetingUrl: createdEventObj ? createdEventObj.onlineMeetingUrl : result.createdEvent?.url,
|
meetingUrl: createdEventObj ? createdEventObj.onlineMeetingUrl : result.createdEvent?.url,
|
||||||
externalCalendarId: isCalendarType ? evt.destinationCalendar?.externalId : undefined,
|
externalCalendarId: isCalendarType ? result.externalId : undefined,
|
||||||
credentialId: isCalendarType ? evt.destinationCalendar?.credentialId : result.credentialId,
|
credentialId: isCalendarType ? result.credentialId : undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -203,8 +205,8 @@ export default class EventManager {
|
||||||
meetingId: result.createdEvent?.id?.toString(),
|
meetingId: result.createdEvent?.id?.toString(),
|
||||||
meetingPassword: result.createdEvent?.password,
|
meetingPassword: result.createdEvent?.password,
|
||||||
meetingUrl: result.createdEvent?.url,
|
meetingUrl: result.createdEvent?.url,
|
||||||
externalCalendarId: evt.destinationCalendar?.externalId,
|
externalCalendarId: result.externalId,
|
||||||
credentialId: result.credentialId ?? evt.destinationCalendar?.credentialId,
|
credentialId: result.credentialId ?? undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -332,29 +334,52 @@ export default class EventManager {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async createAllCalendarEvents(event: CalendarEvent) {
|
private async createAllCalendarEvents(event: CalendarEvent) {
|
||||||
/** Can I use destinationCalendar here? */
|
|
||||||
/* How can I link a DC to a cred? */
|
|
||||||
|
|
||||||
let createdEvents: EventResult<NewCalendarEventType>[] = [];
|
let createdEvents: EventResult<NewCalendarEventType>[] = [];
|
||||||
if (event.destinationCalendar) {
|
if (event.destinationCalendar && event.destinationCalendar.length > 0) {
|
||||||
if (event.destinationCalendar.credentialId) {
|
for (const destination of event.destinationCalendar) {
|
||||||
const credential = this.calendarCredentials.find(
|
if (destination.credentialId) {
|
||||||
(c) => c.id === event.destinationCalendar?.credentialId
|
let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
|
||||||
);
|
if (!credential) {
|
||||||
|
// Fetch credential from DB
|
||||||
if (credential) {
|
const credentialFromDB = await prisma.credential.findUnique({
|
||||||
const createdEvent = await createEvent(credential, event);
|
include: {
|
||||||
if (createdEvent) {
|
app: {
|
||||||
createdEvents.push(createdEvent);
|
select: {
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: destination.credentialId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (credentialFromDB && credentialFromDB.app?.slug) {
|
||||||
|
credential = {
|
||||||
|
appName: credentialFromDB?.app.slug ?? "",
|
||||||
|
id: credentialFromDB.id,
|
||||||
|
type: credentialFromDB.type,
|
||||||
|
key: credentialFromDB.key,
|
||||||
|
userId: credentialFromDB.userId,
|
||||||
|
teamId: credentialFromDB.teamId,
|
||||||
|
invalid: credentialFromDB.invalid,
|
||||||
|
appId: credentialFromDB.appId,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (credential) {
|
||||||
|
const createdEvent = await createEvent(credential, event, destination.externalId);
|
||||||
|
if (createdEvent) {
|
||||||
|
createdEvents.push(createdEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const destinationCalendarCredentials = this.calendarCredentials.filter(
|
||||||
|
(c) => c.type === destination.integration
|
||||||
|
);
|
||||||
|
createdEvents = createdEvents.concat(
|
||||||
|
await Promise.all(destinationCalendarCredentials.map(async (c) => await createEvent(c, event)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const destinationCalendarCredentials = this.calendarCredentials.filter(
|
|
||||||
(c) => c.type === event.destinationCalendar?.integration
|
|
||||||
);
|
|
||||||
createdEvents = createdEvents.concat(
|
|
||||||
await Promise.all(destinationCalendarCredentials.map(async (c) => await createEvent(c, event)))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/**
|
/**
|
||||||
|
@ -451,7 +476,7 @@ export default class EventManager {
|
||||||
booking: PartialBooking,
|
booking: PartialBooking,
|
||||||
newBookingId?: number
|
newBookingId?: number
|
||||||
): Promise<Array<EventResult<NewCalendarEventType>>> {
|
): Promise<Array<EventResult<NewCalendarEventType>>> {
|
||||||
let calendarReference: PartialReference | undefined = undefined,
|
let calendarReference: PartialReference[] | undefined = undefined,
|
||||||
credential;
|
credential;
|
||||||
try {
|
try {
|
||||||
// If a newBookingId is given, update that calendar event
|
// If a newBookingId is given, update that calendar event
|
||||||
|
@ -468,33 +493,62 @@ export default class EventManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
calendarReference = newBooking?.references.length
|
calendarReference = newBooking?.references.length
|
||||||
? newBooking.references.find((reference) => reference.type.includes("_calendar"))
|
? newBooking.references.filter((reference) => reference.type.includes("_calendar"))
|
||||||
: booking.references.find((reference) => reference.type.includes("_calendar"));
|
: booking.references.filter((reference) => reference.type.includes("_calendar"));
|
||||||
|
|
||||||
if (!calendarReference) {
|
if (calendarReference.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = calendarReference;
|
// process all calendar references
|
||||||
let calenderExternalId: string | null = null;
|
|
||||||
if (bookingExternalCalendarId) {
|
|
||||||
calenderExternalId = bookingExternalCalendarId;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = [];
|
let result = [];
|
||||||
if (calendarReference.credentialId) {
|
for (const reference of calendarReference) {
|
||||||
credential = this.calendarCredentials.filter(
|
const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = reference;
|
||||||
(credential) => credential.id === calendarReference?.credentialId
|
let calenderExternalId: string | null = null;
|
||||||
)[0];
|
if (bookingExternalCalendarId) {
|
||||||
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
|
calenderExternalId = bookingExternalCalendarId;
|
||||||
} else {
|
}
|
||||||
const credentials = this.calendarCredentials.filter(
|
|
||||||
(credential) => credential.type === calendarReference?.type
|
if (reference.credentialId) {
|
||||||
);
|
credential = this.calendarCredentials.filter(
|
||||||
for (const credential of credentials) {
|
(credential) => credential.id === reference?.credentialId
|
||||||
|
)[0];
|
||||||
|
if (!credential) {
|
||||||
|
// Fetch credential from DB
|
||||||
|
const credentialFromDB = await prisma.credential.findUnique({
|
||||||
|
include: {
|
||||||
|
app: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: reference.credentialId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (credentialFromDB && credentialFromDB.app?.slug) {
|
||||||
|
credential = {
|
||||||
|
appName: credentialFromDB?.app.slug ?? "",
|
||||||
|
id: credentialFromDB.id,
|
||||||
|
type: credentialFromDB.type,
|
||||||
|
key: credentialFromDB.key,
|
||||||
|
userId: credentialFromDB.userId,
|
||||||
|
teamId: credentialFromDB.teamId,
|
||||||
|
invalid: credentialFromDB.invalid,
|
||||||
|
appId: credentialFromDB.appId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
|
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
|
||||||
|
} else {
|
||||||
|
const credentials = this.calendarCredentials.filter(
|
||||||
|
(credential) => credential.type === reference?.type
|
||||||
|
);
|
||||||
|
for (const credential of credentials) {
|
||||||
|
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are merging two calendar events we should delete the old calendar event
|
// If we are merging two calendar events we should delete the old calendar event
|
||||||
if (newBookingId) {
|
if (newBookingId) {
|
||||||
const oldCalendarEvent = booking.references.find((reference) => reference.type.includes("_calendar"));
|
const oldCalendarEvent = booking.references.find((reference) => reference.type.includes("_calendar"));
|
||||||
|
@ -516,17 +570,17 @@ export default class EventManager {
|
||||||
.filter((cred) => cred.type.includes("other_calendar"))
|
.filter((cred) => cred.type.includes("other_calendar"))
|
||||||
.map(async (cred) => {
|
.map(async (cred) => {
|
||||||
const calendarReference = booking.references.find((ref) => ref.type === cred.type);
|
const calendarReference = booking.references.find((ref) => ref.type === cred.type);
|
||||||
if (!calendarReference)
|
|
||||||
if (!calendarReference) {
|
if (!calendarReference) {
|
||||||
return {
|
return {
|
||||||
appName: cred.appName,
|
appName: cred.appName,
|
||||||
type: cred.type,
|
type: cred.type,
|
||||||
success: false,
|
success: false,
|
||||||
uid: "",
|
uid: "",
|
||||||
originalEvent: event,
|
originalEvent: event,
|
||||||
credentialId: cred.id,
|
credentialId: cred.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { externalCalendarId: bookingExternalCalendarId, meetingId: bookingRefUid } =
|
const { externalCalendarId: bookingExternalCalendarId, meetingId: bookingRefUid } =
|
||||||
calendarReference;
|
calendarReference;
|
||||||
return await updateEvent(cred, event, bookingRefUid ?? null, bookingExternalCalendarId ?? null);
|
return await updateEvent(cred, event, bookingRefUid ?? null, bookingExternalCalendarId ?? null);
|
||||||
|
@ -539,17 +593,19 @@ export default class EventManager {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
message = message.replace("{thing}", error.message);
|
message = message.replace("{thing}", error.message);
|
||||||
}
|
}
|
||||||
console.error(message);
|
|
||||||
return Promise.resolve([
|
return Promise.resolve(
|
||||||
{
|
calendarReference?.map((reference) => {
|
||||||
appName: "none",
|
return {
|
||||||
type: calendarReference?.type || "calendar",
|
appName: "none",
|
||||||
success: false,
|
type: reference?.type || "calendar",
|
||||||
uid: "",
|
success: false,
|
||||||
originalEvent: event,
|
uid: "",
|
||||||
credentialId: 0,
|
originalEvent: event,
|
||||||
},
|
credentialId: 0,
|
||||||
]);
|
};
|
||||||
|
}) ?? ([] as Array<EventResult<NewCalendarEventType>>)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Prisma, Booking } from "@prisma/client";
|
import type { Booking } from "@prisma/client";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class CalendarEventClass implements CalendarEvent {
|
||||||
uid?: string | null;
|
uid?: string | null;
|
||||||
videoCallData?: VideoCallData;
|
videoCallData?: VideoCallData;
|
||||||
paymentInfo?: any;
|
paymentInfo?: any;
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
destinationCalendar?: DestinationCalendar[] | null;
|
||||||
cancellationReason?: string | null;
|
cancellationReason?: string | null;
|
||||||
rejectionReason?: string | null;
|
rejectionReason?: string | null;
|
||||||
hideCalendarNotes?: boolean;
|
hideCalendarNotes?: boolean;
|
||||||
|
|
|
@ -85,8 +85,9 @@ export const BrokenIntegrationEmail = (
|
||||||
|
|
||||||
if (type === "calendar") {
|
if (type === "calendar") {
|
||||||
// The calendar name is stored as name_calendar
|
// The calendar name is stored as name_calendar
|
||||||
let calendar = calEvent.destinationCalendar
|
const [mainHostDestinationCalendar] = calEvent.destinationCalendar ?? [];
|
||||||
? calEvent.destinationCalendar?.integration.split("_")
|
let calendar = mainHostDestinationCalendar
|
||||||
|
? mainHostDestinationCalendar?.integration.split("_")
|
||||||
: "calendar";
|
: "calendar";
|
||||||
|
|
||||||
if (Array.isArray(calendar)) {
|
if (Array.isArray(calendar)) {
|
||||||
|
|
|
@ -387,7 +387,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
||||||
if (trigger === "update") {
|
if (trigger === "update") {
|
||||||
return {
|
return {
|
||||||
...token,
|
...token,
|
||||||
locale: session?.locale ?? token.locale,
|
locale: session?.locale ?? token.locale ?? "en",
|
||||||
name: session?.name ?? token.name,
|
name: session?.name ?? token.name,
|
||||||
username: session?.username ?? token.username,
|
username: session?.username ?? token.username,
|
||||||
email: session?.email ?? token.email,
|
email: session?.email ?? token.email,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||||
|
@ -23,8 +24,13 @@ export const DatePicker = () => {
|
||||||
return (
|
return (
|
||||||
<DatePickerComponent
|
<DatePickerComponent
|
||||||
isLoading={schedule.isLoading}
|
isLoading={schedule.isLoading}
|
||||||
onChange={(date) => setSelectedDate(date ? date.format("YYYY-MM-DD") : date)}
|
onChange={(date: Dayjs | null) => {
|
||||||
onMonthChange={(date) => setMonth(date.format("YYYY-MM"))}
|
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"));
|
||||||
|
}}
|
||||||
|
onMonthChange={(date: Dayjs) => {
|
||||||
|
setMonth(date.format("YYYY-MM"));
|
||||||
|
setSelectedDate(date.format("YYYY-MM-DD"));
|
||||||
|
}}
|
||||||
includedDates={nonEmptyScheduleDays}
|
includedDates={nonEmptyScheduleDays}
|
||||||
locale={i18n.language}
|
locale={i18n.language}
|
||||||
browsingDate={month ? dayjs(month) : undefined}
|
browsingDate={month ? dayjs(month) : undefined}
|
||||||
|
|
|
@ -154,14 +154,19 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
|
||||||
},
|
},
|
||||||
selectedDate: getQueryParam("date") || null,
|
selectedDate: getQueryParam("date") || null,
|
||||||
setSelectedDate: (selectedDate: string | null) => {
|
setSelectedDate: (selectedDate: string | null) => {
|
||||||
|
// unset selected date
|
||||||
|
if (!selectedDate) {
|
||||||
|
removeQueryParam("date");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentSelection = dayjs(get().selectedDate);
|
const currentSelection = dayjs(get().selectedDate);
|
||||||
const newSelection = dayjs(selectedDate);
|
const newSelection = dayjs(selectedDate);
|
||||||
set({ selectedDate });
|
set({ selectedDate });
|
||||||
updateQueryParam("date", selectedDate ?? "");
|
updateQueryParam("date", selectedDate ?? "");
|
||||||
|
|
||||||
// Setting month make sure small calendar in fullscreen layouts also updates.
|
// Setting month make sure small calendar in fullscreen layouts also updates.
|
||||||
// If selectedDate is null, prevents setting month to Invalid-Date
|
if (newSelection.month() !== currentSelection.month()) {
|
||||||
if (selectedDate && newSelection.month() !== currentSelection.month()) {
|
|
||||||
set({ month: newSelection.format("YYYY-MM") });
|
set({ month: newSelection.format("YYYY-MM") });
|
||||||
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
updateQueryParam("month", newSelection.format("YYYY-MM"));
|
||||||
}
|
}
|
||||||
|
@ -194,6 +199,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
|
||||||
setMonth: (month: string | null) => {
|
setMonth: (month: string | null) => {
|
||||||
set({ month, selectedTimeslot: null });
|
set({ month, selectedTimeslot: null });
|
||||||
updateQueryParam("month", month ?? "");
|
updateQueryParam("month", month ?? "");
|
||||||
|
get().setSelectedDate(null);
|
||||||
},
|
},
|
||||||
isTeamEvent: false,
|
isTeamEvent: false,
|
||||||
seatedEventData: {
|
seatedEventData: {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
@ -26,6 +28,7 @@ type Avatar = {
|
||||||
type AvatarWithRequiredImage = Avatar & { image: string };
|
type AvatarWithRequiredImage = Avatar & { image: string };
|
||||||
|
|
||||||
export const EventMembers = ({ schedulingType, users, profile, entity }: EventMembersProps) => {
|
export const EventMembers = ({ schedulingType, users, profile, entity }: EventMembersProps) => {
|
||||||
|
const pathname = usePathname();
|
||||||
const showMembers = schedulingType !== SchedulingType.ROUND_ROBIN;
|
const showMembers = schedulingType !== SchedulingType.ROUND_ROBIN;
|
||||||
const shownUsers = showMembers ? users : [];
|
const shownUsers = showMembers ? users : [];
|
||||||
|
|
||||||
|
@ -57,7 +60,9 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe
|
||||||
title: `${profile.name || profile.username}`,
|
title: `${profile.name || profile.username}`,
|
||||||
image: "logo" in profile && profile.logo ? `${profile.logo}` : undefined,
|
image: "logo" in profile && profile.logo ? `${profile.logo}` : undefined,
|
||||||
alt: profile.name || undefined,
|
alt: profile.name || undefined,
|
||||||
href: profile.username ? `${CAL_URL}/${profile.username}` : undefined,
|
href: profile.username
|
||||||
|
? `${CAL_URL}` + (pathname.indexOf("/team/") !== -1 ? "/team" : "") + `/${profile.username}`
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const uniqueAvatars = avatars
|
const uniqueAvatars = avatars
|
||||||
|
|
|
@ -248,7 +248,11 @@ async function handler(req: CustomRequest) {
|
||||||
? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
|
? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
|
||||||
: undefined,
|
: undefined,
|
||||||
location: bookingToDelete?.location,
|
location: bookingToDelete?.location,
|
||||||
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
destinationCalendar: bookingToDelete?.destinationCalendar
|
||||||
|
? [bookingToDelete?.destinationCalendar]
|
||||||
|
: bookingToDelete?.user.destinationCalendar
|
||||||
|
? [bookingToDelete?.user.destinationCalendar]
|
||||||
|
: [],
|
||||||
cancellationReason: cancellationReason,
|
cancellationReason: cancellationReason,
|
||||||
...(teamMembers && { team: { name: "", members: teamMembers } }),
|
...(teamMembers && { team: { name: "", members: teamMembers } }),
|
||||||
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
|
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
|
||||||
|
@ -411,58 +415,71 @@ async function handler(req: CustomRequest) {
|
||||||
|
|
||||||
const apiDeletes = [];
|
const apiDeletes = [];
|
||||||
|
|
||||||
const bookingCalendarReference = bookingToDelete.references.find((reference) =>
|
const bookingCalendarReference = bookingToDelete.references.filter((reference) =>
|
||||||
reference.type.includes("_calendar")
|
reference.type.includes("_calendar")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (bookingCalendarReference) {
|
if (bookingCalendarReference.length > 0) {
|
||||||
const { credentialId, uid, externalCalendarId } = bookingCalendarReference;
|
for (const reference of bookingCalendarReference) {
|
||||||
// If the booking calendar reference contains a credentialId
|
const { credentialId, uid, externalCalendarId } = reference;
|
||||||
if (credentialId) {
|
// If the booking calendar reference contains a credentialId
|
||||||
// Find the correct calendar credential under user credentials
|
if (credentialId) {
|
||||||
const calendarCredential = bookingToDelete.user.credentials.find(
|
// Find the correct calendar credential under user credentials
|
||||||
(credential) => credential.id === credentialId
|
let calendarCredential = bookingToDelete.user.credentials.find(
|
||||||
);
|
(credential) => credential.id === credentialId
|
||||||
if (calendarCredential) {
|
);
|
||||||
const calendar = await getCalendar(calendarCredential);
|
if (!calendarCredential) {
|
||||||
if (
|
// get credential from DB
|
||||||
bookingToDelete.eventType?.recurringEvent &&
|
const foundCalendarCredential = await prisma.credential.findUnique({
|
||||||
bookingToDelete.recurringEventId &&
|
where: {
|
||||||
allRemainingBookings
|
id: credentialId,
|
||||||
) {
|
},
|
||||||
const promises = bookingToDelete.user.credentials
|
});
|
||||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
if (foundCalendarCredential) {
|
||||||
.map(async (credential) => {
|
calendarCredential = foundCalendarCredential;
|
||||||
const calendar = await getCalendar(credential);
|
|
||||||
for (const updBooking of updatedBookings) {
|
|
||||||
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
|
|
||||||
if (bookingRef) {
|
|
||||||
const { uid, externalCalendarId } = bookingRef;
|
|
||||||
const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
|
|
||||||
apiDeletes.push(deletedEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await Promise.all(promises);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
logger.error(error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
if (calendarCredential) {
|
||||||
|
const calendar = await getCalendar(calendarCredential);
|
||||||
|
if (
|
||||||
|
bookingToDelete.eventType?.recurringEvent &&
|
||||||
|
bookingToDelete.recurringEventId &&
|
||||||
|
allRemainingBookings
|
||||||
|
) {
|
||||||
|
const promises = bookingToDelete.user.credentials
|
||||||
|
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||||
|
.map(async (credential) => {
|
||||||
|
const calendar = await getCalendar(credential);
|
||||||
|
for (const updBooking of updatedBookings) {
|
||||||
|
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
|
||||||
|
if (bookingRef) {
|
||||||
|
const { uid, externalCalendarId } = bookingRef;
|
||||||
|
const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
|
||||||
|
apiDeletes.push(deletedEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await Promise.all(promises);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
logger.error(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For bookings made before the refactor we go through the old behavior of running through each calendar credential
|
||||||
|
const calendarCredentials = bookingToDelete.user.credentials.filter((credential) =>
|
||||||
|
credential.type.endsWith("_calendar")
|
||||||
|
);
|
||||||
|
for (const credential of calendarCredentials) {
|
||||||
|
const calendar = await getCalendar(credential);
|
||||||
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
|
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// For bookings made before the refactor we go through the old behaviour of running through each calendar credential
|
|
||||||
const calendarCredentials = bookingToDelete.user.credentials.filter((credential) =>
|
|
||||||
credential.type.endsWith("_calendar")
|
|
||||||
);
|
|
||||||
for (const credential of calendarCredentials) {
|
|
||||||
const calendar = await getCalendar(credential);
|
|
||||||
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,7 +525,11 @@ async function handler(req: CustomRequest) {
|
||||||
attendees: attendeesList,
|
attendees: attendeesList,
|
||||||
location: bookingToDelete.location ?? "",
|
location: bookingToDelete.location ?? "",
|
||||||
uid: bookingToDelete.uid ?? "",
|
uid: bookingToDelete.uid ?? "",
|
||||||
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
destinationCalendar: bookingToDelete?.destinationCalendar
|
||||||
|
? [bookingToDelete?.destinationCalendar]
|
||||||
|
: bookingToDelete?.user.destinationCalendar
|
||||||
|
? [bookingToDelete?.user.destinationCalendar]
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const successPayment = bookingToDelete.payment.find((payment) => payment.success);
|
const successPayment = bookingToDelete.payment.find((payment) => payment.success);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { App, Attendee, Credential, EventTypeCustomInput } from "@prisma/client";
|
import type { App, Attendee, Credential, EventTypeCustomInput, DestinationCalendar } from "@prisma/client";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import async from "async";
|
import async from "async";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
|
@ -367,7 +367,7 @@ async function ensureAvailableUsers(
|
||||||
) {
|
) {
|
||||||
const availableUsers: IsFixedAwareUser[] = [];
|
const availableUsers: IsFixedAwareUser[] = [];
|
||||||
|
|
||||||
const orginalBookingDuration = input.originalRescheduledBooking
|
const originalBookingDuration = input.originalRescheduledBooking
|
||||||
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
||||||
dayjs(input.originalRescheduledBooking.startTime),
|
dayjs(input.originalRescheduledBooking.startTime),
|
||||||
"minutes"
|
"minutes"
|
||||||
|
@ -380,7 +380,7 @@ async function ensureAvailableUsers(
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
duration: orginalBookingDuration,
|
duration: originalBookingDuration,
|
||||||
...input,
|
...input,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -686,8 +686,7 @@ async function handler(
|
||||||
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
|
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
|
||||||
|
|
||||||
const isTeamEventType =
|
const isTeamEventType =
|
||||||
eventType.schedulingType === SchedulingType.COLLECTIVE ||
|
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
|
||||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN;
|
|
||||||
|
|
||||||
const paymentAppData = getPaymentAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
|
||||||
|
@ -722,31 +721,46 @@ async function handler(
|
||||||
throw new HttpError({ statusCode: 400, message: error.message });
|
throw new HttpError({ statusCode: 400, message: error.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadUsers = async () =>
|
const loadUsers = async () => {
|
||||||
!eventTypeId
|
try {
|
||||||
? await prisma.user.findMany({
|
if (!eventTypeId) {
|
||||||
|
if (!Array.isArray(dynamicUserList) || dynamicUserList.length === 0) {
|
||||||
|
throw new Error("dynamicUserList is not properly defined or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
username: {
|
username: { in: dynamicUserList },
|
||||||
in: dynamicUserList,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
...userSelect.select,
|
...userSelect.select,
|
||||||
credentials: true, // Don't leak to client
|
credentials: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
organization: {
|
|
||||||
select: {
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
: eventType.hosts?.length
|
|
||||||
? eventType.hosts.map(({ user, isFixed }) => ({
|
return users;
|
||||||
|
} else {
|
||||||
|
const hosts = eventType.hosts || [];
|
||||||
|
|
||||||
|
if (!Array.isArray(hosts)) {
|
||||||
|
throw new Error("eventType.hosts is not properly defined.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = hosts.map(({ user, isFixed }) => ({
|
||||||
...user,
|
...user,
|
||||||
isFixed,
|
isFixed,
|
||||||
}))
|
}));
|
||||||
: eventType.users || [];
|
|
||||||
|
return users.length ? users : eventType.users;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError || error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: error.message });
|
||||||
|
}
|
||||||
|
throw new HttpError({ statusCode: 500, message: "Unable to load users" });
|
||||||
|
}
|
||||||
|
};
|
||||||
// loadUsers allows type inferring
|
// loadUsers allows type inferring
|
||||||
let users: (Awaited<ReturnType<typeof loadUsers>>[number] & {
|
let users: (Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||||
isFixed?: boolean;
|
isFixed?: boolean;
|
||||||
|
@ -970,20 +984,26 @@ async function handler(
|
||||||
: getLocationValueForDB(locationBodyString, eventType.locations);
|
: getLocationValueForDB(locationBodyString, eventType.locations);
|
||||||
|
|
||||||
const customInputs = getCustomInputsResponses(reqBody, eventType.customInputs);
|
const customInputs = getCustomInputsResponses(reqBody, eventType.customInputs);
|
||||||
const teamMemberPromises =
|
const teamDestinationCalendars: DestinationCalendar[] = [];
|
||||||
users.length > 1
|
|
||||||
? users.slice(1).map(async function (user) {
|
// Organizer or user owner of this event type it's not listed as a team member.
|
||||||
return {
|
const teamMemberPromises = users.slice(1).map(async (user) => {
|
||||||
email: user.email || "",
|
// push to teamDestinationCalendars if it's a team event but collective only
|
||||||
name: user.name || "",
|
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE" && user.destinationCalendar) {
|
||||||
timeZone: user.timeZone,
|
teamDestinationCalendars.push(user.destinationCalendar);
|
||||||
language: {
|
}
|
||||||
translate: await getTranslation(user.locale ?? "en", "common"),
|
return {
|
||||||
locale: user.locale ?? "en",
|
email: user.email ?? "",
|
||||||
},
|
name: user.name ?? "",
|
||||||
};
|
firstName: "",
|
||||||
})
|
lastName: "",
|
||||||
: [];
|
timeZone: user.timeZone,
|
||||||
|
language: {
|
||||||
|
translate: await getTranslation(user.locale ?? "en", "common"),
|
||||||
|
locale: user.locale ?? "en",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const teamMembers = await Promise.all(teamMemberPromises);
|
const teamMembers = await Promise.all(teamMemberPromises);
|
||||||
|
|
||||||
|
@ -1040,16 +1060,24 @@ async function handler(
|
||||||
attendees: attendeesList,
|
attendees: attendeesList,
|
||||||
location: bookingLocation, // Will be processed by the EventManager later.
|
location: bookingLocation, // Will be processed by the EventManager later.
|
||||||
conferenceCredentialId,
|
conferenceCredentialId,
|
||||||
/** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */
|
destinationCalendar: eventType.destinationCalendar
|
||||||
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
|
? [eventType.destinationCalendar]
|
||||||
|
: organizerUser.destinationCalendar
|
||||||
|
? [organizerUser.destinationCalendar]
|
||||||
|
: null,
|
||||||
hideCalendarNotes: eventType.hideCalendarNotes,
|
hideCalendarNotes: eventType.hideCalendarNotes,
|
||||||
requiresConfirmation: requiresConfirmation ?? false,
|
requiresConfirmation: requiresConfirmation ?? false,
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
// if seats are not enabled we should default true
|
// if seats are not enabled we should default true
|
||||||
seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true,
|
seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true,
|
||||||
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
||||||
|
schedulingType: eventType.schedulingType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") {
|
||||||
|
evt.destinationCalendar?.push(...teamDestinationCalendars);
|
||||||
|
}
|
||||||
|
|
||||||
/* Used for seats bookings to update evt object with video data */
|
/* Used for seats bookings to update evt object with video data */
|
||||||
const addVideoCallDataToEvt = (bookingReferences: BookingReference[]) => {
|
const addVideoCallDataToEvt = (bookingReferences: BookingReference[]) => {
|
||||||
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
||||||
|
@ -1843,11 +1871,12 @@ async function handler(
|
||||||
id: organizerUser.id,
|
id: organizerUser.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
destinationCalendar: evt.destinationCalendar
|
destinationCalendar:
|
||||||
? {
|
evt.destinationCalendar && evt.destinationCalendar.length > 0
|
||||||
connect: { id: evt.destinationCalendar.id },
|
? {
|
||||||
}
|
connect: { id: evt.destinationCalendar[0].id },
|
||||||
: undefined,
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (reqBody.recurringEventId) {
|
if (reqBody.recurringEventId) {
|
||||||
|
|
|
@ -30,7 +30,7 @@ export type DatePickerProps = {
|
||||||
/** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */
|
/** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */
|
||||||
excludedDates?: string[];
|
excludedDates?: string[];
|
||||||
/** defaults to all, which dates are bookable (inverse of excludedDates) */
|
/** defaults to all, which dates are bookable (inverse of excludedDates) */
|
||||||
includedDates?: string[] | null;
|
includedDates?: string[];
|
||||||
/** allows adding classes to the container */
|
/** allows adding classes to the container */
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Shows a small loading spinner next to the month name */
|
/** Shows a small loading spinner next to the month name */
|
||||||
|
@ -100,6 +100,40 @@ const NoAvailabilityOverlay = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes care of selecting a valid date in the month if the selected date is not available in the month
|
||||||
|
*/
|
||||||
|
const useHandleInitialDateSelection = ({
|
||||||
|
daysToRenderForTheMonth,
|
||||||
|
selected,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
daysToRenderForTheMonth: { day: Dayjs | null; disabled: boolean }[];
|
||||||
|
selected: Dayjs | Dayjs[] | null | undefined;
|
||||||
|
onChange: (date: Dayjs | null) => void;
|
||||||
|
}) => {
|
||||||
|
// Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment
|
||||||
|
if (selected instanceof Array) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day;
|
||||||
|
|
||||||
|
const isSelectedDateAvailable = selected
|
||||||
|
? daysToRenderForTheMonth.some(({ day, disabled }) => {
|
||||||
|
if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true;
|
||||||
|
})
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) {
|
||||||
|
// If selected date not available in the month, select the first available date of the month
|
||||||
|
onChange(firstAvailableDateOfTheMonth);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstAvailableDateOfTheMonth) {
|
||||||
|
onChange(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Days = ({
|
const Days = ({
|
||||||
minDate = dayjs.utc(),
|
minDate = dayjs.utc(),
|
||||||
excludedDates = [],
|
excludedDates = [],
|
||||||
|
@ -121,7 +155,7 @@ const Days = ({
|
||||||
// Create placeholder elements for empty days in first week
|
// Create placeholder elements for empty days in first week
|
||||||
const weekdayOfFirst = browsingDate.date(1).day();
|
const weekdayOfFirst = browsingDate.date(1).day();
|
||||||
const currentDate = minDate.utcOffset(browsingDate.utcOffset());
|
const currentDate = minDate.utcOffset(browsingDate.utcOffset());
|
||||||
const availableDates = (includedDates: string[] | undefined | null) => {
|
const availableDates = (includedDates: string[] | undefined) => {
|
||||||
const dates = [];
|
const dates = [];
|
||||||
const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate));
|
const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate));
|
||||||
for (
|
for (
|
||||||
|
@ -148,21 +182,6 @@ const Days = ({
|
||||||
days.push(date);
|
days.push(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
const daysToRenderForTheMonth = days.map((day) => {
|
|
||||||
if (!day) return { day: null, disabled: true };
|
|
||||||
return {
|
|
||||||
day: day,
|
|
||||||
disabled:
|
|
||||||
(includedDates && !includedDates.includes(yyyymmdd(day))) || excludedDates.includes(yyyymmdd(day)),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
useHandleInitialDateSelection({
|
|
||||||
daysToRenderForTheMonth,
|
|
||||||
selected,
|
|
||||||
onChange: props.onChange,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow);
|
const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow);
|
||||||
|
|
||||||
const isActive = (day: dayjs.Dayjs) => {
|
const isActive = (day: dayjs.Dayjs) => {
|
||||||
|
@ -190,6 +209,21 @@ const Days = ({
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const daysToRenderForTheMonth = days.map((day) => {
|
||||||
|
if (!day) return { day: null, disabled: true };
|
||||||
|
return {
|
||||||
|
day: day,
|
||||||
|
disabled:
|
||||||
|
(includedDates && !includedDates.includes(yyyymmdd(day))) || excludedDates.includes(yyyymmdd(day)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useHandleInitialDateSelection({
|
||||||
|
daysToRenderForTheMonth,
|
||||||
|
selected,
|
||||||
|
onChange: props.onChange,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{daysToRenderForTheMonth.map(({ day, disabled }, idx) => (
|
{daysToRenderForTheMonth.map(({ day, disabled }, idx) => (
|
||||||
|
@ -305,41 +339,4 @@ const DatePicker = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes care of selecting a valid date in the month if the selected date is not available in the month
|
|
||||||
*/
|
|
||||||
const useHandleInitialDateSelection = ({
|
|
||||||
daysToRenderForTheMonth,
|
|
||||||
selected,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
daysToRenderForTheMonth: { day: Dayjs | null; disabled: boolean }[];
|
|
||||||
selected: Dayjs | Dayjs[] | null | undefined;
|
|
||||||
onChange: (date: Dayjs | null) => void;
|
|
||||||
}) => {
|
|
||||||
// Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment
|
|
||||||
if (selected instanceof Array) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day;
|
|
||||||
|
|
||||||
const isSelectedDateAvailable = selected
|
|
||||||
? daysToRenderForTheMonth.some(({ day, disabled }) => {
|
|
||||||
if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true;
|
|
||||||
})
|
|
||||||
: false;
|
|
||||||
|
|
||||||
if (firstAvailableDateOfTheMonth) {
|
|
||||||
// If selected date not available in the month, select the first available date of the month
|
|
||||||
if (!isSelectedDateAvailable) {
|
|
||||||
onChange(firstAvailableDateOfTheMonth);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No date is available and if we were asked to select something inform that it couldn't be selected. This would actually help in not showing the timeslots section(with No Time Available) when no date in the month is available
|
|
||||||
if (selected) {
|
|
||||||
onChange(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DatePicker;
|
export default DatePicker;
|
||||||
|
|
|
@ -96,6 +96,7 @@ function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
|
||||||
)}
|
)}
|
||||||
data-disabled={isDisabled}
|
data-disabled={isDisabled}
|
||||||
data-slot={timeSlot.toISOString()}
|
data-slot={timeSlot.toISOString()}
|
||||||
|
data-testid="calendar-empty-cell"
|
||||||
style={{
|
style={{
|
||||||
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
|
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
|
|
|
@ -59,7 +59,9 @@ function AdminOrgTable() {
|
||||||
</div>
|
</div>
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell widthClassNames="w-auto">
|
<Cell widthClassNames="w-auto">
|
||||||
<span className="break-all">{org.members[0].user.email}</span>
|
<span className="break-all">
|
||||||
|
{org.members.length ? org.members[0].user.email : "No members"}
|
||||||
|
</span>
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>
|
<Cell>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"@sendgrid/mail": "^7.6.2",
|
"@sendgrid/mail": "^7.6.2",
|
||||||
"libphonenumber-js": "^1.10.12",
|
"libphonenumber-js": "^1.10.12",
|
||||||
"twilio": "^3.80.1",
|
"twilio": "^3.80.1",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/tsconfig": "*"
|
"@calcom/tsconfig": "*"
|
||||||
|
|
|
@ -149,7 +149,11 @@ export async function handlePaymentSuccess(
|
||||||
},
|
},
|
||||||
attendees: attendeesList,
|
attendees: attendeesList,
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
destinationCalendar: booking.destinationCalendar
|
||||||
|
? [booking.destinationCalendar]
|
||||||
|
: user.destinationCalendar
|
||||||
|
? [user.destinationCalendar]
|
||||||
|
: [],
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ async function getBooking(bookingId: number) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const attendeesList = await Promise.all(attendeesListPromises);
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
|
||||||
const evt: CalendarEvent = {
|
const evt: CalendarEvent = {
|
||||||
type: booking.title,
|
type: booking.title,
|
||||||
title: booking.title,
|
title: booking.title,
|
||||||
|
@ -116,7 +116,7 @@ async function getBooking(bookingId: number) {
|
||||||
},
|
},
|
||||||
attendees: attendeesList,
|
attendees: attendeesList,
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
|
||||||
recurringEvent: parseRecurringEvent(eventType?.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventType?.recurringEvent),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -204,7 +204,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const attendeesList = await Promise.all(attendeesListPromises);
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
|
||||||
const evt: CalendarEvent = {
|
const evt: CalendarEvent = {
|
||||||
type: booking.title,
|
type: booking.title,
|
||||||
title: booking.title,
|
title: booking.title,
|
||||||
|
@ -226,7 +226,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
||||||
attendees: attendeesList,
|
attendees: attendeesList,
|
||||||
location: booking.location,
|
location: booking.location,
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
|
||||||
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { GoogleWorkspaceInviteButton } from "./GoogleWorkspaceInviteButton";
|
||||||
|
|
||||||
type MemberInvitationModalProps = {
|
type MemberInvitationModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
justEmailInvites?: boolean;
|
||||||
onExit: () => void;
|
onExit: () => void;
|
||||||
orgMembers?: RouterOutputs["viewer"]["organizations"]["getMembers"];
|
orgMembers?: RouterOutputs["viewer"]["organizations"]["getMembers"];
|
||||||
onSubmit: (values: NewMemberForm, resetFields: () => void) => void;
|
onSubmit: (values: NewMemberForm, resetFields: () => void) => void;
|
||||||
|
@ -206,7 +207,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
||||||
render={({ field: { onChange }, fieldState: { error } }) => (
|
render={({ field: { onChange }, fieldState: { error } }) => (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
label={t("email_or_username")}
|
label={props.justEmailInvites ? t("email") : t("email_or_username")}
|
||||||
id="inviteUser"
|
id="inviteUser"
|
||||||
name="inviteUser"
|
name="inviteUser"
|
||||||
placeholder="email@example.com"
|
placeholder="email@example.com"
|
||||||
|
|
|
@ -15,9 +15,9 @@ export default function TeamPill(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={classNames("text-medium self-center rounded-md px-1 py-0.5 text-xs ltr:mr-1 rtl:ml-1", {
|
className={classNames("text-medium self-center rounded-md px-1 py-0.5 text-xs ltr:mr-1 rtl:ml-1", {
|
||||||
" bg-subtle text-emphasis": !props.color,
|
" bg-subtle text-emphasis": !props.color,
|
||||||
" bg-info text-blue-800": props.color === "blue",
|
" bg-info text-info": props.color === "blue",
|
||||||
" bg-error text-red-800 ": props.color === "red",
|
" bg-error text-error ": props.color === "red",
|
||||||
" bg-attention text-orange-800": props.color === "orange",
|
" bg-attention text-attention": props.color === "orange",
|
||||||
})}>
|
})}>
|
||||||
{props.text}
|
{props.text}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -65,7 +65,7 @@ export function TeamsListing() {
|
||||||
{
|
{
|
||||||
icon: <Mail className="h-5 w-5 text-orange-500" />,
|
icon: <Mail className="h-5 w-5 text-orange-500" />,
|
||||||
title: t("sms_attendee_action"),
|
title: t("sms_attendee_action"),
|
||||||
description: t("make_it_easy_to_book"),
|
description: t("send_reminder_sms"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <Video className="h-5 w-5 text-purple-500" />,
|
icon: <Video className="h-5 w-5 text-purple-500" />,
|
||||||
|
|
|
@ -117,10 +117,8 @@ export const updateQuantitySubscriptionFromStripe = async (teamId: number) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newQuantity = membershipCount - subscriptionQuantity;
|
|
||||||
|
|
||||||
await stripe.subscriptions.update(subscriptionId, {
|
await stripe.subscriptions.update(subscriptionId, {
|
||||||
items: [{ quantity: membershipCount + newQuantity, id: subscriptionItemId }],
|
items: [{ quantity: membershipCount, id: subscriptionItemId }],
|
||||||
});
|
});
|
||||||
console.info(
|
console.info(
|
||||||
`Updated subscription ${subscriptionId} for team ${teamId} to ${team.members.length} seats.`
|
`Updated subscription ${subscriptionId} for team ${teamId} to ${team.members.length} seats.`
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import type { Workflow, WorkflowStep, Membership } from "@prisma/client";
|
import type { Workflow, WorkflowStep, Membership } from "@prisma/client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
@ -19,6 +20,7 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Badge,
|
Badge,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
ArrowButton,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Edit2, Link as LinkIcon, MoreHorizontal, Trash2 } from "@calcom/ui/components/icon";
|
import { Edit2, Link as LinkIcon, MoreHorizontal, Trash2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
@ -56,192 +58,240 @@ export default function WorkflowListPage({ workflows }: Props) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [workflowToDeleteId, setwWorkflowToDeleteId] = useState(0);
|
const [workflowToDeleteId, setwWorkflowToDeleteId] = useState(0);
|
||||||
|
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const orgBranding = useOrgBranding();
|
const orgBranding = useOrgBranding();
|
||||||
const urlPrefix = orgBranding ? `${orgBranding.slug}.${subdomainSuffix()}` : CAL_URL;
|
const urlPrefix = orgBranding ? `${orgBranding.slug}.${subdomainSuffix()}` : CAL_URL;
|
||||||
|
|
||||||
|
const mutation = trpc.viewer.workflowOrder.useMutation({
|
||||||
|
onError: async (err) => {
|
||||||
|
console.error(err.message);
|
||||||
|
await utils.viewer.workflows.filteredList.cancel();
|
||||||
|
await utils.viewer.workflows.filteredList.invalidate();
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
utils.viewer.workflows.filteredList.invalidate();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function moveWorkflow(index: number, increment: 1 | -1) {
|
||||||
|
const types = workflows!;
|
||||||
|
|
||||||
|
const newList = [...types];
|
||||||
|
|
||||||
|
const type = types[index];
|
||||||
|
const tmp = types[index + increment];
|
||||||
|
if (tmp) {
|
||||||
|
newList[index] = tmp;
|
||||||
|
newList[index + increment] = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.viewer.appRoutingForms.forms.cancel();
|
||||||
|
|
||||||
|
mutation.mutate({
|
||||||
|
ids: newList?.map((type) => type.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{workflows && workflows.length > 0 ? (
|
{workflows && workflows.length > 0 ? (
|
||||||
<div className="bg-default border-subtle overflow-hidden rounded-md border sm:mx-0">
|
<div className="bg-default border-subtle overflow-hidden rounded-md border sm:mx-0">
|
||||||
<ul className="divide-subtle divide-y" data-testid="workflow-list">
|
<ul className="divide-subtle !static w-full divide-y" data-testid="workflow-list" ref={parent}>
|
||||||
{workflows.map((workflow) => (
|
{workflows.map((workflow, index) => {
|
||||||
<li key={workflow.id}>
|
const firstItem = workflows[0];
|
||||||
<div className="first-line:group hover:bg-muted flex w-full items-center justify-between p-4 sm:px-6">
|
const lastItem = workflows[workflows.length - 1];
|
||||||
<Link href={"/workflows/" + workflow.id} className="flex-grow cursor-pointer">
|
return (
|
||||||
<div className="rtl:space-x-reverse">
|
<li
|
||||||
<div className="flex">
|
key={workflow.id}
|
||||||
<div
|
className="group flex w-full max-w-full items-center justify-between overflow-hidden">
|
||||||
className={classNames(
|
{!(firstItem && firstItem.id === workflow.id) && (
|
||||||
"max-w-56 text-emphasis truncate text-sm font-medium leading-6 md:max-w-max",
|
<ArrowButton onClick={() => moveWorkflow(index, -1)} arrowDirection="up" />
|
||||||
workflow.name ? "text-emphasis" : "text-subtle"
|
)}
|
||||||
)}>
|
{!(lastItem && lastItem.id === workflow.id) && (
|
||||||
{workflow.name
|
<ArrowButton onClick={() => moveWorkflow(index, 1)} arrowDirection="down" />
|
||||||
? workflow.name
|
)}
|
||||||
: workflow.steps[0]
|
<div className="first-line:group hover:bg-muted flex w-full items-center justify-between p-4 sm:px-6">
|
||||||
? "Untitled (" +
|
<Link href={"/workflows/" + workflow.id} className="flex-grow cursor-pointer">
|
||||||
`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`
|
<div className="rtl:space-x-reverse">
|
||||||
.charAt(0)
|
<div className="flex">
|
||||||
.toUpperCase() +
|
<div
|
||||||
`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`.slice(1) +
|
className={classNames(
|
||||||
")"
|
"max-w-56 text-emphasis truncate text-sm font-medium leading-6 md:max-w-max",
|
||||||
: "Untitled"}
|
workflow.name ? "text-emphasis" : "text-subtle"
|
||||||
|
)}>
|
||||||
|
{workflow.name
|
||||||
|
? workflow.name
|
||||||
|
: workflow.steps[0]
|
||||||
|
? "Untitled (" +
|
||||||
|
`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`
|
||||||
|
.charAt(0)
|
||||||
|
.toUpperCase() +
|
||||||
|
`${t(`${workflow.steps[0].action.toLowerCase()}_action`)}`.slice(1) +
|
||||||
|
")"
|
||||||
|
: "Untitled"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{workflow.readOnly && (
|
||||||
|
<Badge variant="gray" className="ml-2 ">
|
||||||
|
{t("readonly")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{workflow.readOnly && (
|
<ul className="mt-1 flex flex-wrap space-x-2 sm:flex-nowrap ">
|
||||||
<Badge variant="gray" className="ml-2 ">
|
<li>
|
||||||
{t("readonly")}
|
<Badge variant="gray">
|
||||||
|
<div>
|
||||||
|
{getActionIcon(workflow.steps)}
|
||||||
|
|
||||||
|
<span className="mr-1">{t("triggers")}</span>
|
||||||
|
{workflow.timeUnit && workflow.time && (
|
||||||
|
<span className="mr-1">
|
||||||
|
{t(`${workflow.timeUnit.toLowerCase()}`, { count: workflow.time })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span>{t(`${workflow.trigger.toLowerCase()}_trigger`)}</span>
|
||||||
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
</li>
|
||||||
</div>
|
<li>
|
||||||
</div>
|
<Badge variant="gray">
|
||||||
|
{workflow.activeOn && workflow.activeOn.length > 0 ? (
|
||||||
<ul className="mt-1 flex flex-wrap space-x-2 sm:flex-nowrap ">
|
<Tooltip
|
||||||
<li>
|
content={workflow.activeOn
|
||||||
<Badge variant="gray">
|
.filter((wf) => (workflow.teamId ? wf.eventType.parentId === null : true))
|
||||||
<div>
|
.map((activeOn, key) => (
|
||||||
{getActionIcon(workflow.steps)}
|
<p key={key}>
|
||||||
|
{activeOn.eventType.title}
|
||||||
<span className="mr-1">{t("triggers")}</span>
|
{activeOn.eventType._count.children > 0
|
||||||
{workflow.timeUnit && workflow.time && (
|
? ` (+${activeOn.eventType._count.children})`
|
||||||
<span className="mr-1">
|
: ""}
|
||||||
{t(`${workflow.timeUnit.toLowerCase()}`, { count: workflow.time })}
|
</p>
|
||||||
</span>
|
))}>
|
||||||
)}
|
<div>
|
||||||
<span>{t(`${workflow.trigger.toLowerCase()}_trigger`)}</span>
|
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||||
</div>
|
{t("active_on_event_types", {
|
||||||
</Badge>
|
count: workflow.activeOn.filter((wf) =>
|
||||||
</li>
|
workflow.teamId ? wf.eventType.parentId === null : true
|
||||||
<li>
|
).length,
|
||||||
<Badge variant="gray">
|
})}
|
||||||
{workflow.activeOn && workflow.activeOn.length > 0 ? (
|
</div>
|
||||||
<Tooltip
|
</Tooltip>
|
||||||
content={workflow.activeOn
|
) : (
|
||||||
.filter((wf) => (workflow.teamId ? wf.eventType.parentId === null : true))
|
|
||||||
.map((activeOn, key) => (
|
|
||||||
<p key={key}>
|
|
||||||
{activeOn.eventType.title}
|
|
||||||
{activeOn.eventType._count.children > 0
|
|
||||||
? ` (+${activeOn.eventType._count.children})`
|
|
||||||
: ""}
|
|
||||||
</p>
|
|
||||||
))}>
|
|
||||||
<div>
|
<div>
|
||||||
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||||
{t("active_on_event_types", {
|
{t("no_active_event_types")}
|
||||||
count: workflow.activeOn.filter((wf) =>
|
|
||||||
workflow.teamId ? wf.eventType.parentId === null : true
|
|
||||||
).length,
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
)}
|
||||||
) : (
|
</Badge>
|
||||||
<div>
|
</li>
|
||||||
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
<div className="block md:hidden">
|
||||||
{t("no_active_event_types")}
|
{workflow.team?.name && (
|
||||||
</div>
|
<li>
|
||||||
|
<Badge variant="gray">
|
||||||
|
<>{workflow.team.name}</>
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<div className="hidden md:block">
|
||||||
|
{workflow.team?.name && (
|
||||||
|
<Badge className="mr-4 mt-1 p-[1px] px-2" variant="gray">
|
||||||
|
<Avatar
|
||||||
|
alt={workflow.team?.name || ""}
|
||||||
|
href={
|
||||||
|
workflow.team?.id
|
||||||
|
? `/settings/teams/${workflow.team?.id}/profile`
|
||||||
|
: "/settings/my-account/profile"
|
||||||
|
}
|
||||||
|
imageSrc={getPlaceholderAvatar(
|
||||||
|
workflow?.team.logo,
|
||||||
|
workflow.team?.name as string
|
||||||
|
)}
|
||||||
|
size="xxs"
|
||||||
|
className="mt-[3px] inline-flex justify-center"
|
||||||
|
/>
|
||||||
|
<div>{workflow.team.name}</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
</li>
|
)}
|
||||||
<div className="block md:hidden">
|
</div>
|
||||||
{workflow.team?.name && (
|
|
||||||
<li>
|
|
||||||
<Badge variant="gray">
|
|
||||||
<>{workflow.team.name}</>
|
|
||||||
</Badge>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
|
||||||
<div>
|
<div className="flex flex-shrink-0">
|
||||||
<div className="hidden md:block">
|
<div className="hidden sm:block">
|
||||||
{workflow.team?.name && (
|
<ButtonGroup combined>
|
||||||
<Badge className="mr-4 mt-1 p-[1px] px-2" variant="gray">
|
<Tooltip content={t("edit") as string}>
|
||||||
<Avatar
|
<Button
|
||||||
alt={workflow.team?.name || ""}
|
type="button"
|
||||||
href={
|
color="secondary"
|
||||||
workflow.team?.id
|
variant="icon"
|
||||||
? `/settings/teams/${workflow.team?.id}/profile`
|
StartIcon={Edit2}
|
||||||
: "/settings/my-account/profile"
|
disabled={workflow.readOnly}
|
||||||
}
|
onClick={async () => await router.replace("/workflows/" + workflow.id)}
|
||||||
imageSrc={getPlaceholderAvatar(
|
/>
|
||||||
workflow?.team.logo,
|
</Tooltip>
|
||||||
workflow.team?.name as string
|
<Tooltip content={t("delete") as string}>
|
||||||
)}
|
<Button
|
||||||
size="xxs"
|
onClick={() => {
|
||||||
className="mt-[3px] inline-flex justify-center"
|
setDeleteDialogOpen(true);
|
||||||
/>
|
setwWorkflowToDeleteId(workflow.id);
|
||||||
<div>{workflow.team.name}</div>
|
}}
|
||||||
</Badge>
|
color="secondary"
|
||||||
|
variant="icon"
|
||||||
|
disabled={workflow.readOnly}
|
||||||
|
StartIcon={Trash2}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
{!workflow.readOnly && (
|
||||||
|
<div className="block sm:hidden">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
color="minimal"
|
||||||
|
variant="icon"
|
||||||
|
StartIcon={MoreHorizontal}
|
||||||
|
/>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem
|
||||||
|
type="button"
|
||||||
|
StartIcon={Edit2}
|
||||||
|
onClick={async () => await router.replace("/workflows/" + workflow.id)}>
|
||||||
|
{t("edit")}
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem
|
||||||
|
type="button"
|
||||||
|
color="destructive"
|
||||||
|
StartIcon={Trash2}
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
setwWorkflowToDeleteId(workflow.id);
|
||||||
|
}}>
|
||||||
|
{t("delete")}
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</li>
|
||||||
<div className="flex flex-shrink-0">
|
);
|
||||||
<div className="hidden sm:block">
|
})}
|
||||||
<ButtonGroup combined>
|
|
||||||
<Tooltip content={t("edit") as string}>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
color="secondary"
|
|
||||||
variant="icon"
|
|
||||||
StartIcon={Edit2}
|
|
||||||
disabled={workflow.readOnly}
|
|
||||||
onClick={async () => await router.replace("/workflows/" + workflow.id)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content={t("delete") as string}>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setDeleteDialogOpen(true);
|
|
||||||
setwWorkflowToDeleteId(workflow.id);
|
|
||||||
}}
|
|
||||||
color="secondary"
|
|
||||||
variant="icon"
|
|
||||||
disabled={workflow.readOnly}
|
|
||||||
StartIcon={Trash2}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
|
||||||
{!workflow.readOnly && (
|
|
||||||
<div className="block sm:hidden">
|
|
||||||
<Dropdown>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button type="button" color="minimal" variant="icon" StartIcon={MoreHorizontal} />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<DropdownItem
|
|
||||||
type="button"
|
|
||||||
StartIcon={Edit2}
|
|
||||||
onClick={async () => await router.replace("/workflows/" + workflow.id)}>
|
|
||||||
{t("edit")}
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>
|
|
||||||
<DropdownItem
|
|
||||||
type="button"
|
|
||||||
color="destructive"
|
|
||||||
StartIcon={Trash2}
|
|
||||||
onClick={() => {
|
|
||||||
setDeleteDialogOpen(true);
|
|
||||||
setwWorkflowToDeleteId(workflow.id);
|
|
||||||
}}>
|
|
||||||
{t("delete")}
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
isOpenDialog={deleteDialogOpen}
|
isOpenDialog={deleteDialogOpen}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import type { ControlProps } from "react-select";
|
||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
import { shallow } from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
|
import type { Dayjs } from "@calcom/dayjs";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { AvailableTimes } from "@calcom/features/bookings";
|
import { AvailableTimes } from "@calcom/features/bookings";
|
||||||
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||||
|
@ -38,7 +39,7 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
TimezoneSelect,
|
TimezoneSelect,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { ArrowDown, ArrowLeft, ArrowUp, Sun } from "@calcom/ui/components/icon";
|
import { ArrowLeft, Sun } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { getDimension } from "./lib/getDimension";
|
import { getDimension } from "./lib/getDimension";
|
||||||
import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState } from "./types";
|
import type { EmbedTabs, EmbedType, EmbedTypes, PreviewState } from "./types";
|
||||||
|
@ -228,8 +229,10 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username:
|
||||||
<div className="text-default text-sm">{t("select_date")}</div>
|
<div className="text-default text-sm">{t("select_date")}</div>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
isLoading={schedule.isLoading}
|
isLoading={schedule.isLoading}
|
||||||
onChange={(date) => setSelectedDate(date ? date.format("YYYY-MM-DD") : date)}
|
onChange={(date: Dayjs | null) => {
|
||||||
onMonthChange={(date) => {
|
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"));
|
||||||
|
}}
|
||||||
|
onMonthChange={(date: Dayjs) => {
|
||||||
setMonth(date.format("YYYY-MM"));
|
setMonth(date.format("YYYY-MM"));
|
||||||
setSelectedDate(date.format("YYYY-MM-DD"));
|
setSelectedDate(date.format("YYYY-MM-DD"));
|
||||||
}}
|
}}
|
||||||
|
@ -245,36 +248,24 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username:
|
||||||
</div>
|
</div>
|
||||||
{selectedDate ? (
|
{selectedDate ? (
|
||||||
<div className="mt-[9px] font-medium ">
|
<div className="mt-[9px] font-medium ">
|
||||||
<Collapsible open>
|
{selectTime && selectedDate ? (
|
||||||
<CollapsibleContent>
|
<div className="flex h-full w-full flex-row gap-4">
|
||||||
<div
|
<AvailableTimes
|
||||||
className="text-default mb-[9px] flex cursor-pointer items-center justify-between text-sm"
|
className="w-full"
|
||||||
onClick={() => setSelectTime((prev) => !prev)}>
|
date={dayjs(selectedDate)}
|
||||||
<p>{t("select_time")}</p>{" "}
|
selectedSlots={
|
||||||
<>
|
eventType.slug &&
|
||||||
{!selectedDate || !selectTime ? <ArrowDown className="w-4" /> : <ArrowUp className="w-4" />}
|
selectedDatesAndTimes &&
|
||||||
</>
|
selectedDatesAndTimes[eventType.slug] &&
|
||||||
</div>
|
selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
||||||
{selectTime && selectedDate ? (
|
? selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
||||||
<div className="flex h-full w-full flex-row gap-4">
|
: undefined
|
||||||
<AvailableTimes
|
}
|
||||||
className="w-full"
|
onTimeSelect={onTimeSelect}
|
||||||
date={dayjs(selectedDate)}
|
slots={slots}
|
||||||
selectedSlots={
|
/>
|
||||||
eventType.slug &&
|
</div>
|
||||||
selectedDatesAndTimes &&
|
) : null}
|
||||||
selectedDatesAndTimes[eventType.slug] &&
|
|
||||||
selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
|
||||||
? selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onTimeSelect={onTimeSelect}
|
|
||||||
slots={slots}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mb-[9px] font-medium ">
|
<div className="mb-[9px] font-medium ">
|
||||||
|
|
|
@ -51,11 +51,7 @@ const DateOverrideForm = ({
|
||||||
|
|
||||||
const [selectedDates, setSelectedDates] = useState<Dayjs[]>(value ? [dayjs.utc(value[0].start)] : []);
|
const [selectedDates, setSelectedDates] = useState<Dayjs[]>(value ? [dayjs.utc(value[0].start)] : []);
|
||||||
|
|
||||||
const onDateChange = (newDate: Dayjs | null) => {
|
const onDateChange = (newDate: Dayjs) => {
|
||||||
// If no date is selected, do nothing
|
|
||||||
if (!newDate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If clicking on a selected date unselect it
|
// If clicking on a selected date unselect it
|
||||||
if (selectedDates.some((date) => yyyymmdd(date) === yyyymmdd(newDate))) {
|
if (selectedDates.some((date) => yyyymmdd(date) === yyyymmdd(newDate))) {
|
||||||
setSelectedDates(selectedDates.filter((date) => yyyymmdd(date) !== yyyymmdd(newDate)));
|
setSelectedDates(selectedDates.filter((date) => yyyymmdd(date) !== yyyymmdd(newDate)));
|
||||||
|
@ -154,7 +150,9 @@ const DateOverrideForm = ({
|
||||||
excludedDates={excludedDates}
|
excludedDates={excludedDates}
|
||||||
weekStart={0}
|
weekStart={0}
|
||||||
selected={selectedDates}
|
selected={selectedDates}
|
||||||
onChange={(day) => onDateChange(day)}
|
onChange={(day) => {
|
||||||
|
if (day) onDateChange(day);
|
||||||
|
}}
|
||||||
onMonthChange={(newMonth) => {
|
onMonthChange={(newMonth) => {
|
||||||
setBrowsingDate(newMonth);
|
setBrowsingDate(newMonth);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -369,7 +369,7 @@ function UserDropdown({ small }: UserDropdownProps) {
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
small ? "h-4 w-4" : "h-5 w-5 ltr:mr-2 rtl:ml-2",
|
small ? "h-4 w-4" : "h-5 w-5 ltr:mr-2 rtl:ml-2",
|
||||||
"relative flex-shrink-0 rounded-full bg-gray-300"
|
"relative flex-shrink-0 rounded-full "
|
||||||
)}>
|
)}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={small ? "xs" : "xsm"}
|
size={small ? "xs" : "xsm"}
|
||||||
|
@ -668,7 +668,7 @@ const NavigationItem: React.FC<{
|
||||||
aria-current={current ? "page" : undefined}>
|
aria-current={current ? "page" : undefined}>
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<item.icon
|
<item.icon
|
||||||
className="mr-2 h-4 w-4 flex-shrink-0 ltr:mr-2 rtl:ml-2 [&[aria-current='page']]:text-inherit"
|
className="mr-2 h-4 w-4 flex-shrink-0 rtl:ml-2 md:ltr:mx-auto lg:ltr:mr-2 [&[aria-current='page']]:text-inherit"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
aria-current={current ? "page" : undefined}
|
aria-current={current ? "page" : undefined}
|
||||||
/>
|
/>
|
||||||
|
@ -906,7 +906,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
||||||
<Icon
|
<Icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"h-4 w-4 flex-shrink-0 [&[aria-current='page']]:text-inherit",
|
"h-4 w-4 flex-shrink-0 [&[aria-current='page']]:text-inherit",
|
||||||
"me-3 md:ltr:mr-2 md:rtl:ml-2"
|
"me-3 md:mx-auto lg:ltr:mr-2 lg:rtl:ml-2"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -35,7 +35,7 @@ function MoreInfoFooter() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SheetClose asChild>
|
<SheetClose asChild>
|
||||||
<Button color="secondary" type="button" className="justify-center md:w-1/5">
|
<Button color="secondary" type="button" className="w-full justify-center lg:w-1/5">
|
||||||
{t("close")}
|
{t("close")}
|
||||||
</Button>
|
</Button>
|
||||||
</SheetClose>
|
</SheetClose>
|
||||||
|
|
|
@ -60,6 +60,7 @@ export function InviteMemberModal(props: Props) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
teamId={orgId}
|
teamId={orgId}
|
||||||
|
justEmailInvites={!!orgId}
|
||||||
isLoading={inviteMemberMutation.isLoading}
|
isLoading={inviteMemberMutation.isLoading}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
inviteMemberMutation.mutate({
|
inviteMemberMutation.mutate({
|
||||||
|
|
|
@ -153,12 +153,14 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||||
if (error || !iCalString)
|
if (error || !iCalString)
|
||||||
throw new Error(`Error creating iCalString:=> ${error?.message} : ${error?.name} `);
|
throw new Error(`Error creating iCalString:=> ${error?.message} : ${error?.name} `);
|
||||||
|
|
||||||
|
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||||
|
|
||||||
// We create the event directly on iCal
|
// We create the event directly on iCal
|
||||||
const responses = await Promise.all(
|
const responses = await Promise.all(
|
||||||
calendars
|
calendars
|
||||||
.filter((c) =>
|
.filter((c) =>
|
||||||
event.destinationCalendar?.externalId
|
mainHostDestinationCalendar?.externalId
|
||||||
? c.externalId === event.destinationCalendar.externalId
|
? c.externalId === mainHostDestinationCalendar.externalId
|
||||||
: true
|
: true
|
||||||
)
|
)
|
||||||
.map((calendar) =>
|
.map((calendar) =>
|
||||||
|
@ -504,13 +506,13 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||||
|
|
||||||
return calendars.reduce<IntegrationCalendar[]>((newCalendars, calendar) => {
|
return calendars.reduce<IntegrationCalendar[]>((newCalendars, calendar) => {
|
||||||
if (!calendar.components?.includes("VEVENT")) return newCalendars;
|
if (!calendar.components?.includes("VEVENT")) return newCalendars;
|
||||||
|
const [mainHostDestinationCalendar] = event?.destinationCalendar ?? [];
|
||||||
newCalendars.push({
|
newCalendars.push({
|
||||||
externalId: calendar.url,
|
externalId: calendar.url,
|
||||||
/** @url https://github.com/calcom/cal.com/issues/7186 */
|
/** @url https://github.com/calcom/cal.com/issues/7186 */
|
||||||
name: typeof calendar.displayName === "string" ? calendar.displayName : "",
|
name: typeof calendar.displayName === "string" ? calendar.displayName : "",
|
||||||
primary: event?.destinationCalendar?.externalId
|
primary: mainHostDestinationCalendar?.externalId
|
||||||
? event.destinationCalendar.externalId === calendar.url
|
? mainHostDestinationCalendar.externalId === calendar.url
|
||||||
: false,
|
: false,
|
||||||
integration: this.integrationName,
|
integration: this.integrationName,
|
||||||
email: this.credentials.username ?? "",
|
email: this.credentials.username ?? "",
|
||||||
|
|
|
@ -59,7 +59,8 @@ const handlePayment = async (
|
||||||
},
|
},
|
||||||
booking.id,
|
booking.id,
|
||||||
bookerEmail,
|
bookerEmail,
|
||||||
paymentOption
|
paymentOption,
|
||||||
|
evt.title
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
|
||||||
import { getSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
|
import { getSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
|
||||||
import prisma, { baseEventTypeSelect } from "@calcom/prisma";
|
import prisma, { baseEventTypeSelect } from "@calcom/prisma";
|
||||||
import { AppCategories, SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "../../../constants";
|
import { WEBAPP_URL } from "../../../constants";
|
||||||
|
@ -32,7 +31,17 @@ export async function getTeamWithMembers(args: {
|
||||||
selectedCalendars: true,
|
selectedCalendars: true,
|
||||||
credentials: {
|
credentials: {
|
||||||
include: {
|
include: {
|
||||||
app: true,
|
app: {
|
||||||
|
select: {
|
||||||
|
slug: true,
|
||||||
|
categories: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
destinationCalendars: {
|
||||||
|
select: {
|
||||||
|
externalId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -124,43 +133,39 @@ export async function getTeamWithMembers(args: {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!team) return null;
|
if (!team) return null;
|
||||||
const members = await Promise.all(
|
|
||||||
team.members.map(async (obj) => {
|
|
||||||
const calendarCredentials = getCalendarCredentials(obj.user.credentials);
|
|
||||||
|
|
||||||
const { connectedCalendars } = await getConnectedCalendars(
|
// This should improve performance saving already app data found.
|
||||||
calendarCredentials,
|
const appDataMap = new Map();
|
||||||
obj.user.selectedCalendars,
|
|
||||||
obj.user.destinationCalendar?.externalId
|
const members = team.members.map((obj) => {
|
||||||
);
|
return {
|
||||||
const connectedApps = obj.user.credentials
|
...obj.user,
|
||||||
.map(({ app, id }) => {
|
role: obj.role,
|
||||||
const appMetaData = getAppFromSlug(app?.slug);
|
accepted: obj.accepted,
|
||||||
|
disableImpersonation: obj.disableImpersonation,
|
||||||
|
avatar: `${WEBAPP_URL}/${obj.user.username}/avatar.png`,
|
||||||
|
connectedApps: obj.user.credentials.map((cred) => {
|
||||||
|
const appSlug = cred.app?.slug;
|
||||||
|
let appData = appDataMap.get(appSlug);
|
||||||
|
|
||||||
|
if (!appData) {
|
||||||
|
appData = getAppFromSlug(appSlug);
|
||||||
|
appDataMap.set(appSlug, appData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCalendar = cred?.app?.categories.includes("calendar");
|
||||||
|
const externalId = isCalendar ? cred.destinationCalendars[0]?.externalId : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: appData?.name,
|
||||||
|
logo: appData?.logo,
|
||||||
|
app: cred.app,
|
||||||
|
externalId,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (app?.categories.includes(AppCategories.calendar)) {
|
|
||||||
const externalId = connectedCalendars.find((cal) => cal.credentialId == id)?.primary?.email;
|
|
||||||
return { name: appMetaData?.name, logo: appMetaData?.logo, slug: appMetaData?.slug, externalId };
|
|
||||||
}
|
|
||||||
return { name: appMetaData?.name, logo: appMetaData?.logo, slug: appMetaData?.slug };
|
|
||||||
})
|
|
||||||
.sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? ""));
|
|
||||||
// Prevent credentials from leaking to frontend
|
|
||||||
const {
|
|
||||||
credentials: _credentials,
|
|
||||||
destinationCalendar: _destinationCalendar,
|
|
||||||
selectedCalendars: _selectedCalendars,
|
|
||||||
...rest
|
|
||||||
} = {
|
|
||||||
...obj.user,
|
|
||||||
role: obj.role,
|
|
||||||
accepted: obj.accepted,
|
|
||||||
disableImpersonation: obj.disableImpersonation,
|
|
||||||
avatar: `${WEBAPP_URL}/${obj.user.username}/avatar.png`,
|
|
||||||
connectedApps,
|
|
||||||
};
|
|
||||||
return rest;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const eventTypes = team.eventTypes.map((eventType) => ({
|
const eventTypes = team.eventTypes.map((eventType) => ({
|
||||||
...eventType,
|
...eventType,
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
|
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
|
||||||
|
|
|
@ -19,9 +19,6 @@ export const telemetryEventTypes = {
|
||||||
onboardingStarted: "onboarding_started",
|
onboardingStarted: "onboarding_started",
|
||||||
signup: "signup",
|
signup: "signup",
|
||||||
team_created: "team_created",
|
team_created: "team_created",
|
||||||
website: {
|
|
||||||
pageView: "website_page_view",
|
|
||||||
},
|
|
||||||
slugReplacementAction: "slug_replacement_action",
|
slugReplacementAction: "slug_replacement_action",
|
||||||
org_created: "org_created",
|
org_created: "org_created",
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "App_RoutingForms_Form" ADD COLUMN "position" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Workflow" ADD COLUMN "position" INTEGER NOT NULL DEFAULT 0;
|
|
@ -28,7 +28,7 @@
|
||||||
"@prisma/generator-helper": "^5.0.0",
|
"@prisma/generator-helper": "^5.0.0",
|
||||||
"prisma": "^5.0.0",
|
"prisma": "^5.0.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"zod": "^3.20.2",
|
"zod": "^3.22.2",
|
||||||
"zod-prisma": "^0.5.4"
|
"zod-prisma": "^0.5.4"
|
||||||
},
|
},
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
|
|
|
@ -662,6 +662,7 @@ model App {
|
||||||
model App_RoutingForms_Form {
|
model App_RoutingForms_Form {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
description String?
|
description String?
|
||||||
|
position Int @default(0)
|
||||||
routes Json?
|
routes Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
@ -746,6 +747,7 @@ model WorkflowStep {
|
||||||
|
|
||||||
model Workflow {
|
model Workflow {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
position Int @default(0)
|
||||||
name String
|
name String
|
||||||
userId Int?
|
userId Int?
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import z, { ZodNullable, ZodObject, ZodOptional } from "zod";
|
||||||
|
|
||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
import type {
|
import type {
|
||||||
|
AnyZodObject,
|
||||||
objectInputType,
|
objectInputType,
|
||||||
objectOutputType,
|
objectOutputType,
|
||||||
ZodNullableDef,
|
ZodNullableDef,
|
||||||
|
@ -528,11 +529,13 @@ export const optionToValueSchema = <T extends z.ZodTypeAny>(valueSchema: T) =>
|
||||||
* @url https://github.com/colinhacks/zod/discussions/1655#discussioncomment-4367368
|
* @url https://github.com/colinhacks/zod/discussions/1655#discussioncomment-4367368
|
||||||
*/
|
*/
|
||||||
export const getParserWithGeneric =
|
export const getParserWithGeneric =
|
||||||
<T extends z.ZodTypeAny>(valueSchema: T) =>
|
<T extends AnyZodObject>(valueSchema: T) =>
|
||||||
<Data>(data: Data) => {
|
<Data>(data: Data) => {
|
||||||
type Output = z.infer<typeof valueSchema>;
|
type Output = z.infer<T>;
|
||||||
|
type SimpleFormValues = string | number | null | undefined;
|
||||||
return valueSchema.parse(data) as {
|
return valueSchema.parse(data) as {
|
||||||
[key in keyof Data]: key extends keyof Output ? Output[key] : Data[key];
|
// TODO: Invesitage why this broke on zod 3.22.2 upgrade
|
||||||
|
[key in keyof Data]: Data[key] extends SimpleFormValues ? Data[key] : Output[key];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export const sendDailyVideoRecordingEmailsSchema = z.object({
|
export const sendDailyVideoRecordingEmailsSchema = z.object({
|
||||||
|
|
|
@ -17,6 +17,6 @@
|
||||||
"@trpc/react-query": "^10.13.0",
|
"@trpc/react-query": "^10.13.0",
|
||||||
"@trpc/server": "^10.13.0",
|
"@trpc/server": "^10.13.0",
|
||||||
"superjson": "1.9.1",
|
"superjson": "1.9.1",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.22.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,12 @@ import { ZGetCalVideoRecordingsInputSchema } from "./getCalVideoRecordings.schem
|
||||||
import { ZGetDownloadLinkOfCalVideoRecordingsInputSchema } from "./getDownloadLinkOfCalVideoRecordings.schema";
|
import { ZGetDownloadLinkOfCalVideoRecordingsInputSchema } from "./getDownloadLinkOfCalVideoRecordings.schema";
|
||||||
import { ZIntegrationsInputSchema } from "./integrations.schema";
|
import { ZIntegrationsInputSchema } from "./integrations.schema";
|
||||||
import { ZLocationOptionsInputSchema } from "./locationOptions.schema";
|
import { ZLocationOptionsInputSchema } from "./locationOptions.schema";
|
||||||
|
import { ZRoutingFormOrderInputSchema } from "./routingFormOrder.schema";
|
||||||
import { ZSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
|
import { ZSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
|
||||||
import { ZSubmitFeedbackInputSchema } from "./submitFeedback.schema";
|
import { ZSubmitFeedbackInputSchema } from "./submitFeedback.schema";
|
||||||
import { ZUpdateProfileInputSchema } from "./updateProfile.schema";
|
import { ZUpdateProfileInputSchema } from "./updateProfile.schema";
|
||||||
import { ZUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
|
import { ZUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
|
||||||
|
import { ZWorkflowOrderInputSchema } from "./workflowOrder.schema";
|
||||||
|
|
||||||
type AppsRouterHandlerCache = {
|
type AppsRouterHandlerCache = {
|
||||||
me?: typeof import("./me.handler").meHandler;
|
me?: typeof import("./me.handler").meHandler;
|
||||||
|
@ -31,6 +33,8 @@ type AppsRouterHandlerCache = {
|
||||||
stripeCustomer?: typeof import("./stripeCustomer.handler").stripeCustomerHandler;
|
stripeCustomer?: typeof import("./stripeCustomer.handler").stripeCustomerHandler;
|
||||||
updateProfile?: typeof import("./updateProfile.handler").updateProfileHandler;
|
updateProfile?: typeof import("./updateProfile.handler").updateProfileHandler;
|
||||||
eventTypeOrder?: typeof import("./eventTypeOrder.handler").eventTypeOrderHandler;
|
eventTypeOrder?: typeof import("./eventTypeOrder.handler").eventTypeOrderHandler;
|
||||||
|
routingFormOrder?: typeof import("./routingFormOrder.handler").routingFormOrderHandler;
|
||||||
|
workflowOrder?: typeof import("./workflowOrder.handler").workflowOrderHandler;
|
||||||
submitFeedback?: typeof import("./submitFeedback.handler").submitFeedbackHandler;
|
submitFeedback?: typeof import("./submitFeedback.handler").submitFeedbackHandler;
|
||||||
locationOptions?: typeof import("./locationOptions.handler").locationOptionsHandler;
|
locationOptions?: typeof import("./locationOptions.handler").locationOptionsHandler;
|
||||||
deleteCredential?: typeof import("./deleteCredential.handler").deleteCredentialHandler;
|
deleteCredential?: typeof import("./deleteCredential.handler").deleteCredentialHandler;
|
||||||
|
@ -230,6 +234,34 @@ export const loggedInViewerRouter = router({
|
||||||
return UNSTABLE_HANDLER_CACHE.eventTypeOrder({ ctx, input });
|
return UNSTABLE_HANDLER_CACHE.eventTypeOrder({ ctx, input });
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
routingFormOrder: authedProcedure.input(ZRoutingFormOrderInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.routingFormOrder) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.routingFormOrder = (
|
||||||
|
await import("./routingFormOrder.handler")
|
||||||
|
).routingFormOrderHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.routingFormOrder) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.routingFormOrder({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
|
workflowOrder: authedProcedure.input(ZWorkflowOrderInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.workflowOrder) {
|
||||||
|
UNSTABLE_HANDLER_CACHE.workflowOrder = (await import("./workflowOrder.handler")).workflowOrderHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable code but required for type safety
|
||||||
|
if (!UNSTABLE_HANDLER_CACHE.workflowOrder) {
|
||||||
|
throw new Error("Failed to load handler");
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNSTABLE_HANDLER_CACHE.workflowOrder({ ctx, input });
|
||||||
|
}),
|
||||||
|
|
||||||
//Comment for PR: eventTypePosition is not used anywhere
|
//Comment for PR: eventTypePosition is not used anywhere
|
||||||
submitFeedback: authedProcedure.input(ZSubmitFeedbackInputSchema).mutation(async ({ ctx, input }) => {
|
submitFeedback: authedProcedure.input(ZSubmitFeedbackInputSchema).mutation(async ({ ctx, input }) => {
|
||||||
if (!UNSTABLE_HANDLER_CACHE.submitFeedback) {
|
if (!UNSTABLE_HANDLER_CACHE.submitFeedback) {
|
||||||
|
|
|
@ -287,7 +287,11 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||||
location: booking.location,
|
location: booking.location,
|
||||||
destinationCalendar: booking.destinationCalendar || booking.user?.destinationCalendar,
|
destinationCalendar: booking.destinationCalendar
|
||||||
|
? [booking.destinationCalendar]
|
||||||
|
: booking.user?.destinationCalendar
|
||||||
|
? [booking.user?.destinationCalendar]
|
||||||
|
: [],
|
||||||
cancellationReason: "Payment method removed by organizer",
|
cancellationReason: "Payment method removed by organizer",
|
||||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||||
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TRoutingFormOrderInputSchema } from "./routingFormOrder.schema";
|
||||||
|
|
||||||
|
type RoutingFormOrderOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TRoutingFormOrderInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const routingFormOrderHandler = async ({ ctx, input }: RoutingFormOrderOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
|
||||||
|
const forms = await prisma.app_RoutingForms_Form.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
team: {
|
||||||
|
include: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
responses: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const allFormIds = new Set(forms.map((form) => form.id));
|
||||||
|
if (input.ids.some((id) => !allFormIds.has(id))) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
input.ids.reverse().map((id, position) => {
|
||||||
|
return prisma.app_RoutingForms_Form.update({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZRoutingFormOrderInputSchema = z.object({
|
||||||
|
ids: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TRoutingFormOrderInputSchema = z.infer<typeof ZRoutingFormOrderInputSchema>;
|
|
@ -0,0 +1,173 @@
|
||||||
|
import type { TFormSchema } from "@calcom/app-store/routing-forms/trpc/forms.schema";
|
||||||
|
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
|
import { entries } from "@calcom/prisma/zod-utils";
|
||||||
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { TWorkflowOrderInputSchema } from "./workflowOrder.schema";
|
||||||
|
|
||||||
|
type RoutingFormOrderOptions = {
|
||||||
|
ctx: {
|
||||||
|
user: NonNullable<TrpcSessionUser>;
|
||||||
|
};
|
||||||
|
input: TWorkflowOrderInputSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const workflowOrderHandler = async ({ ctx, input }: RoutingFormOrderOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
|
||||||
|
const includedFields = {
|
||||||
|
activeOn: {
|
||||||
|
select: {
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
parentId: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
children: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
steps: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
members: true,
|
||||||
|
logo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const allWorkflows = await prisma.workflow.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: includedFields,
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
position: "desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const allWorkflowIds = new Set(allWorkflows.map((workflow) => workflow.id));
|
||||||
|
if (input.ids.some((id) => !allWorkflowIds.has(id))) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
input.ids.reverse().map((id, position) => {
|
||||||
|
return prisma.workflow.update({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPrismaWhereFromFilters(
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
},
|
||||||
|
filters: NonNullable<TFormSchema>["filters"]
|
||||||
|
) {
|
||||||
|
const where = {
|
||||||
|
OR: [] as Prisma.App_RoutingForms_FormWhereInput[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const prismaQueries: Record<
|
||||||
|
keyof NonNullable<typeof filters>,
|
||||||
|
(...args: [number[]]) => Prisma.App_RoutingForms_FormWhereInput
|
||||||
|
> & {
|
||||||
|
all: () => Prisma.App_RoutingForms_FormWhereInput;
|
||||||
|
} = {
|
||||||
|
userIds: (userIds: number[]) => ({
|
||||||
|
userId: {
|
||||||
|
in: userIds,
|
||||||
|
},
|
||||||
|
teamId: null,
|
||||||
|
}),
|
||||||
|
teamIds: (teamIds: number[]) => ({
|
||||||
|
team: {
|
||||||
|
id: {
|
||||||
|
in: teamIds ?? [],
|
||||||
|
},
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
all: () => ({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!filters || !hasFilter(filters)) {
|
||||||
|
where.OR.push(prismaQueries.all());
|
||||||
|
} else {
|
||||||
|
for (const entry of entries(filters)) {
|
||||||
|
if (!entry) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [filterName, filter] = entry;
|
||||||
|
const getPrismaQuery = prismaQueries[filterName];
|
||||||
|
// filter might be accidentally set undefined as well
|
||||||
|
if (!getPrismaQuery || !filter) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
where.OR.push(getPrismaQuery(filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ZWorkflowOrderInputSchema = z.object({
|
||||||
|
ids: z.array(z.number()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TWorkflowOrderInputSchema = z.infer<typeof ZWorkflowOrderInputSchema>;
|
|
@ -172,7 +172,11 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
|
||||||
attendees: attendeesList,
|
attendees: attendeesList,
|
||||||
location: booking.location ?? "",
|
location: booking.location ?? "",
|
||||||
uid: booking.uid,
|
uid: booking.uid,
|
||||||
destinationCalendar: booking?.destinationCalendar || user.destinationCalendar,
|
destinationCalendar: booking?.destinationCalendar
|
||||||
|
? [booking.destinationCalendar]
|
||||||
|
: user.destinationCalendar
|
||||||
|
? [user.destinationCalendar]
|
||||||
|
: [],
|
||||||
requiresConfirmation: booking?.eventType?.requiresConfirmation ?? false,
|
requiresConfirmation: booking?.eventType?.requiresConfirmation ?? false,
|
||||||
eventTypeId: booking.eventType?.id,
|
eventTypeId: booking.eventType?.id,
|
||||||
};
|
};
|
||||||
|
|
|
@ -82,7 +82,11 @@ export const editLocationHandler = async ({ ctx, input }: EditLocationOptions) =
|
||||||
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||||
location,
|
location,
|
||||||
conferenceCredentialId: details?.credentialId,
|
conferenceCredentialId: details?.credentialId,
|
||||||
destinationCalendar: booking?.destinationCalendar || booking?.user?.destinationCalendar,
|
destinationCalendar: booking?.destinationCalendar
|
||||||
|
? [booking?.destinationCalendar]
|
||||||
|
: booking?.user?.destinationCalendar
|
||||||
|
? [booking?.user?.destinationCalendar]
|
||||||
|
: [],
|
||||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||||
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
||||||
};
|
};
|
||||||
|
|
|
@ -237,7 +237,9 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||||
),
|
),
|
||||||
uid: bookingToReschedule?.uid,
|
uid: bookingToReschedule?.uid,
|
||||||
location: bookingToReschedule?.location,
|
location: bookingToReschedule?.location,
|
||||||
destinationCalendar: bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
destinationCalendar: bookingToReschedule?.destinationCalendar
|
||||||
|
? [bookingToReschedule?.destinationCalendar]
|
||||||
|
: [],
|
||||||
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -260,6 +260,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||||
const { default_currency } = stripeDataSchema.parse(paymentCredential.key);
|
const { default_currency } = stripeDataSchema.parse(paymentCredential.key);
|
||||||
data.currency = default_currency;
|
data.currency = default_currency;
|
||||||
}
|
}
|
||||||
|
if (paymentCredential?.type === "paypal_payment" && input.metadata?.apps?.paypal?.currency) {
|
||||||
|
data.currency = input.metadata?.apps?.paypal?.currency.toLowerCase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectedLink = await ctx.prisma.hashedLink.findFirst({
|
const connectedLink = await ctx.prisma.hashedLink.findFirst({
|
||||||
|
|
|
@ -148,9 +148,9 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
||||||
organization: {
|
organization: {
|
||||||
create: {
|
create: {
|
||||||
name,
|
name,
|
||||||
...(!IS_TEAM_BILLING_ENABLED && { slug }),
|
...(IS_TEAM_BILLING_ENABLED ? { slug } : {}),
|
||||||
metadata: {
|
metadata: {
|
||||||
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
|
...(IS_TEAM_BILLING_ENABLED ? { requestedSlug: slug } : {}),
|
||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
isOrganizationVerified: false,
|
isOrganizationVerified: false,
|
||||||
isOrganizationConfigured,
|
isOrganizationConfigured,
|
||||||
|
|
|
@ -24,11 +24,12 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||||
// A user can only have one org so we pass in their currentOrgId here
|
// A user can only have one org so we pass in their currentOrgId here
|
||||||
const currentOrgId = ctx.user?.organization?.id || input.orgId;
|
const currentOrgId = ctx.user?.organization?.id || input.orgId;
|
||||||
|
|
||||||
if (!currentOrgId || ctx.user.role !== UserPermissionRole.ADMIN)
|
const isUserOrganizationAdmin = currentOrgId && (await isOrganisationAdmin(ctx.user?.id, currentOrgId));
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
const isUserRoleAdmin = ctx.user.role === UserPermissionRole.ADMIN;
|
||||||
|
|
||||||
if (!(await isOrganisationAdmin(ctx.user?.id, currentOrgId)) || ctx.user.role !== UserPermissionRole.ADMIN)
|
const isUserAuthorizedToUpdate = !!(isUserOrganizationAdmin || isUserRoleAdmin);
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
|
if (!currentOrgId || !isUserAuthorizedToUpdate) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|
||||||
if (input.slug) {
|
if (input.slug) {
|
||||||
const userConflict = await prisma.team.findMany({
|
const userConflict = await prisma.team.findMany({
|
||||||
|
|
|
@ -56,7 +56,7 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
|
||||||
});
|
});
|
||||||
const invitee = await getUserToInviteOrThrowIfExists({
|
const invitee = await getUserToInviteOrThrowIfExists({
|
||||||
usernameOrEmail,
|
usernameOrEmail,
|
||||||
orgId: input.teamId,
|
teamId: input.teamId,
|
||||||
isOrg: input.isOrg,
|
isOrg: input.isOrg,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -66,17 +66,21 @@ export async function getEmailsToInvite(usernameOrEmail: string | string[]) {
|
||||||
|
|
||||||
export async function getUserToInviteOrThrowIfExists({
|
export async function getUserToInviteOrThrowIfExists({
|
||||||
usernameOrEmail,
|
usernameOrEmail,
|
||||||
orgId,
|
teamId,
|
||||||
isOrg,
|
isOrg,
|
||||||
}: {
|
}: {
|
||||||
usernameOrEmail: string;
|
usernameOrEmail: string;
|
||||||
orgId: number;
|
teamId: number;
|
||||||
isOrg?: boolean;
|
isOrg?: boolean;
|
||||||
}) {
|
}) {
|
||||||
// Check if user exists in ORG or exists all together
|
// Check if user exists in ORG or exists all together
|
||||||
|
|
||||||
|
const orgWhere = isOrg && {
|
||||||
|
organizationId: teamId,
|
||||||
|
};
|
||||||
const invitee = await prisma.user.findFirst({
|
const invitee = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ username: usernameOrEmail, organizationId: orgId }, { email: usernameOrEmail }],
|
OR: [{ username: usernameOrEmail, ...orgWhere }, { email: usernameOrEmail }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -70,9 +70,14 @@ export const filteredListHandler = async ({ ctx, input }: FilteredListOptions) =
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: includedFields,
|
include: includedFields,
|
||||||
orderBy: {
|
orderBy: [
|
||||||
id: "asc",
|
{
|
||||||
},
|
position: "desc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!filtered) {
|
if (!filtered) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import type z from "zod";
|
||||||
import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
import type { Calendar } from "@calcom/features/calendars/weeklyview";
|
import type { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||||
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
|
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import type { Frequency } from "@calcom/prisma/zod-utils";
|
import type { Frequency } from "@calcom/prisma/zod-utils";
|
||||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||||
|
|
||||||
|
@ -167,7 +168,7 @@ export interface CalendarEvent {
|
||||||
videoCallData?: VideoCallData;
|
videoCallData?: VideoCallData;
|
||||||
paymentInfo?: PaymentInfo | null;
|
paymentInfo?: PaymentInfo | null;
|
||||||
requiresConfirmation?: boolean | null;
|
requiresConfirmation?: boolean | null;
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
destinationCalendar?: DestinationCalendar[] | null;
|
||||||
cancellationReason?: string | null;
|
cancellationReason?: string | null;
|
||||||
rejectionReason?: string | null;
|
rejectionReason?: string | null;
|
||||||
hideCalendarNotes?: boolean;
|
hideCalendarNotes?: boolean;
|
||||||
|
@ -178,6 +179,7 @@ export interface CalendarEvent {
|
||||||
seatsShowAttendees?: boolean | null;
|
seatsShowAttendees?: boolean | null;
|
||||||
attendeeSeatId?: string;
|
attendeeSeatId?: string;
|
||||||
seatsPerTimeSlot?: number | null;
|
seatsPerTimeSlot?: number | null;
|
||||||
|
schedulingType?: SchedulingType | null;
|
||||||
iCalUID?: string | null;
|
iCalUID?: string | null;
|
||||||
|
|
||||||
// It has responses to all the fields(system + user)
|
// It has responses to all the fields(system + user)
|
||||||
|
@ -216,7 +218,7 @@ export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Calendar {
|
export interface Calendar {
|
||||||
createEvent(event: CalendarEvent): Promise<NewCalendarEventType>;
|
createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType>;
|
||||||
|
|
||||||
updateEvent(
|
updateEvent(
|
||||||
uid: string,
|
uid: string,
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface EventResult<T> {
|
||||||
calError?: string;
|
calError?: string;
|
||||||
calWarnings?: string[];
|
calWarnings?: string[];
|
||||||
credentialId?: number;
|
credentialId?: number;
|
||||||
|
externalId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateUpdateResult {
|
export interface CreateUpdateResult {
|
||||||
|
|
|
@ -14,7 +14,8 @@ export interface IAbstractPaymentService {
|
||||||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||||
bookingId: Booking["id"],
|
bookingId: Booking["id"],
|
||||||
bookerEmail: string,
|
bookerEmail: string,
|
||||||
paymentOption: PaymentOption
|
paymentOption: PaymentOption,
|
||||||
|
eventTitle?: string
|
||||||
): Promise<Payment>;
|
): Promise<Payment>;
|
||||||
/* This method is to collect card details to charge at a later date ex. no-show fees */
|
/* This method is to collect card details to charge at a later date ex. no-show fees */
|
||||||
collectCard(
|
collectCard(
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { ArrowUp, ArrowDown } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
export type ArrowButtonProps = {
|
||||||
|
arrowDirection: "up" | "down";
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ArrowButton(props: ArrowButtonProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.arrowDirection === "up" ? (
|
||||||
|
<button
|
||||||
|
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -ml-4 -mt-4 mb-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
||||||
|
onClick={props.onClick}>
|
||||||
|
<ArrowUp className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] -ml-4 mt-8 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
||||||
|
onClick={props.onClick}>
|
||||||
|
<ArrowDown className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { ArrowButton } from "./ArrowButton";
|
||||||
|
export type { ArrowButtonProps } from "./ArrowButton";
|
|
@ -40,7 +40,7 @@ export function Avatar(props: AvatarProps) {
|
||||||
let avatar = (
|
let avatar = (
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"bg-emphasis item-center relative aspect-square justify-center overflow-hidden rounded-full",
|
"bg-emphasis item-center relative inline-flex aspect-square justify-center overflow-hidden rounded-full",
|
||||||
props.className,
|
props.className,
|
||||||
sizesPropsBySize[size]
|
sizesPropsBySize[size]
|
||||||
)}>
|
)}>
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const buttonClasses = cva(
|
||||||
minimal:
|
minimal:
|
||||||
"text-emphasis hover:bg-subtle focus-visible:bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset focus-visible:ring-empthasis disabled:border-subtle disabled:bg-opacity-30 disabled:text-muted disabled:hover:bg-transparent disabled:hover:text-muted disabled:hover:border-subtle",
|
"text-emphasis hover:bg-subtle focus-visible:bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset focus-visible:ring-empthasis disabled:border-subtle disabled:bg-opacity-30 disabled:text-muted disabled:hover:bg-transparent disabled:hover:text-muted disabled:hover:border-subtle",
|
||||||
destructive:
|
destructive:
|
||||||
"border border-default text-emphasis hover:text-red-700 focus-visible:text-red-700 hover:border-red-100 focus-visible:border-red-100 hover:bg-error focus-visible:bg-error focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset focus-visible:ring-red-700 disabled:bg-red-100 disabled:border-red-200 disabled:text-red-700 disabled:hover:border-red-200 disabled:opacity-40",
|
"border border-default text-emphasis hover:text-red-700 dark:hover:text-red-100 focus-visible:text-red-700 hover:border-red-100 focus-visible:border-red-100 hover:bg-error focus-visible:bg-error focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset focus-visible:ring-red-700 disabled:bg-red-100 disabled:border-red-200 disabled:text-red-700 disabled:hover:border-red-200 disabled:opacity-40",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
sm: "px-3 py-2 leading-4 rounded-sm" /** For backwards compatibility */,
|
sm: "px-3 py-2 leading-4 rounded-sm" /** For backwards compatibility */,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -17,7 +18,7 @@ import { Button } from "./Button";
|
||||||
|
|
||||||
<Meta title="UI/Button" component={Button} />
|
<Meta title="UI/Button" component={Button} />
|
||||||
|
|
||||||
<Title title="Buttons" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
|
<Title title="Buttons" suffix="Brief" subtitle="Version 2.1 — Last Update: 24 Aug 2023" />
|
||||||
|
|
||||||
## Definition
|
## Definition
|
||||||
|
|
||||||
|
@ -170,6 +171,15 @@ Button are clickable elements that initiates user actions. Labels in the button
|
||||||
</Story>
|
</Story>
|
||||||
<Story
|
<Story
|
||||||
name="Button Playground"
|
name="Button Playground"
|
||||||
|
play={({ canvasElement }) => {
|
||||||
|
const darkVariantContainer = canvasElement.querySelector('[data-mode="dark"]');
|
||||||
|
const buttonElement = darkVariantContainer.querySelector("button");
|
||||||
|
buttonElement?.addEventListener("mouseover", () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelector('[data-testid="tooltip"]').classList.add("dark");
|
||||||
|
}, 55);
|
||||||
|
});
|
||||||
|
}}
|
||||||
args={{
|
args={{
|
||||||
color: "primary",
|
color: "primary",
|
||||||
size: "base",
|
size: "base",
|
||||||
|
@ -177,6 +187,7 @@ Button are clickable elements that initiates user actions. Labels in the button
|
||||||
disabled: false,
|
disabled: false,
|
||||||
children: "Button text",
|
children: "Button text",
|
||||||
className: "",
|
className: "",
|
||||||
|
tooltip: "tooltip",
|
||||||
}}
|
}}
|
||||||
argTypes={{
|
argTypes={{
|
||||||
color: {
|
color: {
|
||||||
|
@ -212,19 +223,20 @@ Button are clickable elements that initiates user actions. Labels in the button
|
||||||
options: ["", "sb-pseudo--hover", "sb-pseudo--focus"],
|
options: ["", "sb-pseudo--hover", "sb-pseudo--focus"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
tooltip: {
|
||||||
|
control: {
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
}}>
|
}}>
|
||||||
{({ color, size, loading, disabled, children, className }) => (
|
{({ children, ...args }) => (
|
||||||
<VariantsTable titles={["Light & Dark Modes"]} columnMinWidth={150}>
|
<VariantsTable titles={["Light & Dark Modes"]} columnMinWidth={150}>
|
||||||
<VariantRow variant="Button">
|
<VariantRow variant="Button">
|
||||||
<Button
|
<TooltipProvider>
|
||||||
color={color}
|
<Button variant="default" {...args}>
|
||||||
size={size}
|
{children}
|
||||||
variant="default"
|
</Button>
|
||||||
loading={loading}
|
</TooltipProvider>
|
||||||
disabled={disabled}
|
|
||||||
className={className}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
</VariantRow>
|
</VariantRow>
|
||||||
</VariantsTable>
|
</VariantsTable>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function ConfirmationDialogContent(props: PropsWithChildren<ConfirmationD
|
||||||
<div className="mt-0.5 ltr:mr-3">
|
<div className="mt-0.5 ltr:mr-3">
|
||||||
{variety === "danger" && (
|
{variety === "danger" && (
|
||||||
<div className="bg-error mx-auto rounded-full p-2 text-center">
|
<div className="bg-error mx-auto rounded-full p-2 text-center">
|
||||||
<AlertCircle className="h-5 w-5 text-red-600" />
|
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-100" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{variety === "warning" && (
|
{variety === "warning" && (
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Examples,
|
||||||
|
Example,
|
||||||
|
Title,
|
||||||
|
VariantsTable,
|
||||||
|
CustomArgsTable,
|
||||||
|
VariantRow,
|
||||||
|
} from "@calcom/storybook/components";
|
||||||
|
|
||||||
|
import ErrorBoundary from "./ErrorBoundary";
|
||||||
|
import { Tooltip } from "../tooltip";
|
||||||
|
|
||||||
|
<Meta title="UI/ErrorBoundary" component={ErrorBoundary} />
|
||||||
|
|
||||||
|
<Title title="ErrorBoundary" suffix="Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
|
||||||
|
|
||||||
|
## Definition
|
||||||
|
|
||||||
|
ErrorBoundary is an element that catches JavaScript errors in their child component tree, log those errors and display a fallback UI.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
ErrorBoundary offers a flexible component capable of catching JavaScript errors and displaying it in the UI.
|
||||||
|
|
||||||
|
<CustomArgsTable of={ErrorBoundary} />
|
||||||
|
|
||||||
|
<Examples title="ErrorBoundary">
|
||||||
|
<Example title="Default">
|
||||||
|
<ErrorBoundary message="There is a problem with the App">
|
||||||
|
<p>Child Component</p>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Example>
|
||||||
|
<Example title="With Error">
|
||||||
|
<ErrorBoundary message="There is a problem with the App">
|
||||||
|
<Tooltip />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
|
||||||
|
<Title offset title="ErrorBoundary" suffix="Variants" />
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="ErrorBoundary"
|
||||||
|
args={{
|
||||||
|
children: <p class="text-default">Child Component</p>,
|
||||||
|
message: "There is a problem with the App",
|
||||||
|
}}>
|
||||||
|
{({ children, message }) => (
|
||||||
|
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||||
|
<VariantRow variant="Default">
|
||||||
|
<ErrorBoundary message={message}>{children}</ErrorBoundary>
|
||||||
|
</VariantRow>
|
||||||
|
<VariantRow variant="With Error">
|
||||||
|
<ErrorBoundary message={message}>
|
||||||
|
<Tooltip />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</VariantRow>
|
||||||
|
</VariantsTable>
|
||||||
|
)}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
|
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Example,
|
||||||
|
Title,
|
||||||
|
CustomArgsTable,
|
||||||
|
VariantRow,
|
||||||
|
VariantsTable,
|
||||||
|
} from "@calcom/storybook/components";
|
||||||
|
|
||||||
|
import ColorPicker from "./colorpicker";
|
||||||
|
|
||||||
|
<Meta title="UI/Form/ColorPicker" component={ColorPicker} />
|
||||||
|
|
||||||
|
<Title title="ColorPicker" suffix="Brief" subtitle="Version 1.0 — Last Update: 16 Aug 2023" />
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
`Color Picker` is used to select custom hex colors for from a range of values.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
The `Color Picker` takes in several props
|
||||||
|
|
||||||
|
<CustomArgsTable of={ColorPicker} />
|
||||||
|
|
||||||
|
## Default:
|
||||||
|
|
||||||
|
<Example title="Default">
|
||||||
|
<ColorPicker defaultValue="#000000" />
|
||||||
|
</Example>
|
||||||
|
|
||||||
|
## ColorPicker Story
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Default"
|
||||||
|
args={{
|
||||||
|
defaultValue: "#21aef3",
|
||||||
|
onChange: (value) => {
|
||||||
|
console.debug(value);
|
||||||
|
},
|
||||||
|
resetDefaultValue: "#000000",
|
||||||
|
className: "w-[200px]",
|
||||||
|
popoverAlign: "start",
|
||||||
|
}}
|
||||||
|
argTypes={{
|
||||||
|
defaultValue: {
|
||||||
|
control: {
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resetDefaultValue: {
|
||||||
|
control: {
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
popoverAlign: {
|
||||||
|
control: {
|
||||||
|
type: "inline-radio",
|
||||||
|
options: ["center", "start", "end"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
{({ defaultValue, onChange, resetDefaultValue, className, popoverAlign }) => (
|
||||||
|
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||||
|
<VariantRow>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip content="color picker">
|
||||||
|
<ColorPicker
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={onChange}
|
||||||
|
resetDefaultValue={resetDefaultValue}
|
||||||
|
className={className}
|
||||||
|
popoverAlign={popoverAlign}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</VariantRow>
|
||||||
|
</VariantsTable>
|
||||||
|
)}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
|
@ -138,7 +138,9 @@ export const DropdownItem = (props: DropdownItemProps) => {
|
||||||
{...rest}
|
{...rest}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"hover:text-emphasis text-default inline-flex w-full items-center space-x-2 px-3 py-2 disabled:cursor-not-allowed",
|
"hover:text-emphasis text-default inline-flex w-full items-center space-x-2 px-3 py-2 disabled:cursor-not-allowed",
|
||||||
color === "destructive" ? "hover:bg-error hover:text-red-700" : "hover:bg-subtle",
|
color === "destructive"
|
||||||
|
? "hover:bg-error hover:text-red-700 dark:hover:text-red-100"
|
||||||
|
: "hover:bg-subtle",
|
||||||
props.className
|
props.className
|
||||||
)}>
|
)}>
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -0,0 +1,361 @@
|
||||||
|
import { Canvas, Story, Meta } from "@storybook/addon-docs";
|
||||||
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Examples,
|
||||||
|
Example,
|
||||||
|
Title,
|
||||||
|
CustomArgsTable,
|
||||||
|
VariantRow,
|
||||||
|
VariantsTable,
|
||||||
|
} from "@calcom/storybook/components";
|
||||||
|
|
||||||
|
import { Plus, Trash, Copy } from "@calcom/ui/components/icon";
|
||||||
|
import {
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from "./Dropdown";
|
||||||
|
|
||||||
|
<Meta title="UI/Form/Dropdown" component={Dropdown} />
|
||||||
|
|
||||||
|
<Title title="Dropdown" suffix="Brief" subtitle="Version 1.0 — Last Update: 29 Aug 2023" />
|
||||||
|
|
||||||
|
## Definition
|
||||||
|
|
||||||
|
`Dropdown` is an element that displays a menu to the user—such as a set of actions or functions.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
The `Dropdown` component can be used to display a menu to the user.
|
||||||
|
|
||||||
|
<CustomArgsTable of={Dropdown} />
|
||||||
|
|
||||||
|
### Dropdown components that have arguments:
|
||||||
|
|
||||||
|
#### DropdownMenuTrigger
|
||||||
|
|
||||||
|
<CustomArgsTable of={DropdownMenuTrigger} />
|
||||||
|
|
||||||
|
#### DropdownMenuContent
|
||||||
|
|
||||||
|
<CustomArgsTable of={DropdownMenuContent} />
|
||||||
|
|
||||||
|
#### DropdownMenuItem
|
||||||
|
|
||||||
|
<CustomArgsTable of={DropdownMenuItem} />
|
||||||
|
|
||||||
|
#### DropdownMenuSeparator
|
||||||
|
|
||||||
|
<CustomArgsTable of={DropdownMenuSeparator} />
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<Examples title="Dropdown">
|
||||||
|
<Example title="Simple">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="No modal">
|
||||||
|
<Dropdown modal={false}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
<Examples title="Dropdown Menu Trigger">
|
||||||
|
<Example title="Simple Button">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="Button Icon">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button StartIcon={Plus} />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="Disabled">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger disabled={true} asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="Not as child">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
<Examples title="Dropdown Menu Content">
|
||||||
|
<Example title="Custom width">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="Align start">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent style={{ minWidth: "80px" }} align={"start"}>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="Align center">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent style={{ minWidth: "80px" }} align={"center"}>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="With Menu Separator">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="With side offset">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent sideOffset={50}>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
<Example title="With Some icons">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem StartIcon={Trash}>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem StartIcon={Plus}>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem StartIcon={Copy}>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
<Examples title="Dropdown Menu Label">
|
||||||
|
<Example title="Simple">
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align={"start"}>
|
||||||
|
<DropdownMenuLabel>Number</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
|
||||||
|
## Dropdown Story
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Dropdown"
|
||||||
|
args={{
|
||||||
|
asChild: false,
|
||||||
|
align: "start",
|
||||||
|
disabled: false,
|
||||||
|
sideOffset: 0
|
||||||
|
}}
|
||||||
|
argTypes={{
|
||||||
|
sideOffset: {
|
||||||
|
control: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
asChild: {
|
||||||
|
control: {
|
||||||
|
type: "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
align: {
|
||||||
|
control: {
|
||||||
|
type: "inline-radio",
|
||||||
|
options: ["center", "start", "end"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: {
|
||||||
|
type: "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{({ dir, asChild, sideOffset, align, disabled }) => (
|
||||||
|
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||||
|
<VariantRow>
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownMenuTrigger disabled={disabled} asChild>
|
||||||
|
<Button>more</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align={align} sideOffset={sideOffset}>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem StartIcon={Trash}>First</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem StartIcon={Plus}>Second</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem StartIcon={Copy}>Third</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
</VariantRow>
|
||||||
|
</VariantsTable>
|
||||||
|
)}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
|
@ -1,9 +1,8 @@
|
||||||
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Examples,
|
Examples,
|
||||||
Example,
|
Example,
|
||||||
Note,
|
|
||||||
Title,
|
Title,
|
||||||
CustomArgsTable,
|
CustomArgsTable,
|
||||||
VariantRow,
|
VariantRow,
|
||||||
|
@ -15,7 +14,7 @@ import { SelectField } from "./Select";
|
||||||
|
|
||||||
<Meta title="UI/Form/Select Field" component={SelectField} />
|
<Meta title="UI/Form/Select Field" component={SelectField} />
|
||||||
|
|
||||||
<Title title="Select" suffix="Brief" subtitle="Version 2.0 — Last Update: 22 Aug 2022" />
|
<Title title="Select" suffix="Brief" subtitle="Version 2.0 — Last Update: 29 Aug 2022" />
|
||||||
|
|
||||||
## Definition
|
## Definition
|
||||||
|
|
||||||
|
@ -101,11 +100,31 @@ const GoodSelect = (props) => <Select {...props} components={{ Control }} />;
|
||||||
## Select Story
|
## Select Story
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="Default">
|
<Story
|
||||||
<VariantsTable titles={["Default"]} columnMinWidth={450}>
|
name="Default"
|
||||||
<VariantRow>
|
args={{
|
||||||
<SelectField options={options} label={"Default Select"} />
|
required: false,
|
||||||
</VariantRow>
|
name: "select-field",
|
||||||
</VariantsTable>
|
error: "Some error",
|
||||||
|
variant: "default",
|
||||||
|
label: "Select an item",
|
||||||
|
isMulti: false,
|
||||||
|
options,
|
||||||
|
}}
|
||||||
|
argTypes={{
|
||||||
|
variant: {
|
||||||
|
control: {
|
||||||
|
type: "select",
|
||||||
|
options: ["default", "checkbox"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
{(args) => (
|
||||||
|
<VariantsTable titles={["Default"]} columnMinWidth={300}>
|
||||||
|
<VariantRow>
|
||||||
|
<SelectField {...args} />
|
||||||
|
</VariantRow>
|
||||||
|
</VariantsTable>
|
||||||
|
)}
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
|
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Examples,
|
||||||
|
Example,
|
||||||
|
Title,
|
||||||
|
VariantsTable,
|
||||||
|
CustomArgsTable,
|
||||||
|
VariantRow,
|
||||||
|
} from "@calcom/storybook/components";
|
||||||
|
|
||||||
|
import Switch from "./Switch";
|
||||||
|
|
||||||
|
<Meta title="UI/Form/Switch" component={Switch} />
|
||||||
|
|
||||||
|
<Title title="Switch" suffix="Brief" subtitle="Version 1.0 — Last Update: 16 Aug 2023" />
|
||||||
|
|
||||||
|
## Definition
|
||||||
|
|
||||||
|
Switch is a customizable toggle switch component that allows users to change between two states.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
The `Switch` component can be used to create toggle switches for various purposes. It provides options for adding labels, icons, and tooltips.
|
||||||
|
|
||||||
|
<CustomArgsTable of={Switch} />
|
||||||
|
|
||||||
|
<Examples title="States">
|
||||||
|
<Example title="Default">
|
||||||
|
<Switch />
|
||||||
|
</Example>
|
||||||
|
<Example title="Disabled">
|
||||||
|
<Switch disabled />
|
||||||
|
</Example>
|
||||||
|
<Example title="Checked">
|
||||||
|
<Switch checked />
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
|
||||||
|
<Examples title="Labels">
|
||||||
|
<Example title="With Label and labelOnLeading">
|
||||||
|
<Switch label="Enable Feature" labelOnLeading />
|
||||||
|
</Example>
|
||||||
|
<Example title="With Label">
|
||||||
|
<Switch label="Enable Feature" />
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
|
||||||
|
<Examples title="Hover">
|
||||||
|
<Example title="With Tooltip (Hover me)">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Switch tooltip="Toggle to enable/disable the feature" />
|
||||||
|
</TooltipProvider>
|
||||||
|
</Example>
|
||||||
|
<Example title="Without Tooltip (Hover me)">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Switch />
|
||||||
|
</TooltipProvider>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
|
||||||
|
<Title offset title="Switch" suffix="Variants" />
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Switch"
|
||||||
|
args={{
|
||||||
|
label: "Enable Feature",
|
||||||
|
tooltip: "Toggle to enable/disable the feature",
|
||||||
|
checked: false,
|
||||||
|
disabled: false,
|
||||||
|
fitToHeight: false,
|
||||||
|
labelOnLeading: false,
|
||||||
|
}}
|
||||||
|
argTypes={{
|
||||||
|
label: { control: { type: "text" } },
|
||||||
|
tooltip: { control: { type: "text" } },
|
||||||
|
checked: { control: { type: "boolean" } },
|
||||||
|
disabled: { control: { type: "boolean" } },
|
||||||
|
fitToHeight: { control: { type: "boolean" } },
|
||||||
|
labelOnLeading: { control: { type: "boolean" } },
|
||||||
|
}}>
|
||||||
|
{(props) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||||
|
<VariantRow>
|
||||||
|
<Switch
|
||||||
|
{...props}
|
||||||
|
onCheckedChange={(checkedValue) => console.log("Switch value:", checkedValue)}
|
||||||
|
/>
|
||||||
|
</VariantRow>
|
||||||
|
</VariantsTable>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
|
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomArgsTable,
|
||||||
|
Examples,
|
||||||
|
Example,
|
||||||
|
Title,
|
||||||
|
VariantsTable,
|
||||||
|
VariantRow,
|
||||||
|
} from "@calcom/storybook/components";
|
||||||
|
|
||||||
|
import { StorybookTrpcProvider } from "../../mocks/trpc";
|
||||||
|
import { TimezoneSelect } from "./TimezoneSelect";
|
||||||
|
|
||||||
|
<Meta title="UI/Form/TimezoneSelect" component={TimezoneSelect} />
|
||||||
|
|
||||||
|
<Title title="TimezoneSelect" suffix="Brief" subtitle="Version 1.0 — Last Update: 25 Aug 2023" />
|
||||||
|
|
||||||
|
## Definition
|
||||||
|
|
||||||
|
The `TimezoneSelect` component is used to display timezone options.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
The `TimezoneSelect` component can be used to display timezone options.
|
||||||
|
|
||||||
|
<CustomArgsTable of={TimezoneSelect} />
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<Examples title="TimezoneSelect">
|
||||||
|
<Example title="Default">
|
||||||
|
<StorybookTrpcProvider>
|
||||||
|
<TimezoneSelect value="Africa/Douala" />
|
||||||
|
</StorybookTrpcProvider>
|
||||||
|
</Example>
|
||||||
|
<Example title="Disabled">
|
||||||
|
<StorybookTrpcProvider>
|
||||||
|
<TimezoneSelect value="Africa/Douala" isDisabled />
|
||||||
|
</StorybookTrpcProvider>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
|
||||||
|
## TimezoneSelect Story
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="TimezoneSelect"
|
||||||
|
args={{
|
||||||
|
className: "mt-24",
|
||||||
|
value: "Africa/Douala",
|
||||||
|
variant: "default",
|
||||||
|
isDisabled: false,
|
||||||
|
timezones: {
|
||||||
|
"Timezone 1": "City 1",
|
||||||
|
"Timezone 2": "City 2",
|
||||||
|
"Timezone 3": "City 3",
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
}}
|
||||||
|
argTypes={{
|
||||||
|
value: {
|
||||||
|
control: { disable: true },
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
control: {
|
||||||
|
type: "inline-radio",
|
||||||
|
options: ["default", "minimal"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
{(args) => (
|
||||||
|
<VariantsTable titles={["Default"]} columnMinWidth={350}>
|
||||||
|
<VariantRow>
|
||||||
|
<StorybookTrpcProvider>
|
||||||
|
<TimezoneSelect {...args} />
|
||||||
|
</StorybookTrpcProvider>
|
||||||
|
</VariantRow>
|
||||||
|
</VariantsTable>
|
||||||
|
)}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
|
@ -49,6 +49,7 @@ export const ToggleGroup = ({ options, onValueChange, isFullWidth, ...props }: T
|
||||||
<RadixToggleGroup.Item
|
<RadixToggleGroup.Item
|
||||||
disabled={option.disabled}
|
disabled={option.disabled}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
|
data-testid={`toggle-group-item-${option.value}`}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"aria-checked:bg-emphasis relative rounded-[4px] px-3 py-1 text-sm leading-tight transition-colors",
|
"aria-checked:bg-emphasis relative rounded-[4px] px-3 py-1 text-sm leading-tight transition-colors",
|
||||||
option.disabled
|
option.disabled
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||||
|
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomArgsTable,
|
||||||
|
Examples,
|
||||||
|
Example,
|
||||||
|
Title,
|
||||||
|
VariantsTable,
|
||||||
|
VariantRow,
|
||||||
|
} from "@calcom/storybook/components";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { ToggleGroup } from "./ToggleGroup";
|
||||||
|
|
||||||
|
<Meta title="UI/Form/ToggleGroup" component={ToggleGroup} />
|
||||||
|
|
||||||
|
<Title title="ToggleGroup" suffix="Brief" subtitle="Version 1.0 — Last Update: 17 Aug 2023" />
|
||||||
|
|
||||||
|
## Definition
|
||||||
|
|
||||||
|
The `ToggleGroup` component is used to create a group of toggle items with optional tooltips.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
<CustomArgsTable of={ToggleGroup} />
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
<Examples title="Toggle Group With Icon Left">
|
||||||
|
<Example>
|
||||||
|
<TooltipProvider>
|
||||||
|
<ToggleGroup
|
||||||
|
options={[
|
||||||
|
{ value: "option1", label: "Option 1", tooltip: "Tooltip for Option 1", iconLeft: <ArrowRight /> },
|
||||||
|
{ value: "option2", label: "Option 2", iconLeft: <ArrowRight /> },
|
||||||
|
{ value: "option3", label: "Option 3", iconLeft: <ArrowRight /> },
|
||||||
|
{
|
||||||
|
value: "option4",
|
||||||
|
label: "Option 4",
|
||||||
|
tooltip: "Tooltip for Option 4",
|
||||||
|
iconLeft: <ArrowRight />,
|
||||||
|
},
|
||||||
|
{ value: "option5", label: "Option 5", iconLeft: <ArrowRight />, disabled: true },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
|
||||||
|
## ToggleGroup Story
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story
|
||||||
|
name="Default"
|
||||||
|
args={{
|
||||||
|
options: [
|
||||||
|
{ value: "option1", label: "Option 1", tooltip: "Tooltip for Option 1" },
|
||||||
|
{ value: "option2", label: "Option 2" },
|
||||||
|
{ value: "option3", label: "Option 3" },
|
||||||
|
{
|
||||||
|
value: "option4",
|
||||||
|
label: "Option 4",
|
||||||
|
tooltip: "Tooltip for Option 4",
|
||||||
|
},
|
||||||
|
{ value: "option5", label: "Option 5", disabled: true },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
argTypes={{
|
||||||
|
options: {
|
||||||
|
value: {
|
||||||
|
control: {
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lable: {
|
||||||
|
control: {
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
control: {
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
control: {
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isFullWidth: {
|
||||||
|
control: {
|
||||||
|
type: "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
{({ options, isFullWidth }) => (
|
||||||
|
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||||
|
<VariantRow>
|
||||||
|
<TooltipProvider>
|
||||||
|
<ToggleGroup options={options} isFullWidth={isFullWidth} />
|
||||||
|
</TooltipProvider>
|
||||||
|
</VariantRow>
|
||||||
|
</VariantsTable>
|
||||||
|
)}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
|
@ -10,20 +10,88 @@ import {
|
||||||
VariantRow,
|
VariantRow,
|
||||||
} from "@calcom/storybook/components";
|
} from "@calcom/storybook/components";
|
||||||
|
|
||||||
import { List, ListItem } from "./List";
|
import { List, ListItem, ListItemTitle, ListItemText } from "./List";
|
||||||
|
|
||||||
|
export const listItems = [
|
||||||
|
{ title: "Title 1", description: "Description 1" },
|
||||||
|
{ title: "Title 2", description: "Description 2" },
|
||||||
|
{ title: "Title 3", description: "Description 3" },
|
||||||
|
];
|
||||||
|
|
||||||
<Meta title="UI/List" component={List} />
|
<Meta title="UI/List" component={List} />
|
||||||
|
|
||||||
<Title title="List" suffix="Brief" subtitle="Version 2.0 — Last Update: 05 jan 2023" />
|
<Title title="List" suffix="Brief" subtitle="Version 2.0 — Last Update: 24 Aug 2023" />
|
||||||
|
|
||||||
## Definition
|
## Definition
|
||||||
|
|
||||||
Sums it up nicely.
|
The List component is used to render an unordered list with default styling
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
List takes an array of objects to display a list in the UI
|
||||||
|
### List
|
||||||
|
<CustomArgsTable of={List} />
|
||||||
|
|
||||||
|
### ListItem
|
||||||
|
<CustomArgsTable of={ListItem} />
|
||||||
|
|
||||||
|
<Examples>
|
||||||
|
<Example title="Default">
|
||||||
|
<List>
|
||||||
|
{listItems.map((item) => (
|
||||||
|
<ListItem rounded={false}>
|
||||||
|
<ListItemTitle className="mr-2">{item.title}</ListItemTitle>
|
||||||
|
<ListItemText>{item.description}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Example>
|
||||||
|
<Example title="Round Container">
|
||||||
|
<List roundContainer={false}>
|
||||||
|
{listItems.map((item) => (
|
||||||
|
<ListItem rounded={false}>
|
||||||
|
<ListItemTitle className="mr-2">{item.title}</ListItemTitle>
|
||||||
|
<ListItemText>{item.description}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Example>
|
||||||
|
<Example title="No Border Treatment">
|
||||||
|
<List noBorderTreatment={true}>
|
||||||
|
{listItems.map((item) => (
|
||||||
|
<ListItem rounded={false}>
|
||||||
|
<ListItemTitle className="mr-2">{item.title}</ListItemTitle>
|
||||||
|
<ListItemText>{item.description}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Example>
|
||||||
|
</Examples>
|
||||||
|
|
||||||
|
<Title offset title="List" suffix="Variants" />
|
||||||
|
|
||||||
<Canvas>
|
<Canvas>
|
||||||
<Story name="List">
|
<Story
|
||||||
<VariantsTable titles={[]} columnMinWidth={150}>
|
name="List"
|
||||||
<VariantRow variant="Default">TODO!</VariantRow>
|
args={{
|
||||||
</VariantsTable>
|
roundContainer: true,
|
||||||
|
noBorderTreatment: false,
|
||||||
|
rounded: false,
|
||||||
|
expanded: false
|
||||||
|
}}>
|
||||||
|
{({ roundContainer, noBorderTreatment, rounded, expanded }) => (
|
||||||
|
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||||
|
<VariantRow>
|
||||||
|
<List roundContainer={roundContainer} noBorderTreatment={noBorderTreatment}>
|
||||||
|
{listItems.map((item) => (
|
||||||
|
<ListItem rounded={rounded} expanded={expanded}>
|
||||||
|
<ListItemTitle className="mr-2">{item.title}</ListItemTitle>
|
||||||
|
<ListItemText>{item.description}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</VariantRow>
|
||||||
|
</VariantsTable>
|
||||||
|
)}
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user