merge with remote main
This commit is contained in:
commit
4652b3ee01
|
@ -9,12 +9,11 @@
|
|||
"langchain": "^0.0.131",
|
||||
"mailparser": "^3.6.5",
|
||||
"next": "^13.4.6",
|
||||
"zod": "^3.20.2"
|
||||
"supports-color": "8.1.1",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mailparser": "^3.4.0",
|
||||
"@types/node": "^20.5.1",
|
||||
"typescript": "^4.9.4"
|
||||
"@types/mailparser": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
|
|
|
@ -23,6 +23,7 @@ export const env = createEnv({
|
|||
NODE_ENV: process.env.NODE_ENV,
|
||||
OPENAI_API_KEY: process.env.OPENAI_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"]),
|
||||
OPENAI_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",
|
||||
"tzdata": "^1.0.30",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.20.2"
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -248,10 +248,10 @@
|
|||
--cal-bg-inverted: #f3f4f6;
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #dee9fc;
|
||||
--cal-bg-success: #e2fbe8;
|
||||
--cal-bg-attention: #fceed8;
|
||||
--cal-bg-error: #f9e3e2;
|
||||
--cal-bg-info: #263fa9;
|
||||
--cal-bg-success: #306339;
|
||||
--cal-bg-attention: #8e3b1f;
|
||||
--cal-bg-error: #8c2822;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
/* Borders */
|
||||
|
@ -269,10 +269,10 @@
|
|||
--cal-text-inverted: #101010;
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #253985;
|
||||
--cal-text-success: #285231;
|
||||
--cal-text-attention: #73321b;
|
||||
--cal-text-error: #752522;
|
||||
--cal-text-info: #dee9fc;
|
||||
--cal-text-success: #e2fbe8;
|
||||
--cal-text-attention: #fceed8;
|
||||
--cal-text-error: #f9e3e2;
|
||||
|
||||
/* Brand shenanigans
|
||||
-> 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.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
|
||||
process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
|
||||
|
||||
|
@ -226,7 +227,7 @@ const nextConfig = {
|
|||
async rewrites() {
|
||||
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
|
||||
...(process.env.ORGANIZATIONS_ENABLED
|
||||
...(isOrganizationsEnabled
|
||||
? [
|
||||
{
|
||||
...matcherConfigRootPath,
|
||||
|
@ -333,44 +334,46 @@ const nextConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
...[
|
||||
{
|
||||
...matcherConfigRootPath,
|
||||
headers: [
|
||||
...(isOrganizationsEnabled
|
||||
? [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/team/:orgSlug",
|
||||
...matcherConfigRootPath,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/team/:orgSlug",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...matcherConfigUserRoute,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/org/:orgSlug/:user",
|
||||
...matcherConfigUserRoute,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/org/:orgSlug/:user",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...matcherConfigUserTypeRoute,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/org/:orgSlug/:user/:type",
|
||||
...matcherConfigUserTypeRoute,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/org/:orgSlug/:user/:type",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...matcherConfigUserTypeEmbedRoute,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/org/:orgSlug/:user/:type/embed",
|
||||
...matcherConfigUserTypeEmbedRoute,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/org/:orgSlug/:user/:type/embed",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
|
@ -447,6 +450,13 @@ const nextConfig = {
|
|||
},
|
||||
{
|
||||
source: "/support",
|
||||
missing: [
|
||||
{
|
||||
type: "header",
|
||||
key: "host",
|
||||
value: orgHostPath,
|
||||
},
|
||||
],
|
||||
destination: "/event-types?openIntercom=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
|
||||
...(process.env.NODE_ENV === "development" &&
|
||||
// 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
|
||||
process.env.NEXT_PUBLIC_WEBAPP_URL !== "http://localhost:3000"
|
||||
? [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.2.6",
|
||||
"version": "3.2.7.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
@ -129,7 +129,7 @@
|
|||
"turndown": "^7.1.1",
|
||||
"uuid": "^8.3.2",
|
||||
"web3": "^1.7.5",
|
||||
"zod": "^3.20.2"
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
|
||||
const evt: CalendarEvent = {
|
||||
type: booking.title,
|
||||
title: booking.title,
|
||||
|
@ -127,7 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
attendees: attendeesList,
|
||||
uid: booking.uid,
|
||||
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
|
||||
};
|
||||
|
||||
await sendOrganizerRequestReminderEmail(evt);
|
||||
|
|
|
@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs";
|
|||
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||
import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { availabilityAsString } from "@calcom/lib/availability";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
@ -17,11 +18,6 @@ import {
|
|||
ConfirmationDialogContent,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Form,
|
||||
Label,
|
||||
showToast,
|
||||
|
@ -32,7 +28,7 @@ import {
|
|||
Tooltip,
|
||||
VerticalDivider,
|
||||
} 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 { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
@ -95,7 +91,7 @@ export default function Availability() {
|
|||
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
|
||||
const fromEventType = searchParams?.get("fromEventType");
|
||||
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(
|
||||
{ scheduleId },
|
||||
{
|
||||
|
@ -225,33 +221,60 @@ export default function Availability() {
|
|||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<VerticalDivider className="hidden sm:inline" />
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="sm:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="destructive"
|
||||
StartIcon={Trash}
|
||||
onClick={() => setDeleteDialogOpen(true)}>
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={deleteMutation.isLoading}
|
||||
variety="danger"
|
||||
title={t("delete_schedule")}
|
||||
confirmBtnText={t("delete")}
|
||||
loadingText={t("delete")}
|
||||
onConfirm={() => {
|
||||
schedule !== undefined && deleteMutation.mutate({ scheduleId: schedule.id });
|
||||
}}>
|
||||
{t("delete_schedule_description")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex h-9 flex-row items-center justify-between px-4 py-2 hover:bg-gray-100">
|
||||
<div
|
||||
className={classNames(
|
||||
openSidebar
|
||||
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
|
||||
: ""
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"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",
|
||||
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||
)}>
|
||||
<div className="flex flex-row items-center pt-5">
|
||||
<Button StartIcon={ArrowLeft} color="minimal" onClick={() => setOpenSidebar(false)} />
|
||||
<p className="-ml-2">{t("availability_settings")}</p>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
StartIcon={Trash}
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
aria-label={t("delete")}
|
||||
className="ml-16 inline"
|
||||
disabled={schedule?.isLastSchedule}
|
||||
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<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
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
|
@ -267,9 +290,44 @@ export default function Availability() {
|
|||
}}
|
||||
/>
|
||||
</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" />
|
||||
<Button
|
||||
className="ml-4 lg:ml-0"
|
||||
|
@ -278,6 +336,13 @@ export default function Availability() {
|
|||
loading={updateMutation.isLoading}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-3 sm:hidden"
|
||||
StartIcon={MoreVertical}
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
onClick={() => setOpenSidebar(true)}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<div className="mt-4 w-full md:mt-0">
|
||||
|
@ -313,7 +378,7 @@ export default function Availability() {
|
|||
{schedule?.workingHours && <DateOverride workingHours={schedule.workingHours} />}
|
||||
</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>
|
||||
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
|
||||
|
@ -335,7 +400,7 @@ export default function Availability() {
|
|||
/>
|
||||
</div>
|
||||
<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">
|
||||
{t("something_doesnt_look_right")}
|
||||
</Skeleton>
|
||||
|
|
|
@ -55,10 +55,9 @@ import {
|
|||
Skeleton,
|
||||
Switch,
|
||||
Tooltip,
|
||||
ArrowButton,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Clipboard,
|
||||
Code,
|
||||
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="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) && (
|
||||
<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={() => moveEventType(index, -1)}>
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
</button>
|
||||
<ArrowButton onClick={() => moveEventType(index, -1)} arrowDirection="up" />
|
||||
)}
|
||||
{!(lastItem && lastItem.id === eventType.id) && (
|
||||
<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={() => moveEventType(index, 1)}>
|
||||
<ArrowDown className="h-5 w-5" />
|
||||
</button>
|
||||
<ArrowButton onClick={() => moveEventType(index, 1)} arrowDirection="down" />
|
||||
)}
|
||||
<MemoizedItem eventType={eventType} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
|
@ -887,7 +878,7 @@ const Main = ({
|
|||
{isMobile ? (
|
||||
<MobileTeamsTab eventTypeGroups={data} />
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
<div className="mt-4 flex flex-col" key={group.profile.slug}>
|
||||
<EventTypeListHeading
|
||||
profile={data[0].users[0] || data[0].team}
|
||||
membershipCount={data[0].team?.members.length || 0}
|
||||
|
|
|
@ -25,7 +25,7 @@ const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
|
|||
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
|
||||
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);
|
||||
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]",
|
||||
destinationCalendar: null,
|
||||
destinationCalendar: [],
|
||||
// hideCalendarNotes: false,
|
||||
requiresConfirmation: "[redacted/dynamic]",
|
||||
eventTypeId: "[redacted/dynamic]",
|
||||
|
|
|
@ -529,7 +529,7 @@
|
|||
"location": "Ort",
|
||||
"address": "Adresse",
|
||||
"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",
|
||||
"no": "Nein",
|
||||
"additional_notes": "Zusätzliche Notizen",
|
||||
|
@ -539,7 +539,7 @@
|
|||
"booking_confirmation": "Bestätigen Sie {{eventTypeTitle}} mit {{profileName}}",
|
||||
"booking_reschedule_confirmation": "Planen Sie Ihr {{eventTypeTitle}} mit {{profileName}} um",
|
||||
"in_person_meeting": "Vor-Ort-Termin",
|
||||
"in_person": "Persönlich (Organisator-Adresse)",
|
||||
"in_person": "Vor Ort (Organisator-Adresse)",
|
||||
"link_meeting": "Termin verknüpfen",
|
||||
"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.",
|
||||
"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",
|
||||
"availability_settings": "Availability Settings",
|
||||
"continue_without_calendar": "Continue without calendar",
|
||||
"connect_your_calendar": "Connect your calendar",
|
||||
"connect_your_video_app": "Connect your video apps",
|
||||
|
@ -1107,6 +1108,7 @@
|
|||
"email_attendee_action": "send email to attendees",
|
||||
"sms_attendee_action": "Send SMS to attendee",
|
||||
"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_attendee_action": "send WhatsApp message to attendee",
|
||||
"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.",
|
||||
"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",
|
||||
"availability_settings": "Paramètres de disponibilité",
|
||||
"continue_without_calendar": "Continuer sans calendrier",
|
||||
"connect_your_calendar": "Connectez votre calendrier",
|
||||
"connect_your_video_app": "Connectez vos applications vidéo",
|
||||
|
@ -1107,6 +1108,7 @@
|
|||
"email_attendee_action": "envoyer un e-mail aux participants",
|
||||
"sms_attendee_action": "Envoyer un SMS au participant",
|
||||
"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_attendee_action": "envoyer un message WhatsApp au participant",
|
||||
"workflows": "Workflows",
|
||||
|
|
|
@ -58,10 +58,10 @@
|
|||
--cal-bg-inverted: #f3f4f6;
|
||||
|
||||
/* background -> components*/
|
||||
--cal-bg-info: #dee9fc;
|
||||
--cal-bg-success: #e2fbe8;
|
||||
--cal-bg-attention: #fceed8;
|
||||
--cal-bg-error: #f9e3e2;
|
||||
--cal-bg-info: #263fa9;
|
||||
--cal-bg-success: #306339;
|
||||
--cal-bg-attention: #8e3b1f;
|
||||
--cal-bg-error: #8c2822;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
/* Borders */
|
||||
|
@ -80,10 +80,10 @@
|
|||
--cal-text-inverted: #101010;
|
||||
|
||||
/* Content/Text -> components */
|
||||
--cal-text-info: #253985;
|
||||
--cal-text-success: #285231;
|
||||
--cal-text-attention: #73321b;
|
||||
--cal-text-error: #752522;
|
||||
--cal-text-info: #dee9fc;
|
||||
--cal-text-success: #e2fbe8;
|
||||
--cal-text-attention: #fceed8;
|
||||
--cal-text-error: #f9e3e2;
|
||||
|
||||
/* Brand shenanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
|
|
|
@ -31,7 +31,7 @@ beforeAll(async () => {
|
|||
describe("next.config.js - Org Rewrite", () => {
|
||||
const orgHostRegExp = (subdomainRegExp: string) =>
|
||||
// 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", () => {
|
||||
it("https://app.cal.com", () => {
|
||||
|
@ -87,6 +87,11 @@ describe("next.config.js - Org Rewrite", () => {
|
|||
?.orgSlug
|
||||
).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", () => {
|
||||
|
|
|
@ -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 }) => ({
|
||||
...rest,
|
||||
responseStatus: "accepted",
|
||||
|
@ -97,6 +97,10 @@ export default class GoogleCalendarService implements Calendar {
|
|||
responseStatus: "accepted",
|
||||
})) || [];
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const [mainHostDestinationCalendar] =
|
||||
calEventRaw?.destinationCalendar && calEventRaw?.destinationCalendar.length > 0
|
||||
? calEventRaw.destinationCalendar
|
||||
: [];
|
||||
const myGoogleAuth = await this.auth.getToken();
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: calEventRaw.title,
|
||||
|
@ -115,8 +119,8 @@ export default class GoogleCalendarService implements Calendar {
|
|||
id: String(calEventRaw.organizer.id),
|
||||
responseStatus: "accepted",
|
||||
organizer: true,
|
||||
email: calEventRaw.destinationCalendar?.externalId
|
||||
? calEventRaw.destinationCalendar.externalId
|
||||
email: mainHostDestinationCalendar?.externalId
|
||||
? mainHostDestinationCalendar.externalId
|
||||
: calEventRaw.organizer.email,
|
||||
},
|
||||
...eventAttendees,
|
||||
|
@ -138,13 +142,16 @@ export default class GoogleCalendarService implements Calendar {
|
|||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
});
|
||||
const selectedCalendar = calEventRaw.destinationCalendar?.externalId
|
||||
? calEventRaw.destinationCalendar.externalId
|
||||
: "primary";
|
||||
// Find in calEventRaw.destinationCalendar the one with the same credentialId
|
||||
|
||||
const selectedCalendar = calEventRaw.destinationCalendar?.find(
|
||||
(cal) => cal.credentialId === credentialId
|
||||
)?.externalId;
|
||||
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: selectedCalendar,
|
||||
calendarId: selectedCalendar || "primary",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
sendUpdates: "none",
|
||||
|
@ -188,6 +195,8 @@ export default class GoogleCalendarService implements Calendar {
|
|||
|
||||
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId: string): Promise<any> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const [mainHostDestinationCalendar] =
|
||||
event?.destinationCalendar && event?.destinationCalendar.length > 0 ? event.destinationCalendar : [];
|
||||
const myGoogleAuth = await this.auth.getToken();
|
||||
const eventAttendees = event.attendees.map(({ ...rest }) => ({
|
||||
...rest,
|
||||
|
@ -216,8 +225,8 @@ export default class GoogleCalendarService implements Calendar {
|
|||
id: String(event.organizer.id),
|
||||
organizer: true,
|
||||
responseStatus: "accepted",
|
||||
email: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
email: mainHostDestinationCalendar?.externalId
|
||||
? mainHostDestinationCalendar.externalId
|
||||
: event.organizer.email,
|
||||
},
|
||||
...(eventAttendees as any),
|
||||
|
@ -244,7 +253,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||
|
||||
const selectedCalendar = externalCalendarId
|
||||
? externalCalendarId
|
||||
: event.destinationCalendar?.externalId;
|
||||
: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
|
||||
|
||||
calendar.events.update(
|
||||
{
|
||||
|
@ -303,7 +312,9 @@ export default class GoogleCalendarService implements Calendar {
|
|||
});
|
||||
|
||||
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(
|
||||
{
|
||||
|
|
|
@ -125,7 +125,8 @@ export default class LarkCalendarService implements Calendar {
|
|||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
let eventId = "";
|
||||
let eventRespData;
|
||||
const calendarId = event.destinationCalendar?.externalId;
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
const calendarId = mainHostDestinationCalendar?.externalId;
|
||||
if (!calendarId) {
|
||||
throw new Error("no calendar id");
|
||||
}
|
||||
|
@ -160,7 +161,8 @@ export default class LarkCalendarService implements Calendar {
|
|||
}
|
||||
|
||||
private createAttendees = async (event: CalendarEvent, eventId: string) => {
|
||||
const calendarId = event.destinationCalendar?.externalId;
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
const calendarId = mainHostDestinationCalendar?.externalId;
|
||||
if (!calendarId) {
|
||||
this.log.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) {
|
||||
const eventId = uid;
|
||||
let eventRespData;
|
||||
const calendarId = externalCalendarId || event.destinationCalendar?.externalId;
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
const calendarId = externalCalendarId || mainHostDestinationCalendar?.externalId;
|
||||
if (!calendarId) {
|
||||
this.log.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
|
||||
*/
|
||||
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) {
|
||||
this.log.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> {
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
try {
|
||||
const eventsUrl = event.destinationCalendar?.externalId
|
||||
? `/me/calendars/${event.destinationCalendar?.externalId}/events`
|
||||
const eventsUrl = mainHostDestinationCalendar?.externalId
|
||||
? `/me/calendars/${mainHostDestinationCalendar?.externalId}/events`
|
||||
: "/me/calendar/events";
|
||||
|
||||
const response = await this.fetcher(eventsUrl, {
|
||||
|
|
|
@ -4,7 +4,6 @@ import z from "zod";
|
|||
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
|
||||
import { findPaymentCredentials } from "@calcom/features/ee/payments/api/paypal-webhook";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
@ -78,12 +77,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
return;
|
||||
} 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`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class Paypal {
|
|||
}
|
||||
|
||||
private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
|
||||
this.getAccessToken();
|
||||
await this.getAccessToken();
|
||||
return fetch(`${this.url}${endpoint}`, {
|
||||
method: "get",
|
||||
...init,
|
||||
|
@ -173,7 +173,7 @@ class Paypal {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
throw error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -41,25 +41,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
}}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 ",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"dotenv": "^16.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"json-logic-js": "^2.0.2",
|
||||
"react-awesome-query-builder": "^5.1.2"
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// TODO: i18n
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useEffect } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
|
@ -30,6 +31,7 @@ import {
|
|||
List,
|
||||
ListLinkItem,
|
||||
Tooltip,
|
||||
ArrowButton,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
BarChart,
|
||||
|
@ -83,6 +85,20 @@ export default function RoutingForms({
|
|||
const { hasPaidPlan } = useHasPaidPlan();
|
||||
const routerQuery = useRouterQuery();
|
||||
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(() => {
|
||||
hookForm.reset({});
|
||||
// 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 (
|
||||
<LicenseRequired>
|
||||
<ShellMain
|
||||
|
@ -177,8 +216,8 @@ export default function RoutingForms({
|
|||
}
|
||||
SkeletonLoader={SkeletonLoaderTeamList}>
|
||||
<div className="bg-default mb-16 overflow-hidden">
|
||||
<List data-testid="routing-forms-list">
|
||||
{forms?.map(({ form, readOnly }) => {
|
||||
<List data-testid="routing-forms-list" ref={parent}>
|
||||
{forms?.map(({ form, readOnly }, index) => {
|
||||
if (!form) {
|
||||
return null;
|
||||
}
|
||||
|
@ -187,116 +226,129 @@ export default function RoutingForms({
|
|||
form.routes = form.routes || [];
|
||||
const fields = form.fields || [];
|
||||
const userRoutes = form.routes.filter((route) => !isFallbackRoute(route));
|
||||
const firstItem = forms[0].form;
|
||||
const lastItem = forms[forms.length - 1].form;
|
||||
|
||||
return (
|
||||
<ListLinkItem
|
||||
key={form.id}
|
||||
href={appUrl + "/form-edit/" + form.id}
|
||||
heading={form.name}
|
||||
disabled={readOnly}
|
||||
subHeading={description}
|
||||
className="space-x-2 rtl:space-x-reverse"
|
||||
actions={
|
||||
<>
|
||||
{form.team?.name && (
|
||||
<div className="border-r-2 border-neutral-300">
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{form.team.name}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<FormAction
|
||||
disabled={readOnly}
|
||||
className="self-center"
|
||||
action="toggle"
|
||||
routingForm={form}
|
||||
/>
|
||||
<ButtonGroup combined>
|
||||
<Tooltip content={t("preview")}>
|
||||
<div
|
||||
className="group flex w-full max-w-full items-center justify-between overflow-hidden"
|
||||
key={form.id}>
|
||||
{!(firstItem && firstItem.id === form.id) && (
|
||||
<ArrowButton onClick={() => moveRoutingForm(index, -1)} arrowDirection="up" />
|
||||
)}
|
||||
|
||||
{!(lastItem && lastItem.id === form.id) && (
|
||||
<ArrowButton onClick={() => moveRoutingForm(index, 1)} arrowDirection="down" />
|
||||
)}
|
||||
<ListLinkItem
|
||||
href={appUrl + "/form-edit/" + form.id}
|
||||
heading={form.name}
|
||||
disabled={readOnly}
|
||||
subHeading={description}
|
||||
className="space-x-2 rtl:space-x-reverse"
|
||||
actions={
|
||||
<>
|
||||
{form.team?.name && (
|
||||
<div className="border-r-2 border-neutral-300">
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{form.team.name}
|
||||
</Badge>
|
||||
</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
|
||||
action="preview"
|
||||
routingForm={form}
|
||||
target="_blank"
|
||||
StartIcon={ExternalLink}
|
||||
action="copyLink"
|
||||
color="secondary"
|
||||
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
|
||||
action="edit"
|
||||
routingForm={form}
|
||||
color="minimal"
|
||||
className="!flex"
|
||||
StartIcon={Edit}>
|
||||
{t("edit")}
|
||||
</FormAction>
|
||||
<FormAction
|
||||
action="download"
|
||||
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 ? (
|
||||
action="embed"
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={Code}
|
||||
tooltip={t("embed")}
|
||||
/>
|
||||
<FormActionsDropdown disabled={readOnly}>
|
||||
<FormAction
|
||||
data-testid="copy-redirect-url"
|
||||
action="edit"
|
||||
routingForm={form}
|
||||
action="copyRedirectUrl"
|
||||
color="minimal"
|
||||
type="button"
|
||||
StartIcon={LinkIcon}>
|
||||
{t("Copy Typeform Redirect Url")}
|
||||
className="!flex"
|
||||
StartIcon={Edit}>
|
||||
{t("edit")}
|
||||
</FormAction>
|
||||
) : 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>
|
||||
<FormAction
|
||||
action="download"
|
||||
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
|
||||
data-testid="copy-redirect-url"
|
||||
routingForm={form}
|
||||
action="copyRedirectUrl"
|
||||
color="minimal"
|
||||
type="button"
|
||||
StartIcon={LinkIcon}>
|
||||
{t("Copy Typeform Redirect Url")}
|
||||
</FormAction>
|
||||
) : 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>
|
||||
|
|
|
@ -21,7 +21,7 @@ test.describe("Routing Forms", () => {
|
|||
|
||||
await page.waitForSelector('[data-testid="routing-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 expect(page.locator("text=Test Form Name")).toBeVisible();
|
||||
|
|
|
@ -59,6 +59,7 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt
|
|||
fields: true,
|
||||
settings: true,
|
||||
teamId: true,
|
||||
position: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -26,9 +26,14 @@ export const formsHandler = async ({ ctx, input }: FormsHandlerOptions) => {
|
|||
|
||||
const forms = await prisma.app_RoutingForms_Form.findMany({
|
||||
where,
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
position: "desc",
|
||||
},
|
||||
{
|
||||
createdAt: "asc",
|
||||
},
|
||||
],
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
|
|
|
@ -50,7 +50,8 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||
bookingId: Booking["id"],
|
||||
bookerEmail: string,
|
||||
paymentOption: PaymentOption
|
||||
paymentOption: PaymentOption,
|
||||
eventTitle?: string
|
||||
) {
|
||||
try {
|
||||
// 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,
|
||||
payment_method_types: ["card"],
|
||||
customer: customer.id,
|
||||
metadata: {
|
||||
identifier: "cal.com",
|
||||
bookingId,
|
||||
bookerEmail,
|
||||
eventName: eventTitle || "",
|
||||
},
|
||||
};
|
||||
|
||||
const paymentIntent = await this.stripe.paymentIntents.create(params, {
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"@stripe/stripe-js": "^1.35.0",
|
||||
"stripe": "^9.16.0",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.20.2"
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*",
|
||||
|
|
|
@ -217,7 +217,8 @@ export const getBusyCalendarTimes = async (
|
|||
|
||||
export const createEvent = async (
|
||||
credential: CredentialWithAppName,
|
||||
calEvent: CalendarEvent
|
||||
calEvent: CalendarEvent,
|
||||
externalId?: string
|
||||
): Promise<EventResult<NewCalendarEventType>> => {
|
||||
const uid: string = getUid(calEvent);
|
||||
const calendar = await getCalendar(credential);
|
||||
|
@ -226,29 +227,31 @@ export const createEvent = async (
|
|||
|
||||
// Check if the disabledNotes flag is set to true
|
||||
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
|
||||
const creationResult = calendar
|
||||
? await calendar.createEvent(calEvent).catch(async (error: { code: number; calError: string }) => {
|
||||
success = false;
|
||||
/**
|
||||
* There is a time when selectedCalendar externalId doesn't match witch certain credential
|
||||
* so google returns 404.
|
||||
* */
|
||||
if (error?.code === 404) {
|
||||
? await calendar
|
||||
.createEvent(calEvent, credential.id)
|
||||
.catch(async (error: { code: number; calError: string }) => {
|
||||
success = false;
|
||||
/**
|
||||
* There is a time when selectedCalendar externalId doesn't match witch certain credential
|
||||
* 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;
|
||||
}
|
||||
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;
|
||||
|
||||
return {
|
||||
|
@ -261,6 +264,8 @@ export const createEvent = async (
|
|||
originalEvent: calEvent,
|
||||
calError,
|
||||
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
|
||||
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";
|
||||
}
|
||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
||||
|
@ -164,8 +166,8 @@ export default class EventManager {
|
|||
meetingId: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString(),
|
||||
meetingPassword: createdEventObj ? createdEventObj.password : result.createdEvent?.password,
|
||||
meetingUrl: createdEventObj ? createdEventObj.onlineMeetingUrl : result.createdEvent?.url,
|
||||
externalCalendarId: isCalendarType ? evt.destinationCalendar?.externalId : undefined,
|
||||
credentialId: isCalendarType ? evt.destinationCalendar?.credentialId : result.credentialId,
|
||||
externalCalendarId: isCalendarType ? result.externalId : undefined,
|
||||
credentialId: isCalendarType ? result.credentialId : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -203,8 +205,8 @@ export default class EventManager {
|
|||
meetingId: result.createdEvent?.id?.toString(),
|
||||
meetingPassword: result.createdEvent?.password,
|
||||
meetingUrl: result.createdEvent?.url,
|
||||
externalCalendarId: evt.destinationCalendar?.externalId,
|
||||
credentialId: result.credentialId ?? evt.destinationCalendar?.credentialId,
|
||||
externalCalendarId: result.externalId,
|
||||
credentialId: result.credentialId ?? undefined,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -332,29 +334,52 @@ export default class EventManager {
|
|||
* @private
|
||||
*/
|
||||
private async createAllCalendarEvents(event: CalendarEvent) {
|
||||
/** Can I use destinationCalendar here? */
|
||||
/* How can I link a DC to a cred? */
|
||||
|
||||
let createdEvents: EventResult<NewCalendarEventType>[] = [];
|
||||
if (event.destinationCalendar) {
|
||||
if (event.destinationCalendar.credentialId) {
|
||||
const credential = this.calendarCredentials.find(
|
||||
(c) => c.id === event.destinationCalendar?.credentialId
|
||||
);
|
||||
|
||||
if (credential) {
|
||||
const createdEvent = await createEvent(credential, event);
|
||||
if (createdEvent) {
|
||||
createdEvents.push(createdEvent);
|
||||
if (event.destinationCalendar && event.destinationCalendar.length > 0) {
|
||||
for (const destination of event.destinationCalendar) {
|
||||
if (destination.credentialId) {
|
||||
let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId);
|
||||
if (!credential) {
|
||||
// Fetch credential from DB
|
||||
const credentialFromDB = await prisma.credential.findUnique({
|
||||
include: {
|
||||
app: {
|
||||
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 {
|
||||
/**
|
||||
|
@ -451,7 +476,7 @@ export default class EventManager {
|
|||
booking: PartialBooking,
|
||||
newBookingId?: number
|
||||
): Promise<Array<EventResult<NewCalendarEventType>>> {
|
||||
let calendarReference: PartialReference | undefined = undefined,
|
||||
let calendarReference: PartialReference[] | undefined = undefined,
|
||||
credential;
|
||||
try {
|
||||
// If a newBookingId is given, update that calendar event
|
||||
|
@ -468,33 +493,62 @@ export default class EventManager {
|
|||
}
|
||||
|
||||
calendarReference = newBooking?.references.length
|
||||
? newBooking.references.find((reference) => reference.type.includes("_calendar"))
|
||||
: booking.references.find((reference) => reference.type.includes("_calendar"));
|
||||
? newBooking.references.filter((reference) => reference.type.includes("_calendar"))
|
||||
: booking.references.filter((reference) => reference.type.includes("_calendar"));
|
||||
|
||||
if (!calendarReference) {
|
||||
if (calendarReference.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = calendarReference;
|
||||
let calenderExternalId: string | null = null;
|
||||
if (bookingExternalCalendarId) {
|
||||
calenderExternalId = bookingExternalCalendarId;
|
||||
}
|
||||
|
||||
// process all calendar references
|
||||
let result = [];
|
||||
if (calendarReference.credentialId) {
|
||||
credential = this.calendarCredentials.filter(
|
||||
(credential) => credential.id === calendarReference?.credentialId
|
||||
)[0];
|
||||
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
|
||||
} else {
|
||||
const credentials = this.calendarCredentials.filter(
|
||||
(credential) => credential.type === calendarReference?.type
|
||||
);
|
||||
for (const credential of credentials) {
|
||||
for (const reference of calendarReference) {
|
||||
const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = reference;
|
||||
let calenderExternalId: string | null = null;
|
||||
if (bookingExternalCalendarId) {
|
||||
calenderExternalId = bookingExternalCalendarId;
|
||||
}
|
||||
|
||||
if (reference.credentialId) {
|
||||
credential = this.calendarCredentials.filter(
|
||||
(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));
|
||||
} 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 (newBookingId) {
|
||||
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"))
|
||||
.map(async (cred) => {
|
||||
const calendarReference = booking.references.find((ref) => ref.type === cred.type);
|
||||
if (!calendarReference)
|
||||
if (!calendarReference) {
|
||||
return {
|
||||
appName: cred.appName,
|
||||
type: cred.type,
|
||||
success: false,
|
||||
uid: "",
|
||||
originalEvent: event,
|
||||
credentialId: cred.id,
|
||||
};
|
||||
}
|
||||
|
||||
if (!calendarReference) {
|
||||
return {
|
||||
appName: cred.appName,
|
||||
type: cred.type,
|
||||
success: false,
|
||||
uid: "",
|
||||
originalEvent: event,
|
||||
credentialId: cred.id,
|
||||
};
|
||||
}
|
||||
const { externalCalendarId: bookingExternalCalendarId, meetingId: bookingRefUid } =
|
||||
calendarReference;
|
||||
return await updateEvent(cred, event, bookingRefUid ?? null, bookingExternalCalendarId ?? null);
|
||||
|
@ -539,17 +593,19 @@ export default class EventManager {
|
|||
if (error instanceof Error) {
|
||||
message = message.replace("{thing}", error.message);
|
||||
}
|
||||
console.error(message);
|
||||
return Promise.resolve([
|
||||
{
|
||||
appName: "none",
|
||||
type: calendarReference?.type || "calendar",
|
||||
success: false,
|
||||
uid: "",
|
||||
originalEvent: event,
|
||||
credentialId: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
return Promise.resolve(
|
||||
calendarReference?.map((reference) => {
|
||||
return {
|
||||
appName: "none",
|
||||
type: reference?.type || "calendar",
|
||||
success: false,
|
||||
uid: "",
|
||||
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 { v5 as uuidv5 } from "uuid";
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ class CalendarEventClass implements CalendarEvent {
|
|||
uid?: string | null;
|
||||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: any;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
destinationCalendar?: DestinationCalendar[] | null;
|
||||
cancellationReason?: string | null;
|
||||
rejectionReason?: string | null;
|
||||
hideCalendarNotes?: boolean;
|
||||
|
|
|
@ -85,8 +85,9 @@ export const BrokenIntegrationEmail = (
|
|||
|
||||
if (type === "calendar") {
|
||||
// The calendar name is stored as name_calendar
|
||||
let calendar = calEvent.destinationCalendar
|
||||
? calEvent.destinationCalendar?.integration.split("_")
|
||||
const [mainHostDestinationCalendar] = calEvent.destinationCalendar ?? [];
|
||||
let calendar = mainHostDestinationCalendar
|
||||
? mainHostDestinationCalendar?.integration.split("_")
|
||||
: "calendar";
|
||||
|
||||
if (Array.isArray(calendar)) {
|
||||
|
|
|
@ -387,7 +387,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||
if (trigger === "update") {
|
||||
return {
|
||||
...token,
|
||||
locale: session?.locale ?? token.locale,
|
||||
locale: session?.locale ?? token.locale ?? "en",
|
||||
name: session?.name ?? token.name,
|
||||
username: session?.username ?? token.username,
|
||||
email: session?.email ?? token.email,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
|
||||
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
|
||||
|
@ -23,8 +24,13 @@ export const DatePicker = () => {
|
|||
return (
|
||||
<DatePickerComponent
|
||||
isLoading={schedule.isLoading}
|
||||
onChange={(date) => setSelectedDate(date ? date.format("YYYY-MM-DD") : date)}
|
||||
onMonthChange={(date) => setMonth(date.format("YYYY-MM"))}
|
||||
onChange={(date: Dayjs | null) => {
|
||||
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}
|
||||
locale={i18n.language}
|
||||
browsingDate={month ? dayjs(month) : undefined}
|
||||
|
|
|
@ -154,14 +154,19 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
|
|||
},
|
||||
selectedDate: getQueryParam("date") || null,
|
||||
setSelectedDate: (selectedDate: string | null) => {
|
||||
// unset selected date
|
||||
if (!selectedDate) {
|
||||
removeQueryParam("date");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSelection = dayjs(get().selectedDate);
|
||||
const newSelection = dayjs(selectedDate);
|
||||
set({ selectedDate });
|
||||
updateQueryParam("date", selectedDate ?? "");
|
||||
|
||||
// Setting month make sure small calendar in fullscreen layouts also updates.
|
||||
// If selectedDate is null, prevents setting month to Invalid-Date
|
||||
if (selectedDate && newSelection.month() !== currentSelection.month()) {
|
||||
if (newSelection.month() !== currentSelection.month()) {
|
||||
set({ 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) => {
|
||||
set({ month, selectedTimeslot: null });
|
||||
updateQueryParam("month", month ?? "");
|
||||
get().setSelectedDate(null);
|
||||
},
|
||||
isTeamEvent: false,
|
||||
seatedEventData: {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
@ -26,6 +28,7 @@ type Avatar = {
|
|||
type AvatarWithRequiredImage = Avatar & { image: string };
|
||||
|
||||
export const EventMembers = ({ schedulingType, users, profile, entity }: EventMembersProps) => {
|
||||
const pathname = usePathname();
|
||||
const showMembers = schedulingType !== SchedulingType.ROUND_ROBIN;
|
||||
const shownUsers = showMembers ? users : [];
|
||||
|
||||
|
@ -57,7 +60,9 @@ export const EventMembers = ({ schedulingType, users, profile, entity }: EventMe
|
|||
title: `${profile.name || profile.username}`,
|
||||
image: "logo" in profile && profile.logo ? `${profile.logo}` : 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
|
||||
|
|
|
@ -248,7 +248,11 @@ async function handler(req: CustomRequest) {
|
|||
? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
|
||||
: undefined,
|
||||
location: bookingToDelete?.location,
|
||||
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
||||
destinationCalendar: bookingToDelete?.destinationCalendar
|
||||
? [bookingToDelete?.destinationCalendar]
|
||||
: bookingToDelete?.user.destinationCalendar
|
||||
? [bookingToDelete?.user.destinationCalendar]
|
||||
: [],
|
||||
cancellationReason: cancellationReason,
|
||||
...(teamMembers && { team: { name: "", members: teamMembers } }),
|
||||
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
|
||||
|
@ -411,58 +415,71 @@ async function handler(req: CustomRequest) {
|
|||
|
||||
const apiDeletes = [];
|
||||
|
||||
const bookingCalendarReference = bookingToDelete.references.find((reference) =>
|
||||
const bookingCalendarReference = bookingToDelete.references.filter((reference) =>
|
||||
reference.type.includes("_calendar")
|
||||
);
|
||||
|
||||
if (bookingCalendarReference) {
|
||||
const { credentialId, uid, externalCalendarId } = bookingCalendarReference;
|
||||
// If the booking calendar reference contains a credentialId
|
||||
if (credentialId) {
|
||||
// Find the correct calendar credential under user credentials
|
||||
const calendarCredential = bookingToDelete.user.credentials.find(
|
||||
(credential) => credential.id === credentialId
|
||||
);
|
||||
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);
|
||||
}
|
||||
if (bookingCalendarReference.length > 0) {
|
||||
for (const reference of bookingCalendarReference) {
|
||||
const { credentialId, uid, externalCalendarId } = reference;
|
||||
// If the booking calendar reference contains a credentialId
|
||||
if (credentialId) {
|
||||
// Find the correct calendar credential under user credentials
|
||||
let calendarCredential = bookingToDelete.user.credentials.find(
|
||||
(credential) => credential.id === credentialId
|
||||
);
|
||||
if (!calendarCredential) {
|
||||
// get credential from DB
|
||||
const foundCalendarCredential = await prisma.credential.findUnique({
|
||||
where: {
|
||||
id: credentialId,
|
||||
},
|
||||
});
|
||||
if (foundCalendarCredential) {
|
||||
calendarCredential = foundCalendarCredential;
|
||||
}
|
||||
} 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>);
|
||||
}
|
||||
}
|
||||
} 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,
|
||||
location: bookingToDelete.location ?? "",
|
||||
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);
|
||||
|
|
|
@ -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 async from "async";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
|
@ -367,7 +367,7 @@ async function ensureAvailableUsers(
|
|||
) {
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
|
||||
const orginalBookingDuration = input.originalRescheduledBooking
|
||||
const originalBookingDuration = input.originalRescheduledBooking
|
||||
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
||||
dayjs(input.originalRescheduledBooking.startTime),
|
||||
"minutes"
|
||||
|
@ -380,7 +380,7 @@ async function ensureAvailableUsers(
|
|||
{
|
||||
userId: user.id,
|
||||
eventTypeId: eventType.id,
|
||||
duration: orginalBookingDuration,
|
||||
duration: originalBookingDuration,
|
||||
...input,
|
||||
},
|
||||
{
|
||||
|
@ -686,8 +686,7 @@ async function handler(
|
|||
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
|
||||
|
||||
const isTeamEventType =
|
||||
eventType.schedulingType === SchedulingType.COLLECTIVE ||
|
||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN;
|
||||
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
|
||||
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
|
||||
|
@ -722,31 +721,46 @@ async function handler(
|
|||
throw new HttpError({ statusCode: 400, message: error.message });
|
||||
}
|
||||
|
||||
const loadUsers = async () =>
|
||||
!eventTypeId
|
||||
? await prisma.user.findMany({
|
||||
const loadUsers = async () => {
|
||||
try {
|
||||
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: {
|
||||
username: {
|
||||
in: dynamicUserList,
|
||||
},
|
||||
username: { in: dynamicUserList },
|
||||
},
|
||||
select: {
|
||||
...userSelect.select,
|
||||
credentials: true, // Don't leak to client
|
||||
credentials: 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,
|
||||
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
|
||||
let users: (Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||
isFixed?: boolean;
|
||||
|
@ -970,20 +984,26 @@ async function handler(
|
|||
: getLocationValueForDB(locationBodyString, eventType.locations);
|
||||
|
||||
const customInputs = getCustomInputsResponses(reqBody, eventType.customInputs);
|
||||
const teamMemberPromises =
|
||||
users.length > 1
|
||||
? users.slice(1).map(async function (user) {
|
||||
return {
|
||||
email: user.email || "",
|
||||
name: user.name || "",
|
||||
timeZone: user.timeZone,
|
||||
language: {
|
||||
translate: await getTranslation(user.locale ?? "en", "common"),
|
||||
locale: user.locale ?? "en",
|
||||
},
|
||||
};
|
||||
})
|
||||
: [];
|
||||
const teamDestinationCalendars: DestinationCalendar[] = [];
|
||||
|
||||
// Organizer or user owner of this event type it's not listed as a team member.
|
||||
const teamMemberPromises = users.slice(1).map(async (user) => {
|
||||
// push to teamDestinationCalendars if it's a team event but collective only
|
||||
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE" && user.destinationCalendar) {
|
||||
teamDestinationCalendars.push(user.destinationCalendar);
|
||||
}
|
||||
return {
|
||||
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);
|
||||
|
||||
|
@ -1040,16 +1060,24 @@ async function handler(
|
|||
attendees: attendeesList,
|
||||
location: bookingLocation, // Will be processed by the EventManager later.
|
||||
conferenceCredentialId,
|
||||
/** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */
|
||||
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
|
||||
destinationCalendar: eventType.destinationCalendar
|
||||
? [eventType.destinationCalendar]
|
||||
: organizerUser.destinationCalendar
|
||||
? [organizerUser.destinationCalendar]
|
||||
: null,
|
||||
hideCalendarNotes: eventType.hideCalendarNotes,
|
||||
requiresConfirmation: requiresConfirmation ?? false,
|
||||
eventTypeId: eventType.id,
|
||||
// if seats are not enabled we should default true
|
||||
seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true,
|
||||
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 */
|
||||
const addVideoCallDataToEvt = (bookingReferences: BookingReference[]) => {
|
||||
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
||||
|
@ -1843,11 +1871,12 @@ async function handler(
|
|||
id: organizerUser.id,
|
||||
},
|
||||
},
|
||||
destinationCalendar: evt.destinationCalendar
|
||||
? {
|
||||
connect: { id: evt.destinationCalendar.id },
|
||||
}
|
||||
: undefined,
|
||||
destinationCalendar:
|
||||
evt.destinationCalendar && evt.destinationCalendar.length > 0
|
||||
? {
|
||||
connect: { id: evt.destinationCalendar[0].id },
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
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"] */
|
||||
excludedDates?: string[];
|
||||
/** defaults to all, which dates are bookable (inverse of excludedDates) */
|
||||
includedDates?: string[] | null;
|
||||
includedDates?: string[];
|
||||
/** allows adding classes to the container */
|
||||
className?: string;
|
||||
/** 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 = ({
|
||||
minDate = dayjs.utc(),
|
||||
excludedDates = [],
|
||||
|
@ -121,7 +155,7 @@ const Days = ({
|
|||
// Create placeholder elements for empty days in first week
|
||||
const weekdayOfFirst = browsingDate.date(1).day();
|
||||
const currentDate = minDate.utcOffset(browsingDate.utcOffset());
|
||||
const availableDates = (includedDates: string[] | undefined | null) => {
|
||||
const availableDates = (includedDates: string[] | undefined) => {
|
||||
const dates = [];
|
||||
const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate));
|
||||
for (
|
||||
|
@ -148,21 +182,6 @@ const Days = ({
|
|||
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 isActive = (day: dayjs.Dayjs) => {
|
||||
|
@ -190,6 +209,21 @@ const Days = ({
|
|||
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 (
|
||||
<>
|
||||
{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;
|
||||
|
|
|
@ -96,6 +96,7 @@ function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
|
|||
)}
|
||||
data-disabled={isDisabled}
|
||||
data-slot={timeSlot.toISOString()}
|
||||
data-testid="calendar-empty-cell"
|
||||
style={{
|
||||
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
|
||||
overflow: "visible",
|
||||
|
|
|
@ -59,7 +59,9 @@ function AdminOrgTable() {
|
|||
</div>
|
||||
</Cell>
|
||||
<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>
|
||||
<div className="space-x-2">
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"@sendgrid/mail": "^7.6.2",
|
||||
"libphonenumber-js": "^1.10.12",
|
||||
"twilio": "^3.80.1",
|
||||
"zod": "^3.20.2"
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*"
|
||||
|
|
|
@ -149,7 +149,11 @@ export async function handlePaymentSuccess(
|
|||
},
|
||||
attendees: attendeesList,
|
||||
uid: booking.uid,
|
||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||
destinationCalendar: booking.destinationCalendar
|
||||
? [booking.destinationCalendar]
|
||||
: user.destinationCalendar
|
||||
? [user.destinationCalendar]
|
||||
: [],
|
||||
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
||||
};
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ async function getBooking(bookingId: number) {
|
|||
});
|
||||
|
||||
const attendeesList = await Promise.all(attendeesListPromises);
|
||||
|
||||
const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
|
||||
const evt: CalendarEvent = {
|
||||
type: booking.title,
|
||||
title: booking.title,
|
||||
|
@ -116,7 +116,7 @@ async function getBooking(bookingId: number) {
|
|||
},
|
||||
attendees: attendeesList,
|
||||
uid: booking.uid,
|
||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
|
||||
recurringEvent: parseRecurringEvent(eventType?.recurringEvent),
|
||||
};
|
||||
|
||||
|
@ -204,7 +204,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
|||
});
|
||||
|
||||
const attendeesList = await Promise.all(attendeesListPromises);
|
||||
|
||||
const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;
|
||||
const evt: CalendarEvent = {
|
||||
type: booking.title,
|
||||
title: booking.title,
|
||||
|
@ -226,7 +226,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
|||
attendees: attendeesList,
|
||||
location: booking.location,
|
||||
uid: booking.uid,
|
||||
destinationCalendar: booking.destinationCalendar || user.destinationCalendar,
|
||||
destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],
|
||||
recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent),
|
||||
};
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import { GoogleWorkspaceInviteButton } from "./GoogleWorkspaceInviteButton";
|
|||
|
||||
type MemberInvitationModalProps = {
|
||||
isOpen: boolean;
|
||||
justEmailInvites?: boolean;
|
||||
onExit: () => void;
|
||||
orgMembers?: RouterOutputs["viewer"]["organizations"]["getMembers"];
|
||||
onSubmit: (values: NewMemberForm, resetFields: () => void) => void;
|
||||
|
@ -206,7 +207,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
render={({ field: { onChange }, fieldState: { error } }) => (
|
||||
<>
|
||||
<TextField
|
||||
label={t("email_or_username")}
|
||||
label={props.justEmailInvites ? t("email") : t("email_or_username")}
|
||||
id="inviteUser"
|
||||
name="inviteUser"
|
||||
placeholder="email@example.com"
|
||||
|
|
|
@ -15,9 +15,9 @@ export default function TeamPill(props: Props) {
|
|||
<div
|
||||
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-info text-blue-800": props.color === "blue",
|
||||
" bg-error text-red-800 ": props.color === "red",
|
||||
" bg-attention text-orange-800": props.color === "orange",
|
||||
" bg-info text-info": props.color === "blue",
|
||||
" bg-error text-error ": props.color === "red",
|
||||
" bg-attention text-attention": props.color === "orange",
|
||||
})}>
|
||||
{props.text}
|
||||
</div>
|
||||
|
|
|
@ -65,7 +65,7 @@ export function TeamsListing() {
|
|||
{
|
||||
icon: <Mail className="h-5 w-5 text-orange-500" />,
|
||||
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" />,
|
||||
|
|
|
@ -117,10 +117,8 @@ export const updateQuantitySubscriptionFromStripe = async (teamId: number) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const newQuantity = membershipCount - subscriptionQuantity;
|
||||
|
||||
await stripe.subscriptions.update(subscriptionId, {
|
||||
items: [{ quantity: membershipCount + newQuantity, id: subscriptionItemId }],
|
||||
items: [{ quantity: membershipCount, id: subscriptionItemId }],
|
||||
});
|
||||
console.info(
|
||||
`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 Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
Tooltip,
|
||||
Badge,
|
||||
Avatar,
|
||||
ArrowButton,
|
||||
} from "@calcom/ui";
|
||||
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 [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [workflowToDeleteId, setwWorkflowToDeleteId] = useState(0);
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const router = useRouter();
|
||||
|
||||
const orgBranding = useOrgBranding();
|
||||
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 (
|
||||
<>
|
||||
{workflows && workflows.length > 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">
|
||||
{workflows.map((workflow) => (
|
||||
<li key={workflow.id}>
|
||||
<div className="first-line:group hover:bg-muted flex w-full items-center justify-between p-4 sm:px-6">
|
||||
<Link href={"/workflows/" + workflow.id} className="flex-grow cursor-pointer">
|
||||
<div className="rtl:space-x-reverse">
|
||||
<div className="flex">
|
||||
<div
|
||||
className={classNames(
|
||||
"max-w-56 text-emphasis truncate text-sm font-medium leading-6 md:max-w-max",
|
||||
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"}
|
||||
<ul className="divide-subtle !static w-full divide-y" data-testid="workflow-list" ref={parent}>
|
||||
{workflows.map((workflow, index) => {
|
||||
const firstItem = workflows[0];
|
||||
const lastItem = workflows[workflows.length - 1];
|
||||
return (
|
||||
<li
|
||||
key={workflow.id}
|
||||
className="group flex w-full max-w-full items-center justify-between overflow-hidden">
|
||||
{!(firstItem && firstItem.id === workflow.id) && (
|
||||
<ArrowButton onClick={() => moveWorkflow(index, -1)} arrowDirection="up" />
|
||||
)}
|
||||
{!(lastItem && lastItem.id === workflow.id) && (
|
||||
<ArrowButton onClick={() => moveWorkflow(index, 1)} arrowDirection="down" />
|
||||
)}
|
||||
<div className="first-line:group hover:bg-muted flex w-full items-center justify-between p-4 sm:px-6">
|
||||
<Link href={"/workflows/" + workflow.id} className="flex-grow cursor-pointer">
|
||||
<div className="rtl:space-x-reverse">
|
||||
<div className="flex">
|
||||
<div
|
||||
className={classNames(
|
||||
"max-w-56 text-emphasis truncate text-sm font-medium leading-6 md:max-w-max",
|
||||
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>
|
||||
{workflow.readOnly && (
|
||||
<Badge variant="gray" className="ml-2 ">
|
||||
{t("readonly")}
|
||||
|
||||
<ul className="mt-1 flex flex-wrap space-x-2 sm:flex-nowrap ">
|
||||
<li>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="mt-1 flex flex-wrap space-x-2 sm:flex-nowrap ">
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>
|
||||
<Badge variant="gray">
|
||||
{workflow.activeOn && workflow.activeOn.length > 0 ? (
|
||||
<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>
|
||||
))}>
|
||||
</li>
|
||||
<li>
|
||||
<Badge variant="gray">
|
||||
{workflow.activeOn && workflow.activeOn.length > 0 ? (
|
||||
<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>
|
||||
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("active_on_event_types", {
|
||||
count: workflow.activeOn.filter((wf) =>
|
||||
workflow.teamId ? wf.eventType.parentId === null : true
|
||||
).length,
|
||||
})}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div>
|
||||
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("active_on_event_types", {
|
||||
count: workflow.activeOn.filter((wf) =>
|
||||
workflow.teamId ? wf.eventType.parentId === null : true
|
||||
).length,
|
||||
})}
|
||||
{t("no_active_event_types")}
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div>
|
||||
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("no_active_event_types")}
|
||||
</div>
|
||||
)}
|
||||
</Badge>
|
||||
</li>
|
||||
<div className="block md:hidden">
|
||||
{workflow.team?.name && (
|
||||
<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>
|
||||
</li>
|
||||
<div className="block md:hidden">
|
||||
{workflow.team?.name && (
|
||||
<li>
|
||||
<Badge variant="gray">
|
||||
<>{workflow.team.name}</>
|
||||
</Badge>
|
||||
</li>
|
||||
)}
|
||||
</div>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<DeleteDialog
|
||||
isOpenDialog={deleteDialogOpen}
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { ControlProps } from "react-select";
|
|||
import { components } from "react-select";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { AvailableTimes } from "@calcom/features/bookings";
|
||||
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
|
@ -38,7 +39,7 @@ import {
|
|||
TextField,
|
||||
TimezoneSelect,
|
||||
} 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 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>
|
||||
<DatePicker
|
||||
isLoading={schedule.isLoading}
|
||||
onChange={(date) => setSelectedDate(date ? date.format("YYYY-MM-DD") : date)}
|
||||
onMonthChange={(date) => {
|
||||
onChange={(date: Dayjs | null) => {
|
||||
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"));
|
||||
}}
|
||||
onMonthChange={(date: Dayjs) => {
|
||||
setMonth(date.format("YYYY-MM"));
|
||||
setSelectedDate(date.format("YYYY-MM-DD"));
|
||||
}}
|
||||
|
@ -245,36 +248,24 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username:
|
|||
</div>
|
||||
{selectedDate ? (
|
||||
<div className="mt-[9px] font-medium ">
|
||||
<Collapsible open>
|
||||
<CollapsibleContent>
|
||||
<div
|
||||
className="text-default mb-[9px] flex cursor-pointer items-center justify-between text-sm"
|
||||
onClick={() => setSelectTime((prev) => !prev)}>
|
||||
<p>{t("select_time")}</p>{" "}
|
||||
<>
|
||||
{!selectedDate || !selectTime ? <ArrowDown className="w-4" /> : <ArrowUp className="w-4" />}
|
||||
</>
|
||||
</div>
|
||||
{selectTime && selectedDate ? (
|
||||
<div className="flex h-full w-full flex-row gap-4">
|
||||
<AvailableTimes
|
||||
className="w-full"
|
||||
date={dayjs(selectedDate)}
|
||||
selectedSlots={
|
||||
eventType.slug &&
|
||||
selectedDatesAndTimes &&
|
||||
selectedDatesAndTimes[eventType.slug] &&
|
||||
selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
||||
? selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
||||
: undefined
|
||||
}
|
||||
onTimeSelect={onTimeSelect}
|
||||
slots={slots}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{selectTime && selectedDate ? (
|
||||
<div className="flex h-full w-full flex-row gap-4">
|
||||
<AvailableTimes
|
||||
className="w-full"
|
||||
date={dayjs(selectedDate)}
|
||||
selectedSlots={
|
||||
eventType.slug &&
|
||||
selectedDatesAndTimes &&
|
||||
selectedDatesAndTimes[eventType.slug] &&
|
||||
selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
||||
? selectedDatesAndTimes[eventType.slug][selectedDate as string]
|
||||
: undefined
|
||||
}
|
||||
onTimeSelect={onTimeSelect}
|
||||
slots={slots}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mb-[9px] font-medium ">
|
||||
|
|
|
@ -51,11 +51,7 @@ const DateOverrideForm = ({
|
|||
|
||||
const [selectedDates, setSelectedDates] = useState<Dayjs[]>(value ? [dayjs.utc(value[0].start)] : []);
|
||||
|
||||
const onDateChange = (newDate: Dayjs | null) => {
|
||||
// If no date is selected, do nothing
|
||||
if (!newDate) {
|
||||
return;
|
||||
}
|
||||
const onDateChange = (newDate: Dayjs) => {
|
||||
// If clicking on a selected date unselect it
|
||||
if (selectedDates.some((date) => yyyymmdd(date) === yyyymmdd(newDate))) {
|
||||
setSelectedDates(selectedDates.filter((date) => yyyymmdd(date) !== yyyymmdd(newDate)));
|
||||
|
@ -154,7 +150,9 @@ const DateOverrideForm = ({
|
|||
excludedDates={excludedDates}
|
||||
weekStart={0}
|
||||
selected={selectedDates}
|
||||
onChange={(day) => onDateChange(day)}
|
||||
onChange={(day) => {
|
||||
if (day) onDateChange(day);
|
||||
}}
|
||||
onMonthChange={(newMonth) => {
|
||||
setBrowsingDate(newMonth);
|
||||
}}
|
||||
|
|
|
@ -369,7 +369,7 @@ function UserDropdown({ small }: UserDropdownProps) {
|
|||
<span
|
||||
className={classNames(
|
||||
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
|
||||
size={small ? "xs" : "xsm"}
|
||||
|
@ -668,7 +668,7 @@ const NavigationItem: React.FC<{
|
|||
aria-current={current ? "page" : undefined}>
|
||||
{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-current={current ? "page" : undefined}
|
||||
/>
|
||||
|
@ -906,7 +906,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
|||
<Icon
|
||||
className={classNames(
|
||||
"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"
|
||||
/>
|
||||
|
|
|
@ -35,7 +35,7 @@ function MoreInfoFooter() {
|
|||
return (
|
||||
<>
|
||||
<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")}
|
||||
</Button>
|
||||
</SheetClose>
|
||||
|
|
|
@ -60,6 +60,7 @@ export function InviteMemberModal(props: Props) {
|
|||
});
|
||||
}}
|
||||
teamId={orgId}
|
||||
justEmailInvites={!!orgId}
|
||||
isLoading={inviteMemberMutation.isLoading}
|
||||
onSubmit={(values) => {
|
||||
inviteMemberMutation.mutate({
|
||||
|
|
|
@ -153,12 +153,14 @@ export default abstract class BaseCalendarService implements Calendar {
|
|||
if (error || !iCalString)
|
||||
throw new Error(`Error creating iCalString:=> ${error?.message} : ${error?.name} `);
|
||||
|
||||
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
|
||||
|
||||
// We create the event directly on iCal
|
||||
const responses = await Promise.all(
|
||||
calendars
|
||||
.filter((c) =>
|
||||
event.destinationCalendar?.externalId
|
||||
? c.externalId === event.destinationCalendar.externalId
|
||||
mainHostDestinationCalendar?.externalId
|
||||
? c.externalId === mainHostDestinationCalendar.externalId
|
||||
: true
|
||||
)
|
||||
.map((calendar) =>
|
||||
|
@ -504,13 +506,13 @@ export default abstract class BaseCalendarService implements Calendar {
|
|||
|
||||
return calendars.reduce<IntegrationCalendar[]>((newCalendars, calendar) => {
|
||||
if (!calendar.components?.includes("VEVENT")) return newCalendars;
|
||||
|
||||
const [mainHostDestinationCalendar] = event?.destinationCalendar ?? [];
|
||||
newCalendars.push({
|
||||
externalId: calendar.url,
|
||||
/** @url https://github.com/calcom/cal.com/issues/7186 */
|
||||
name: typeof calendar.displayName === "string" ? calendar.displayName : "",
|
||||
primary: event?.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId === calendar.url
|
||||
primary: mainHostDestinationCalendar?.externalId
|
||||
? mainHostDestinationCalendar.externalId === calendar.url
|
||||
: false,
|
||||
integration: this.integrationName,
|
||||
email: this.credentials.username ?? "",
|
||||
|
|
|
@ -59,7 +59,8 @@ const handlePayment = async (
|
|||
},
|
||||
booking.id,
|
||||
bookerEmail,
|
||||
paymentOption
|
||||
paymentOption,
|
||||
evt.title
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { getSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
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 { WEBAPP_URL } from "../../../constants";
|
||||
|
@ -32,7 +31,17 @@ export async function getTeamWithMembers(args: {
|
|||
selectedCalendars: true,
|
||||
credentials: {
|
||||
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;
|
||||
const members = await Promise.all(
|
||||
team.members.map(async (obj) => {
|
||||
const calendarCredentials = getCalendarCredentials(obj.user.credentials);
|
||||
|
||||
const { connectedCalendars } = await getConnectedCalendars(
|
||||
calendarCredentials,
|
||||
obj.user.selectedCalendars,
|
||||
obj.user.destinationCalendar?.externalId
|
||||
);
|
||||
const connectedApps = obj.user.credentials
|
||||
.map(({ app, id }) => {
|
||||
const appMetaData = getAppFromSlug(app?.slug);
|
||||
// This should improve performance saving already app data found.
|
||||
const appDataMap = new Map();
|
||||
|
||||
const members = team.members.map((obj) => {
|
||||
return {
|
||||
...obj.user,
|
||||
role: obj.role,
|
||||
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) => ({
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),
|
||||
|
|
|
@ -19,9 +19,6 @@ export const telemetryEventTypes = {
|
|||
onboardingStarted: "onboarding_started",
|
||||
signup: "signup",
|
||||
team_created: "team_created",
|
||||
website: {
|
||||
pageView: "website_page_view",
|
||||
},
|
||||
slugReplacementAction: "slug_replacement_action",
|
||||
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": "^5.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"zod": "^3.20.2",
|
||||
"zod": "^3.22.2",
|
||||
"zod-prisma": "^0.5.4"
|
||||
},
|
||||
"main": "index.ts",
|
||||
|
|
|
@ -662,6 +662,7 @@ model App {
|
|||
model App_RoutingForms_Form {
|
||||
id String @id @default(cuid())
|
||||
description String?
|
||||
position Int @default(0)
|
||||
routes Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
@ -746,6 +747,7 @@ model WorkflowStep {
|
|||
|
||||
model Workflow {
|
||||
id Int @id @default(autoincrement())
|
||||
position Int @default(0)
|
||||
name String
|
||||
userId Int?
|
||||
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 */
|
||||
import type {
|
||||
AnyZodObject,
|
||||
objectInputType,
|
||||
objectOutputType,
|
||||
ZodNullableDef,
|
||||
|
@ -528,11 +529,13 @@ export const optionToValueSchema = <T extends z.ZodTypeAny>(valueSchema: T) =>
|
|||
* @url https://github.com/colinhacks/zod/discussions/1655#discussioncomment-4367368
|
||||
*/
|
||||
export const getParserWithGeneric =
|
||||
<T extends z.ZodTypeAny>(valueSchema: T) =>
|
||||
<T extends AnyZodObject>(valueSchema: T) =>
|
||||
<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 {
|
||||
[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({
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
"@trpc/react-query": "^10.13.0",
|
||||
"@trpc/server": "^10.13.0",
|
||||
"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 { ZIntegrationsInputSchema } from "./integrations.schema";
|
||||
import { ZLocationOptionsInputSchema } from "./locationOptions.schema";
|
||||
import { ZRoutingFormOrderInputSchema } from "./routingFormOrder.schema";
|
||||
import { ZSetDestinationCalendarInputSchema } from "./setDestinationCalendar.schema";
|
||||
import { ZSubmitFeedbackInputSchema } from "./submitFeedback.schema";
|
||||
import { ZUpdateProfileInputSchema } from "./updateProfile.schema";
|
||||
import { ZUpdateUserDefaultConferencingAppInputSchema } from "./updateUserDefaultConferencingApp.schema";
|
||||
import { ZWorkflowOrderInputSchema } from "./workflowOrder.schema";
|
||||
|
||||
type AppsRouterHandlerCache = {
|
||||
me?: typeof import("./me.handler").meHandler;
|
||||
|
@ -31,6 +33,8 @@ type AppsRouterHandlerCache = {
|
|||
stripeCustomer?: typeof import("./stripeCustomer.handler").stripeCustomerHandler;
|
||||
updateProfile?: typeof import("./updateProfile.handler").updateProfileHandler;
|
||||
eventTypeOrder?: typeof import("./eventTypeOrder.handler").eventTypeOrderHandler;
|
||||
routingFormOrder?: typeof import("./routingFormOrder.handler").routingFormOrderHandler;
|
||||
workflowOrder?: typeof import("./workflowOrder.handler").workflowOrderHandler;
|
||||
submitFeedback?: typeof import("./submitFeedback.handler").submitFeedbackHandler;
|
||||
locationOptions?: typeof import("./locationOptions.handler").locationOptionsHandler;
|
||||
deleteCredential?: typeof import("./deleteCredential.handler").deleteCredentialHandler;
|
||||
|
@ -230,6 +234,34 @@ export const loggedInViewerRouter = router({
|
|||
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
|
||||
submitFeedback: authedProcedure.input(ZSubmitFeedbackInputSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.submitFeedback) {
|
||||
|
|
|
@ -287,7 +287,11 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
|||
uid: booking.uid,
|
||||
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||
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",
|
||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||
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,
|
||||
location: booking.location ?? "",
|
||||
uid: booking.uid,
|
||||
destinationCalendar: booking?.destinationCalendar || user.destinationCalendar,
|
||||
destinationCalendar: booking?.destinationCalendar
|
||||
? [booking.destinationCalendar]
|
||||
: user.destinationCalendar
|
||||
? [user.destinationCalendar]
|
||||
: [],
|
||||
requiresConfirmation: booking?.eventType?.requiresConfirmation ?? false,
|
||||
eventTypeId: booking.eventType?.id,
|
||||
};
|
||||
|
|
|
@ -82,7 +82,11 @@ export const editLocationHandler = async ({ ctx, input }: EditLocationOptions) =
|
|||
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||
location,
|
||||
conferenceCredentialId: details?.credentialId,
|
||||
destinationCalendar: booking?.destinationCalendar || booking?.user?.destinationCalendar,
|
||||
destinationCalendar: booking?.destinationCalendar
|
||||
? [booking?.destinationCalendar]
|
||||
: booking?.user?.destinationCalendar
|
||||
? [booking?.user?.destinationCalendar]
|
||||
: [],
|
||||
seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
|
||||
seatsShowAttendees: booking.eventType?.seatsShowAttendees,
|
||||
};
|
||||
|
|
|
@ -237,7 +237,9 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
|||
),
|
||||
uid: bookingToReschedule?.uid,
|
||||
location: bookingToReschedule?.location,
|
||||
destinationCalendar: bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
||||
destinationCalendar: bookingToReschedule?.destinationCalendar
|
||||
? [bookingToReschedule?.destinationCalendar]
|
||||
: [],
|
||||
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);
|
||||
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({
|
||||
|
|
|
@ -148,9 +148,9 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
|||
organization: {
|
||||
create: {
|
||||
name,
|
||||
...(!IS_TEAM_BILLING_ENABLED && { slug }),
|
||||
...(IS_TEAM_BILLING_ENABLED ? { slug } : {}),
|
||||
metadata: {
|
||||
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
|
||||
...(IS_TEAM_BILLING_ENABLED ? { requestedSlug: slug } : {}),
|
||||
isOrganization: true,
|
||||
isOrganizationVerified: false,
|
||||
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
|
||||
const currentOrgId = ctx.user?.organization?.id || input.orgId;
|
||||
|
||||
if (!currentOrgId || ctx.user.role !== UserPermissionRole.ADMIN)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
const isUserOrganizationAdmin = currentOrgId && (await isOrganisationAdmin(ctx.user?.id, currentOrgId));
|
||||
const isUserRoleAdmin = ctx.user.role === UserPermissionRole.ADMIN;
|
||||
|
||||
if (!(await isOrganisationAdmin(ctx.user?.id, currentOrgId)) || ctx.user.role !== UserPermissionRole.ADMIN)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
const isUserAuthorizedToUpdate = !!(isUserOrganizationAdmin || isUserRoleAdmin);
|
||||
|
||||
if (!currentOrgId || !isUserAuthorizedToUpdate) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
if (input.slug) {
|
||||
const userConflict = await prisma.team.findMany({
|
||||
|
|
|
@ -56,7 +56,7 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
|
|||
});
|
||||
const invitee = await getUserToInviteOrThrowIfExists({
|
||||
usernameOrEmail,
|
||||
orgId: input.teamId,
|
||||
teamId: input.teamId,
|
||||
isOrg: input.isOrg,
|
||||
});
|
||||
|
||||
|
|
|
@ -66,17 +66,21 @@ export async function getEmailsToInvite(usernameOrEmail: string | string[]) {
|
|||
|
||||
export async function getUserToInviteOrThrowIfExists({
|
||||
usernameOrEmail,
|
||||
orgId,
|
||||
teamId,
|
||||
isOrg,
|
||||
}: {
|
||||
usernameOrEmail: string;
|
||||
orgId: number;
|
||||
teamId: number;
|
||||
isOrg?: boolean;
|
||||
}) {
|
||||
// Check if user exists in ORG or exists all together
|
||||
|
||||
const orgWhere = isOrg && {
|
||||
organizationId: teamId,
|
||||
};
|
||||
const invitee = await prisma.user.findFirst({
|
||||
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,
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
position: "desc",
|
||||
},
|
||||
{
|
||||
id: "asc",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!filtered) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import type z from "zod";
|
|||
import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import type { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { Frequency } from "@calcom/prisma/zod-utils";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
|
@ -167,7 +168,7 @@ export interface CalendarEvent {
|
|||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: PaymentInfo | null;
|
||||
requiresConfirmation?: boolean | null;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
destinationCalendar?: DestinationCalendar[] | null;
|
||||
cancellationReason?: string | null;
|
||||
rejectionReason?: string | null;
|
||||
hideCalendarNotes?: boolean;
|
||||
|
@ -178,6 +179,7 @@ export interface CalendarEvent {
|
|||
seatsShowAttendees?: boolean | null;
|
||||
attendeeSeatId?: string;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
schedulingType?: SchedulingType | null;
|
||||
iCalUID?: string | null;
|
||||
|
||||
// It has responses to all the fields(system + user)
|
||||
|
@ -216,7 +218,7 @@ export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "
|
|||
}
|
||||
|
||||
export interface Calendar {
|
||||
createEvent(event: CalendarEvent): Promise<NewCalendarEventType>;
|
||||
createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType>;
|
||||
|
||||
updateEvent(
|
||||
uid: string,
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface EventResult<T> {
|
|||
calError?: string;
|
||||
calWarnings?: string[];
|
||||
credentialId?: number;
|
||||
externalId?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateUpdateResult {
|
||||
|
|
|
@ -14,7 +14,8 @@ export interface IAbstractPaymentService {
|
|||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||
bookingId: Booking["id"],
|
||||
bookerEmail: string,
|
||||
paymentOption: PaymentOption
|
||||
paymentOption: PaymentOption,
|
||||
eventTitle?: string
|
||||
): Promise<Payment>;
|
||||
/* This method is to collect card details to charge at a later date ex. no-show fees */
|
||||
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 = (
|
||||
<AvatarPrimitive.Root
|
||||
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,
|
||||
sizesPropsBySize[size]
|
||||
)}>
|
||||
|
|
|
@ -52,7 +52,7 @@ export const buttonClasses = cva(
|
|||
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",
|
||||
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: {
|
||||
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 {
|
||||
|
@ -17,7 +18,7 @@ import { Button } from "./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
|
||||
|
||||
|
@ -170,6 +171,15 @@ Button are clickable elements that initiates user actions. Labels in the button
|
|||
</Story>
|
||||
<Story
|
||||
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={{
|
||||
color: "primary",
|
||||
size: "base",
|
||||
|
@ -177,6 +187,7 @@ Button are clickable elements that initiates user actions. Labels in the button
|
|||
disabled: false,
|
||||
children: "Button text",
|
||||
className: "",
|
||||
tooltip: "tooltip",
|
||||
}}
|
||||
argTypes={{
|
||||
color: {
|
||||
|
@ -212,19 +223,20 @@ Button are clickable elements that initiates user actions. Labels in the button
|
|||
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}>
|
||||
<VariantRow variant="Button">
|
||||
<Button
|
||||
color={color}
|
||||
size={size}
|
||||
variant="default"
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
className={className}>
|
||||
{children}
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Button variant="default" {...args}>
|
||||
{children}
|
||||
</Button>
|
||||
</TooltipProvider>
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
|
|
|
@ -41,7 +41,7 @@ export function ConfirmationDialogContent(props: PropsWithChildren<ConfirmationD
|
|||
<div className="mt-0.5 ltr:mr-3">
|
||||
{variety === "danger" && (
|
||||
<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>
|
||||
)}
|
||||
{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}
|
||||
className={classNames(
|
||||
"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
|
||||
)}>
|
||||
<>
|
||||
|
|
|
@ -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 {
|
||||
Examples,
|
||||
Example,
|
||||
Note,
|
||||
Title,
|
||||
CustomArgsTable,
|
||||
VariantRow,
|
||||
|
@ -15,7 +14,7 @@ import { SelectField } from "./Select";
|
|||
|
||||
<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
|
||||
|
||||
|
@ -101,11 +100,31 @@ const GoodSelect = (props) => <Select {...props} components={{ Control }} />;
|
|||
## Select Story
|
||||
|
||||
<Canvas>
|
||||
<Story name="Default">
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={450}>
|
||||
<VariantRow>
|
||||
<SelectField options={options} label={"Default Select"} />
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
<Story
|
||||
name="Default"
|
||||
args={{
|
||||
required: false,
|
||||
name: "select-field",
|
||||
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>
|
||||
</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
|
||||
disabled={option.disabled}
|
||||
value={option.value}
|
||||
data-testid={`toggle-group-item-${option.value}`}
|
||||
className={classNames(
|
||||
"aria-checked:bg-emphasis relative rounded-[4px] px-3 py-1 text-sm leading-tight transition-colors",
|
||||
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,
|
||||
} 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} />
|
||||
|
||||
<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
|
||||
|
||||
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>
|
||||
<Story name="List">
|
||||
<VariantsTable titles={[]} columnMinWidth={150}>
|
||||
<VariantRow variant="Default">TODO!</VariantRow>
|
||||
</VariantsTable>
|
||||
<Story
|
||||
name="List"
|
||||
args={{
|
||||
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>
|
||||
</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