Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4b38b9bc3 | ||
|
|
d32719ad8b | ||
|
|
99d0816d9f | ||
|
|
95f4b89fe8 | ||
|
|
3e63c2b717 | ||
|
|
42cd56578d | ||
|
|
31d4cb4eb7 | ||
|
|
fde3e8bea9 | ||
|
|
874ad29676 | ||
|
|
5503d9d432 | ||
|
|
43b3d68447 | ||
|
|
e3c747a867 | ||
|
|
e020a2c9a4 | ||
|
|
e6e6f09547 | ||
|
|
68ec946c59 |
@@ -35,6 +35,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import { Copy, Edit } from "@calcom/ui/components/icon";
|
||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
|
||||
@@ -286,36 +287,38 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}}>
|
||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (eventType.hashedLink) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
{!IS_VISUAL_REGRESSION_TESTING && (
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (eventType.hashedLink) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
<hr className="border-subtle" />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -51,7 +51,7 @@ class MyDocument extends Document<Props> {
|
||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
|
||||
{(!IS_PRODUCTION || process.env.VERCEL_ENV === "preview") && (
|
||||
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
|
||||
// eslint-disable-next-line @next/next/no-sync-scripts
|
||||
<script
|
||||
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
|
||||
import { getLayout } from "@calcom/features/MainLayout";
|
||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
@@ -135,7 +134,6 @@ export default function AvailabilityPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
// Get a new searchParams string by merging the current
|
||||
// searchParams with a provided key/value pair
|
||||
@@ -156,23 +154,22 @@ export default function AvailabilityPage() {
|
||||
subtitle={t("configure_availability")}
|
||||
CTA={
|
||||
<div className="flex gap-2">
|
||||
{orgBranding && (
|
||||
<ToggleGroup
|
||||
className="hidden md:block"
|
||||
defaultValue={searchParams?.get("type") ?? "mine"}
|
||||
onValueChange={(value) => {
|
||||
router.push(`${pathname}?${createQueryString("type", value)}`);
|
||||
}}
|
||||
options={[
|
||||
{ value: "mine", label: t("my_availability") },
|
||||
{ value: "team", label: t("team_availability") },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<ToggleGroup
|
||||
className="hidden md:block"
|
||||
defaultValue={searchParams?.get("type") ?? "mine"}
|
||||
onValueChange={(value) => {
|
||||
if (!value) return;
|
||||
router.push(`${pathname}?${createQueryString("type", value)}`);
|
||||
}}
|
||||
options={[
|
||||
{ value: "mine", label: t("my_availability") },
|
||||
{ value: "team", label: t("team_availability") },
|
||||
]}
|
||||
/>
|
||||
<NewScheduleButton />
|
||||
</div>
|
||||
}>
|
||||
{searchParams?.get("type") === "team" && orgBranding ? (
|
||||
{searchParams?.get("type") === "team" ? (
|
||||
<AvailabilitySliderTable />
|
||||
) : (
|
||||
<WithQuery
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import UnverifiedOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/unverifiedOrgPage";
|
||||
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const Page = UnverifiedOrgsPage as CalPageWrapper;
|
||||
const Page = AdminOrgsPage as CalPageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -2010,6 +2010,21 @@
|
||||
"member_removed": "Member removed",
|
||||
"my_availability": "My Availability",
|
||||
"team_availability": "Team Availability",
|
||||
"admin_org_notification_email_subject": "New organization created: pending action",
|
||||
"hi_admin": "Hi Administrator",
|
||||
"admin_org_notification_email_title": "An organization requires DNS setup",
|
||||
"admin_org_notification_email_body_part1": "An organization with slug \"{{orgSlug}}\" was created.<br /><br />Please be sure to configure your DNS registry to point the subdomain corresponding to the new organization to where the main app is running. Otherwise the organization will not work.<br /><br />Here are just the very basic options to configure a subdomain to point to their app so it loads the organization profile page.<br /><br />You can do it either with the A Record:",
|
||||
"admin_org_notification_email_body_part2": "Or the CNAME record:",
|
||||
"admin_org_notification_email_body_part3": "Once you configure the subdomain, please mark the DNS configuration as done in Organizations Admin Settings.",
|
||||
"admin_org_notification_email_cta": "Go to Organizations Admin Settings",
|
||||
"org_has_been_processed": "Org has been processed",
|
||||
"org_error_processing": "There has been an error processing this organization",
|
||||
"orgs_page_description": "A list of all organizations. Accepting an organization will allow all users with that email domain to sign up WITHOUT email verifciation.",
|
||||
"unverified": "Unverified",
|
||||
"dns_missing": "DNS Missing",
|
||||
"mark_dns_configured": "Mark as DNS configured",
|
||||
"value": "Value",
|
||||
"your_organization_updated_sucessfully": "Your organization updated successfully",
|
||||
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -2006,5 +2006,17 @@
|
||||
"member_removed": "Membre supprimé",
|
||||
"my_availability": "Mes disponibilités",
|
||||
"team_availability": "Disponibilités de l'équipe",
|
||||
"admin_org_notification_email_subject": "Nouvelle organisation créée : action en attente",
|
||||
"admin_org_notification_email_title": "Une organisation nécessite une configuration DNS",
|
||||
"admin_org_notification_email_body_part2": "Ou l'enregistrement CNAME :",
|
||||
"admin_org_notification_email_body_part3": "Une fois le sous-domaine configuré, veuillez marquer la configuration DNS comme terminée dans les paramètres d'administration des organisations.",
|
||||
"admin_org_notification_email_cta": "Accéder aux paramètres d'administration des organisations",
|
||||
"org_error_processing": "Une erreur s'est produite lors du traitement de cette organisation",
|
||||
"orgs_page_description": "Une liste de toutes les organisations. Accepter une organisation permettra à tous les utilisateurs avec ce domaine de messagerie de s'inscrire SANS vérification d'e-mail.",
|
||||
"unverified": "Non vérifié",
|
||||
"dns_missing": "DNS manquant",
|
||||
"mark_dns_configured": "Marquer comme DNS configuré",
|
||||
"value": "Valeur",
|
||||
"your_organization_updated_sucessfully": "Votre organisation a été mise à jour avec succès",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { App_RoutingForms_Form } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { getSerializableForm } from "../lib/getSerializableForm";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
|
||||
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { PrismaClient, Prisma } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } 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";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
|
||||
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
|
||||
import { getSerializableForm } from "../lib/getSerializableForm";
|
||||
|
||||
@@ -9,6 +9,8 @@ import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import type { EmailVerifyLink } from "./templates/account-verify-email";
|
||||
import AccountVerifyEmail from "./templates/account-verify-email";
|
||||
import type { OrganizationNotification } from "./templates/admin-organization-notification";
|
||||
import AdminOrganizationNotification from "./templates/admin-organization-notification";
|
||||
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
|
||||
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
|
||||
import AttendeeCancelledSeatEmail from "./templates/attendee-cancelled-seat-email";
|
||||
@@ -376,3 +378,7 @@ export const sendDailyVideoRecordingEmails = async (calEvent: CalendarEvent, dow
|
||||
export const sendOrganizationEmailVerification = async (sendOrgInput: OrganizationEmailVerify) => {
|
||||
await sendEmail(() => new OrganizationEmailVerification(sendOrgInput));
|
||||
};
|
||||
|
||||
export const sendAdminOrganizationNotification = async (input: OrganizationNotification) => {
|
||||
await sendEmail(() => new AdminOrganizationNotification(input));
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { CallToActionIcon } from "./CallToActionIcon";
|
||||
import { Info } from "./Info";
|
||||
|
||||
export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Trans, type TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { BaseEmailHtml, CallToAction } from "../components";
|
||||
|
||||
type AdminOrganizationNotification = {
|
||||
language: TFunction;
|
||||
orgSlug: string;
|
||||
webappIPAddress: string;
|
||||
};
|
||||
|
||||
const dnsTable = (type: string, name: string, value: string, t: TFunction) => (
|
||||
<table
|
||||
role="presentation"
|
||||
border={0}
|
||||
cellSpacing="0"
|
||||
cellPadding="0"
|
||||
style={{
|
||||
verticalAlign: "top",
|
||||
marginTop: "10px",
|
||||
borderRadius: "6px",
|
||||
borderCollapse: "separate",
|
||||
border: "solid black 1px",
|
||||
}}
|
||||
width="100%">
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
backgroundColor: "black",
|
||||
color: "white",
|
||||
fontSize: "14px",
|
||||
lineHeight: "24px",
|
||||
}}>
|
||||
<td
|
||||
align="center"
|
||||
width="33%"
|
||||
style={{ borderTopLeftRadius: "5px", borderRight: "1px solid white" }}>
|
||||
{t("type")}
|
||||
</td>
|
||||
<td align="center" width="33%" style={{ borderRight: "1px solid white" }}>
|
||||
{t("name")}
|
||||
</td>
|
||||
<td align="center" style={{ borderTopRightRadius: "5px" }}>
|
||||
{t("value")}
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr style={{ lineHeight: "24px" }}>
|
||||
<td align="center" style={{ borderBottomLeftRadius: "5px", borderRight: "1px solid black" }}>
|
||||
{type}
|
||||
</td>
|
||||
<td align="center" style={{ borderRight: "1px solid black" }}>
|
||||
{name}
|
||||
</td>
|
||||
<td align="center" style={{ borderBottomRightRadius: "5px" }}>
|
||||
{value}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
export const AdminOrganizationNotificationEmail = ({
|
||||
orgSlug,
|
||||
webappIPAddress,
|
||||
language,
|
||||
}: AdminOrganizationNotification) => {
|
||||
const webAppUrl = WEBAPP_URL.replace("https://", "")?.replace("http://", "").replace(/(:.*)/, "");
|
||||
return (
|
||||
<BaseEmailHtml
|
||||
subject={language("admin_org_notification_email_subject", { appName: APP_NAME })}
|
||||
callToAction={
|
||||
<CallToAction
|
||||
label={language("admin_org_notification_email_cta")}
|
||||
href={`${WEBAPP_URL}/settings/admin/organizations`}
|
||||
endIconName="white-arrow-right"
|
||||
/>
|
||||
}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "24px",
|
||||
lineHeight: "38px",
|
||||
}}>
|
||||
<>{language("admin_org_notification_email_title")}</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400 }}>
|
||||
<>{language("hi_admin")}!</>
|
||||
</p>
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
<Trans i18nKey="admin_org_notification_email_body_part1" t={language} values={{ orgSlug }}>
|
||||
An organization with slug {`"${orgSlug}"`} was created.
|
||||
<br />
|
||||
<br />
|
||||
Please be sure to configure your DNS registry to point the subdomain corresponding to the new
|
||||
organization to where the main app is running. Otherwise the organization will not work.
|
||||
<br />
|
||||
<br />
|
||||
Here are just the very basic options to configure a subdomain to point to their app so it loads the
|
||||
organization profile page.
|
||||
<br />
|
||||
<br />
|
||||
You can do it either with the A Record:
|
||||
</Trans>
|
||||
</p>
|
||||
{dnsTable("A", orgSlug, webappIPAddress, language)}
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
{language("admin_org_notification_email_body_part2")}
|
||||
</p>
|
||||
{dnsTable("CNAME", orgSlug, webAppUrl, language)}
|
||||
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||
{language("admin_org_notification_email_body_part3")}
|
||||
</p>
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
@@ -29,3 +29,4 @@ export * from "@calcom/app-store/routing-forms/emails/components";
|
||||
export { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
|
||||
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
|
||||
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
|
||||
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import BaseEmail from "./_base-email";
|
||||
|
||||
export type OrganizationNotification = {
|
||||
t: TFunction;
|
||||
instanceAdmins: { email: string }[];
|
||||
ownerEmail: string;
|
||||
orgSlug: string;
|
||||
webappIPAddress: string;
|
||||
};
|
||||
|
||||
export default class AdminOrganizationNotification extends BaseEmail {
|
||||
input: OrganizationNotification;
|
||||
|
||||
constructor(input: OrganizationNotification) {
|
||||
super();
|
||||
this.name = "SEND_ADMIN_ORG_NOTIFICATION";
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {
|
||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||
to: this.input.instanceAdmins.map((admin) => admin.email).join(","),
|
||||
subject: `${this.input.t("admin_org_notification_email_subject")}`,
|
||||
html: renderEmail("AdminOrganizationNotificationEmail", {
|
||||
orgSlug: this.input.orgSlug,
|
||||
webappIPAddress: this.input.webappIPAddress,
|
||||
language: this.input.t,
|
||||
}),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getTextBody(): string {
|
||||
return `${this.input.t("hi_admin")}, ${this.input.t("admin_org_notification_email_title").toLowerCase()}
|
||||
${this.input.t("admin_org_notification_email_body")}`.trim();
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,12 @@ export const EventMeta = () => {
|
||||
)}
|
||||
{!isLoading && !!event && (
|
||||
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}>
|
||||
<EventMembers schedulingType={event.schedulingType} users={event.users} profile={event.profile} />
|
||||
<EventMembers
|
||||
schedulingType={event.schedulingType}
|
||||
users={event.users}
|
||||
profile={event.profile}
|
||||
entity={event.entity}
|
||||
/>
|
||||
<EventTitle className="my-2">{event?.title}</EventTitle>
|
||||
{event.description && (
|
||||
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { AvatarGroup } from "@calcom/ui";
|
||||
|
||||
@@ -12,6 +13,7 @@ export interface EventMembersProps {
|
||||
schedulingType: PublicEvent["schedulingType"];
|
||||
users: PublicEvent["users"];
|
||||
profile: PublicEvent["profile"];
|
||||
entity: PublicEvent["entity"];
|
||||
}
|
||||
|
||||
type Avatar = {
|
||||
@@ -23,7 +25,7 @@ type Avatar = {
|
||||
|
||||
type AvatarWithRequiredImage = Avatar & { image: string };
|
||||
|
||||
export const EventMembers = ({ schedulingType, users, profile }: EventMembersProps) => {
|
||||
export const EventMembers = ({ schedulingType, users, profile, entity }: EventMembersProps) => {
|
||||
const showMembers = schedulingType !== SchedulingType.ROUND_ROBIN;
|
||||
const shownUsers = showMembers ? users : [];
|
||||
|
||||
@@ -37,9 +39,19 @@ export const EventMembers = ({ schedulingType, users, profile }: EventMembersPro
|
||||
title: `${user.name}`,
|
||||
image: "image" in user ? `${user.image}` : `/${user.username}/avatar.png`,
|
||||
alt: user.name || undefined,
|
||||
href: user.username ? `${CAL_URL}/${user.username}` : undefined,
|
||||
href: `/${user.username}`,
|
||||
}));
|
||||
|
||||
// Add organization avatar
|
||||
if (entity.orgSlug) {
|
||||
avatars.unshift({
|
||||
title: `${entity.name}`,
|
||||
image: `${WEBAPP_URL}/team/${entity.orgSlug}/avatar.png`,
|
||||
alt: entity.name || undefined,
|
||||
href: getOrgFullDomain(entity.orgSlug),
|
||||
});
|
||||
}
|
||||
|
||||
// Add profile later since we don't want to force creating an avatar for this if it doesn't exist.
|
||||
avatars.unshift({
|
||||
title: `${profile.name}`,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
type BookingSelect = {
|
||||
|
||||
@@ -223,7 +223,7 @@ function preprocess<T extends z.ZodType>({
|
||||
if (optionValue) {
|
||||
// `typeOfOptionInput` can be any of the main types. So, we the same validations should run for `optionValue`
|
||||
if (typeOfOptionInput === "phone") {
|
||||
if (!phoneSchema.safeParse(optionValue).success) {
|
||||
if (!(await phoneSchema.safeParseAsync(optionValue)).success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Prisma, PrismaClient, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
|
||||
import type { Prisma, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
|
||||
|
||||
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||
import type { EventManagerUser } from "@calcom/core/EventManager";
|
||||
@@ -11,6 +11,7 @@ import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function ApiKeyDialogForm({
|
||||
: `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`}
|
||||
</span>
|
||||
</div>
|
||||
<DialogFooter showDivider className="!mt-12">
|
||||
<DialogFooter showDivider className="relative">
|
||||
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
|
||||
{t("done")}
|
||||
</Button>
|
||||
@@ -209,7 +209,7 @@ export default function ApiKeyDialogForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter showDivider className="!mt-12">
|
||||
<DialogFooter showDivider className="relative">
|
||||
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import cache from "memory-cache";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
const CACHING_TIME = 86400000; // 24 hours in milliseconds
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
export async function getDeploymentKey(prisma: PrismaClient) {
|
||||
const deployment = await prisma.deployment.findUnique({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { PrismaClient, Prisma } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { sendSlugReplacementEmail } from "@calcom/emails/email-manager";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { _EventTypeModel } from "@calcom/prisma/zod";
|
||||
import { allManagedEventTypeProps, unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -48,7 +48,7 @@ export function subdomainSuffix() {
|
||||
}
|
||||
|
||||
export function getOrgFullDomain(slug: string, options: { protocol: boolean } = { protocol: true }) {
|
||||
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}/`;
|
||||
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}`;
|
||||
}
|
||||
|
||||
export function getSlugOrRequestedSlug(slug: string) {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import NoSSR from "@calcom/core/components/NoSSR";
|
||||
import LicenseRequired from "@calcom/ee/common/components/LicenseRequired";
|
||||
import { extractDomainFromWebsiteUrl } from "@calcom/ee/organizations/lib/utils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Meta, DropdownActions, showToast, Table, Badge } from "@calcom/ui";
|
||||
import { X, Check, CheckCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
import { getLayout } from "../../../../../settings/layouts/SettingsLayout";
|
||||
|
||||
const { Body, Cell, ColumnTitle, Header, Row } = Table;
|
||||
|
||||
function AdminOrgTable() {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const [data] = trpc.viewer.organizations.adminGetAll.useSuspenseQuery();
|
||||
const verifyMutation = trpc.viewer.organizations.adminVerify.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(t("org_has_been_processed"), "success");
|
||||
await utils.viewer.organizations.adminGetAll.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err.message);
|
||||
showToast(t("org_error_processing"), "error");
|
||||
},
|
||||
});
|
||||
const updateMutation = trpc.viewer.organizations.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(t("org_has_been_processed"), "success");
|
||||
await utils.viewer.organizations.adminGetAll.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err.message);
|
||||
showToast(t("org_error_processing"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<Header>
|
||||
<ColumnTitle widthClassNames="w-auto">{t("organization")}</ColumnTitle>
|
||||
<ColumnTitle widthClassNames="w-auto">{t("owner")}</ColumnTitle>
|
||||
<ColumnTitle widthClassNames="w-auto">
|
||||
<span className="sr-only">{t("edit")}</span>
|
||||
</ColumnTitle>
|
||||
</Header>
|
||||
|
||||
<Body>
|
||||
{data.map((org) => (
|
||||
<Row key={org.id}>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<div className="text-subtle font-medium">
|
||||
<span className="text-default">{org.name}</span>
|
||||
<br />
|
||||
<span className="text-muted">
|
||||
{org.slug}.{extractDomainFromWebsiteUrl}
|
||||
</span>
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<span className="break-all">{org.members[0].user.email}</span>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<div className="space-x-2">
|
||||
{!org.metadata?.isOrganizationVerified && <Badge variant="blue">{t("unverified")}</Badge>}
|
||||
{!org.metadata?.isOrganizationConfigured && <Badge variant="red">{t("dns_missing")}</Badge>}
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<div className="flex w-full justify-end">
|
||||
{(!org.metadata?.isOrganizationVerified || !org.metadata?.isOrganizationConfigured) && (
|
||||
<DropdownActions
|
||||
actions={[
|
||||
...(!org.metadata?.isOrganizationVerified
|
||||
? [
|
||||
{
|
||||
id: "accept",
|
||||
label: t("accept"),
|
||||
onClick: () => {
|
||||
verifyMutation.mutate({
|
||||
orgId: org.id,
|
||||
status: "ACCEPT",
|
||||
});
|
||||
},
|
||||
icon: Check,
|
||||
},
|
||||
{
|
||||
id: "reject",
|
||||
label: t("reject"),
|
||||
onClick: () => {
|
||||
verifyMutation.mutate({
|
||||
orgId: org.id,
|
||||
status: "DENY",
|
||||
});
|
||||
},
|
||||
icon: X,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!org.metadata?.isOrganizationConfigured
|
||||
? [
|
||||
{
|
||||
id: "dns",
|
||||
label: t("mark_dns_configured"),
|
||||
onClick: () => {
|
||||
updateMutation.mutate({
|
||||
orgId: org.id,
|
||||
metadata: {
|
||||
isOrganizationConfigured: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
icon: CheckCheck,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AdminOrgList = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta title={t("organizations")} description={t("orgs_page_description")} />
|
||||
<NoSSR>
|
||||
<AdminOrgTable />
|
||||
</NoSSR>
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
AdminOrgList.getLayout = getLayout;
|
||||
|
||||
export default AdminOrgList;
|
||||
@@ -1,108 +0,0 @@
|
||||
import NoSSR from "@calcom/core/components/NoSSR";
|
||||
import LicenseRequired from "@calcom/ee/common/components/LicenseRequired";
|
||||
import { extractDomainFromWebsiteUrl } from "@calcom/ee/organizations/lib/utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Meta } from "@calcom/ui";
|
||||
import { DropdownActions, showToast, Table } from "@calcom/ui";
|
||||
import { Check, X } from "@calcom/ui/components/icon";
|
||||
|
||||
import { getLayout } from "../../../../../settings/layouts/SettingsLayout";
|
||||
|
||||
const { Body, Cell, ColumnTitle, Header, Row } = Table;
|
||||
|
||||
function UnverifiedOrgTable() {
|
||||
const utils = trpc.useContext();
|
||||
const [data] = trpc.viewer.organizations.adminGetUnverified.useSuspenseQuery();
|
||||
const mutation = trpc.viewer.organizations.adminVerify.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast("Org has been processed", "success");
|
||||
await utils.viewer.organizations.adminGetUnverified.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err.message);
|
||||
showToast("There has been an error processing this org.", "error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<Header>
|
||||
<ColumnTitle widthClassNames="w-auto">Organization</ColumnTitle>
|
||||
<ColumnTitle widthClassNames="w-auto">Owner</ColumnTitle>
|
||||
<ColumnTitle widthClassNames="w-auto">
|
||||
<span className="sr-only">Edit</span>
|
||||
</ColumnTitle>
|
||||
</Header>
|
||||
|
||||
<Body>
|
||||
{data.map((org) => (
|
||||
<Row key={org.id}>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<div className="text-subtle font-medium">
|
||||
<span className="text-default">{org.name}</span>
|
||||
<br />
|
||||
<span className="text-muted">
|
||||
{org.slug}.{extractDomainFromWebsiteUrl}
|
||||
</span>
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell widthClassNames="w-auto">
|
||||
<span className="break-all">{org.members[0].user.email}</span>
|
||||
</Cell>
|
||||
|
||||
<Cell widthClassNames="w-auto">
|
||||
<div className="flex w-full justify-end">
|
||||
<DropdownActions
|
||||
actions={[
|
||||
{
|
||||
id: "accept",
|
||||
label: "Accept",
|
||||
onClick: () => {
|
||||
mutation.mutate({
|
||||
orgId: org.id,
|
||||
status: "ACCEPT",
|
||||
});
|
||||
},
|
||||
icon: Check,
|
||||
},
|
||||
{
|
||||
id: "reject",
|
||||
label: "Reject",
|
||||
onClick: () => {
|
||||
mutation.mutate({
|
||||
orgId: org.id,
|
||||
status: "DENY",
|
||||
});
|
||||
},
|
||||
icon: X,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const UnverifiedOrgList = () => {
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta
|
||||
title="Organizations"
|
||||
description="A list of all organizations that need verification based on their email domain. Accepting an organization will allow all users with that email domain to sign up WITHOUT email verifciation."
|
||||
/>
|
||||
<NoSSR>
|
||||
<UnverifiedOrgTable />
|
||||
</NoSSR>
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
UnverifiedOrgList.getLayout = getLayout;
|
||||
|
||||
export default UnverifiedOrgList;
|
||||
@@ -57,7 +57,7 @@ const OrgProfileView = () => {
|
||||
},
|
||||
async onSuccess() {
|
||||
await utils.viewer.teams.get.invalidate();
|
||||
showToast(t("your_organisation_updated_sucessfully"), "success");
|
||||
showToast(t("your_organization_updated_sucessfully"), "success");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson";
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
|
||||
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || "";
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function TeamInviteList(props: Props) {
|
||||
const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
|
||||
async onSuccess() {
|
||||
await utils.viewer.teams.list.invalidate();
|
||||
await utils.viewer.teams.get.invalidate();
|
||||
await utils.viewer.organizations.listMembers.invalidate();
|
||||
},
|
||||
async onError(err) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
DropdownMenuTrigger,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { Ban, Check, MoreHorizontal, X } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -39,7 +40,9 @@ export default function TeamInviteListItem(props: Props) {
|
||||
|
||||
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(t("success"), "success");
|
||||
await utils.viewer.teams.get.invalidate();
|
||||
await utils.viewer.teams.hasTeamPlan.invalidate();
|
||||
await utils.viewer.teams.list.invalidate();
|
||||
await utils.viewer.organizations.listMembers.invalidate();
|
||||
},
|
||||
|
||||
@@ -67,7 +67,10 @@ export default function TeamListItem(props: Props) {
|
||||
|
||||
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("success"), "success");
|
||||
utils.viewer.teams.get.invalidate();
|
||||
utils.viewer.teams.list.invalidate();
|
||||
utils.viewer.teams.hasTeamPlan.invalidate();
|
||||
utils.viewer.teams.listInvites.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/or
|
||||
import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||
import {
|
||||
bookerLayoutOptions,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { AppFlags } from "../config";
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
|
||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
||||
|
||||
export default function TimezoneChangeDialog() {
|
||||
const { t } = useLocale();
|
||||
@@ -43,7 +44,7 @@ export default function TimezoneChangeDialog() {
|
||||
const tzDifferent =
|
||||
!isLoading && dayjs.tz(undefined, currentTz).utcOffset() !== dayjs.tz(undefined, userTz).utcOffset();
|
||||
const showDialog = tzDifferent && !document.cookie.includes("calcom-timezone-dialog=1");
|
||||
setOpen(showDialog);
|
||||
setOpen(!IS_VISUAL_REGRESSION_TESTING && showDialog);
|
||||
}, [currentTz, isLoading, userTz]);
|
||||
|
||||
// save cookie to not show again
|
||||
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
Zap,
|
||||
} from "@calcom/ui/components/icon";
|
||||
import { Discord } from "@calcom/ui/components/icon/Discord";
|
||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
||||
|
||||
import { useOrgBranding } from "../ee/organizations/context/provider";
|
||||
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
|
||||
@@ -920,7 +921,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
||||
</ButtonOrLink>
|
||||
</Tooltip>
|
||||
))}
|
||||
<Credits />
|
||||
{!IS_VISUAL_REGRESSION_TESTING && <Credits />}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,14 @@ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useMemo, useRef, useCallback, useEffect, useState } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui";
|
||||
|
||||
import { UpgradeTip } from "../../tips/UpgradeTip";
|
||||
import { TBContext, createTimezoneBuddyStore } from "../store";
|
||||
import { TimeDial } from "./TimeDial";
|
||||
|
||||
@@ -20,6 +23,32 @@ export interface SliderUser {
|
||||
dateRanges: DateRange[];
|
||||
}
|
||||
|
||||
function UpgradeTeamTip() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<UpgradeTip
|
||||
title={t("calcom_is_better_with_team", { appName: APP_NAME }) as string}
|
||||
description="add_your_team_members"
|
||||
background="/tips/teams"
|
||||
features={[]}
|
||||
buttons={
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("create_team")}
|
||||
</Button>
|
||||
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
}>
|
||||
<></>
|
||||
</UpgradeTip>
|
||||
);
|
||||
}
|
||||
|
||||
export function AvailabilitySliderTable() {
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [browsingDate, setBrowsingDate] = useState(dayjs());
|
||||
@@ -143,6 +172,9 @@ export function AvailabilitySliderTable() {
|
||||
fetchMoreOnBottomReached(tableContainerRef.current);
|
||||
}, [fetchMoreOnBottomReached]);
|
||||
|
||||
// This means they are not apart of any teams so we show the upgrade tip
|
||||
if (!flatData.length) return <UpgradeTeamTip />;
|
||||
|
||||
return (
|
||||
<TBContext.Provider
|
||||
value={createTimezoneBuddyStore({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import defaultPrisma from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
|
||||
export type GetSubscriberOptions = {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
type BookingSelect = {
|
||||
description: true;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { getLocationGroupedOptions } from "@calcom/app-store/server";
|
||||
@@ -10,6 +9,7 @@ import { parseBookingLimit, parseDurationLimit, parseRecurringEvent } from "@cal
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { SchedulingType, MembershipRole, AppCategories } from "@calcom/prisma/enums";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { SomeZodObject, z } from "zod";
|
||||
|
||||
import objectKeys from "./objectKeys";
|
||||
|
||||
/**
|
||||
* The intention behind these helpers is to make it easier to work with metadata.
|
||||
* @param schema This is the zod schema that you want to use to parse the metadata
|
||||
* @param rawMetadata This is the metadata that you want to parse
|
||||
* @returns An object with the parsed metadata, a get function to get a specific key of the metadata, and a mergeMetadata function to merge new metadata with the old one
|
||||
* @example
|
||||
* const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema, team.metadata);
|
||||
* const newMetadata = mergeMetadata({ someKey: "someValue" });
|
||||
* prisma.team.update({ ..., data: { metadata: newMetadata } });
|
||||
*/
|
||||
export function getMetadataHelpers<T extends SomeZodObject>(schema: T, rawMetadata: unknown) {
|
||||
const metadata = schema.parse(rawMetadata) as z.infer<T>;
|
||||
return {
|
||||
metadata,
|
||||
get: (key: keyof z.infer<T>) => metadata[key],
|
||||
/** This method prevents overwriting the metadata fields that you don't want to change. */
|
||||
mergeMetadata: (newMetadata: z.infer<T>) => {
|
||||
const newMetadataToReturn = { ...metadata, ...newMetadata };
|
||||
// We check for each key of newMetadata and if it's explicitly undefined, we delete it from newMetadataToReturn
|
||||
objectKeys(newMetadata).forEach((key) => {
|
||||
if (newMetadata[key] === undefined) {
|
||||
delete newMetadataToReturn[key];
|
||||
}
|
||||
});
|
||||
return newMetadataToReturn as z.infer<T>;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
export async function maybeGetBookingUidFromSeat(prisma: PrismaClient, uid: string) {
|
||||
// Look bookingUid in bookingSeat
|
||||
|
||||
@@ -8,10 +8,12 @@ describe("slugify", () => {
|
||||
expect(slugify("HELLO")).toEqual("hello");
|
||||
});
|
||||
|
||||
it("should convert spaces, _, and any other special character to -", () => {
|
||||
it("should convert spaces, _, +, # and any other special character to -", () => {
|
||||
expect(slugify("hello there")).toEqual("hello-there");
|
||||
expect(slugify("hello_there")).toEqual("hello-there");
|
||||
expect(slugify("hello$there")).toEqual("hello-there");
|
||||
expect(slugify("hello+there")).toEqual("hello-there");
|
||||
expect(slugify("#hellothere")).toEqual("hellothere");
|
||||
});
|
||||
|
||||
it("should keep numbers as is", () => {
|
||||
|
||||
@@ -5,10 +5,9 @@ export const slugify = (str: string) => {
|
||||
.normalize("NFD") // Normalize to decomposed form for handling accents
|
||||
.replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters
|
||||
.replace(/[^\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode) with a dash
|
||||
.replace(/[\s_]+/g, "-") // Replace whitespace and underscores with a single dash
|
||||
.replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash
|
||||
.replace(/^-+/, "") // Remove dashes from start
|
||||
.replace(/-+$/, "") // Remove dashes from end
|
||||
.replace(/\+/g, "-"); // Transform all + to -
|
||||
.replace(/-+$/, ""); // Remove dashes from end
|
||||
};
|
||||
|
||||
export default slugify;
|
||||
|
||||
@@ -31,6 +31,13 @@ turndownService.addRule("enter", {
|
||||
},
|
||||
});
|
||||
|
||||
turndownService.addRule("ignoreEmphasized", {
|
||||
filter: "em",
|
||||
replacement: function (content) {
|
||||
return content;
|
||||
},
|
||||
});
|
||||
|
||||
function isShiftEnter(node: HTMLElement) {
|
||||
let currentNode: HTMLElement | null | ParentNode = node;
|
||||
|
||||
|
||||
@@ -1,28 +1,65 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { PrismaClient as PrismaClientWithoutExtension } from "@prisma/client";
|
||||
|
||||
import { bookingReferenceMiddleware } from "./middleware";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: PrismaClient | undefined;
|
||||
var prisma: typeof prismaWithClientExtensions;
|
||||
}
|
||||
|
||||
const prismaOptions: Prisma.PrismaClientOptions = {};
|
||||
|
||||
if (!!process.env.NEXT_PUBLIC_DEBUG) prismaOptions.log = ["query", "error", "warn"];
|
||||
|
||||
export const prisma = globalThis.prisma || new PrismaClient(prismaOptions);
|
||||
const prismaWithoutClientExtensions = new PrismaClientWithoutExtension(prismaOptions);
|
||||
|
||||
export const customPrisma = (options: Prisma.PrismaClientOptions) =>
|
||||
new PrismaClient({ ...prismaOptions, ...options });
|
||||
new PrismaClientWithoutExtension({ ...prismaOptions, ...options });
|
||||
|
||||
// If any changed on middleware server restart is required
|
||||
// TODO: Migrate it to $extends
|
||||
bookingReferenceMiddleware(prismaWithoutClientExtensions);
|
||||
|
||||
// FIXME: Due to some reason, there are types failing in certain places due to the $extends. Fix it and then enable it
|
||||
// Specifically we get errors like `Type 'string | Date | null | undefined' is not assignable to type 'Exact<string | Date | null | undefined, string | Date | null | undefined>'`
|
||||
// const prismaWithClientExtensions = prismaWithoutClientExtensions.$extends({
|
||||
// query: {
|
||||
// $allModels: {
|
||||
// async $allOperations({ model, operation, args, query }) {
|
||||
// const start = performance.now();
|
||||
// /* your custom logic here */
|
||||
// const res = await query(args);
|
||||
// const end = performance.now();
|
||||
// logger.debug("Query Perf: ", `${model}.${operation} took ${(end - start).toFixed(2)}ms\n`);
|
||||
// return res;
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// .$extends({
|
||||
// name: "teamUpdateWithMetadata",
|
||||
// query: {
|
||||
// team: {
|
||||
// async update({ model, operation, args, query }) {
|
||||
// if (args.data.metadata) {
|
||||
// // Prepare args.data with merged metadata
|
||||
// }
|
||||
// return query(args);
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
const prismaWithClientExtensions = prismaWithoutClientExtensions;
|
||||
|
||||
export const prisma = (globalThis.prisma as typeof prismaWithClientExtensions) || prismaWithClientExtensions;
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalThis.prisma = prisma;
|
||||
}
|
||||
// If any changed on middleware server restart is required
|
||||
bookingReferenceMiddleware(prisma);
|
||||
|
||||
export type PrismaClient = typeof prisma;
|
||||
export default prisma;
|
||||
|
||||
export * from "./selects";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
function middleware(prisma: PrismaClient) {
|
||||
/***********************************/
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* This script can be used to seed the database with a lot of data for performance testing.
|
||||
* TODO: Make it more structured and configurable from CLI
|
||||
* Run it as `npx ts-node --transpile-only ./seed-performance-testing.ts`
|
||||
*/
|
||||
import { uuid } from "short-uuid";
|
||||
|
||||
import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
|
||||
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
|
||||
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { createUserAndEventType } from "./seed-utils";
|
||||
|
||||
async function createManyDifferentUsersWithDifferentEventTypesAndBookings({
|
||||
tillUser,
|
||||
startFrom = 0,
|
||||
}: {
|
||||
tillUser: number;
|
||||
startFrom?: number;
|
||||
}) {
|
||||
for (let i = startFrom; i < tillUser; i++) {
|
||||
await createUserAndEventType({
|
||||
user: {
|
||||
email: `pro${i}@example.com`,
|
||||
name: "Pro Example",
|
||||
password: "1111",
|
||||
username: `pro${i}`,
|
||||
theme: "light",
|
||||
},
|
||||
eventTypes: [
|
||||
{
|
||||
title: "30min",
|
||||
slug: "30min",
|
||||
length: 30,
|
||||
_bookings: [
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "30min",
|
||||
startTime: dayjs().add(1, "day").toDate(),
|
||||
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "30min",
|
||||
startTime: dayjs().add(2, "day").toDate(),
|
||||
endTime: dayjs().add(2, "day").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "60min",
|
||||
slug: "60min",
|
||||
length: 60,
|
||||
},
|
||||
{
|
||||
title: "Multiple duration",
|
||||
slug: "multiple-duration",
|
||||
length: 75,
|
||||
metadata: {
|
||||
multipleDuration: [30, 75, 90],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "paid",
|
||||
slug: "paid",
|
||||
length: 60,
|
||||
price: 100,
|
||||
},
|
||||
{
|
||||
title: "In person meeting",
|
||||
slug: "in-person",
|
||||
length: 60,
|
||||
locations: [{ type: "inPerson", address: "London" }],
|
||||
},
|
||||
{
|
||||
title: "Zoom Event",
|
||||
slug: "zoom",
|
||||
length: 60,
|
||||
locations: [{ type: zoomMeta.appData?.location?.type }],
|
||||
},
|
||||
{
|
||||
title: "Daily Event",
|
||||
slug: "daily",
|
||||
length: 60,
|
||||
locations: [{ type: dailyMeta.appData?.location?.type }],
|
||||
},
|
||||
{
|
||||
title: "Google Meet",
|
||||
slug: "google-meet",
|
||||
length: 60,
|
||||
locations: [{ type: googleMeetMeta.appData?.location?.type }],
|
||||
},
|
||||
{
|
||||
title: "Yoga class",
|
||||
slug: "yoga-class",
|
||||
length: 30,
|
||||
recurringEvent: { freq: 2, count: 12, interval: 1 },
|
||||
_bookings: [
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Yoga class",
|
||||
recurringEventId: Buffer.from("yoga-class").toString("base64"),
|
||||
startTime: dayjs().add(1, "day").toDate(),
|
||||
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Yoga class",
|
||||
recurringEventId: Buffer.from("yoga-class").toString("base64"),
|
||||
startTime: dayjs().add(1, "day").add(1, "week").toDate(),
|
||||
endTime: dayjs().add(1, "day").add(1, "week").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Yoga class",
|
||||
recurringEventId: Buffer.from("yoga-class").toString("base64"),
|
||||
startTime: dayjs().add(1, "day").add(2, "week").toDate(),
|
||||
endTime: dayjs().add(1, "day").add(2, "week").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Yoga class",
|
||||
recurringEventId: Buffer.from("yoga-class").toString("base64"),
|
||||
startTime: dayjs().add(1, "day").add(3, "week").toDate(),
|
||||
endTime: dayjs().add(1, "day").add(3, "week").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Yoga class",
|
||||
recurringEventId: Buffer.from("yoga-class").toString("base64"),
|
||||
startTime: dayjs().add(1, "day").add(4, "week").toDate(),
|
||||
endTime: dayjs().add(1, "day").add(4, "week").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Yoga class",
|
||||
recurringEventId: Buffer.from("yoga-class").toString("base64"),
|
||||
startTime: dayjs().add(1, "day").add(5, "week").toDate(),
|
||||
endTime: dayjs().add(1, "day").add(5, "week").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Seeded Yoga class",
|
||||
description: "seeded",
|
||||
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
|
||||
startTime: dayjs().subtract(4, "day").toDate(),
|
||||
endTime: dayjs().subtract(4, "day").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Seeded Yoga class",
|
||||
description: "seeded",
|
||||
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
|
||||
startTime: dayjs().subtract(4, "day").add(1, "week").toDate(),
|
||||
endTime: dayjs().subtract(4, "day").add(1, "week").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Seeded Yoga class",
|
||||
description: "seeded",
|
||||
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
|
||||
startTime: dayjs().subtract(4, "day").add(2, "week").toDate(),
|
||||
endTime: dayjs().subtract(4, "day").add(2, "week").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Seeded Yoga class",
|
||||
description: "seeded",
|
||||
recurringEventId: Buffer.from("seeded-yoga-class").toString("base64"),
|
||||
startTime: dayjs().subtract(4, "day").add(3, "week").toDate(),
|
||||
endTime: dayjs().subtract(4, "day").add(3, "week").add(30, "minutes").toDate(),
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tennis class",
|
||||
slug: "tennis-class",
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: 10, interval: 2 },
|
||||
requiresConfirmation: true,
|
||||
_bookings: [
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Tennis class",
|
||||
recurringEventId: Buffer.from("tennis-class").toString("base64"),
|
||||
startTime: dayjs().add(2, "day").toDate(),
|
||||
endTime: dayjs().add(2, "day").add(60, "minutes").toDate(),
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Tennis class",
|
||||
recurringEventId: Buffer.from("tennis-class").toString("base64"),
|
||||
startTime: dayjs().add(2, "day").add(2, "week").toDate(),
|
||||
endTime: dayjs().add(2, "day").add(2, "week").add(60, "minutes").toDate(),
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Tennis class",
|
||||
recurringEventId: Buffer.from("tennis-class").toString("base64"),
|
||||
startTime: dayjs().add(2, "day").add(4, "week").toDate(),
|
||||
endTime: dayjs().add(2, "day").add(4, "week").add(60, "minutes").toDate(),
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Tennis class",
|
||||
recurringEventId: Buffer.from("tennis-class").toString("base64"),
|
||||
startTime: dayjs().add(2, "day").add(8, "week").toDate(),
|
||||
endTime: dayjs().add(2, "day").add(8, "week").add(60, "minutes").toDate(),
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
{
|
||||
uid: uuid(),
|
||||
title: "Tennis class",
|
||||
recurringEventId: Buffer.from("tennis-class").toString("base64"),
|
||||
startTime: dayjs().add(2, "day").add(10, "week").toDate(),
|
||||
endTime: dayjs().add(2, "day").add(10, "week").add(60, "minutes").toDate(),
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createAUserWithManyBookings() {
|
||||
const random = Math.random();
|
||||
await createUserAndEventType({
|
||||
user: {
|
||||
email: `pro-${random}@example.com`,
|
||||
name: "Pro Example",
|
||||
password: "1111",
|
||||
username: `pro-${random}`,
|
||||
theme: "light",
|
||||
},
|
||||
eventTypes: [
|
||||
{
|
||||
title: "30min",
|
||||
slug: "30min",
|
||||
length: 30,
|
||||
_numBookings: 100,
|
||||
},
|
||||
{
|
||||
title: "60min",
|
||||
slug: "60min",
|
||||
length: 60,
|
||||
_numBookings: 100,
|
||||
},
|
||||
{
|
||||
title: "Multiple duration",
|
||||
slug: "multiple-duration",
|
||||
length: 75,
|
||||
metadata: {
|
||||
multipleDuration: [30, 75, 90],
|
||||
},
|
||||
_numBookings: 100,
|
||||
},
|
||||
{
|
||||
title: "paid",
|
||||
slug: "paid",
|
||||
length: 60,
|
||||
price: 100,
|
||||
_numBookings: 100,
|
||||
},
|
||||
{
|
||||
title: "Zoom Event",
|
||||
slug: "zoom",
|
||||
length: 60,
|
||||
locations: [{ type: zoomMeta.appData?.location?.type }],
|
||||
_numBookings: 100,
|
||||
},
|
||||
{
|
||||
title: "Daily Event",
|
||||
slug: "daily",
|
||||
length: 60,
|
||||
locations: [{ type: dailyMeta.appData?.location?.type }],
|
||||
_numBookings: 100,
|
||||
},
|
||||
{
|
||||
title: "Google Meet",
|
||||
slug: "google-meet",
|
||||
length: 60,
|
||||
locations: [{ type: googleMeetMeta.appData?.location?.type }],
|
||||
_numBookings: 100,
|
||||
},
|
||||
{
|
||||
title: "Yoga class",
|
||||
slug: "yoga-class",
|
||||
length: 30,
|
||||
_numBookings: 100,
|
||||
},
|
||||
{
|
||||
title: "Tennis class",
|
||||
slug: "tennis-class",
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: 10, interval: 2 },
|
||||
requiresConfirmation: true,
|
||||
_numBookings: 100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// createManyDifferentUsersWithDifferentEventTypesAndBookings({
|
||||
// tillUser: 20000,
|
||||
// startFrom: 10000,
|
||||
// });
|
||||
|
||||
createAUserWithManyBookings();
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { Prisma, UserPermissionRole } from "@prisma/client";
|
||||
import { uuid } from "short-uuid";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
|
||||
import prisma from ".";
|
||||
|
||||
export async function createUserAndEventType({
|
||||
user,
|
||||
eventTypes = [],
|
||||
}: {
|
||||
user: {
|
||||
email: string;
|
||||
password: string;
|
||||
username: string;
|
||||
name: string;
|
||||
completedOnboarding?: boolean;
|
||||
timeZone?: string;
|
||||
role?: UserPermissionRole;
|
||||
theme?: "dark" | "light";
|
||||
};
|
||||
eventTypes?: Array<
|
||||
Prisma.EventTypeUncheckedCreateInput & {
|
||||
_bookings?: Prisma.BookingCreateInput[];
|
||||
_numBookings?: number;
|
||||
}
|
||||
>;
|
||||
}) {
|
||||
const userData = {
|
||||
...user,
|
||||
password: await hashPassword(user.password),
|
||||
emailVerified: new Date(),
|
||||
completedOnboarding: user.completedOnboarding ?? true,
|
||||
locale: "en",
|
||||
schedules:
|
||||
user.completedOnboarding ?? true
|
||||
? {
|
||||
create: {
|
||||
name: "Working Hours",
|
||||
availability: {
|
||||
createMany: {
|
||||
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const theUser = await prisma.user.upsert({
|
||||
where: { email_username: { email: user.email, username: user.username } },
|
||||
update: userData,
|
||||
create: userData,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`👤 Upserted '${user.username}' with email "${user.email}" & password "${user.password}". Booking page 👉 ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}`
|
||||
);
|
||||
|
||||
for (const eventTypeInput of eventTypes) {
|
||||
const { _bookings, _numBookings, ...eventTypeData } = eventTypeInput;
|
||||
let bookingFields;
|
||||
if (_bookings && _numBookings) {
|
||||
throw new Error("You can't set both _bookings and _numBookings");
|
||||
} else if (_numBookings) {
|
||||
bookingFields = [...Array(_numBookings).keys()].map((i) => ({
|
||||
startTime: dayjs()
|
||||
.add(1, "day")
|
||||
.add(i * 5 + 0, "minutes")
|
||||
.toDate(),
|
||||
endTime: dayjs()
|
||||
.add(1, "day")
|
||||
.add(i * 5 + 30, "minutes")
|
||||
.toDate(),
|
||||
title: `${eventTypeInput.title}:${i + 1}`,
|
||||
uid: uuid(),
|
||||
}));
|
||||
} else {
|
||||
bookingFields = _bookings || [];
|
||||
}
|
||||
eventTypeData.userId = theUser.id;
|
||||
eventTypeData.users = { connect: { id: theUser.id } };
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
slug: eventTypeData.slug,
|
||||
users: {
|
||||
some: {
|
||||
id: eventTypeData.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (eventType) {
|
||||
console.log(
|
||||
`\t📆 Event type ${eventTypeData.slug} already seems seeded - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const { id } = await prisma.eventType.create({
|
||||
data: eventTypeData,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`\t📆 Event type ${eventTypeData.slug} with id ${id}, length ${eventTypeData.length}min - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
|
||||
);
|
||||
|
||||
for (const bookingInput of bookingFields) {
|
||||
await prisma.booking.create({
|
||||
data: {
|
||||
...bookingInput,
|
||||
user: {
|
||||
connect: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
timeZone: "Europe/London",
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
status: bookingInput.status,
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`\t\t☎️ Created booking ${bookingInput.title} at ${new Date(
|
||||
bookingInput.startTime
|
||||
).toLocaleDateString()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
console.log("👤 User with it's event-types and bookings created", theUser.email);
|
||||
return theUser;
|
||||
}
|
||||
+2
-121
@@ -1,4 +1,4 @@
|
||||
import type { Prisma, UserPermissionRole } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { uuid } from "short-uuid";
|
||||
import type z from "zod";
|
||||
|
||||
@@ -6,132 +6,13 @@ import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
|
||||
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
|
||||
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import prisma from ".";
|
||||
import mainAppStore from "./seed-app-store";
|
||||
import { createUserAndEventType } from "./seed-utils";
|
||||
import type { teamMetadataSchema } from "./zod-utils";
|
||||
|
||||
async function createUserAndEventType({
|
||||
user,
|
||||
eventTypes = [],
|
||||
}: {
|
||||
user: {
|
||||
email: string;
|
||||
password: string;
|
||||
username: string;
|
||||
name: string;
|
||||
completedOnboarding?: boolean;
|
||||
timeZone?: string;
|
||||
role?: UserPermissionRole;
|
||||
theme?: "dark" | "light";
|
||||
};
|
||||
eventTypes?: Array<
|
||||
Prisma.EventTypeUncheckedCreateInput & {
|
||||
_bookings?: Prisma.BookingCreateInput[];
|
||||
}
|
||||
>;
|
||||
}) {
|
||||
const userData = {
|
||||
...user,
|
||||
password: await hashPassword(user.password),
|
||||
emailVerified: new Date(),
|
||||
completedOnboarding: user.completedOnboarding ?? true,
|
||||
locale: "en",
|
||||
schedules:
|
||||
user.completedOnboarding ?? true
|
||||
? {
|
||||
create: {
|
||||
name: "Working Hours",
|
||||
availability: {
|
||||
createMany: {
|
||||
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const theUser = await prisma.user.upsert({
|
||||
where: { email_username: { email: user.email, username: user.username } },
|
||||
update: userData,
|
||||
create: userData,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`👤 Upserted '${user.username}' with email "${user.email}" & password "${user.password}". Booking page 👉 ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}`
|
||||
);
|
||||
|
||||
for (const eventTypeInput of eventTypes) {
|
||||
const { _bookings: bookingFields = [], ...eventTypeData } = eventTypeInput;
|
||||
eventTypeData.userId = theUser.id;
|
||||
eventTypeData.users = { connect: { id: theUser.id } };
|
||||
|
||||
const eventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
slug: eventTypeData.slug,
|
||||
users: {
|
||||
some: {
|
||||
id: eventTypeData.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (eventType) {
|
||||
console.log(
|
||||
`\t📆 Event type ${eventTypeData.slug} already seems seeded - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const { id } = await prisma.eventType.create({
|
||||
data: eventTypeData,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`\t📆 Event type ${eventTypeData.slug} with id ${id}, length ${eventTypeData.length}min - ${process.env.NEXT_PUBLIC_WEBAPP_URL}/${user.username}/${eventTypeData.slug}`
|
||||
);
|
||||
for (const bookingInput of bookingFields) {
|
||||
await prisma.booking.create({
|
||||
data: {
|
||||
...bookingInput,
|
||||
user: {
|
||||
connect: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
timeZone: "Europe/London",
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
status: bookingInput.status,
|
||||
},
|
||||
});
|
||||
console.log(
|
||||
`\t\t☎️ Created booking ${bookingInput.title} at ${new Date(
|
||||
bookingInput.startTime
|
||||
).toLocaleDateString()}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return theUser;
|
||||
}
|
||||
|
||||
async function createTeamAndAddUsers(
|
||||
teamInput: Prisma.TeamCreateInput,
|
||||
users: { id: number; username: string; role?: MembershipRole }[] = []
|
||||
|
||||
@@ -328,6 +328,7 @@ export const teamMetadataSchema = z
|
||||
subscriptionItemId: z.string().nullable(),
|
||||
isOrganization: z.boolean().nullable(),
|
||||
isOrganizationVerified: z.boolean().nullable(),
|
||||
isOrganizationConfigured: z.boolean().nullable(),
|
||||
orgAutoAcceptEmail: z.string().nullable(),
|
||||
})
|
||||
.partial()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -131,17 +132,25 @@ const getUserSession = async (ctx: TRPCContextInner) => {
|
||||
|
||||
return { user, session };
|
||||
};
|
||||
const sessionMiddleware = middleware(async ({ ctx, next }) => {
|
||||
const { user, session } = await getUserSession(ctx);
|
||||
|
||||
const sessionMiddleware = middleware(async ({ ctx, next }) => {
|
||||
const middlewareStart = performance.now();
|
||||
const { user, session } = await getUserSession(ctx);
|
||||
const middlewareEnd = performance.now();
|
||||
logger.debug("Perf:t.sessionMiddleware", middlewareEnd - middlewareStart);
|
||||
return next({
|
||||
ctx: { user, session },
|
||||
});
|
||||
});
|
||||
|
||||
export const isAuthed = middleware(async ({ ctx, next }) => {
|
||||
const middlewareStart = performance.now();
|
||||
|
||||
const { user, session } = await getUserSession(ctx);
|
||||
|
||||
const middlewareEnd = performance.now();
|
||||
logger.debug("Perf:t.isAuthed", middlewareEnd - middlewareStart);
|
||||
|
||||
if (!user || !session) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type PrismaClient } from "@prisma/client";
|
||||
|
||||
import { isOrganization, withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils";
|
||||
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -1,136 +1,52 @@
|
||||
import sessionMiddleware from "../../middlewares/sessionMiddleware";
|
||||
import publicProcedure from "../../procedures/publicProcedure";
|
||||
import { router } from "../../trpc";
|
||||
import { importHandler, router } from "../../trpc";
|
||||
import { slotsRouter } from "../viewer/slots/_router";
|
||||
import { ZEventInputSchema } from "./event.schema";
|
||||
import { i18nInputSchema } from "./i18n.schema";
|
||||
import { ZSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
|
||||
|
||||
type PublicViewerRouterHandlerCache = {
|
||||
session?: typeof import("./session.handler").sessionHandler;
|
||||
i18n?: typeof import("./i18n.handler").i18nHandler;
|
||||
countryCode?: typeof import("./countryCode.handler").countryCodeHandler;
|
||||
samlTenantProduct?: typeof import("./samlTenantProduct.handler").samlTenantProductHandler;
|
||||
stripeCheckoutSession?: typeof import("./stripeCheckoutSession.handler").stripeCheckoutSessionHandler;
|
||||
cityTimezones?: typeof import("./cityTimezones.handler").cityTimezonesHandler;
|
||||
event?: typeof import("./event.handler").eventHandler;
|
||||
};
|
||||
const NAMESPACE = "publicViewer";
|
||||
|
||||
const UNSTABLE_HANDLER_CACHE: PublicViewerRouterHandlerCache = {};
|
||||
const namespaced = (s: string) => `${NAMESPACE}.${s}`;
|
||||
|
||||
// things that unauthenticated users can query about themselves
|
||||
export const publicViewerRouter = router({
|
||||
session: publicProcedure.use(sessionMiddleware).query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.session) {
|
||||
UNSTABLE_HANDLER_CACHE.session = await import("./session.handler").then((mod) => mod.sessionHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.session) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.session({
|
||||
ctx,
|
||||
});
|
||||
session: publicProcedure.use(sessionMiddleware).query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("session"), () => import("./session.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
|
||||
i18n: publicProcedure.input(i18nInputSchema).query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.i18n) {
|
||||
UNSTABLE_HANDLER_CACHE.i18n = await import("./i18n.handler").then((mod) => mod.i18nHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.i18n) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.i18n({ ctx, input });
|
||||
i18n: publicProcedure.input(i18nInputSchema).query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("i18n"), () => import("./i18n.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
|
||||
countryCode: publicProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.countryCode) {
|
||||
UNSTABLE_HANDLER_CACHE.countryCode = await import("./countryCode.handler").then(
|
||||
(mod) => mod.countryCodeHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.countryCode) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.countryCode({
|
||||
ctx,
|
||||
});
|
||||
countryCode: publicProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("countryCode"), () => import("./countryCode.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
|
||||
samlTenantProduct: publicProcedure.input(ZSamlTenantProductInputSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.samlTenantProduct) {
|
||||
UNSTABLE_HANDLER_CACHE.samlTenantProduct = await import("./samlTenantProduct.handler").then(
|
||||
(mod) => mod.samlTenantProductHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.samlTenantProduct) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.samlTenantProduct({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
samlTenantProduct: publicProcedure.input(ZSamlTenantProductInputSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("samlTenantProduct"),
|
||||
() => import("./samlTenantProduct.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
|
||||
stripeCheckoutSession: publicProcedure.input(ZStripeCheckoutSessionInputSchema).query(async ({ input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.stripeCheckoutSession) {
|
||||
UNSTABLE_HANDLER_CACHE.stripeCheckoutSession = await import("./stripeCheckoutSession.handler").then(
|
||||
(mod) => mod.stripeCheckoutSessionHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.stripeCheckoutSession) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.stripeCheckoutSession({
|
||||
input,
|
||||
});
|
||||
stripeCheckoutSession: publicProcedure.input(ZStripeCheckoutSessionInputSchema).query(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("stripeCheckoutSession"),
|
||||
() => import("./stripeCheckoutSession.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
|
||||
cityTimezones: publicProcedure.query(async () => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.cityTimezones) {
|
||||
UNSTABLE_HANDLER_CACHE.cityTimezones = await import("./cityTimezones.handler").then(
|
||||
(mod) => mod.cityTimezonesHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.cityTimezones) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.cityTimezones();
|
||||
const handler = await importHandler(namespaced("cityTimezones"), () => import("./cityTimezones.handler"));
|
||||
return handler();
|
||||
}),
|
||||
|
||||
// REVIEW: This router is part of both the public and private viewer router?
|
||||
slots: slotsRouter,
|
||||
event: publicProcedure.input(ZEventInputSchema).query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.event) {
|
||||
UNSTABLE_HANDLER_CACHE.event = await import("./event.handler").then((mod) => mod.eventHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.event) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.event({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
event: publicProcedure.input(ZEventInputSchema).query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("event"), () => import("./event.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -28,3 +28,5 @@ export const cityTimezonesHandler = async () => {
|
||||
|
||||
return uniqueCities;
|
||||
};
|
||||
|
||||
export default cityTimezonesHandler;
|
||||
|
||||
@@ -10,3 +10,5 @@ export const countryCodeHandler = async ({ ctx }: CountryCodeOptions) => {
|
||||
const countryCode: string | string[] = req?.headers?.["x-vercel-ip-country"] ?? "";
|
||||
return { countryCode: Array.isArray(countryCode) ? countryCode[0] : countryCode };
|
||||
};
|
||||
|
||||
export default countryCodeHandler;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { TEventInputSchema } from "./event.schema";
|
||||
|
||||
@@ -19,3 +18,5 @@ export const eventHandler = async ({ ctx, input }: EventHandlerOptions) => {
|
||||
);
|
||||
return event;
|
||||
};
|
||||
|
||||
export default eventHandler;
|
||||
|
||||
@@ -21,3 +21,5 @@ export const i18nHandler = async ({ input }: I18nOptions) => {
|
||||
locale,
|
||||
};
|
||||
};
|
||||
|
||||
export default i18nHandler;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { TSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||
|
||||
@@ -17,3 +16,5 @@ export const samlTenantProductHandler = ({ ctx, input }: SamlTenantProductOption
|
||||
|
||||
return samlTenantProduct(prisma, email);
|
||||
};
|
||||
|
||||
export default samlTenantProductHandler;
|
||||
|
||||
@@ -9,3 +9,5 @@ type SessionOptions = {
|
||||
export const sessionHandler = async ({ ctx }: SessionOptions) => {
|
||||
return ctx.session;
|
||||
};
|
||||
|
||||
export default sessionHandler;
|
||||
|
||||
@@ -61,3 +61,5 @@ export const stripeCheckoutSessionHandler = async ({ input }: StripeCheckoutSess
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default stripeCheckoutSessionHandler;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
||||
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
||||
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
// import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
||||
import { sendDisabledAppEmail } from "@calcom/emails";
|
||||
import { getTranslation } from "@calcom/lib/server";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
+148
-50
@@ -1,3 +1,6 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||
import { buildDateRanges } from "@calcom/lib/date-ranges";
|
||||
@@ -15,25 +18,32 @@ type GetOptions = {
|
||||
input: TListTeamAvailaiblityScheme;
|
||||
};
|
||||
|
||||
export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) => {
|
||||
const organizationId = ctx.user.organizationId;
|
||||
async function getTeamMembers({
|
||||
teamId,
|
||||
teamIds,
|
||||
cursor,
|
||||
limit,
|
||||
}: {
|
||||
teamId?: number;
|
||||
teamIds?: number[];
|
||||
cursor: number | null | undefined;
|
||||
limit: number;
|
||||
}) {
|
||||
let whereQuery: Prisma.MembershipWhereInput = {
|
||||
teamId,
|
||||
};
|
||||
|
||||
if (!organizationId) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization." });
|
||||
if (teamIds) {
|
||||
whereQuery = {
|
||||
teamId: {
|
||||
in: teamIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { cursor, limit } = input;
|
||||
|
||||
const getTotalMembers = await prisma.membership.count({
|
||||
return await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
// I couldnt get this query to work direct on membership table
|
||||
const teamMembers = await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: organizationId,
|
||||
...whereQuery,
|
||||
accepted: true,
|
||||
},
|
||||
select: {
|
||||
@@ -54,7 +64,128 @@ export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) =>
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
distinct: ["userId"],
|
||||
});
|
||||
}
|
||||
|
||||
type Member = Awaited<ReturnType<typeof getTeamMembers>>[number];
|
||||
|
||||
async function buildMember(member: Member, dateFrom: Dayjs, dateTo: Dayjs) {
|
||||
if (!member.user.defaultScheduleId) {
|
||||
return {
|
||||
id: member.user.id,
|
||||
username: member.user.username,
|
||||
email: member.user.email,
|
||||
timeZone: member.user.timeZone,
|
||||
role: member.role,
|
||||
dateRanges: [] as DateRange[],
|
||||
};
|
||||
}
|
||||
|
||||
const schedule = await prisma.schedule.findUnique({
|
||||
where: { id: member.user.defaultScheduleId },
|
||||
select: { availability: true, timeZone: true },
|
||||
});
|
||||
const timeZone = schedule?.timeZone || member.user.timeZone;
|
||||
|
||||
const dateRanges = buildDateRanges({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
timeZone,
|
||||
availability: schedule?.availability ?? [],
|
||||
});
|
||||
|
||||
return {
|
||||
id: member.user.id,
|
||||
username: member.user.username,
|
||||
email: member.user.email,
|
||||
timeZone,
|
||||
role: member.role,
|
||||
dateRanges,
|
||||
};
|
||||
}
|
||||
|
||||
async function getInfoForAllTeams({ ctx, input }: GetOptions) {
|
||||
const { cursor, limit } = input;
|
||||
|
||||
// Get all teamIds for the user
|
||||
const teamIds = await prisma.membership
|
||||
.findMany({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
.then((memberships) => memberships.map((membership) => membership.id));
|
||||
|
||||
if (!teamIds.length) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization or team." });
|
||||
}
|
||||
|
||||
const getTotalMembers = await prisma.$queryRaw<{
|
||||
count: number;
|
||||
}>(Prisma.sql`
|
||||
SELECT
|
||||
COUNT(DISTINCT "userId") as "count"
|
||||
FROM "Membership"
|
||||
WHERE "teamId" IN (${Prisma.join(teamIds)})
|
||||
`);
|
||||
|
||||
const teamMembers = await getTeamMembers({
|
||||
teamIds,
|
||||
cursor,
|
||||
limit,
|
||||
});
|
||||
|
||||
return {
|
||||
teamMembers,
|
||||
totalTeamMembers: getTotalMembers.count,
|
||||
};
|
||||
}
|
||||
|
||||
export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) => {
|
||||
const { cursor, limit } = input;
|
||||
const teamId = input.teamId || ctx.user.organizationId;
|
||||
|
||||
let teamMembers: Member[] = [];
|
||||
let totalTeamMembers = 0;
|
||||
|
||||
if (!teamId) {
|
||||
// Get all users TODO:
|
||||
const teamAllInfo = await getInfoForAllTeams({ ctx, input });
|
||||
|
||||
teamMembers = teamAllInfo.teamMembers;
|
||||
totalTeamMembers = teamAllInfo.totalTeamMembers;
|
||||
} else {
|
||||
const isMember = await prisma.membership.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isMember) {
|
||||
teamMembers = [];
|
||||
totalTeamMembers = 0;
|
||||
} else {
|
||||
const { cursor, limit } = input;
|
||||
|
||||
totalTeamMembers = await prisma.membership.count({
|
||||
where: {
|
||||
teamId: teamId,
|
||||
},
|
||||
});
|
||||
|
||||
// I couldnt get this query to work direct on membership table
|
||||
teamMembers = await getTeamMembers({
|
||||
teamId,
|
||||
cursor,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let nextCursor: typeof cursor | undefined = undefined;
|
||||
if (teamMembers && teamMembers.length > limit) {
|
||||
@@ -65,40 +196,7 @@ export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) =>
|
||||
const dateFrom = dayjs(input.startDate).tz(input.loggedInUsersTz).subtract(1, "day");
|
||||
const dateTo = dayjs(input.endDate).tz(input.loggedInUsersTz).add(1, "day");
|
||||
|
||||
const buildMembers = teamMembers?.map(async (member) => {
|
||||
if (!member.user.defaultScheduleId) {
|
||||
return {
|
||||
id: member.user.id,
|
||||
username: member.user.username,
|
||||
email: member.user.email,
|
||||
timeZone: member.user.timeZone,
|
||||
role: member.role,
|
||||
dateRanges: [] as DateRange[],
|
||||
};
|
||||
}
|
||||
|
||||
const schedule = await prisma.schedule.findUnique({
|
||||
where: { id: member.user.defaultScheduleId },
|
||||
select: { availability: true, timeZone: true },
|
||||
});
|
||||
const timeZone = schedule?.timeZone || member.user.timeZone;
|
||||
|
||||
const dateRanges = buildDateRanges({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
timeZone,
|
||||
availability: schedule?.availability ?? [],
|
||||
});
|
||||
|
||||
return {
|
||||
id: member.user.id,
|
||||
username: member.user.username,
|
||||
email: member.user.email,
|
||||
timeZone,
|
||||
role: member.role,
|
||||
dateRanges,
|
||||
};
|
||||
});
|
||||
const buildMembers = teamMembers?.map((member) => buildMember(member, dateFrom, dateTo));
|
||||
|
||||
const members = await Promise.all(buildMembers);
|
||||
|
||||
@@ -106,7 +204,7 @@ export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) =>
|
||||
rows: members || [],
|
||||
nextCursor,
|
||||
meta: {
|
||||
totalRowCount: getTotalMembers || 0,
|
||||
totalRowCount: totalTeamMembers,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export const ZListTeamAvailaiblityScheme = z.object({
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
loggedInUsersTz: z.string(),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TListTeamAvailaiblityScheme = z.infer<typeof ZListTeamAvailaiblityScheme>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Availability as AvailabilityModel, Schedule as ScheduleModel, User } from "@prisma/client";
|
||||
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { Schedule } from "@calcom/types/schedule";
|
||||
|
||||
export const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { Prisma, PrismaClient } from "@calcom/prisma/client";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -72,6 +73,62 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
unconfirmed: { startTime: "asc" },
|
||||
};
|
||||
|
||||
const passedBookingsStatusFilter = bookingListingFilters[bookingListingByStatus];
|
||||
const orderBy = bookingListingOrderby[bookingListingByStatus];
|
||||
|
||||
const { bookings, recurringInfo } = await getBookings({
|
||||
user,
|
||||
prisma,
|
||||
passedBookingsStatusFilter,
|
||||
filters: input.filters,
|
||||
orderBy,
|
||||
take,
|
||||
skip,
|
||||
});
|
||||
|
||||
const bookingsFetched = bookings.length;
|
||||
let nextCursor: typeof skip | null = skip;
|
||||
if (bookingsFetched > take) {
|
||||
nextCursor += bookingsFetched;
|
||||
} else {
|
||||
nextCursor = null;
|
||||
}
|
||||
|
||||
return {
|
||||
bookings,
|
||||
recurringInfo,
|
||||
nextCursor,
|
||||
};
|
||||
};
|
||||
|
||||
const set = new Set();
|
||||
const getUniqueBookings = <T extends { uid: string }>(arr: T[]) => {
|
||||
const unique = arr.filter((booking) => {
|
||||
const duplicate = set.has(booking.uid);
|
||||
set.add(booking.uid);
|
||||
return !duplicate;
|
||||
});
|
||||
set.clear();
|
||||
return unique;
|
||||
};
|
||||
|
||||
async function getBookings({
|
||||
user,
|
||||
prisma,
|
||||
passedBookingsStatusFilter,
|
||||
filters,
|
||||
orderBy,
|
||||
take,
|
||||
skip,
|
||||
}: {
|
||||
user: { id: number; email: string };
|
||||
filters: TGetInputSchema["filters"];
|
||||
prisma: PrismaClient;
|
||||
passedBookingsStatusFilter: Prisma.BookingWhereInput;
|
||||
orderBy: Prisma.BookingOrderByWithAggregationInput;
|
||||
take: number;
|
||||
skip: number;
|
||||
}) {
|
||||
// TODO: Fix record typing
|
||||
const bookingWhereInputFilters: Record<string, Prisma.BookingWhereInput> = {
|
||||
teamIds: {
|
||||
@@ -80,7 +137,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
eventType: {
|
||||
team: {
|
||||
id: {
|
||||
in: input.filters?.teamIds,
|
||||
in: filters?.teamIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -94,7 +151,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
users: {
|
||||
some: {
|
||||
id: {
|
||||
in: input.filters?.userIds,
|
||||
in: filters?.userIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -106,7 +163,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
AND: [
|
||||
{
|
||||
eventTypeId: {
|
||||
in: input.filters?.eventTypeIds,
|
||||
in: filters?.eventTypeIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -114,21 +171,100 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
};
|
||||
|
||||
const filtersCombined: Prisma.BookingWhereInput[] =
|
||||
input.filters &&
|
||||
Object.keys(input.filters).map((key) => {
|
||||
filters &&
|
||||
Object.keys(filters).map((key) => {
|
||||
return bookingWhereInputFilters[key];
|
||||
});
|
||||
|
||||
const passedBookingsStatusFilter = bookingListingFilters[bookingListingByStatus];
|
||||
const orderBy = bookingListingOrderby[bookingListingByStatus];
|
||||
const bookingSelect = {
|
||||
...bookingMinimalSelect,
|
||||
uid: true,
|
||||
recurringEventId: true,
|
||||
location: true,
|
||||
eventType: {
|
||||
select: {
|
||||
slug: true,
|
||||
id: true,
|
||||
eventName: true,
|
||||
price: true,
|
||||
recurringEvent: true,
|
||||
currency: true,
|
||||
metadata: true,
|
||||
seatsShowAttendees: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
status: true,
|
||||
paid: true,
|
||||
payment: {
|
||||
select: {
|
||||
paymentOption: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
rescheduled: true,
|
||||
references: true,
|
||||
isRecorded: true,
|
||||
seatsReferences: {
|
||||
where: {
|
||||
attendee: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
referenceUid: true,
|
||||
attendee: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [bookingsQuery, recurringInfoBasic, recurringInfoExtended] = await Promise.all([
|
||||
const [
|
||||
// Quering these in parallel to save time.
|
||||
// Note that because we are applying `take` to individual queries, we will usually get more bookings then we need. It is okay to have more bookings faster than having what we need slower
|
||||
bookingsQueryUserId,
|
||||
bookingsQueryAttendees,
|
||||
bookingsQueryTeamMember,
|
||||
bookingsQuerySeatReference,
|
||||
//////////////////////////
|
||||
|
||||
recurringInfoBasic,
|
||||
recurringInfoExtended,
|
||||
// We need all promises to be successful, so we are not using Promise.allSettled
|
||||
] = await Promise.all([
|
||||
prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
],
|
||||
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
|
||||
},
|
||||
orderBy,
|
||||
take: take + 1,
|
||||
skip,
|
||||
}),
|
||||
prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
attendees: {
|
||||
some: {
|
||||
@@ -136,6 +272,16 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
|
||||
},
|
||||
orderBy,
|
||||
take: take + 1,
|
||||
skip,
|
||||
}),
|
||||
prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
eventType: {
|
||||
team: {
|
||||
@@ -150,6 +296,16 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
|
||||
},
|
||||
orderBy,
|
||||
take: take + 1,
|
||||
skip,
|
||||
}),
|
||||
prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
seatsReferences: {
|
||||
some: {
|
||||
@@ -162,65 +318,6 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
],
|
||||
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
uid: true,
|
||||
recurringEventId: true,
|
||||
location: true,
|
||||
eventType: {
|
||||
select: {
|
||||
slug: true,
|
||||
id: true,
|
||||
eventName: true,
|
||||
price: true,
|
||||
recurringEvent: true,
|
||||
currency: true,
|
||||
metadata: true,
|
||||
seatsShowAttendees: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
status: true,
|
||||
paid: true,
|
||||
payment: {
|
||||
select: {
|
||||
paymentOption: true,
|
||||
amount: true,
|
||||
currency: true,
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
rescheduled: true,
|
||||
references: true,
|
||||
isRecorded: true,
|
||||
seatsReferences: {
|
||||
where: {
|
||||
attendee: {
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
referenceUid: true,
|
||||
attendee: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy,
|
||||
take: take + 1,
|
||||
skip,
|
||||
@@ -285,7 +382,25 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
}
|
||||
);
|
||||
|
||||
const bookings = bookingsQuery.map((booking) => {
|
||||
const plainBookings = getUniqueBookings(
|
||||
bookingsQueryUserId
|
||||
.concat(bookingsQueryAttendees)
|
||||
.concat(bookingsQueryTeamMember)
|
||||
.concat(bookingsQuerySeatReference)
|
||||
);
|
||||
|
||||
// Now enrich bookings with relation data. We could have queried the relation data along with the bookings, but that would cause unnecessary queries to the database.
|
||||
// Because Prisma is also going to query the select relation data sequentially, we are fine querying it separately here as it would be just 1 query instead of 4
|
||||
const bookings = (
|
||||
await prisma.booking.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: plainBookings.map((booking) => booking.id),
|
||||
},
|
||||
},
|
||||
select: bookingSelect,
|
||||
})
|
||||
).map((booking) => {
|
||||
// If seats are enabled and the event is not set to show attendees, filter out attendees that are not the current user
|
||||
if (booking.seatsReferences.length && !booking.eventType?.seatsShowAttendees) {
|
||||
booking.attendees = booking.attendees.filter((attendee) => attendee.email === user.email);
|
||||
@@ -303,18 +418,5 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
endTime: booking.endTime.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const bookingsFetched = bookings.length;
|
||||
let nextCursor: typeof skip | null = skip;
|
||||
if (bookingsFetched > take) {
|
||||
nextCursor += bookingsFetched;
|
||||
} else {
|
||||
nextCursor = null;
|
||||
}
|
||||
|
||||
return {
|
||||
bookings,
|
||||
recurringInfo,
|
||||
nextCursor,
|
||||
};
|
||||
};
|
||||
return { bookings, recurringInfo };
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
|
||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import type { TGetInputSchema } from "./get.schema";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type PrismaClient, Prisma } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { orderBy } from "lodash";
|
||||
|
||||
@@ -7,6 +7,7 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiResponse, GetServerSidePropsContext } from "next";
|
||||
|
||||
@@ -8,6 +7,7 @@ import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getTranslation } from "@calcom/lib/server";
|
||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
|
||||
@@ -63,11 +63,8 @@ export const viewerOrganizationsRouter = router({
|
||||
const handler = await importHandler(namespaced("getMembers"), () => import("./getMembers.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
adminGetUnverified: authedAdminProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("adminGetUnverified"),
|
||||
() => import("./adminGetUnverified.handler")
|
||||
);
|
||||
adminGetAll: authedAdminProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("adminGetAll"), () => import("./adminGetAll.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
adminVerify: authedAdminProcedure.input(ZAdminVerifyInput).mutation(async (opts) => {
|
||||
|
||||
+6
-10
@@ -1,15 +1,16 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
|
||||
type AdminGetUnverifiedOptions = {
|
||||
type AdminGetAllOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
};
|
||||
|
||||
export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetUnverifiedOptions) => {
|
||||
const unVerifiedTeams = await prisma.team.findMany({
|
||||
export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetAllOptions) => {
|
||||
const allOrgs = await prisma.team.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
@@ -18,18 +19,13 @@ export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetUnverifiedOptio
|
||||
equals: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
path: ["isOrganizationVerified"],
|
||||
equals: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
members: {
|
||||
where: {
|
||||
role: "OWNER",
|
||||
@@ -47,7 +43,7 @@ export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetUnverifiedOptio
|
||||
},
|
||||
});
|
||||
|
||||
return unVerifiedTeams;
|
||||
return allOrgs.map((org) => ({ ...org, metadata: teamMetadataSchema.parse(org.metadata) }));
|
||||
};
|
||||
|
||||
export default adminGetUnverifiedHandler;
|
||||
@@ -1,19 +1,21 @@
|
||||
import { createHash } from "crypto";
|
||||
import { lookup } from "dns";
|
||||
import { totp } from "otplib";
|
||||
|
||||
import { sendOrganizationEmailVerification } from "@calcom/emails";
|
||||
import { sendAdminOrganizationNotification } from "@calcom/emails";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
import {
|
||||
IS_CALCOM,
|
||||
IS_TEAM_BILLING_ENABLED,
|
||||
RESERVED_SUBDOMAINS,
|
||||
IS_PRODUCTION,
|
||||
WEBAPP_URL,
|
||||
} from "@calcom/lib/constants";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -27,6 +29,15 @@ type CreateOptions = {
|
||||
input: TCreateInputSchema;
|
||||
};
|
||||
|
||||
const getIPAddress = async (url: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
lookup(url, (err, address) => {
|
||||
if (err) reject(err);
|
||||
resolve(address);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const vercelCreateDomain = async (domain: string) => {
|
||||
const response = await fetch(
|
||||
`https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains?teamId=${process.env.TEAM_ID_VERCEL}`,
|
||||
@@ -43,12 +54,10 @@ const vercelCreateDomain = async (domain: string) => {
|
||||
const data = await response.json();
|
||||
|
||||
// Domain is already owned by another team but you can request delegation to access it
|
||||
if (data.error?.code === "forbidden")
|
||||
throw new TRPCError({ code: "CONFLICT", message: "domain_taken_team" });
|
||||
if (data.error?.code === "forbidden") return false;
|
||||
|
||||
// Domain is already being used by a different project
|
||||
if (data.error?.code === "domain_taken")
|
||||
throw new TRPCError({ code: "CONFLICT", message: "domain_taken_project" });
|
||||
if (data.error?.code === "domain_taken") return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -85,9 +94,35 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
||||
|
||||
const t = await getTranslation(ctx.user.locale ?? "en", "common");
|
||||
const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);
|
||||
let isOrganizationConfigured = false;
|
||||
|
||||
if (check === false) {
|
||||
if (IS_CALCOM) await vercelCreateDomain(slug);
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
if (process.env.VERCEL) {
|
||||
// We only want to proceed to register the subdomain for the org in Vercel
|
||||
// within a Vercel context
|
||||
isOrganizationConfigured = await vercelCreateDomain(slug);
|
||||
} else {
|
||||
// Otherwise, we proceed to send an administrative email to admins regarding
|
||||
// the need to configure DNS registry to support the newly created org
|
||||
const instanceAdmins = await prisma.user.findMany({
|
||||
where: { role: UserPermissionRole.ADMIN },
|
||||
select: { email: true },
|
||||
});
|
||||
if (instanceAdmins.length) {
|
||||
await sendAdminOrganizationNotification({
|
||||
instanceAdmins,
|
||||
orgSlug: slug,
|
||||
ownerEmail: adminEmail,
|
||||
webappIPAddress: await getIPAddress(
|
||||
WEBAPP_URL.replace("https://", "")?.replace("http://", "").replace(/(:.*)/, "")
|
||||
),
|
||||
t,
|
||||
});
|
||||
} else {
|
||||
console.warn("Organization created: subdomain not configured and couldn't notify adminnistrators");
|
||||
}
|
||||
}
|
||||
|
||||
const createOwnerOrg = await prisma.user.create({
|
||||
data: {
|
||||
@@ -118,6 +153,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
||||
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
|
||||
isOrganization: true,
|
||||
isOrganizationVerified: false,
|
||||
isOrganizationConfigured,
|
||||
orgAutoAcceptEmail: emailDomain,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
|
||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { closeComUpdateTeam } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -20,11 +22,13 @@ type UpdateOptions = {
|
||||
|
||||
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;
|
||||
const currentOrgId = ctx.user?.organization?.id || input.orgId;
|
||||
|
||||
if (!currentOrgId) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
if (!currentOrgId || ctx.user.role !== UserPermissionRole.ADMIN)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
if (!(await isOrganisationAdmin(ctx.user?.id, currentOrgId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
if (!(await isOrganisationAdmin(ctx.user?.id, currentOrgId)) || ctx.user.role !== UserPermissionRole.ADMIN)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
if (input.slug) {
|
||||
const userConflict = await prisma.team.findMany({
|
||||
@@ -51,6 +55,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
});
|
||||
|
||||
if (!prevOrganisation) throw new TRPCError({ code: "NOT_FOUND", message: "Organisation not found." });
|
||||
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), prevOrganisation.metadata);
|
||||
|
||||
const data: Prisma.TeamUpdateArgs["data"] = {
|
||||
name: input.name,
|
||||
@@ -64,6 +69,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
timeZone: input.timeZone,
|
||||
weekStart: input.weekStart,
|
||||
timeFormat: input.timeFormat,
|
||||
metadata: mergeMetadata({ ...input.metadata }),
|
||||
};
|
||||
|
||||
if (input.slug) {
|
||||
@@ -73,20 +79,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||
!prevOrganisation.slug
|
||||
) {
|
||||
// Save it on the metadata so we can use it later
|
||||
data.metadata = {
|
||||
requestedSlug: input.slug,
|
||||
};
|
||||
data.metadata = mergeMetadata({ requestedSlug: input.slug });
|
||||
} else {
|
||||
data.slug = input.slug;
|
||||
|
||||
// If we save slug, we don't need the requestedSlug anymore
|
||||
const metadataParse = teamMetadataSchema.safeParse(prevOrganisation.metadata);
|
||||
if (metadataParse.success) {
|
||||
const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {};
|
||||
data.metadata = {
|
||||
...cleanMetadata,
|
||||
};
|
||||
}
|
||||
data.metadata = mergeMetadata({
|
||||
// If we save slug, we don't need the requestedSlug anymore
|
||||
requestedSlug: undefined,
|
||||
...input.metadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export const ZUpdateInputSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
orgId: z
|
||||
.string()
|
||||
.regex(/^\d+$/)
|
||||
.transform((id) => parseInt(id))
|
||||
.or(z.number())
|
||||
.optional(),
|
||||
bio: z.string().optional(),
|
||||
logo: z
|
||||
@@ -22,6 +25,7 @@ export const ZUpdateInputSchema = z.object({
|
||||
timeZone: z.string().optional(),
|
||||
weekStart: z.string().optional(),
|
||||
timeFormat: z.number().optional(),
|
||||
metadata: teamMetadataSchema.unwrap().pick({ isOrganizationConfigured: true }).optional(),
|
||||
});
|
||||
|
||||
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import appStore from "@calcom/app-store";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendNoShowFeeChargedEmail } from "@calcom/emails";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { v4 as uuid } from "uuid";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import type { TListMembersInputSchema } from "./listMembers.schema";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
|
||||
import { closeComDeleteTeamMembership } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { Workflow } from "@prisma/client";
|
||||
import emailReminderTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailReminderTemplate";
|
||||
import { SENDER_NAME } from "@calcom/lib/constants";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import {
|
||||
MembershipRole,
|
||||
TimeUnit,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { WorkflowType } from "@calcom/ee/workflows/components/WorkflowListPage";
|
||||
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
|
||||
import type { Prisma, PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { IS_SELF_HOSTED, SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { BookingStatus, WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { removeBookingField, upsertBookingField } from "@calcom/features/eventtypes/lib/bookingFieldsManager";
|
||||
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
|
||||
import type PrismaType from "@calcom/prisma";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { WorkflowStep } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
@@ -20,7 +20,7 @@ export function getSender(
|
||||
|
||||
export async function isAuthorized(
|
||||
workflow: Pick<Workflow, "id" | "teamId" | "userId"> | null,
|
||||
prisma: typeof PrismaType,
|
||||
prisma: PrismaClient,
|
||||
currentUserId: number,
|
||||
readOnly?: boolean
|
||||
) {
|
||||
|
||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import type { PrismaClient } from "@calcom/prisma/client";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import "./next-auth";
|
||||
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
interface Window {
|
||||
Meticulous?: {
|
||||
isRunningAsTest: boolean;
|
||||
};
|
||||
}
|
||||
@@ -43,6 +43,7 @@ const HorizontalTabItem = function ({
|
||||
props.disabled && "pointer-events-none !opacity-30",
|
||||
props.className
|
||||
)}
|
||||
data-testid={`horizontal-tab-${name}`}
|
||||
aria-current={isCurrent ? "page" : undefined}>
|
||||
{props.icon && (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
||||
@@ -67,6 +67,7 @@ const VerticalTabItem = ({
|
||||
"mr-2 h-[16px] w-[16px] stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0",
|
||||
props.iconClassName
|
||||
)}
|
||||
data-testid="icon-component"
|
||||
/>
|
||||
)}
|
||||
<div className="h-fit">
|
||||
@@ -74,7 +75,7 @@ const VerticalTabItem = ({
|
||||
<Skeleton title={t(name)} as="p" className="max-w-36 min-h-4 mt-px truncate">
|
||||
{t(name)}
|
||||
</Skeleton>
|
||||
{props.isExternalLink ? <ExternalLink /> : null}
|
||||
{props.isExternalLink ? <ExternalLink data-testid="external-link" /> : null}
|
||||
</span>
|
||||
{info && (
|
||||
<Skeleton as="p" title={t(info)} className="max-w-44 mt-1 truncate text-xs font-normal">
|
||||
@@ -88,6 +89,7 @@ const VerticalTabItem = ({
|
||||
width={20}
|
||||
height={20}
|
||||
className="text-default h-auto w-[20px] stroke-[1.5px]"
|
||||
data-testid="chevron-right"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/* eslint-disable playwright/missing-playwright-await */
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import HorizontalTabs from "./HorizontalTabs";
|
||||
import VerticalTabs from "./VerticalTabs";
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useUrlMatchesCurrentUrl", () => ({
|
||||
useUrlMatchesCurrentUrl() {
|
||||
return {
|
||||
route: "/",
|
||||
pathname: "",
|
||||
query: "",
|
||||
asPath: "",
|
||||
push: vi.fn(),
|
||||
events: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
},
|
||||
beforePopState: vi.fn(() => null),
|
||||
prefetch: vi.fn(() => null),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useLocale", () => ({
|
||||
useLocale: () => ({ t: (key: string) => key, isLocaleReady: true }),
|
||||
}));
|
||||
|
||||
describe("Tests for navigation folder", () => {
|
||||
describe("Test HorizontalTabs Component", () => {
|
||||
const mockTabs = [
|
||||
{ name: "Tab 1", href: "/tab1" },
|
||||
{ name: "Tab 2", href: "/tab2", avatar: "Avatar" },
|
||||
{ name: "Tab 3", href: "/tab3" },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("Should render tabs with correct name and href", () => {
|
||||
render(<HorizontalTabs tabs={mockTabs} />);
|
||||
mockTabs.forEach((tab) => {
|
||||
const tabLabelElement = screen.getByTestId(`horizontal-tab-${tab.name}`);
|
||||
expect(tabLabelElement).toBeInTheDocument();
|
||||
|
||||
const name = screen.getByText(tab.name);
|
||||
expect(name).toBeInTheDocument();
|
||||
expect(tabLabelElement).toHaveAttribute("href", tab.href);
|
||||
});
|
||||
});
|
||||
|
||||
test("Should render actions correctly", () => {
|
||||
const handleClick = vi.fn();
|
||||
const mockActions = <button onClick={handleClick}>Actions</button>;
|
||||
render(<HorizontalTabs tabs={mockTabs} actions={mockActions} />);
|
||||
const actionsElement = screen.getByText("Actions");
|
||||
expect(actionsElement).toBeInTheDocument();
|
||||
fireEvent.click(actionsElement);
|
||||
|
||||
expect(handleClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test VerticalTabs Component", () => {
|
||||
const mockTabs = [
|
||||
{
|
||||
name: "Tab 1",
|
||||
href: "/tab1",
|
||||
disableChevron: true,
|
||||
disabled: true,
|
||||
icon: PlusIcon,
|
||||
},
|
||||
{ name: "Tab 2", href: "/tab2", isExternalLink: true },
|
||||
{ name: "Tab 3", href: "/tab3", info: "info" },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("Should render tabs with correct name and href", () => {
|
||||
render(<VerticalTabs tabs={mockTabs} />);
|
||||
mockTabs.forEach((tab) => {
|
||||
const tabLabelElement = screen.getByTestId(`vertical-tab-${tab.name}`);
|
||||
expect(tabLabelElement).toBeInTheDocument();
|
||||
|
||||
const name = screen.getByText(tab.name);
|
||||
expect(name).toBeInTheDocument();
|
||||
expect(tabLabelElement).toHaveAttribute("href", tab.href);
|
||||
});
|
||||
});
|
||||
|
||||
test("Should render correctly if props are passed", () => {
|
||||
render(<VerticalTabs tabs={mockTabs} />);
|
||||
|
||||
const iconElement = screen.getAllByTestId("icon-component");
|
||||
const externalLink = screen.getAllByTestId("external-link");
|
||||
const chevronRight = screen.getAllByTestId("chevron-right");
|
||||
|
||||
mockTabs.forEach((tab) => {
|
||||
const tabName = screen.getByText(tab.name);
|
||||
expect(tabName).toBeInTheDocument();
|
||||
|
||||
const aTag = screen.getByTestId(`vertical-tab-${tab.name}`);
|
||||
const tabContainer = tabName.closest("a");
|
||||
const infoElement = tabContainer?.querySelector("p[title='info']");
|
||||
|
||||
expect(chevronRight.length).toEqual(mockTabs.length - 1);
|
||||
if (tab.disabled) {
|
||||
expect(aTag).tabToBeDisabled();
|
||||
} else {
|
||||
expect(aTag).not.tabToBeDisabled();
|
||||
}
|
||||
|
||||
if (tab.info) {
|
||||
expect(infoElement).toBeInTheDocument();
|
||||
} else {
|
||||
expect(infoElement).toBeNull();
|
||||
}
|
||||
|
||||
if (tab.isExternalLink) {
|
||||
expect(aTag).toHaveAttribute("target", "_blank");
|
||||
} else {
|
||||
expect(aTag).toHaveAttribute("target", "_self");
|
||||
}
|
||||
});
|
||||
|
||||
expect(externalLink.length).toEqual(1);
|
||||
expect(iconElement.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Table } from "./Table";
|
||||
import { TableActions } from "./TableActions";
|
||||
import {
|
||||
Table as TableNew,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableCaption,
|
||||
} from "./TableNew";
|
||||
|
||||
export const TableNewExampleComponent = () => (
|
||||
<TableNew>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Header Column 1</TableHead>
|
||||
<TableHead>Header Column 2</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Row 1, Cell 1</TableCell>
|
||||
<TableCell>Row 1, Cell 2</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Row 2, Cell 1</TableCell>
|
||||
<TableCell>Row 2, Cell 2</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell>Row 3(footer), Cell 1</TableCell>
|
||||
<TableCell>Row 3(footer), Cell 2</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
<TableCaption>Table Caption</TableCaption>
|
||||
</TableNew>
|
||||
);
|
||||
|
||||
export const TableExampleComponent = () => (
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnTitle>Title Column 1</Table.ColumnTitle>
|
||||
<Table.ColumnTitle>Title Column 2</Table.ColumnTitle>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.Cell>Row 1, Cell 1</Table.Cell>
|
||||
<Table.Cell>Row 1, Cell 2</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>Row 2, Cell 1</Table.Cell>
|
||||
<Table.Cell>Row 2, Cell 2</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<TableActions
|
||||
actions={[
|
||||
{
|
||||
id: "action1",
|
||||
label: "Action 1",
|
||||
href: "#1",
|
||||
},
|
||||
{
|
||||
id: "action2",
|
||||
label: "Action 2",
|
||||
actions: [
|
||||
{
|
||||
id: "action3",
|
||||
label: "Action 3",
|
||||
href: "#nested-action",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Table.Row>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
);
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import {
|
||||
Examples,
|
||||
Example,
|
||||
Title,
|
||||
VariantsTable,
|
||||
CustomArgsTable,
|
||||
VariantRow,
|
||||
} from "@calcom/storybook/components";
|
||||
|
||||
import { Table } from "./Table";
|
||||
import { TableExampleComponent } from "./TableExamples";
|
||||
|
||||
<Meta title="UI/Table/Table" component={Table} />
|
||||
|
||||
<Title title="Table" suffix="Brief" subtitle="Version 1.0 — Last Update: 21 Aug 2023" />
|
||||
|
||||
## Definition
|
||||
|
||||
The `Table` component is a HTML table that utilizes Tailwind classes to enhance its styles.
|
||||
|
||||
## Structure
|
||||
|
||||
The `Table` component should be used with other table sub-components (present as object attributes) that follow the same pattern.
|
||||
These components render common elements that are styled with Tailwind. These components include:
|
||||
|
||||
- `Body` -> (`tbody`)
|
||||
- `Header` -> (`thead`)
|
||||
- `Row` -> (`tr`)
|
||||
- `ColumnTitle` -> (`th`)
|
||||
- `Cell` -> (`td`)
|
||||
|
||||
<CustomArgsTable of={Table} />
|
||||
|
||||
<Title offset title="Table" suffix="Variants" />
|
||||
|
||||
<Canvas>
|
||||
<Story name="Table">
|
||||
{(props) => (
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||
<VariantRow>
|
||||
<TableExampleComponent />
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
@@ -2,9 +2,9 @@
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { Table, TableActions } from "@calcom/ui";
|
||||
import { TableActions } from "@calcom/ui";
|
||||
|
||||
import { TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "./TableNew";
|
||||
import { TableNewExampleComponent } from "./TableExamples";
|
||||
|
||||
const mockActions = [
|
||||
{
|
||||
@@ -21,31 +21,7 @@ const mockActions = [
|
||||
|
||||
describe("tests for Table component", () => {
|
||||
test("Should render Table component correctly", () => {
|
||||
const { getByRole, getByText } = render(
|
||||
<>
|
||||
<Table>
|
||||
<>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Header Column 1</TableHead>
|
||||
<TableHead>Header Column 2</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Row 1, Cell 1</TableCell>
|
||||
<TableCell>Row 1, Cell 2</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Row 2, Cell 1</TableCell>
|
||||
<TableCell>Row 2, Cell 2</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableCaption>Table Caption</TableCaption>
|
||||
</>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
const { getByRole, getByText } = render(<TableNewExampleComponent />);
|
||||
|
||||
const headerElement1 = getByRole("columnheader", { name: "Header Column 1" });
|
||||
const headerElement2 = getByRole("columnheader", { name: "Header Column 2" });
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Canvas, Meta, Story } from "@storybook/addon-docs";
|
||||
|
||||
import {
|
||||
Examples,
|
||||
Example,
|
||||
Title,
|
||||
VariantsTable,
|
||||
CustomArgsTable,
|
||||
VariantRow,
|
||||
} from "@calcom/storybook/components";
|
||||
|
||||
import { TableNewExampleComponent } from "./TableExamples";
|
||||
import { Table } from "./TableNew";
|
||||
|
||||
<Meta title="UI/Table/TableNew" component={Table} />
|
||||
|
||||
<Title title="Table New" suffix="Brief" subtitle="Version 1.0 — Last Update: 21 Aug 2023" />
|
||||
|
||||
## Definition
|
||||
|
||||
The `Table` component (from TableNew.tsx) is a HTML table that utilizes Tailwind classes to enhance its styles.
|
||||
|
||||
## Structure
|
||||
|
||||
The `Table` component should be used with other table sub-components (exported in the same file) that follow the same pattern.
|
||||
These components render common elements that are styled with Tailwind. These components include:
|
||||
|
||||
- `TableBody` -> (`tbody`)
|
||||
- `TableHeader` -> (`thead`)
|
||||
- `TableRow` -> (`tr`)
|
||||
- `TableHead` -> (`th`)
|
||||
- `TableCell` -> (`td`)
|
||||
- `TableFooter` -> (`tfoot`)
|
||||
- `TableCaption` -> (`caption`)
|
||||
|
||||
<CustomArgsTable of={Table} />
|
||||
|
||||
<Title offset title="Table New" suffix="Variants" />
|
||||
|
||||
<Canvas>
|
||||
<Story name="Table New">
|
||||
{(props) => (
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={150}>
|
||||
<VariantRow>
|
||||
<TableNewExampleComponent />
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
@@ -44,6 +44,16 @@ vi.mock("@calcom/lib/OgImages", async () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
expect.extend({
|
||||
tabToBeDisabled(received) {
|
||||
const isDisabled = received.classList.contains("pointer-events-none");
|
||||
return {
|
||||
pass: isDisabled,
|
||||
message: () => `Expected tab to be disabled`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user