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,
|
Tooltip,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Copy, Edit } from "@calcom/ui/components/icon";
|
import { Copy, Edit } from "@calcom/ui/components/icon";
|
||||||
|
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
||||||
|
|
||||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
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 */}
|
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||||
<div className="lg:-ml-2">
|
<div className="lg:-ml-2">
|
||||||
<TextField
|
{!IS_VISUAL_REGRESSION_TESTING && (
|
||||||
disabled
|
<TextField
|
||||||
name="hashedLink"
|
disabled
|
||||||
label={t("private_link_label")}
|
name="hashedLink"
|
||||||
data-testid="generated-hash-url"
|
label={t("private_link_label")}
|
||||||
labelSrOnly
|
data-testid="generated-hash-url"
|
||||||
type="text"
|
labelSrOnly
|
||||||
hint={t("private_link_hint")}
|
type="text"
|
||||||
defaultValue={placeholderHashedLink}
|
hint={t("private_link_hint")}
|
||||||
addOnSuffix={
|
defaultValue={placeholderHashedLink}
|
||||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
addOnSuffix={
|
||||||
<Button
|
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||||
color="minimal"
|
<Button
|
||||||
size="sm"
|
color="minimal"
|
||||||
type="button"
|
size="sm"
|
||||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
type="button"
|
||||||
aria-label="copy link"
|
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||||
onClick={() => {
|
aria-label="copy link"
|
||||||
navigator.clipboard.writeText(placeholderHashedLink);
|
onClick={() => {
|
||||||
if (eventType.hashedLink) {
|
navigator.clipboard.writeText(placeholderHashedLink);
|
||||||
showToast(t("private_link_copied"), "success");
|
if (eventType.hashedLink) {
|
||||||
} else {
|
showToast(t("private_link_copied"), "success");
|
||||||
showToast(t("enabled_after_update_description"), "warning");
|
} else {
|
||||||
}
|
showToast(t("enabled_after_update_description"), "warning");
|
||||||
}}>
|
}
|
||||||
<Copy className="h-4 w-4" />
|
}}>
|
||||||
</Button>
|
<Copy className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Button>
|
||||||
}
|
</Tooltip>
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
<hr className="border-subtle" />
|
<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",
|
"name": "@calcom/web",
|
||||||
"version": "3.2.2",
|
"version": "3.2.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class MyDocument extends Document<Props> {
|
|||||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
<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: light)" content="#f9fafb" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
|
<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
|
// eslint-disable-next-line @next/next/no-sync-scripts
|
||||||
<script
|
<script
|
||||||
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
|
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
|
|
||||||
import { getLayout } from "@calcom/features/MainLayout";
|
import { getLayout } from "@calcom/features/MainLayout";
|
||||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||||
@@ -135,7 +134,6 @@ export default function AvailabilityPage() {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const orgBranding = useOrgBranding();
|
|
||||||
|
|
||||||
// Get a new searchParams string by merging the current
|
// Get a new searchParams string by merging the current
|
||||||
// searchParams with a provided key/value pair
|
// searchParams with a provided key/value pair
|
||||||
@@ -156,23 +154,22 @@ export default function AvailabilityPage() {
|
|||||||
subtitle={t("configure_availability")}
|
subtitle={t("configure_availability")}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{orgBranding && (
|
<ToggleGroup
|
||||||
<ToggleGroup
|
className="hidden md:block"
|
||||||
className="hidden md:block"
|
defaultValue={searchParams?.get("type") ?? "mine"}
|
||||||
defaultValue={searchParams?.get("type") ?? "mine"}
|
onValueChange={(value) => {
|
||||||
onValueChange={(value) => {
|
if (!value) return;
|
||||||
router.push(`${pathname}?${createQueryString("type", value)}`);
|
router.push(`${pathname}?${createQueryString("type", value)}`);
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: "mine", label: t("my_availability") },
|
{ value: "mine", label: t("my_availability") },
|
||||||
{ value: "team", label: t("team_availability") },
|
{ value: "team", label: t("team_availability") },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<NewScheduleButton />
|
<NewScheduleButton />
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
{searchParams?.get("type") === "team" && orgBranding ? (
|
{searchParams?.get("type") === "team" ? (
|
||||||
<AvailabilitySliderTable />
|
<AvailabilitySliderTable />
|
||||||
) : (
|
) : (
|
||||||
<WithQuery
|
<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 type { CalPageWrapper } from "@components/PageWrapper";
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
const Page = UnverifiedOrgsPage as CalPageWrapper;
|
const Page = AdminOrgsPage as CalPageWrapper;
|
||||||
Page.PageWrapper = PageWrapper;
|
Page.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|||||||
@@ -2010,6 +2010,21 @@
|
|||||||
"member_removed": "Member removed",
|
"member_removed": "Member removed",
|
||||||
"my_availability": "My Availability",
|
"my_availability": "My Availability",
|
||||||
"team_availability": "Team 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",
|
"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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2006,5 +2006,17 @@
|
|||||||
"member_removed": "Membre supprimé",
|
"member_removed": "Membre supprimé",
|
||||||
"my_availability": "Mes disponibilités",
|
"my_availability": "Mes disponibilités",
|
||||||
"team_availability": "Disponibilités de l'équipe",
|
"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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
"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 { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
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 type { App_RoutingForms_Form } from "@prisma/client";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
|
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
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 { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import { getSerializableForm } from "../lib/getSerializableForm";
|
import { getSerializableForm } from "../lib/getSerializableForm";
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
|
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
|
||||||
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
|
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
|
||||||
import logger from "@calcom/lib/logger";
|
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 { entries } from "@calcom/prisma/zod-utils";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
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 logger from "@calcom/lib/logger";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
|
|
||||||
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
|
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
|
|
||||||
import { getSerializableForm } from "../lib/getSerializableForm";
|
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 type { EmailVerifyLink } from "./templates/account-verify-email";
|
||||||
import AccountVerifyEmail 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 AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
|
||||||
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
|
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
|
||||||
import AttendeeCancelledSeatEmail from "./templates/attendee-cancelled-seat-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) => {
|
export const sendOrganizationEmailVerification = async (sendOrgInput: OrganizationEmailVerify) => {
|
||||||
await sendEmail(() => new OrganizationEmailVerification(sendOrgInput));
|
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 logger from "@calcom/lib/logger";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { CallToActionIcon } from "./CallToActionIcon";
|
|
||||||
import { Info } from "./Info";
|
import { Info } from "./Info";
|
||||||
|
|
||||||
export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
|
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 { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
|
||||||
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
|
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
|
||||||
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
|
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 && (
|
{!isLoading && !!event && (
|
||||||
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}>
|
<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>
|
<EventTitle className="my-2">{event?.title}</EventTitle>
|
||||||
{event.description && (
|
{event.description && (
|
||||||
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
|
<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 { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { AvatarGroup } from "@calcom/ui";
|
import { AvatarGroup } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ export interface EventMembersProps {
|
|||||||
schedulingType: PublicEvent["schedulingType"];
|
schedulingType: PublicEvent["schedulingType"];
|
||||||
users: PublicEvent["users"];
|
users: PublicEvent["users"];
|
||||||
profile: PublicEvent["profile"];
|
profile: PublicEvent["profile"];
|
||||||
|
entity: PublicEvent["entity"];
|
||||||
}
|
}
|
||||||
|
|
||||||
type Avatar = {
|
type Avatar = {
|
||||||
@@ -23,7 +25,7 @@ type Avatar = {
|
|||||||
|
|
||||||
type AvatarWithRequiredImage = Avatar & { image: string };
|
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 showMembers = schedulingType !== SchedulingType.ROUND_ROBIN;
|
||||||
const shownUsers = showMembers ? users : [];
|
const shownUsers = showMembers ? users : [];
|
||||||
|
|
||||||
@@ -37,9 +39,19 @@ export const EventMembers = ({ schedulingType, users, profile }: EventMembersPro
|
|||||||
title: `${user.name}`,
|
title: `${user.name}`,
|
||||||
image: "image" in user ? `${user.image}` : `/${user.username}/avatar.png`,
|
image: "image" in user ? `${user.image}` : `/${user.username}/avatar.png`,
|
||||||
alt: user.name || undefined,
|
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.
|
// Add profile later since we don't want to force creating an avatar for this if it doesn't exist.
|
||||||
avatars.unshift({
|
avatars.unshift({
|
||||||
title: `${profile.name}`,
|
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 type { z } from "zod";
|
||||||
|
|
||||||
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
type BookingSelect = {
|
type BookingSelect = {
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ function preprocess<T extends z.ZodType>({
|
|||||||
if (optionValue) {
|
if (optionValue) {
|
||||||
// `typeOfOptionInput` can be any of the main types. So, we the same validations should run for `optionValue`
|
// `typeOfOptionInput` can be any of the main types. So, we the same validations should run for `optionValue`
|
||||||
if (typeOfOptionInput === "phone") {
|
if (typeOfOptionInput === "phone") {
|
||||||
if (!phoneSchema.safeParse(optionValue).success) {
|
if (!(await phoneSchema.safeParseAsync(optionValue)).success) {
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
|
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 { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||||
import type { EventManagerUser } from "@calcom/core/EventManager";
|
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 sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function ApiKeyDialogForm({
|
|||||||
: `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`}
|
: `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter showDivider className="!mt-12">
|
<DialogFooter showDivider className="relative">
|
||||||
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
|
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
|
||||||
{t("done")}
|
{t("done")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -209,7 +209,7 @@ export default function ApiKeyDialogForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter showDivider className="!mt-12">
|
<DialogFooter showDivider className="relative">
|
||||||
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
|
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
import cache from "memory-cache";
|
import cache from "memory-cache";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CONSOLE_URL } from "@calcom/lib/constants";
|
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
const CACHING_TIME = 86400000; // 24 hours in milliseconds
|
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) {
|
export async function getDeploymentKey(prisma: PrismaClient) {
|
||||||
const deployment = await prisma.deployment.findUnique({
|
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 short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
import { sendSlugReplacementEmail } from "@calcom/emails/email-manager";
|
import { sendSlugReplacementEmail } from "@calcom/emails/email-manager";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { _EventTypeModel } from "@calcom/prisma/zod";
|
import { _EventTypeModel } from "@calcom/prisma/zod";
|
||||||
import { allManagedEventTypeProps, unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils";
|
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 }) {
|
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) {
|
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() {
|
async onSuccess() {
|
||||||
await utils.viewer.teams.get.invalidate();
|
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 { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson";
|
||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||||
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
|
|
||||||
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || "";
|
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({
|
const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
await utils.viewer.teams.list.invalidate();
|
await utils.viewer.teams.list.invalidate();
|
||||||
|
await utils.viewer.teams.get.invalidate();
|
||||||
await utils.viewer.organizations.listMembers.invalidate();
|
await utils.viewer.organizations.listMembers.invalidate();
|
||||||
},
|
},
|
||||||
async onError(err) {
|
async onError(err) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
showToast,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Ban, Check, MoreHorizontal, X } from "@calcom/ui/components/icon";
|
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({
|
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
|
showToast(t("success"), "success");
|
||||||
await utils.viewer.teams.get.invalidate();
|
await utils.viewer.teams.get.invalidate();
|
||||||
|
await utils.viewer.teams.hasTeamPlan.invalidate();
|
||||||
await utils.viewer.teams.list.invalidate();
|
await utils.viewer.teams.list.invalidate();
|
||||||
await utils.viewer.organizations.listMembers.invalidate();
|
await utils.viewer.organizations.listMembers.invalidate();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ export default function TeamListItem(props: Props) {
|
|||||||
|
|
||||||
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
|
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
showToast(t("success"), "success");
|
||||||
|
utils.viewer.teams.get.invalidate();
|
||||||
utils.viewer.teams.list.invalidate();
|
utils.viewer.teams.list.invalidate();
|
||||||
|
utils.viewer.teams.hasTeamPlan.invalidate();
|
||||||
utils.viewer.teams.listInvites.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 { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
|
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
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 type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
|
||||||
import {
|
import {
|
||||||
bookerLayoutOptions,
|
bookerLayoutOptions,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { PrismaClient } from "@prisma/client";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
import type { AppFlags } from "../config";
|
import type { AppFlags } from "../config";
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dayjs from "@calcom/dayjs";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
|
import { Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
|
||||||
|
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
||||||
|
|
||||||
export default function TimezoneChangeDialog() {
|
export default function TimezoneChangeDialog() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -43,7 +44,7 @@ export default function TimezoneChangeDialog() {
|
|||||||
const tzDifferent =
|
const tzDifferent =
|
||||||
!isLoading && dayjs.tz(undefined, currentTz).utcOffset() !== dayjs.tz(undefined, userTz).utcOffset();
|
!isLoading && dayjs.tz(undefined, currentTz).utcOffset() !== dayjs.tz(undefined, userTz).utcOffset();
|
||||||
const showDialog = tzDifferent && !document.cookie.includes("calcom-timezone-dialog=1");
|
const showDialog = tzDifferent && !document.cookie.includes("calcom-timezone-dialog=1");
|
||||||
setOpen(showDialog);
|
setOpen(!IS_VISUAL_REGRESSION_TESTING && showDialog);
|
||||||
}, [currentTz, isLoading, userTz]);
|
}, [currentTz, isLoading, userTz]);
|
||||||
|
|
||||||
// save cookie to not show again
|
// save cookie to not show again
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
import { Discord } from "@calcom/ui/components/icon/Discord";
|
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 { useOrgBranding } from "../ee/organizations/context/provider";
|
||||||
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
|
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
|
||||||
@@ -920,7 +921,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
|
|||||||
</ButtonOrLink>
|
</ButtonOrLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
<Credits />
|
{!IS_VISUAL_REGRESSION_TESTING && <Credits />}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|||||||
import { useMemo, useRef, useCallback, useEffect, useState } from "react";
|
import { useMemo, useRef, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||||
import { trpc } from "@calcom/trpc";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui";
|
import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui";
|
||||||
|
|
||||||
|
import { UpgradeTip } from "../../tips/UpgradeTip";
|
||||||
import { TBContext, createTimezoneBuddyStore } from "../store";
|
import { TBContext, createTimezoneBuddyStore } from "../store";
|
||||||
import { TimeDial } from "./TimeDial";
|
import { TimeDial } from "./TimeDial";
|
||||||
|
|
||||||
@@ -20,6 +23,32 @@ export interface SliderUser {
|
|||||||
dateRanges: DateRange[];
|
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() {
|
export function AvailabilitySliderTable() {
|
||||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [browsingDate, setBrowsingDate] = useState(dayjs());
|
const [browsingDate, setBrowsingDate] = useState(dayjs());
|
||||||
@@ -143,6 +172,9 @@ export function AvailabilitySliderTable() {
|
|||||||
fetchMoreOnBottomReached(tableContainerRef.current);
|
fetchMoreOnBottomReached(tableContainerRef.current);
|
||||||
}, [fetchMoreOnBottomReached]);
|
}, [fetchMoreOnBottomReached]);
|
||||||
|
|
||||||
|
// This means they are not apart of any teams so we show the upgrade tip
|
||||||
|
if (!flatData.length) return <UpgradeTeamTip />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TBContext.Provider
|
<TBContext.Provider
|
||||||
value={createTimezoneBuddyStore({
|
value={createTimezoneBuddyStore({
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
import defaultPrisma from "@calcom/prisma";
|
import defaultPrisma from "@calcom/prisma";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
export type GetSubscriberOptions = {
|
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 type { z } from "zod";
|
||||||
|
|
||||||
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
type BookingSelect = {
|
type BookingSelect = {
|
||||||
description: true;
|
description: true;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { getLocationGroupedOptions } from "@calcom/app-store/server";
|
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 { CAL_URL } from "@calcom/lib/constants";
|
||||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { SchedulingType, MembershipRole, AppCategories } from "@calcom/prisma/enums";
|
import { SchedulingType, MembershipRole, AppCategories } from "@calcom/prisma/enums";
|
||||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
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) {
|
export async function maybeGetBookingUidFromSeat(prisma: PrismaClient, uid: string) {
|
||||||
// Look bookingUid in bookingSeat
|
// Look bookingUid in bookingSeat
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ describe("slugify", () => {
|
|||||||
expect(slugify("HELLO")).toEqual("hello");
|
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("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", () => {
|
it("should keep numbers as is", () => {
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ export const slugify = (str: string) => {
|
|||||||
.normalize("NFD") // Normalize to decomposed form for handling accents
|
.normalize("NFD") // Normalize to decomposed form for handling accents
|
||||||
.replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters
|
.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(/[^\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 start
|
||||||
.replace(/-+$/, "") // Remove dashes from end
|
.replace(/-+$/, ""); // Remove dashes from end
|
||||||
.replace(/\+/g, "-"); // Transform all + to -
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default slugify;
|
export default slugify;
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ turndownService.addRule("enter", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
turndownService.addRule("ignoreEmphasized", {
|
||||||
|
filter: "em",
|
||||||
|
replacement: function (content) {
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function isShiftEnter(node: HTMLElement) {
|
function isShiftEnter(node: HTMLElement) {
|
||||||
let currentNode: HTMLElement | null | ParentNode = node;
|
let currentNode: HTMLElement | null | ParentNode = node;
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,65 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient as PrismaClientWithoutExtension } from "@prisma/client";
|
||||||
|
|
||||||
import { bookingReferenceMiddleware } from "./middleware";
|
import { bookingReferenceMiddleware } from "./middleware";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var prisma: PrismaClient | undefined;
|
var prisma: typeof prismaWithClientExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prismaOptions: Prisma.PrismaClientOptions = {};
|
const prismaOptions: Prisma.PrismaClientOptions = {};
|
||||||
|
|
||||||
if (!!process.env.NEXT_PUBLIC_DEBUG) prismaOptions.log = ["query", "error", "warn"];
|
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) =>
|
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") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
globalThis.prisma = prisma;
|
globalThis.prisma = prisma;
|
||||||
}
|
}
|
||||||
// If any changed on middleware server restart is required
|
|
||||||
bookingReferenceMiddleware(prisma);
|
|
||||||
|
|
||||||
|
export type PrismaClient = typeof prisma;
|
||||||
export default prisma;
|
export default prisma;
|
||||||
|
|
||||||
export * from "./selects";
|
export * from "./selects";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import type { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
function middleware(prisma: PrismaClient) {
|
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 { uuid } from "short-uuid";
|
||||||
import type z from "zod";
|
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 googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
|
||||||
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
|
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
|
||||||
import dayjs from "@calcom/dayjs";
|
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 { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import prisma from ".";
|
import prisma from ".";
|
||||||
import mainAppStore from "./seed-app-store";
|
import mainAppStore from "./seed-app-store";
|
||||||
|
import { createUserAndEventType } from "./seed-utils";
|
||||||
import type { teamMetadataSchema } from "./zod-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(
|
async function createTeamAndAddUsers(
|
||||||
teamInput: Prisma.TeamCreateInput,
|
teamInput: Prisma.TeamCreateInput,
|
||||||
users: { id: number; username: string; role?: MembershipRole }[] = []
|
users: { id: number; username: string; role?: MembershipRole }[] = []
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ export const teamMetadataSchema = z
|
|||||||
subscriptionItemId: z.string().nullable(),
|
subscriptionItemId: z.string().nullable(),
|
||||||
isOrganization: z.boolean().nullable(),
|
isOrganization: z.boolean().nullable(),
|
||||||
isOrganizationVerified: z.boolean().nullable(),
|
isOrganizationVerified: z.boolean().nullable(),
|
||||||
|
isOrganizationConfigured: z.boolean().nullable(),
|
||||||
orgAutoAcceptEmail: z.string().nullable(),
|
orgAutoAcceptEmail: z.string().nullable(),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import logger from "@calcom/lib/logger";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
@@ -131,17 +132,25 @@ const getUserSession = async (ctx: TRPCContextInner) => {
|
|||||||
|
|
||||||
return { user, session };
|
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({
|
return next({
|
||||||
ctx: { user, session },
|
ctx: { user, session },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const isAuthed = middleware(async ({ ctx, next }) => {
|
export const isAuthed = middleware(async ({ ctx, next }) => {
|
||||||
|
const middlewareStart = performance.now();
|
||||||
|
|
||||||
const { user, session } = await getUserSession(ctx);
|
const { user, session } = await getUserSession(ctx);
|
||||||
|
|
||||||
|
const middlewareEnd = performance.now();
|
||||||
|
logger.debug("Perf:t.isAuthed", middlewareEnd - middlewareStart);
|
||||||
|
|
||||||
if (!user || !session) {
|
if (!user || !session) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { type PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
import { isOrganization, withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils";
|
import { isOrganization, withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils";
|
||||||
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|||||||
@@ -1,136 +1,52 @@
|
|||||||
import sessionMiddleware from "../../middlewares/sessionMiddleware";
|
import sessionMiddleware from "../../middlewares/sessionMiddleware";
|
||||||
import publicProcedure from "../../procedures/publicProcedure";
|
import publicProcedure from "../../procedures/publicProcedure";
|
||||||
import { router } from "../../trpc";
|
import { importHandler, router } from "../../trpc";
|
||||||
import { slotsRouter } from "../viewer/slots/_router";
|
import { slotsRouter } from "../viewer/slots/_router";
|
||||||
import { ZEventInputSchema } from "./event.schema";
|
import { ZEventInputSchema } from "./event.schema";
|
||||||
import { i18nInputSchema } from "./i18n.schema";
|
import { i18nInputSchema } from "./i18n.schema";
|
||||||
import { ZSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
import { ZSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||||
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
|
import { ZStripeCheckoutSessionInputSchema } from "./stripeCheckoutSession.schema";
|
||||||
|
|
||||||
type PublicViewerRouterHandlerCache = {
|
const NAMESPACE = "publicViewer";
|
||||||
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 UNSTABLE_HANDLER_CACHE: PublicViewerRouterHandlerCache = {};
|
const namespaced = (s: string) => `${NAMESPACE}.${s}`;
|
||||||
|
|
||||||
// things that unauthenticated users can query about themselves
|
// things that unauthenticated users can query about themselves
|
||||||
export const publicViewerRouter = router({
|
export const publicViewerRouter = router({
|
||||||
session: publicProcedure.use(sessionMiddleware).query(async ({ ctx }) => {
|
session: publicProcedure.use(sessionMiddleware).query(async (opts) => {
|
||||||
if (!UNSTABLE_HANDLER_CACHE.session) {
|
const handler = await importHandler(namespaced("session"), () => import("./session.handler"));
|
||||||
UNSTABLE_HANDLER_CACHE.session = await import("./session.handler").then((mod) => mod.sessionHandler);
|
return handler(opts);
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
|
i18n: publicProcedure.input(i18nInputSchema).query(async (opts) => {
|
||||||
i18n: publicProcedure.input(i18nInputSchema).query(async ({ ctx, input }) => {
|
const handler = await importHandler(namespaced("i18n"), () => import("./i18n.handler"));
|
||||||
if (!UNSTABLE_HANDLER_CACHE.i18n) {
|
return handler(opts);
|
||||||
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 });
|
|
||||||
}),
|
}),
|
||||||
|
countryCode: publicProcedure.query(async (opts) => {
|
||||||
countryCode: publicProcedure.query(async ({ ctx }) => {
|
const handler = await importHandler(namespaced("countryCode"), () => import("./countryCode.handler"));
|
||||||
if (!UNSTABLE_HANDLER_CACHE.countryCode) {
|
return handler(opts);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
|
samlTenantProduct: publicProcedure.input(ZSamlTenantProductInputSchema).mutation(async (opts) => {
|
||||||
samlTenantProduct: publicProcedure.input(ZSamlTenantProductInputSchema).mutation(async ({ ctx, input }) => {
|
const handler = await importHandler(
|
||||||
if (!UNSTABLE_HANDLER_CACHE.samlTenantProduct) {
|
namespaced("samlTenantProduct"),
|
||||||
UNSTABLE_HANDLER_CACHE.samlTenantProduct = await import("./samlTenantProduct.handler").then(
|
() => import("./samlTenantProduct.handler")
|
||||||
(mod) => mod.samlTenantProductHandler
|
);
|
||||||
);
|
return handler(opts);
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
|
stripeCheckoutSession: publicProcedure.input(ZStripeCheckoutSessionInputSchema).query(async (opts) => {
|
||||||
stripeCheckoutSession: publicProcedure.input(ZStripeCheckoutSessionInputSchema).query(async ({ input }) => {
|
const handler = await importHandler(
|
||||||
if (!UNSTABLE_HANDLER_CACHE.stripeCheckoutSession) {
|
namespaced("stripeCheckoutSession"),
|
||||||
UNSTABLE_HANDLER_CACHE.stripeCheckoutSession = await import("./stripeCheckoutSession.handler").then(
|
() => import("./stripeCheckoutSession.handler")
|
||||||
(mod) => mod.stripeCheckoutSessionHandler
|
);
|
||||||
);
|
return handler(opts);
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
cityTimezones: publicProcedure.query(async () => {
|
cityTimezones: publicProcedure.query(async () => {
|
||||||
if (!UNSTABLE_HANDLER_CACHE.cityTimezones) {
|
const handler = await importHandler(namespaced("cityTimezones"), () => import("./cityTimezones.handler"));
|
||||||
UNSTABLE_HANDLER_CACHE.cityTimezones = await import("./cityTimezones.handler").then(
|
return handler();
|
||||||
(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();
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// REVIEW: This router is part of both the public and private viewer router?
|
// REVIEW: This router is part of both the public and private viewer router?
|
||||||
slots: slotsRouter,
|
slots: slotsRouter,
|
||||||
event: publicProcedure.input(ZEventInputSchema).query(async ({ ctx, input }) => {
|
event: publicProcedure.input(ZEventInputSchema).query(async (opts) => {
|
||||||
if (!UNSTABLE_HANDLER_CACHE.event) {
|
const handler = await importHandler(namespaced("event"), () => import("./event.handler"));
|
||||||
UNSTABLE_HANDLER_CACHE.event = await import("./event.handler").then((mod) => mod.eventHandler);
|
return handler(opts);
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,3 +28,5 @@ export const cityTimezonesHandler = async () => {
|
|||||||
|
|
||||||
return uniqueCities;
|
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"] ?? "";
|
const countryCode: string | string[] = req?.headers?.["x-vercel-ip-country"] ?? "";
|
||||||
return { countryCode: Array.isArray(countryCode) ? countryCode[0] : countryCode };
|
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 { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
import type { TEventInputSchema } from "./event.schema";
|
import type { TEventInputSchema } from "./event.schema";
|
||||||
|
|
||||||
@@ -19,3 +18,5 @@ export const eventHandler = async ({ ctx, input }: EventHandlerOptions) => {
|
|||||||
);
|
);
|
||||||
return event;
|
return event;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default eventHandler;
|
||||||
|
|||||||
@@ -21,3 +21,5 @@ export const i18nHandler = async ({ input }: I18nOptions) => {
|
|||||||
locale,
|
locale,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default i18nHandler;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
|
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
import type { TSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
import type { TSamlTenantProductInputSchema } from "./samlTenantProduct.schema";
|
||||||
|
|
||||||
@@ -17,3 +16,5 @@ export const samlTenantProductHandler = ({ ctx, input }: SamlTenantProductOption
|
|||||||
|
|
||||||
return samlTenantProduct(prisma, email);
|
return samlTenantProduct(prisma, email);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default samlTenantProductHandler;
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ type SessionOptions = {
|
|||||||
export const sessionHandler = async ({ ctx }: SessionOptions) => {
|
export const sessionHandler = async ({ ctx }: SessionOptions) => {
|
||||||
return ctx.session;
|
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 { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
||||||
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { AppCategories } from "@calcom/prisma/enums";
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import type { TrpcSessionUser } from "../../../trpc";
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
||||||
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import type { AppCategories } from "@calcom/prisma/enums";
|
import type { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
// import prisma from "@calcom/prisma";
|
// import prisma from "@calcom/prisma";
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
||||||
import { sendDisabledAppEmail } from "@calcom/emails";
|
import { sendDisabledAppEmail } from "@calcom/emails";
|
||||||
import { getTranslation } from "@calcom/lib/server";
|
import { getTranslation } from "@calcom/lib/server";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { AppCategories } from "@calcom/prisma/enums";
|
import { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
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 dayjs from "@calcom/dayjs";
|
||||||
import type { DateRange } from "@calcom/lib/date-ranges";
|
import type { DateRange } from "@calcom/lib/date-ranges";
|
||||||
import { buildDateRanges } from "@calcom/lib/date-ranges";
|
import { buildDateRanges } from "@calcom/lib/date-ranges";
|
||||||
@@ -15,25 +18,32 @@ type GetOptions = {
|
|||||||
input: TListTeamAvailaiblityScheme;
|
input: TListTeamAvailaiblityScheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) => {
|
async function getTeamMembers({
|
||||||
const organizationId = ctx.user.organizationId;
|
teamId,
|
||||||
|
teamIds,
|
||||||
|
cursor,
|
||||||
|
limit,
|
||||||
|
}: {
|
||||||
|
teamId?: number;
|
||||||
|
teamIds?: number[];
|
||||||
|
cursor: number | null | undefined;
|
||||||
|
limit: number;
|
||||||
|
}) {
|
||||||
|
let whereQuery: Prisma.MembershipWhereInput = {
|
||||||
|
teamId,
|
||||||
|
};
|
||||||
|
|
||||||
if (!organizationId) {
|
if (teamIds) {
|
||||||
throw new TRPCError({ code: "NOT_FOUND", message: "User is not part of any organization." });
|
whereQuery = {
|
||||||
|
teamId: {
|
||||||
|
in: teamIds,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cursor, limit } = input;
|
return await prisma.membership.findMany({
|
||||||
|
|
||||||
const getTotalMembers = await prisma.membership.count({
|
|
||||||
where: {
|
where: {
|
||||||
teamId: organizationId,
|
...whereQuery,
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// I couldnt get this query to work direct on membership table
|
|
||||||
const teamMembers = await prisma.membership.findMany({
|
|
||||||
where: {
|
|
||||||
teamId: organizationId,
|
|
||||||
accepted: true,
|
accepted: true,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -54,7 +64,128 @@ export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) =>
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
id: "asc",
|
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;
|
let nextCursor: typeof cursor | undefined = undefined;
|
||||||
if (teamMembers && teamMembers.length > limit) {
|
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 dateFrom = dayjs(input.startDate).tz(input.loggedInUsersTz).subtract(1, "day");
|
||||||
const dateTo = dayjs(input.endDate).tz(input.loggedInUsersTz).add(1, "day");
|
const dateTo = dayjs(input.endDate).tz(input.loggedInUsersTz).add(1, "day");
|
||||||
|
|
||||||
const buildMembers = teamMembers?.map(async (member) => {
|
const buildMembers = teamMembers?.map((member) => buildMember(member, dateFrom, dateTo));
|
||||||
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 members = await Promise.all(buildMembers);
|
const members = await Promise.all(buildMembers);
|
||||||
|
|
||||||
@@ -106,7 +204,7 @@ export const listTeamAvailabilityHandler = async ({ ctx, input }: GetOptions) =>
|
|||||||
rows: members || [],
|
rows: members || [],
|
||||||
nextCursor,
|
nextCursor,
|
||||||
meta: {
|
meta: {
|
||||||
totalRowCount: getTotalMembers || 0,
|
totalRowCount: totalTeamMembers,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const ZListTeamAvailaiblityScheme = z.object({
|
|||||||
startDate: z.string(),
|
startDate: z.string(),
|
||||||
endDate: z.string(),
|
endDate: z.string(),
|
||||||
loggedInUsersTz: z.string(),
|
loggedInUsersTz: z.string(),
|
||||||
|
teamId: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TListTeamAvailaiblityScheme = z.infer<typeof ZListTeamAvailaiblityScheme>;
|
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 { 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";
|
import type { Schedule } from "@calcom/types/schedule";
|
||||||
|
|
||||||
export const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => {
|
export const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { bookingMinimalSelect } 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 { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
@@ -72,6 +73,62 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
|||||||
unconfirmed: { startTime: "asc" },
|
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
|
// TODO: Fix record typing
|
||||||
const bookingWhereInputFilters: Record<string, Prisma.BookingWhereInput> = {
|
const bookingWhereInputFilters: Record<string, Prisma.BookingWhereInput> = {
|
||||||
teamIds: {
|
teamIds: {
|
||||||
@@ -80,7 +137,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
|||||||
eventType: {
|
eventType: {
|
||||||
team: {
|
team: {
|
||||||
id: {
|
id: {
|
||||||
in: input.filters?.teamIds,
|
in: filters?.teamIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -94,7 +151,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
|||||||
users: {
|
users: {
|
||||||
some: {
|
some: {
|
||||||
id: {
|
id: {
|
||||||
in: input.filters?.userIds,
|
in: filters?.userIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -106,7 +163,7 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
|||||||
AND: [
|
AND: [
|
||||||
{
|
{
|
||||||
eventTypeId: {
|
eventTypeId: {
|
||||||
in: input.filters?.eventTypeIds,
|
in: filters?.eventTypeIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -114,21 +171,100 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filtersCombined: Prisma.BookingWhereInput[] =
|
const filtersCombined: Prisma.BookingWhereInput[] =
|
||||||
input.filters &&
|
filters &&
|
||||||
Object.keys(input.filters).map((key) => {
|
Object.keys(filters).map((key) => {
|
||||||
return bookingWhereInputFilters[key];
|
return bookingWhereInputFilters[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
const passedBookingsStatusFilter = bookingListingFilters[bookingListingByStatus];
|
const bookingSelect = {
|
||||||
const orderBy = bookingListingOrderby[bookingListingByStatus];
|
...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({
|
prisma.booking.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
|
||||||
|
},
|
||||||
|
orderBy,
|
||||||
|
take: take + 1,
|
||||||
|
skip,
|
||||||
|
}),
|
||||||
|
prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
{
|
{
|
||||||
attendees: {
|
attendees: {
|
||||||
some: {
|
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: {
|
eventType: {
|
||||||
team: {
|
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: {
|
seatsReferences: {
|
||||||
some: {
|
some: {
|
||||||
@@ -162,65 +318,6 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
|||||||
],
|
],
|
||||||
AND: [passedBookingsStatusFilter, ...(filtersCombined ?? [])],
|
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,
|
orderBy,
|
||||||
take: take + 1,
|
take: take + 1,
|
||||||
skip,
|
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 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) {
|
if (booking.seatsReferences.length && !booking.eventType?.seatsShowAttendees) {
|
||||||
booking.attendees = booking.attendees.filter((attendee) => attendee.email === user.email);
|
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(),
|
endTime: booking.endTime.toISOString(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
return { bookings, recurringInfo };
|
||||||
const bookingsFetched = bookings.length;
|
}
|
||||||
let nextCursor: typeof skip | null = skip;
|
|
||||||
if (bookingsFetched > take) {
|
|
||||||
nextCursor += bookingsFetched;
|
|
||||||
} else {
|
|
||||||
nextCursor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
bookings,
|
|
||||||
recurringInfo,
|
|
||||||
nextCursor,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
|
|||||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||||
import getApps from "@calcom/app-store/utils";
|
import getApps from "@calcom/app-store/utils";
|
||||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
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 { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
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 getEventTypeById from "@calcom/lib/getEventTypeById";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
import type { TrpcSessionUser } from "../../../trpc";
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
import type { TGetInputSchema } from "./get.schema";
|
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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { orderBy } from "lodash";
|
import { orderBy } from "lodash";
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE
|
|||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma";
|
import { baseEventTypeSelect } from "@calcom/prisma";
|
||||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { PrismaClient } from "@prisma/client";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import type { NextApiResponse, GetServerSidePropsContext } from "next";
|
import type { NextApiResponse, GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
@@ -8,6 +7,7 @@ import { validateIntervalLimitOrder } from "@calcom/lib";
|
|||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { getTranslation } from "@calcom/lib/server";
|
import { getTranslation } from "@calcom/lib/server";
|
||||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/client";
|
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/client";
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
|||||||
@@ -63,11 +63,8 @@ export const viewerOrganizationsRouter = router({
|
|||||||
const handler = await importHandler(namespaced("getMembers"), () => import("./getMembers.handler"));
|
const handler = await importHandler(namespaced("getMembers"), () => import("./getMembers.handler"));
|
||||||
return handler(opts);
|
return handler(opts);
|
||||||
}),
|
}),
|
||||||
adminGetUnverified: authedAdminProcedure.query(async (opts) => {
|
adminGetAll: authedAdminProcedure.query(async (opts) => {
|
||||||
const handler = await importHandler(
|
const handler = await importHandler(namespaced("adminGetAll"), () => import("./adminGetAll.handler"));
|
||||||
namespaced("adminGetUnverified"),
|
|
||||||
() => import("./adminGetUnverified.handler")
|
|
||||||
);
|
|
||||||
return handler(opts);
|
return handler(opts);
|
||||||
}),
|
}),
|
||||||
adminVerify: authedAdminProcedure.input(ZAdminVerifyInput).mutation(async (opts) => {
|
adminVerify: authedAdminProcedure.input(ZAdminVerifyInput).mutation(async (opts) => {
|
||||||
|
|||||||
+6
-10
@@ -1,15 +1,16 @@
|
|||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import type { TrpcSessionUser } from "../../../trpc";
|
import type { TrpcSessionUser } from "../../../trpc";
|
||||||
|
|
||||||
type AdminGetUnverifiedOptions = {
|
type AdminGetAllOptions = {
|
||||||
ctx: {
|
ctx: {
|
||||||
user: NonNullable<TrpcSessionUser>;
|
user: NonNullable<TrpcSessionUser>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetUnverifiedOptions) => {
|
export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetAllOptions) => {
|
||||||
const unVerifiedTeams = await prisma.team.findMany({
|
const allOrgs = await prisma.team.findMany({
|
||||||
where: {
|
where: {
|
||||||
AND: [
|
AND: [
|
||||||
{
|
{
|
||||||
@@ -18,18 +19,13 @@ export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetUnverifiedOptio
|
|||||||
equals: true,
|
equals: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
metadata: {
|
|
||||||
path: ["isOrganizationVerified"],
|
|
||||||
equals: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
|
metadata: true,
|
||||||
members: {
|
members: {
|
||||||
where: {
|
where: {
|
||||||
role: "OWNER",
|
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;
|
export default adminGetUnverifiedHandler;
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
|
import { lookup } from "dns";
|
||||||
import { totp } from "otplib";
|
import { totp } from "otplib";
|
||||||
|
|
||||||
import { sendOrganizationEmailVerification } from "@calcom/emails";
|
import { sendOrganizationEmailVerification } from "@calcom/emails";
|
||||||
|
import { sendAdminOrganizationNotification } from "@calcom/emails";
|
||||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||||
import {
|
import {
|
||||||
IS_CALCOM,
|
|
||||||
IS_TEAM_BILLING_ENABLED,
|
IS_TEAM_BILLING_ENABLED,
|
||||||
RESERVED_SUBDOMAINS,
|
RESERVED_SUBDOMAINS,
|
||||||
IS_PRODUCTION,
|
IS_PRODUCTION,
|
||||||
|
WEBAPP_URL,
|
||||||
} from "@calcom/lib/constants";
|
} from "@calcom/lib/constants";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
@@ -27,6 +29,15 @@ type CreateOptions = {
|
|||||||
input: TCreateInputSchema;
|
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 vercelCreateDomain = async (domain: string) => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains?teamId=${process.env.TEAM_ID_VERCEL}`,
|
`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();
|
const data = await response.json();
|
||||||
|
|
||||||
// Domain is already owned by another team but you can request delegation to access it
|
// Domain is already owned by another team but you can request delegation to access it
|
||||||
if (data.error?.code === "forbidden")
|
if (data.error?.code === "forbidden") return false;
|
||||||
throw new TRPCError({ code: "CONFLICT", message: "domain_taken_team" });
|
|
||||||
|
|
||||||
// Domain is already being used by a different project
|
// Domain is already being used by a different project
|
||||||
if (data.error?.code === "domain_taken")
|
if (data.error?.code === "domain_taken") return false;
|
||||||
throw new TRPCError({ code: "CONFLICT", message: "domain_taken_project" });
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -85,9 +94,35 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
|||||||
|
|
||||||
const t = await getTranslation(ctx.user.locale ?? "en", "common");
|
const t = await getTranslation(ctx.user.locale ?? "en", "common");
|
||||||
const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);
|
const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);
|
||||||
|
let isOrganizationConfigured = false;
|
||||||
|
|
||||||
if (check === 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({
|
const createOwnerOrg = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -118,6 +153,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
|||||||
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
|
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
|
||||||
isOrganization: true,
|
isOrganization: true,
|
||||||
isOrganizationVerified: false,
|
isOrganizationVerified: false,
|
||||||
|
isOrganizationConfigured,
|
||||||
orgAutoAcceptEmail: emailDomain,
|
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 { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { Prisma } from "@prisma/client";
|
import type { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
|
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
|
||||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||||
import { closeComUpdateTeam } from "@calcom/lib/sync/SyncServiceManager";
|
import { closeComUpdateTeam } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
@@ -20,11 +22,13 @@ type UpdateOptions = {
|
|||||||
|
|
||||||
export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||||
// A user can only have one org so we pass in their currentOrgId here
|
// A user can only have one org so we pass in their currentOrgId here
|
||||||
const currentOrgId = ctx.user?.organization?.id;
|
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) {
|
if (input.slug) {
|
||||||
const userConflict = await prisma.team.findMany({
|
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." });
|
if (!prevOrganisation) throw new TRPCError({ code: "NOT_FOUND", message: "Organisation not found." });
|
||||||
|
const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), prevOrganisation.metadata);
|
||||||
|
|
||||||
const data: Prisma.TeamUpdateArgs["data"] = {
|
const data: Prisma.TeamUpdateArgs["data"] = {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
@@ -64,6 +69,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||||||
timeZone: input.timeZone,
|
timeZone: input.timeZone,
|
||||||
weekStart: input.weekStart,
|
weekStart: input.weekStart,
|
||||||
timeFormat: input.timeFormat,
|
timeFormat: input.timeFormat,
|
||||||
|
metadata: mergeMetadata({ ...input.metadata }),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (input.slug) {
|
if (input.slug) {
|
||||||
@@ -73,20 +79,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||||||
!prevOrganisation.slug
|
!prevOrganisation.slug
|
||||||
) {
|
) {
|
||||||
// Save it on the metadata so we can use it later
|
// Save it on the metadata so we can use it later
|
||||||
data.metadata = {
|
data.metadata = mergeMetadata({ requestedSlug: input.slug });
|
||||||
requestedSlug: input.slug,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
data.slug = input.slug;
|
data.slug = input.slug;
|
||||||
|
data.metadata = mergeMetadata({
|
||||||
// If we save slug, we don't need the requestedSlug anymore
|
// If we save slug, we don't need the requestedSlug anymore
|
||||||
const metadataParse = teamMetadataSchema.safeParse(prevOrganisation.metadata);
|
requestedSlug: undefined,
|
||||||
if (metadataParse.success) {
|
...input.metadata,
|
||||||
const { requestedSlug: _, ...cleanMetadata } = metadataParse.data || {};
|
});
|
||||||
data.metadata = {
|
|
||||||
...cleanMetadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
export const ZUpdateInputSchema = z.object({
|
export const ZUpdateInputSchema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
orgId: z
|
orgId: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^\d+$/)
|
.regex(/^\d+$/)
|
||||||
.transform((id) => parseInt(id))
|
.transform((id) => parseInt(id))
|
||||||
|
.or(z.number())
|
||||||
.optional(),
|
.optional(),
|
||||||
bio: z.string().optional(),
|
bio: z.string().optional(),
|
||||||
logo: z
|
logo: z
|
||||||
@@ -22,6 +25,7 @@ export const ZUpdateInputSchema = z.object({
|
|||||||
timeZone: z.string().optional(),
|
timeZone: z.string().optional(),
|
||||||
weekStart: z.string().optional(),
|
weekStart: z.string().optional(),
|
||||||
timeFormat: z.number().optional(),
|
timeFormat: z.number().optional(),
|
||||||
|
metadata: teamMetadataSchema.unwrap().pick({ isOrganizationConfigured: true }).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
|
export type TUpdateInputSchema = z.infer<typeof ZUpdateInputSchema>;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import appStore from "@calcom/app-store";
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sendNoShowFeeChargedEmail } from "@calcom/emails";
|
import { sendNoShowFeeChargedEmail } from "@calcom/emails";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
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 { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
|
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { v4 as uuid } from "uuid";
|
|||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
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";
|
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 { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import type { TListMembersInputSchema } from "./listMembers.schema";
|
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 { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
|
||||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||||
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
|
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams";
|
||||||
import { closeComDeleteTeamMembership } from "@calcom/lib/sync/SyncServiceManager";
|
import { closeComDeleteTeamMembership } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
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 emailReminderTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailReminderTemplate";
|
||||||
import { SENDER_NAME } from "@calcom/lib/constants";
|
import { SENDER_NAME } from "@calcom/lib/constants";
|
||||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { PrismaClient } from "@calcom/prisma/client";
|
|
||||||
import {
|
import {
|
||||||
MembershipRole,
|
MembershipRole,
|
||||||
TimeUnit,
|
TimeUnit,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { WorkflowType } from "@calcom/ee/workflows/components/WorkflowListPage";
|
import type { WorkflowType } from "@calcom/ee/workflows/components/WorkflowListPage";
|
||||||
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
|
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 { MembershipRole } from "@calcom/prisma/client";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
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 { IS_SELF_HOSTED, SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
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 { BookingStatus, WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@calcom/features/bookings/lib/getBookingFields";
|
} from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { removeBookingField, upsertBookingField } from "@calcom/features/eventtypes/lib/bookingFieldsManager";
|
import { removeBookingField, upsertBookingField } from "@calcom/features/eventtypes/lib/bookingFieldsManager";
|
||||||
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
|
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 type { WorkflowStep } from "@calcom/prisma/client";
|
||||||
import { MembershipRole } from "@calcom/prisma/enums";
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export function getSender(
|
|||||||
|
|
||||||
export async function isAuthorized(
|
export async function isAuthorized(
|
||||||
workflow: Pick<Workflow, "id" | "teamId" | "userId"> | null,
|
workflow: Pick<Workflow, "id" | "teamId" | "userId"> | null,
|
||||||
prisma: typeof PrismaType,
|
prisma: PrismaClient,
|
||||||
currentUserId: number,
|
currentUserId: number,
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
) {
|
) {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import type { IncomingMessage } from "http";
|
import type { IncomingMessage } from "http";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
import type { PrismaClient } from "@calcom/prisma/client";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
|
||||||
import "./next-auth";
|
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.disabled && "pointer-events-none !opacity-30",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
|
data-testid={`horizontal-tab-${name}`}
|
||||||
aria-current={isCurrent ? "page" : undefined}>
|
aria-current={isCurrent ? "page" : undefined}>
|
||||||
{props.icon && (
|
{props.icon && (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// 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",
|
"mr-2 h-[16px] w-[16px] stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0",
|
||||||
props.iconClassName
|
props.iconClassName
|
||||||
)}
|
)}
|
||||||
|
data-testid="icon-component"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="h-fit">
|
<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">
|
<Skeleton title={t(name)} as="p" className="max-w-36 min-h-4 mt-px truncate">
|
||||||
{t(name)}
|
{t(name)}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
{props.isExternalLink ? <ExternalLink /> : null}
|
{props.isExternalLink ? <ExternalLink data-testid="external-link" /> : null}
|
||||||
</span>
|
</span>
|
||||||
{info && (
|
{info && (
|
||||||
<Skeleton as="p" title={t(info)} className="max-w-44 mt-1 truncate text-xs font-normal">
|
<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}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
className="text-default h-auto w-[20px] stroke-[1.5px]"
|
className="text-default h-auto w-[20px] stroke-[1.5px]"
|
||||||
|
data-testid="chevron-right"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { fireEvent, render } from "@testing-library/react";
|
||||||
import { vi } from "vitest";
|
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 = [
|
const mockActions = [
|
||||||
{
|
{
|
||||||
@@ -21,31 +21,7 @@ const mockActions = [
|
|||||||
|
|
||||||
describe("tests for Table component", () => {
|
describe("tests for Table component", () => {
|
||||||
test("Should render Table component correctly", () => {
|
test("Should render Table component correctly", () => {
|
||||||
const { getByRole, getByText } = render(
|
const { getByRole, getByText } = render(<TableNewExampleComponent />);
|
||||||
<>
|
|
||||||
<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 headerElement1 = getByRole("columnheader", { name: "Header Column 1" });
|
const headerElement1 = getByRole("columnheader", { name: "Header Column 1" });
|
||||||
const headerElement2 = getByRole("columnheader", { name: "Header Column 2" });
|
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 {};
|
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(() => ({
|
global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||||
observe: vi.fn(),
|
observe: vi.fn(),
|
||||||
unobserve: 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