Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f4e51b859 | ||
|
|
4f5d0db792 | ||
|
|
8733bcaf7a | ||
|
|
5aaca2b7aa | ||
|
|
eacd05bf3b | ||
|
|
d51e0182b5 | ||
|
|
220897ba86 | ||
|
|
78c0203a45 | ||
|
|
720fd90066 | ||
|
|
ca8ee749a2 | ||
|
|
ce2d3a4b00 | ||
|
|
2923684c7c | ||
|
|
879ae597be | ||
|
|
d21f7ccbc8 |
No files matched your search
Vendored
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Alert } from "@calcom/ui/components/alert";
|
||||||
|
|
||||||
|
export default function Error() {
|
||||||
|
return <Alert severity="error" title="Something went wrong" />;
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { createRouterCaller } from "app/_trpc/context";
|
import { createRouterCaller } from "app/_trpc/context";
|
||||||
import type { PageProps } from "app/_types";
|
import type { PageProps } from "app/_types";
|
||||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
|
|
||||||
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
||||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||||
@@ -25,43 +24,40 @@ export const generateMetadata = async () => {
|
|||||||
const Page = async ({ searchParams: _searchParams }: PageProps) => {
|
const Page = async ({ searchParams: _searchParams }: PageProps) => {
|
||||||
const searchParams = await _searchParams;
|
const searchParams = await _searchParams;
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
try {
|
|
||||||
const [meCaller, availabilityCaller] = await Promise.all([
|
|
||||||
createRouterCaller(meRouter),
|
|
||||||
createRouterCaller(availabilityRouter),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [me, availabilities] = await Promise.all([meCaller.get(), availabilityCaller.list()]);
|
const [meCaller, availabilityCaller] = await Promise.all([
|
||||||
const organizationId = me.organization?.id ?? me.profiles[0]?.organizationId;
|
createRouterCaller(meRouter),
|
||||||
const isOrgPrivate = organizationId
|
createRouterCaller(availabilityRouter),
|
||||||
? await OrganizationRepository.checkIfPrivate({
|
]);
|
||||||
orgId: organizationId,
|
|
||||||
})
|
|
||||||
: false;
|
|
||||||
const canViewTeamAvailability = me.organization?.isOrgAdmin || !isOrgPrivate;
|
|
||||||
|
|
||||||
return (
|
const [me, availabilities] = await Promise.all([meCaller.get(), availabilityCaller.list()]);
|
||||||
<ShellMainAppDir
|
const organizationId = me.organization?.id ?? me.profiles[0]?.organizationId;
|
||||||
heading={t("availability")}
|
const isOrgPrivate = organizationId
|
||||||
subtitle={t("configure_availability")}
|
? await OrganizationRepository.checkIfPrivate({
|
||||||
CTA={
|
orgId: organizationId,
|
||||||
<AvailabilityCTA
|
})
|
||||||
toggleGroupOptions={[
|
: false;
|
||||||
{ value: "mine", label: t("my_availability") },
|
const canViewTeamAvailability = me.organization?.isOrgAdmin || !isOrgPrivate;
|
||||||
...(canViewTeamAvailability ? [{ value: "team", label: t("team_availability") }] : []),
|
|
||||||
]}
|
return (
|
||||||
/>
|
<ShellMainAppDir
|
||||||
}>
|
heading={t("availability")}
|
||||||
{searchParams?.type === "team" && canViewTeamAvailability ? (
|
subtitle={t("configure_availability")}
|
||||||
<AvailabilitySliderTable userTimeFormat={me?.timeFormat ?? null} isOrg={!!organizationId} />
|
CTA={
|
||||||
) : (
|
<AvailabilityCTA
|
||||||
<AvailabilityList availabilities={availabilities ?? { schedules: [] }} me={me} />
|
toggleGroupOptions={[
|
||||||
)}
|
{ value: "mine", label: t("my_availability") },
|
||||||
</ShellMainAppDir>
|
...(canViewTeamAvailability ? [{ value: "team", label: t("team_availability") }] : []),
|
||||||
);
|
]}
|
||||||
} catch (error) {
|
/>
|
||||||
notFound();
|
}>
|
||||||
}
|
{searchParams?.type === "team" && canViewTeamAvailability ? (
|
||||||
|
<AvailabilitySliderTable userTimeFormat={me?.timeFormat ?? null} isOrg={!!organizationId} />
|
||||||
|
) : (
|
||||||
|
<AvailabilityList availabilities={availabilities ?? { schedules: [] }} me={me} />
|
||||||
|
)}
|
||||||
|
</ShellMainAppDir>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
|
||||||
import type { PageProps } from "app/_types";
|
|
||||||
import { _generateMetadata } from "app/_utils";
|
import { _generateMetadata } from "app/_utils";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
|
|
||||||
import { getServerSideProps } from "@lib/apps/getServerSideProps";
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||||
|
import type { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||||
|
|
||||||
import AppsPage from "~/apps/apps-view";
|
import AppsPage from "~/apps/apps-view";
|
||||||
|
|
||||||
@@ -18,13 +20,43 @@ export const generateMetadata = async () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getData = withAppDirSsr(getServerSideProps);
|
const ServerPage = async () => {
|
||||||
|
const req = buildLegacyRequest(await headers(), await cookies());
|
||||||
|
const session = await getServerSession({ req });
|
||||||
|
let appStore, userAdminTeamsIds: number[];
|
||||||
|
if (session?.user?.id) {
|
||||||
|
const userAdminTeams = await UserRepository.getUserAdminTeams(session.user.id);
|
||||||
|
userAdminTeamsIds = userAdminTeams?.teams?.map(({ team }) => team.id) ?? [];
|
||||||
|
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeamsIds);
|
||||||
|
} else {
|
||||||
|
appStore = await getAppRegistry();
|
||||||
|
userAdminTeamsIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
const categoryQuery = appStore.map(({ categories }) => ({
|
||||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
categories: categories || [],
|
||||||
const props = await getData(context);
|
}));
|
||||||
|
const categories = categoryQuery.reduce((c, app) => {
|
||||||
|
for (const category of app.categories) {
|
||||||
|
c[category] = c[category] ? c[category] + 1 : 1;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
return <AppsPage {...props} />;
|
const props = {
|
||||||
|
categories: Object.entries(categories)
|
||||||
|
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||||
|
name: name as AppCategories,
|
||||||
|
count,
|
||||||
|
}))
|
||||||
|
.sort(function (a, b) {
|
||||||
|
return b.count - a.count;
|
||||||
|
}),
|
||||||
|
appStore,
|
||||||
|
userAdminTeams: userAdminTeamsIds,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AppsPage {...props} isAdmin={session?.user?.role === "ADMIN"} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ServerPage;
|
export default ServerPage;
|
||||||
@@ -13,7 +13,8 @@ export function AppHomePageSkeleton() {
|
|||||||
isPublic
|
isPublic
|
||||||
heading={t("app_store")}
|
heading={t("app_store")}
|
||||||
subtitle={t("app_store_description")}
|
subtitle={t("app_store_description")}
|
||||||
headerClassName="sm:hidden lg:block hidden">
|
headerClassName="sm:hidden lg:block hidden"
|
||||||
|
isAdmin={false}>
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
<div className="flex flex-col gap-y-8">
|
<div className="flex flex-col gap-y-8">
|
||||||
{/* Featured Categories section */}
|
{/* Featured Categories section */}
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Alert } from "@calcom/ui/components/alert";
|
||||||
|
|
||||||
|
export default function Error() {
|
||||||
|
return <Alert severity="error" title="Something went wrong" />;
|
||||||
|
}
|
||||||
+10
-17
@@ -1,6 +1,5 @@
|
|||||||
import { type Params } from "app/_types";
|
import { type Params } from "app/_types";
|
||||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
@@ -36,23 +35,17 @@ export const generateMetadata = async ({ params }: { params: Params }) => {
|
|||||||
const Page = async ({ params }: { params: Params }) => {
|
const Page = async ({ params }: { params: Params }) => {
|
||||||
const input = orgIdSchema.safeParse(await params);
|
const input = orgIdSchema.safeParse(await params);
|
||||||
|
|
||||||
if (!input.success) notFound();
|
if (!input.success) throw new Error("Invalid access");
|
||||||
|
|
||||||
try {
|
const org = await OrganizationRepository.adminFindById({ id: input.data.id });
|
||||||
const org = await OrganizationRepository.adminFindById({ id: input.data.id });
|
const t = await getTranslate();
|
||||||
const t = await getTranslate();
|
return (
|
||||||
return (
|
<SettingsHeader title={`${t("editing_org")}: ${org.name}`} description={t("admin_orgs_edit_description")}>
|
||||||
<SettingsHeader
|
<LicenseRequired>
|
||||||
title={`${t("editing_org")}: ${org.name}`}
|
<OrgForm org={org} />
|
||||||
description={t("admin_orgs_edit_description")}>
|
</LicenseRequired>
|
||||||
<LicenseRequired>
|
</SettingsHeader>
|
||||||
<OrgForm org={org} />
|
);
|
||||||
</LicenseRequired>
|
|
||||||
</SettingsHeader>
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Alert } from "@calcom/ui/components/alert";
|
||||||
|
|
||||||
|
export default function Error() {
|
||||||
|
return <Alert severity="error" title="Something went wrong" />;
|
||||||
|
}
|
||||||
+10
-17
@@ -1,6 +1,5 @@
|
|||||||
import { type Params } from "app/_types";
|
import { type Params } from "app/_types";
|
||||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
@@ -36,24 +35,18 @@ export const generateMetadata = async ({ params }: { params: Params }) => {
|
|||||||
const Page = async ({ params }: { params: Params }) => {
|
const Page = async ({ params }: { params: Params }) => {
|
||||||
const input = userIdSchema.safeParse(await params);
|
const input = userIdSchema.safeParse(await params);
|
||||||
|
|
||||||
if (!input.success) {
|
if (!input.success) throw new Error("Invalid access");
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const user = await UserRepository.adminFindById(input.data.id);
|
||||||
const user = await UserRepository.adminFindById(input.data.id);
|
const t = await getTranslate();
|
||||||
const t = await getTranslate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsHeader title={t("editing_user")} description={t("admin_users_edit_description")}>
|
<SettingsHeader title={t("editing_user")} description={t("admin_users_edit_description")}>
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
<UsersEditView user={user} />
|
<UsersEditView user={user} />
|
||||||
</LicenseRequired>
|
</LicenseRequired>
|
||||||
</SettingsHeader>
|
</SettingsHeader>
|
||||||
);
|
);
|
||||||
} catch {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Alert } from "@calcom/ui/components/alert";
|
||||||
|
|
||||||
|
export default function Error() {
|
||||||
|
return <Alert severity="error" title="Something went wrong" />;
|
||||||
|
}
|
||||||
@@ -44,9 +44,7 @@ const Page = async ({ params }: PageProps) => {
|
|||||||
// const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
// const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||||
// const user = session?.user;
|
// const user = session?.user;
|
||||||
const parsed = querySchema.safeParse(await params);
|
const parsed = querySchema.safeParse(await params);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) throw new Error("Invalid workflow id");
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
// const workflow = await WorkflowRepository.getById({ id: +parsed.data.workflow });
|
// const workflow = await WorkflowRepository.getById({ id: +parsed.data.workflow });
|
||||||
// let verifiedEmails, verifiedNumbers;
|
// let verifiedEmails, verifiedNumbers;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -9,15 +8,14 @@ import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
|||||||
|
|
||||||
type AppsLayoutProps = {
|
type AppsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
isAdmin: boolean;
|
||||||
actions?: (className?: string) => JSX.Element;
|
actions?: (className?: string) => JSX.Element;
|
||||||
emptyStore?: boolean;
|
emptyStore?: boolean;
|
||||||
} & Omit<ComponentProps<typeof Shell>, "actions">;
|
} & Omit<ComponentProps<typeof Shell>, "actions">;
|
||||||
|
|
||||||
export default function AppsLayout({ children, actions, emptyStore, ...rest }: AppsLayoutProps) {
|
export default function AppsLayout({ children, actions, emptyStore, isAdmin, ...rest }: AppsLayoutProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const session = useSession();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isAdmin = session.data?.user.role === "ADMIN";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest} actions={actions?.("block")}>
|
<Shell {...rest} actions={actions?.("block")}>
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
|
|
||||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
||||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
|
||||||
import type { AppCategories } from "@calcom/prisma/enums";
|
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const { req } = context;
|
|
||||||
|
|
||||||
const session = await getServerSession({ req });
|
|
||||||
|
|
||||||
let appStore, userAdminTeamsIds: number[];
|
|
||||||
if (session?.user?.id) {
|
|
||||||
const userAdminTeams = await UserRepository.getUserAdminTeams(session.user.id);
|
|
||||||
userAdminTeamsIds = userAdminTeams?.teams?.map(({ team }) => team.id) ?? [];
|
|
||||||
appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeamsIds);
|
|
||||||
} else {
|
|
||||||
appStore = await getAppRegistry();
|
|
||||||
userAdminTeamsIds = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const categoryQuery = appStore.map(({ categories }) => ({
|
|
||||||
categories: categories || [],
|
|
||||||
}));
|
|
||||||
const categories = categoryQuery.reduce((c, app) => {
|
|
||||||
for (const category of app.categories) {
|
|
||||||
c[category] = c[category] ? c[category] + 1 : 1;
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}, {} as Record<string, number>);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
categories: Object.entries(categories)
|
|
||||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
|
||||||
name: name as AppCategories,
|
|
||||||
count,
|
|
||||||
}))
|
|
||||||
.sort(function (a, b) {
|
|
||||||
return b.count - a.count;
|
|
||||||
}),
|
|
||||||
appStore,
|
|
||||||
userAdminTeams: userAdminTeamsIds,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -8,15 +8,14 @@ import { AppStoreCategories } from "@calcom/features/apps/components/Categories"
|
|||||||
import { PopularAppsSlider } from "@calcom/features/apps/components/PopularAppsSlider";
|
import { PopularAppsSlider } from "@calcom/features/apps/components/PopularAppsSlider";
|
||||||
import { RecentAppsSlider } from "@calcom/features/apps/components/RecentAppsSlider";
|
import { RecentAppsSlider } from "@calcom/features/apps/components/RecentAppsSlider";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import type { AppCategories } from "@calcom/prisma/enums";
|
||||||
|
import type { AppFrontendPayload } from "@calcom/types/App";
|
||||||
import classNames from "@calcom/ui/classNames";
|
import classNames from "@calcom/ui/classNames";
|
||||||
import { TextField } from "@calcom/ui/components/form";
|
import { TextField } from "@calcom/ui/components/form";
|
||||||
import { Icon } from "@calcom/ui/components/icon";
|
import { Icon } from "@calcom/ui/components/icon";
|
||||||
import type { HorizontalTabItemProps } from "@calcom/ui/components/navigation";
|
import type { HorizontalTabItemProps } from "@calcom/ui/components/navigation";
|
||||||
import { HorizontalTabs } from "@calcom/ui/components/navigation";
|
import { HorizontalTabs } from "@calcom/ui/components/navigation";
|
||||||
|
|
||||||
import { type getServerSideProps } from "@lib/apps/getServerSideProps";
|
|
||||||
|
|
||||||
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||||
|
|
||||||
const tabs: HorizontalTabItemProps[] = [
|
const tabs: HorizontalTabItemProps[] = [
|
||||||
@@ -51,15 +50,24 @@ function AppsSearch({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
export type PageProps = {
|
||||||
|
categories: {
|
||||||
|
name: AppCategories;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
appStore: AppFrontendPayload[];
|
||||||
|
userAdminTeams: number[];
|
||||||
|
isAdmin: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Apps({ categories, appStore, userAdminTeams }: PageProps) {
|
export default function Apps({ isAdmin, categories, appStore, userAdminTeams }: PageProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppsLayout
|
<AppsLayout
|
||||||
isPublic
|
isPublic
|
||||||
|
isAdmin={isAdmin}
|
||||||
heading={t("app_store")}
|
heading={t("app_store")}
|
||||||
subtitle={t("app_store_description")}
|
subtitle={t("app_store_description")}
|
||||||
actions={(className) => (
|
actions={(className) => (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "5.2.6",
|
"version": "5.2.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { expect, test, beforeEach, vi, describe } from "vitest";
|
|||||||
import "vitest-fetch-mock";
|
import "vitest-fetch-mock";
|
||||||
|
|
||||||
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
|
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
|
||||||
|
import { getTimeMin, getTimeMax } from "@calcom/features/calendar-cache/lib/datesForCache";
|
||||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||||
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
||||||
|
|
||||||
@@ -187,8 +188,8 @@ test("Calendar Cache is being read on cache HIT", async () => {
|
|||||||
await calendarCache.upsertCachedAvailability(
|
await calendarCache.upsertCachedAvailability(
|
||||||
credentialInDb1.id,
|
credentialInDb1.id,
|
||||||
{
|
{
|
||||||
timeMin: dateFrom1,
|
timeMin: getTimeMin(dateFrom1),
|
||||||
timeMax: dateTo1,
|
timeMax: getTimeMax(dateTo1),
|
||||||
items: [{ id: testSelectedCalendar.externalId }],
|
items: [{ id: testSelectedCalendar.externalId }],
|
||||||
},
|
},
|
||||||
JSON.parse(
|
JSON.parse(
|
||||||
|
|||||||
@@ -663,7 +663,13 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
): Promise<calendar_v3.Schema$FreeBusyResponse> {
|
): Promise<calendar_v3.Schema$FreeBusyResponse> {
|
||||||
if (shouldServeCache === false) return await this.fetchAvailability(args);
|
if (shouldServeCache === false) return await this.fetchAvailability(args);
|
||||||
const calendarCache = await CalendarCache.init(null);
|
const calendarCache = await CalendarCache.init(null);
|
||||||
const cached = await calendarCache.getCachedAvailability(this.credential.id, args);
|
const cached = await calendarCache.getCachedAvailability(this.credential.id, {
|
||||||
|
// Expand the start date to the start of the month to increase cache hits
|
||||||
|
timeMin: getTimeMin(args.timeMin),
|
||||||
|
// Expand the end date to the end of the month to increase cache hits
|
||||||
|
timeMax: getTimeMax(args.timeMax),
|
||||||
|
items: args.items,
|
||||||
|
});
|
||||||
if (cached) return cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
|
if (cached) return cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
|
||||||
return await this.fetchAvailability(args);
|
return await this.fetchAvailability(args);
|
||||||
}
|
}
|
||||||
@@ -1055,9 +1061,9 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
|
|
||||||
for (const [_eventTypeId, selectedCalendars] of Array.from(selectedCalendarsPerEventType.entries())) {
|
for (const [_eventTypeId, selectedCalendars] of Array.from(selectedCalendarsPerEventType.entries())) {
|
||||||
const parsedArgs = {
|
const parsedArgs = {
|
||||||
/** Expand the start date to the start of the month */
|
/** Expand the start date to the start of the month to increase cache hits */
|
||||||
timeMin: getTimeMin(),
|
timeMin: getTimeMin(),
|
||||||
/** Expand the end date to the end of the month */
|
/** Expand the end date to the end of the month to increase cache hits */
|
||||||
timeMax: getTimeMax(),
|
timeMax: getTimeMax(),
|
||||||
// Dont use eventTypeId in key because it can be used by any eventType
|
// Dont use eventTypeId in key because it can be used by any eventType
|
||||||
// The only reason we are building it per eventType is because there can be different groups of calendars to lookup the availability for
|
// The only reason we are building it per eventType is because there can be different groups of calendars to lookup the availability for
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
@@ -14,7 +14,7 @@ export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
|
|||||||
}),
|
}),
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: getReplyToEmail(this.calEvent),
|
...getReplyToHeader(this.calEvent),
|
||||||
subject: `${this.t("guests_added_event_type_subject", {
|
subject: `${this.t("guests_added_event_type_subject", {
|
||||||
eventType: this.calEvent.type,
|
eventType: this.calEvent.type,
|
||||||
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
@@ -8,7 +8,7 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail
|
|||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: getReplyToEmail(this.calEvent),
|
...getReplyToHeader(this.calEvent),
|
||||||
subject: `${this.attendee.language.translate("complete_your_booking_subject", {
|
subject: `${this.attendee.language.translate("complete_your_booking_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
@@ -15,7 +15,7 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
|
|||||||
}),
|
}),
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: getReplyToEmail(this.calEvent),
|
...getReplyToHeader(this.calEvent),
|
||||||
subject: `${this.t("event_cancelled_subject", {
|
subject: `${this.t("event_cancelled_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
@@ -8,7 +8,7 @@ export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail {
|
|||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: getReplyToEmail(this.calEvent),
|
...getReplyToHeader(this.calEvent),
|
||||||
subject: `${this.t("event_no_longer_attending_subject", {
|
subject: `${this.t("event_no_longer_attending_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// TODO: We should find a way to keep App specific email templates within the App itself
|
// TODO: We should find a way to keep App specific email templates within the App itself
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
|
|
||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
@@ -26,12 +26,10 @@ export default class AttendeeDailyVideoDownloadRecordingEmail extends BaseEmail
|
|||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [
|
...getReplyToHeader(
|
||||||
...this.calEvent.attendees
|
this.calEvent,
|
||||||
.filter(({ email }) => email !== this.attendee.email)
|
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||||
.map(({ email }) => email),
|
),
|
||||||
getReplyToEmail(this.calEvent),
|
|
||||||
],
|
|
||||||
subject: `${this.t("download_recording_subject", {
|
subject: `${this.t("download_recording_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
|
|
||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
@@ -37,12 +37,10 @@ export default class AttendeeDailyVideoDownloadTranscriptEmail extends BaseEmail
|
|||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [
|
...getReplyToHeader(
|
||||||
...this.calEvent.attendees
|
this.calEvent,
|
||||||
.filter(({ email }) => email !== this.attendee.email)
|
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||||
.map(({ email }) => email),
|
),
|
||||||
getReplyToEmail(this.calEvent),
|
|
||||||
],
|
|
||||||
subject: `${this.t("download_transcript_email_subject", {
|
subject: `${this.t("download_transcript_email_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
@@ -9,7 +9,7 @@ export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
|
|||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: getReplyToEmail(this.calEvent),
|
...getReplyToHeader(this.calEvent),
|
||||||
subject: `${this.t("event_declined_subject", {
|
subject: `${this.t("event_declined_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
@@ -14,7 +14,7 @@ export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail
|
|||||||
}),
|
}),
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: getReplyToEmail(this.calEvent),
|
...getReplyToHeader(this.calEvent),
|
||||||
subject: `${this.t("location_changed_event_type_subject", {
|
subject: `${this.t("location_changed_event_type_subject", {
|
||||||
eventType: this.calEvent.type,
|
eventType: this.calEvent.type,
|
||||||
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
@@ -8,16 +8,13 @@ import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
|||||||
export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
|
export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
|
||||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = this.calEvent.attendees.map((attendee) => attendee.email);
|
const toAddresses = this.calEvent.attendees.map((attendee) => attendee.email);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [
|
...getReplyToHeader(
|
||||||
...this.calEvent.attendees
|
this.calEvent,
|
||||||
.filter(({ email }) => email !== this.attendee.email)
|
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||||
.map(({ email }) => email),
|
),
|
||||||
getReplyToEmail(this.calEvent),
|
|
||||||
],
|
|
||||||
subject: `${this.calEvent.attendees[0].language.translate("booking_submitted_subject", {
|
subject: `${this.calEvent.attendees[0].language.translate("booking_submitted_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
@@ -15,12 +15,10 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
|||||||
}),
|
}),
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [
|
...getReplyToHeader(
|
||||||
...this.calEvent.attendees
|
this.calEvent,
|
||||||
.filter(({ email }) => email !== this.attendee.email)
|
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||||
.map(({ email }) => email),
|
),
|
||||||
getReplyToEmail(this.calEvent),
|
|
||||||
],
|
|
||||||
subject: `${this.attendee.language.translate("event_type_has_been_rescheduled_on_time_date", {
|
subject: `${this.attendee.language.translate("event_type_has_been_rescheduled_on_time_date", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
|
|||||||
import { default as cloneDeep } from "lodash/cloneDeep";
|
import { default as cloneDeep } from "lodash/cloneDeep";
|
||||||
|
|
||||||
import { getRichDescription } from "@calcom/lib/CalEventParser";
|
import { getRichDescription } from "@calcom/lib/CalEventParser";
|
||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
@@ -40,12 +40,10 @@ export default class AttendeeScheduledEmail extends BaseEmail {
|
|||||||
}),
|
}),
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [
|
...getReplyToHeader(
|
||||||
...this.calEvent.attendees
|
this.calEvent,
|
||||||
.filter(({ email }) => email !== this.attendee.email)
|
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||||
.map(({ email }) => email),
|
),
|
||||||
getReplyToEmail(this.calEvent),
|
|
||||||
],
|
|
||||||
subject: `${this.calEvent.title}`,
|
subject: `${this.calEvent.title}`,
|
||||||
html: await this.getHtml(clonedCalEvent, this.attendee),
|
html: await this.getHtml(clonedCalEvent, this.attendee),
|
||||||
text: this.getTextBody(),
|
text: this.getTextBody(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
@@ -15,12 +15,10 @@ export default class AttendeeUpdatedEmail extends AttendeeScheduledEmail {
|
|||||||
}),
|
}),
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [
|
...getReplyToHeader(
|
||||||
...this.calEvent.attendees
|
this.calEvent,
|
||||||
.filter(({ email }) => email !== this.attendee.email)
|
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||||
.map(({ email }) => email),
|
),
|
||||||
getReplyToEmail(this.calEvent),
|
|
||||||
],
|
|
||||||
subject: `${this.attendee.language.translate("event_type_has_been_updated", {
|
subject: `${this.attendee.language.translate("event_type_has_been_updated", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
@@ -9,7 +9,7 @@ export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail {
|
|||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: getReplyToEmail(this.calEvent),
|
...getReplyToHeader(this.calEvent),
|
||||||
subject: `${this.attendee.language.translate("no_show_fee_charged_email_subject", {
|
subject: `${this.attendee.language.translate("no_show_fee_charged_email_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
@@ -16,7 +17,10 @@ export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
|
|||||||
}),
|
}),
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email)],
|
...getReplyToHeader(
|
||||||
|
this.calEvent,
|
||||||
|
this.calEvent.attendees.map(({ email }) => email)
|
||||||
|
),
|
||||||
subject: `${this.t("guests_added_event_type_subject", {
|
subject: `${this.t("guests_added_event_type_subject", {
|
||||||
eventType: this.calEvent.type,
|
eventType: this.calEvent.type,
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
|
|
||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
@@ -23,7 +24,10 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
|
|||||||
return {
|
return {
|
||||||
to: `${this.calEvent.organizer.email}>`,
|
to: `${this.calEvent.organizer.email}>`,
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email)],
|
...getReplyToHeader(
|
||||||
|
this.calEvent,
|
||||||
|
this.calEvent.attendees.map(({ email }) => email)
|
||||||
|
),
|
||||||
subject: `${this.t("download_recording_subject", {
|
subject: `${this.t("download_recording_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
|
|
||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
@@ -35,7 +36,10 @@ export default class OrganizerDailyVideoDownloadTranscriptEmail extends BaseEmai
|
|||||||
return {
|
return {
|
||||||
to: `${this.calEvent.organizer.email}>`,
|
to: `${this.calEvent.organizer.email}>`,
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email)],
|
...getReplyToHeader(
|
||||||
|
this.calEvent,
|
||||||
|
this.calEvent.attendees.map(({ email }) => email)
|
||||||
|
),
|
||||||
subject: `${this.t("download_transcript_email_subject", {
|
subject: `${this.t("download_transcript_email_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
@@ -16,7 +17,10 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai
|
|||||||
}),
|
}),
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email)],
|
...getReplyToHeader(
|
||||||
|
this.calEvent,
|
||||||
|
this.calEvent.attendees.map(({ email }) => email)
|
||||||
|
),
|
||||||
subject: `${this.t("location_changed_event_type_subject", {
|
subject: `${this.t("location_changed_event_type_subject", {
|
||||||
eventType: this.calEvent.type,
|
eventType: this.calEvent.type,
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { checkIfUserHasFeatureController } from "@calcom/features/flags/operations/check-if-user-has-feature.controller";
|
import { checkIfUserHasFeatureController } from "@calcom/features/flags/operations/check-if-user-has-feature.controller";
|
||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
@@ -21,7 +22,10 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
|
|||||||
return {
|
return {
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email)],
|
...getReplyToHeader(
|
||||||
|
this.calEvent,
|
||||||
|
this.calEvent.attendees.map(({ email }) => email)
|
||||||
|
),
|
||||||
subject: `${this.t("awaiting_approval")}: ${this.calEvent.title}`,
|
subject: `${this.t("awaiting_approval")}: ${this.calEvent.title}`,
|
||||||
html: await this.getHtmlRequestEmail(template, this.calEvent, this.calEvent.organizer),
|
html: await this.getHtmlRequestEmail(template, this.calEvent, this.calEvent.organizer),
|
||||||
text: this.getTextBody("event_awaiting_approval"),
|
text: this.getTextBody("event_awaiting_approval"),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import OrganizerRequestEmail from "./organizer-request-email";
|
import OrganizerRequestEmail from "./organizer-request-email";
|
||||||
@@ -10,7 +11,10 @@ export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail
|
|||||||
return {
|
return {
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email)],
|
...getReplyToHeader(
|
||||||
|
this.calEvent,
|
||||||
|
this.calEvent.attendees.map(({ email }) => email)
|
||||||
|
),
|
||||||
subject: `${this.t("event_awaiting_approval_subject", {
|
subject: `${this.t("event_awaiting_approval_subject", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
@@ -9,7 +9,6 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
|||||||
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
||||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: generateIcsFile({
|
icalEvent: generateIcsFile({
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
@@ -18,7 +17,10 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
|||||||
}),
|
}),
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email), getReplyToEmail(this.calEvent)],
|
...getReplyToHeader(
|
||||||
|
this.calEvent,
|
||||||
|
this.calEvent.attendees.map(({ email }) => email)
|
||||||
|
),
|
||||||
subject: `${this.calEvent.organizer.language.translate("event_type_has_been_rescheduled_on_time_date", {
|
subject: `${this.calEvent.organizer.language.translate("event_type_has_been_rescheduled_on_time_date", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { default as cloneDeep } from "lodash/cloneDeep";
|
|||||||
|
|
||||||
import { getRichDescription } from "@calcom/lib/CalEventParser";
|
import { getRichDescription } from "@calcom/lib/CalEventParser";
|
||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
@@ -49,7 +50,10 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
|||||||
}),
|
}),
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email)],
|
...getReplyToHeader(
|
||||||
|
this.calEvent,
|
||||||
|
this.calEvent.attendees.map(({ email }) => email)
|
||||||
|
),
|
||||||
subject: `${this.newSeat ? `${this.t("new_attendee")}: ` : ""}${this.calEvent.title}`,
|
subject: `${this.newSeat ? `${this.t("new_attendee")}: ` : ""}${this.calEvent.title}`,
|
||||||
html: await this.getHtml(
|
html: await this.getHtml(
|
||||||
clonedCalEvent,
|
clonedCalEvent,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type WorkflowEmailData = {
|
|||||||
to: string;
|
to: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
html: string;
|
html: string;
|
||||||
replyTo: string;
|
replyTo?: string;
|
||||||
sender?: string | null;
|
sender?: string | null;
|
||||||
attachments?: Attachment[];
|
attachments?: Attachment[];
|
||||||
};
|
};
|
||||||
@@ -31,7 +31,7 @@ export default class WorkflowEmail extends BaseEmail {
|
|||||||
return {
|
return {
|
||||||
to: this.mailData.to,
|
to: this.mailData.to,
|
||||||
from: `${this.mailData.sender || SENDER_NAME} <${this.getMailerOptions().from}>`,
|
from: `${this.mailData.sender || SENDER_NAME} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: this.mailData.replyTo,
|
...(this.mailData.replyTo && { replyTo: this.mailData.replyTo }),
|
||||||
subject: this.mailData.subject,
|
subject: this.mailData.subject,
|
||||||
html: addHTMLStyles(this.mailData.html),
|
html: addHTMLStyles(this.mailData.html),
|
||||||
attachments: this.mailData.attachments,
|
attachments: this.mailData.attachments,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ vi.mock("@calcom/lib/webstorage", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("Last Booking Response", () => {
|
describe.skip("Last Booking Response", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
|
|
||||||
const responsesToStore = ["email", "name"];
|
// const responsesToStore = ["email", "name"];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const setLastBookingResponse = (responses: Record<string, unknown> | null) => {
|
export const setLastBookingResponse = (responses: Record<string, unknown> | null) => {
|
||||||
if (!responses) return;
|
return;
|
||||||
const prevResponse = Object.fromEntries(
|
// if (!responses) return;
|
||||||
Object.entries(responses).filter(([key]) => responsesToStore.includes(key))
|
// const prevResponse = Object.fromEntries(
|
||||||
);
|
// Object.entries(responses).filter(([key]) => responsesToStore.includes(key))
|
||||||
localStorage.setItem("lastBookingResponse", JSON.stringify(prevResponse));
|
// );
|
||||||
|
// localStorage.setItem("lastBookingResponse", JSON.stringify(prevResponse));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLastBookingResponse = () => {
|
export const getLastBookingResponse = () => {
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ async function getBooking(prisma: PrismaClient, uid: string, isSeatedEvent?: boo
|
|||||||
name: true,
|
name: true,
|
||||||
bookingSeat: true,
|
bookingSeat: true,
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
id: "asc",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import prisma from "@calcom/prisma";
|
|||||||
import type { Calendar, SelectedCalendarEventTypeIds } from "@calcom/types/Calendar";
|
import type { Calendar, SelectedCalendarEventTypeIds } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import type { ICalendarCacheRepository } from "./calendar-cache.repository.interface";
|
import type { ICalendarCacheRepository } from "./calendar-cache.repository.interface";
|
||||||
import { getTimeMax, getTimeMin } from "./lib/datesForCache";
|
|
||||||
|
|
||||||
const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] });
|
const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] });
|
||||||
|
|
||||||
@@ -19,8 +18,8 @@ function parseKeyForCache(args: FreeBusyArgs): string {
|
|||||||
// Ensure that calendarIds are unique
|
// Ensure that calendarIds are unique
|
||||||
const uniqueItems = uniqueBy(args.items, ["id"]);
|
const uniqueItems = uniqueBy(args.items, ["id"]);
|
||||||
const key = JSON.stringify({
|
const key = JSON.stringify({
|
||||||
timeMin: getTimeMin(args.timeMin),
|
timeMin: args.timeMin,
|
||||||
timeMax: getTimeMax(args.timeMax),
|
timeMax: args.timeMax,
|
||||||
items: uniqueItems,
|
items: uniqueItems,
|
||||||
});
|
});
|
||||||
return key;
|
return key;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { getTimeMin, getTimeMax } from "./datesForCache";
|
import { getTimeMin, getTimeMax } from "./datesForCache";
|
||||||
|
|
||||||
describe("getTimeMin", () => {
|
describe("getTimeMin", () => {
|
||||||
// Tested on multiple dates
|
// Tested on multiple dates
|
||||||
// vi.setSystemTime("2030-04-21T00:00:13Z");
|
vi.setSystemTime("2025-04-24T00:00:13Z");
|
||||||
|
|
||||||
it("should return start of current month when no date is passed", () => {
|
it("should return start of current month when no date is passed", () => {
|
||||||
const result = getTimeMin();
|
const result = getTimeMin();
|
||||||
const expected = new Date();
|
const expected = new Date();
|
||||||
@@ -74,4 +75,23 @@ describe("getTimeMax", () => {
|
|||||||
const result = getTimeMax(testDate);
|
const result = getTimeMax(testDate);
|
||||||
expect(result).toMatchInlineSnapshot(`"2024-11-01T00:00:00.000Z"`);
|
expect(result).toMatchInlineSnapshot(`"2024-11-01T00:00:00.000Z"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle year changes", () => {
|
||||||
|
const testDate = "2024-12-24T23:59:59Z";
|
||||||
|
const result = getTimeMax(testDate);
|
||||||
|
expect(result).toMatchInlineSnapshot(`"2025-02-01T00:00:00.000Z"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle next month", () => {
|
||||||
|
const testDate = "2025-05-01T02:59:59.999Z";
|
||||||
|
const result = getTimeMax(testDate);
|
||||||
|
expect(result).toMatchInlineSnapshot(`"2025-06-01T00:00:00.000Z"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle special case where timeMax is more than 2 months from now but less than 3 months", () => {
|
||||||
|
vi.setSystemTime("2025-04-24T00:00:13Z");
|
||||||
|
const testDate = "2025-06-02T23:59:59.999Z";
|
||||||
|
const result = getTimeMax(testDate);
|
||||||
|
expect(result).toMatchInlineSnapshot(`"2025-06-01T00:00:00.000Z"`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -23,44 +23,37 @@ export function getTimeMax(timeMax?: string) {
|
|||||||
const currentMonth = now.getUTCMonth();
|
const currentMonth = now.getUTCMonth();
|
||||||
const currentYear = now.getUTCFullYear();
|
const currentYear = now.getUTCFullYear();
|
||||||
|
|
||||||
|
// If no date is passed, return start of the month two months from *now*.
|
||||||
if (!timeMax) {
|
if (!timeMax) {
|
||||||
// If no date passed, return start of the overnext month from current date
|
|
||||||
const result = new Date(Date.UTC(currentYear, currentMonth + 2, 1));
|
const result = new Date(Date.UTC(currentYear, currentMonth + 2, 1));
|
||||||
result.setUTCHours(0, 0, 0, 0);
|
result.setUTCHours(0, 0, 0, 0);
|
||||||
return result.toISOString();
|
return result.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a date is passed, determine the base month/year for calculation.
|
||||||
const date = new Date(timeMax);
|
const date = new Date(timeMax);
|
||||||
const dateMonth = date.getUTCMonth();
|
const dateMonth = date.getUTCMonth();
|
||||||
const dateYear = date.getUTCFullYear();
|
const dateYear = date.getUTCFullYear();
|
||||||
|
|
||||||
// If the date is within current month or next month, return start of overnext month
|
let baseYear = currentYear;
|
||||||
// Otherwise, return start of overnext month from the date
|
let baseMonth = currentMonth;
|
||||||
let targetYear = dateYear;
|
|
||||||
let targetMonth = dateMonth;
|
|
||||||
|
|
||||||
// Check if date is within current month or next month
|
// Check if date is within the current month or the next two months relative to *now*.
|
||||||
const isWithinCurrentOrNextMonth =
|
const isWithinCurrentOrNextTwoMonths =
|
||||||
(dateYear === currentYear && dateMonth <= currentMonth + 1) ||
|
(dateYear === currentYear && dateMonth <= currentMonth + 2) ||
|
||||||
(dateYear === currentYear + 1 && currentMonth === 11 && dateMonth === 0);
|
(dateYear === currentYear + 1 &&
|
||||||
|
((currentMonth === 10 && dateMonth === 0) || (currentMonth === 11 && dateMonth <= 1)));
|
||||||
|
|
||||||
if (isWithinCurrentOrNextMonth) {
|
// If the input date is beyond the next two months relative to *now*,
|
||||||
// For dates within current month or next month, return start of overnext month
|
// use the *input date's* month/year as the base.
|
||||||
targetMonth = dateMonth + 2;
|
if (!isWithinCurrentOrNextTwoMonths) {
|
||||||
if (targetMonth > 11) {
|
baseYear = dateYear;
|
||||||
targetMonth = 0;
|
baseMonth = dateMonth;
|
||||||
targetYear++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For dates beyond overnext month, return start of overnext month from the date
|
|
||||||
targetMonth = dateMonth + 2;
|
|
||||||
if (targetMonth > 11) {
|
|
||||||
targetMonth = 0;
|
|
||||||
targetYear++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Otherwise, the base remains the current year/month.
|
||||||
|
|
||||||
const result = new Date(Date.UTC(targetYear, targetMonth, 1));
|
// Calculate the start of the month two months after the determined base date.
|
||||||
|
const result = new Date(Date.UTC(baseYear, baseMonth + 2, 1));
|
||||||
result.setUTCHours(0, 0, 0, 0);
|
result.setUTCHours(0, 0, 0, 0);
|
||||||
return result.toISOString();
|
return result.toISOString();
|
||||||
}
|
}
|
||||||
@@ -332,10 +332,12 @@ export async function handler(req: NextRequest) {
|
|||||||
html: emailContent.emailBody,
|
html: emailContent.emailBody,
|
||||||
batchId: batchId,
|
batchId: batchId,
|
||||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||||
replyTo:
|
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||||
reminder.booking?.eventType?.customReplyToEmail ??
|
replyTo:
|
||||||
reminder.booking?.userPrimaryEmail ??
|
reminder.booking?.eventType?.customReplyToEmail ??
|
||||||
reminder.booking.user?.email,
|
reminder.booking?.userPrimaryEmail ??
|
||||||
|
reminder.booking.user?.email,
|
||||||
|
}),
|
||||||
attachments: reminder.workflowStep.includeCalendarEvent
|
attachments: reminder.workflowStep.includeCalendarEvent
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -410,10 +412,12 @@ export async function handler(req: NextRequest) {
|
|||||||
html: emailContent.emailBody,
|
html: emailContent.emailBody,
|
||||||
batchId: batchId,
|
batchId: batchId,
|
||||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||||
replyTo:
|
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||||
reminder.booking?.eventType?.customReplyToEmail ||
|
replyTo:
|
||||||
reminder.booking?.userPrimaryEmail ||
|
reminder.booking?.eventType?.customReplyToEmail ||
|
||||||
reminder.booking.user?.email,
|
reminder.booking?.userPrimaryEmail ||
|
||||||
|
reminder.booking.user?.email,
|
||||||
|
}),
|
||||||
sender: reminder.workflowStep?.sender,
|
sender: reminder.workflowStep?.sender,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
|
|||||||
return {
|
return {
|
||||||
subject: emailContent.emailSubject,
|
subject: emailContent.emailSubject,
|
||||||
html: emailContent.emailBody,
|
html: emailContent.emailBody,
|
||||||
replyTo: evt?.eventType?.customReplyToEmail || evt.organizer.email,
|
...(!evt.hideOrganizerEmail && { replyTo: evt?.eventType?.customReplyToEmail || evt.organizer.email }),
|
||||||
attachments,
|
attachments,
|
||||||
sender,
|
sender,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,18 @@ import type { AppFlags } from "./config";
|
|||||||
import type { IFeaturesRepository } from "./features.repository.interface";
|
import type { IFeaturesRepository } from "./features.repository.interface";
|
||||||
import { getFeatureFlag } from "./server/utils";
|
import { getFeatureFlag } from "./server/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository class for managing feature flags and feature access control.
|
||||||
|
* Implements the IFeaturesRepository interface to provide feature flag functionality
|
||||||
|
* for users, teams, and global application features.
|
||||||
|
*/
|
||||||
export class FeaturesRepository implements IFeaturesRepository {
|
export class FeaturesRepository implements IFeaturesRepository {
|
||||||
|
/**
|
||||||
|
* Checks if a feature is enabled globally in the application.
|
||||||
|
* @param slug - The feature flag identifier to check
|
||||||
|
* @returns Promise<boolean> - True if the feature is enabled globally, false otherwise
|
||||||
|
* @throws Error if the feature flag check fails
|
||||||
|
*/
|
||||||
async checkIfFeatureIsEnabledGlobally(slug: keyof AppFlags) {
|
async checkIfFeatureIsEnabledGlobally(slug: keyof AppFlags) {
|
||||||
try {
|
try {
|
||||||
return await getFeatureFlag(db, slug);
|
return await getFeatureFlag(db, slug);
|
||||||
@@ -15,6 +26,15 @@ export class FeaturesRepository implements IFeaturesRepository {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a specific user has access to a feature.
|
||||||
|
* Checks both direct user feature assignments and team-based feature access.
|
||||||
|
* @param userId - The ID of the user to check
|
||||||
|
* @param slug - The feature identifier to check
|
||||||
|
* @returns Promise<boolean> - True if the user has access to the feature, false otherwise
|
||||||
|
* @throws Error if the feature access check fails
|
||||||
|
*/
|
||||||
async checkIfUserHasFeature(userId: number, slug: string) {
|
async checkIfUserHasFeature(userId: number, slug: string) {
|
||||||
try {
|
try {
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +59,15 @@ export class FeaturesRepository implements IFeaturesRepository {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper method to check if a user belongs to any team that has access to a feature.
|
||||||
|
* @param userId - The ID of the user to check
|
||||||
|
* @param slug - The feature identifier to check
|
||||||
|
* @returns Promise<boolean> - True if the user belongs to a team with the feature, false otherwise
|
||||||
|
* @throws Error if the team feature check fails
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private async checkIfUserBelongsToTeamWithFeature(userId: number, slug: string) {
|
private async checkIfUserBelongsToTeamWithFeature(userId: number, slug: string) {
|
||||||
try {
|
try {
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findUnique({
|
||||||
@@ -65,10 +94,34 @@ export class FeaturesRepository implements IFeaturesRepository {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a team has access to a specific feature.
|
||||||
|
* Also checks if the team's parent organization has the feature.
|
||||||
|
* @param teamId - The ID of the team to check
|
||||||
|
* @param featureId - The feature identifier to check
|
||||||
|
* @returns Promise<boolean> - True if the team has access to the feature, false otherwise
|
||||||
|
* @throws Error if the team feature check fails
|
||||||
|
*/
|
||||||
async checkIfTeamHasFeature(teamId: number, featureId: keyof AppFlags) {
|
async checkIfTeamHasFeature(teamId: number, featureId: keyof AppFlags) {
|
||||||
try {
|
try {
|
||||||
const teamFeature = await db.teamFeatures.findUnique({
|
const teamFeature = await db.teamFeatures.findFirst({
|
||||||
where: { teamId_featureId: { teamId, featureId } },
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ teamId, featureId },
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
parent: {
|
||||||
|
features: {
|
||||||
|
some: {
|
||||||
|
featureId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return !!teamFeature;
|
return !!teamFeature;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const ZSendWorkflowEmailsSchema = z.object({
|
|||||||
to: z.array(z.string()),
|
to: z.array(z.string()),
|
||||||
subject: z.string(),
|
subject: z.string(),
|
||||||
html: z.string(),
|
html: z.string(),
|
||||||
replyTo: z.string(),
|
replyTo: z.string().optional(),
|
||||||
sender: z.string().nullable().optional(),
|
sender: z.string().nullable().optional(),
|
||||||
attachments: z
|
attachments: z
|
||||||
.array(
|
.array(
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export const getBusyCalendarTimes = async (
|
|||||||
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
||||||
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
|
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
|
||||||
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
||||||
const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format();
|
const endDate = dayjs(dateTo).add(14, "hours").format();
|
||||||
|
|
||||||
if (includeTimeZone) {
|
if (includeTimeZone) {
|
||||||
results = await getCalendarsEventsWithTimezones(withCredentials, startDate, endDate, selectedCalendars);
|
results = await getCalendarsEventsWithTimezones(withCredentials, startDate, endDate, selectedCalendars);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
import { getReplyToEmail } from "./getReplyToEmail";
|
||||||
|
|
||||||
|
export function getReplyToHeader(calEvent: CalendarEvent, additionalEmails?: string | string[]) {
|
||||||
|
if (calEvent.hideOrganizerEmail) return {};
|
||||||
|
|
||||||
|
const replyToEmail = getReplyToEmail(calEvent);
|
||||||
|
const replyTo = additionalEmails
|
||||||
|
? Array.isArray(additionalEmails)
|
||||||
|
? [...additionalEmails, replyToEmail]
|
||||||
|
: [additionalEmails, replyToEmail]
|
||||||
|
: replyToEmail;
|
||||||
|
|
||||||
|
return { replyTo };
|
||||||
|
}
|
||||||
@@ -268,51 +268,40 @@ export function isTimeViolatingFutureLimit({
|
|||||||
time,
|
time,
|
||||||
periodLimits,
|
periodLimits,
|
||||||
}: {
|
}: {
|
||||||
time: dayjs.ConfigType;
|
time: string | Date | number;
|
||||||
periodLimits: PeriodLimits;
|
periodLimits: PeriodLimits;
|
||||||
}) {
|
}) {
|
||||||
const log = logger.getSubLogger({ prefix: ["isTimeViolatingFutureLimit"] });
|
const log = logger.getSubLogger({ prefix: ["isTimeViolatingFutureLimit"] });
|
||||||
// Because we are just going to compare times, we need not convert the time to a particular timezone
|
|
||||||
// date.isAfter/isBefore are timezone neutral
|
const dateObj = new Date(time);
|
||||||
const dateInSystemTz = dayjs(time);
|
|
||||||
if (periodLimits.endOfRollingPeriodEndDayInBookerTz) {
|
if (periodLimits.endOfRollingPeriodEndDayInBookerTz) {
|
||||||
const isAfterRollingEndDay = dateInSystemTz.isAfter(periodLimits.endOfRollingPeriodEndDayInBookerTz);
|
const isAfterRollingEndDay =
|
||||||
log.silly("rollingEndDayCheck", {
|
dateObj.valueOf() > periodLimits.endOfRollingPeriodEndDayInBookerTz.valueOf();
|
||||||
formattedDate: dateInSystemTz.format(),
|
|
||||||
isAfterRollingEndDay,
|
|
||||||
endOfRollingPeriodEndDayInBookerTz: periodLimits.endOfRollingPeriodEndDayInBookerTz.format(),
|
|
||||||
});
|
|
||||||
if (isAfterRollingEndDay)
|
if (isAfterRollingEndDay)
|
||||||
log.warn(
|
log.warn(
|
||||||
"Booking is out of bounds due to rolling period end day.",
|
"Booking is out of bounds due to rolling period end day.",
|
||||||
safeStringify({
|
safeStringify({
|
||||||
formattedDate: dateInSystemTz.format(),
|
formattedDate: dateObj.toISOString(),
|
||||||
isAfterRollingEndDay,
|
isAfterRollingEndDay,
|
||||||
endOfRollingPeriodEndDayInBookerTz: periodLimits.endOfRollingPeriodEndDayInBookerTz.format(),
|
endOfRollingPeriodEndDayTs: periodLimits.endOfRollingPeriodEndDayInBookerTz.valueOf(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return isAfterRollingEndDay;
|
return isAfterRollingEndDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (periodLimits.startOfRangeStartDayInEventTz && periodLimits.endOfRangeEndDayInEventTz) {
|
if (periodLimits.startOfRangeStartDayInEventTz && periodLimits.endOfRangeEndDayInEventTz) {
|
||||||
const isBeforeRangeStart = dateInSystemTz.isBefore(periodLimits.startOfRangeStartDayInEventTz);
|
const isBeforeRangeStart = dateObj.valueOf() < periodLimits.startOfRangeStartDayInEventTz.valueOf();
|
||||||
const isAfterRangeEnd = dateInSystemTz.isAfter(periodLimits.endOfRangeEndDayInEventTz);
|
const isAfterRangeEnd = dateObj.valueOf() > periodLimits.endOfRangeEndDayInEventTz.valueOf();
|
||||||
log.silly("rangeCheck", {
|
|
||||||
formattedDate: dateInSystemTz.format(),
|
|
||||||
isAfterRangeEnd,
|
|
||||||
isBeforeRangeStart,
|
|
||||||
startOfRangeStartDayInEventTz: periodLimits.startOfRangeStartDayInEventTz.format(),
|
|
||||||
endOfRangeEndDayInEventTz: periodLimits.endOfRangeEndDayInEventTz.format(),
|
|
||||||
});
|
|
||||||
if (isBeforeRangeStart || isAfterRangeEnd)
|
if (isBeforeRangeStart || isAfterRangeEnd)
|
||||||
log.warn(
|
log.warn(
|
||||||
"Booking is out of bounds due to range start and end.",
|
"Booking is out of bounds due to range start and end.",
|
||||||
safeStringify({
|
safeStringify({
|
||||||
formattedDate: dateInSystemTz.format(),
|
formattedDate: dateObj.toISOString(),
|
||||||
isBeforeRangeStart,
|
isBeforeRangeStart,
|
||||||
isAfterRangeEnd,
|
isAfterRangeEnd,
|
||||||
startOfRangeStartDayInEventTz: periodLimits.startOfRangeStartDayInEventTz.format(),
|
startOfRangeStartDayInEventTz: periodLimits.startOfRangeStartDayInEventTz.toDate().toISOString(),
|
||||||
endOfRangeEndDayInEventTz: periodLimits.endOfRangeEndDayInEventTz.format(),
|
endOfRangeEndDayInEventTz: periodLimits.endOfRangeEndDayInEventTz.toDate().toISOString(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return isBeforeRangeStart || isAfterRangeEnd;
|
return isBeforeRangeStart || isAfterRangeEnd;
|
||||||
@@ -321,7 +310,7 @@ export function isTimeViolatingFutureLimit({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function isOutOfBounds(
|
export default function isOutOfBounds(
|
||||||
time: dayjs.ConfigType,
|
time: NonNullable<dayjs.ConfigType>,
|
||||||
{
|
{
|
||||||
periodType,
|
periodType,
|
||||||
periodDays,
|
periodDays,
|
||||||
@@ -354,7 +343,7 @@ export default function isOutOfBounds(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isOutOfBoundsByPeriod = isTimeViolatingFutureLimit({
|
const isOutOfBoundsByPeriod = isTimeViolatingFutureLimit({
|
||||||
time,
|
time: dayjs.isDayjs(time) ? time.toDate() : time,
|
||||||
periodLimits,
|
periodLimits,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "SelectedCalendar_credentialId_idx" ON "SelectedCalendar"("credentialId");
|
||||||
@@ -790,6 +790,7 @@ model SelectedCalendar {
|
|||||||
@@index([integration])
|
@@index([integration])
|
||||||
@@index([externalId])
|
@@index([externalId])
|
||||||
@@index([eventTypeId])
|
@@index([eventTypeId])
|
||||||
|
@@index([credentialId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EventTypeCustomInputType {
|
enum EventTypeCustomInputType {
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ export const bookingMinimalSelect = Prisma.validator<Prisma.BookingSelect>()({
|
|||||||
customInputs: true,
|
customInputs: true,
|
||||||
startTime: true,
|
startTime: true,
|
||||||
endTime: true,
|
endTime: true,
|
||||||
attendees: {
|
attendees: true,
|
||||||
orderBy: {
|
|
||||||
id: "asc",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metadata: true,
|
metadata: true,
|
||||||
});
|
});
|
||||||
@@ -18,7 +18,7 @@ export const ZGetInputSchema = z.object({
|
|||||||
beforeCreatedDate: z.string().optional(),
|
beforeCreatedDate: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
limit: z.number().min(1).max(100),
|
limit: z.number().min(1).max(100),
|
||||||
offset: z.number(),
|
offset: z.number().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TGetInputSchema = z.infer<typeof ZGetInputSchema>;
|
export type TGetInputSchema = z.infer<typeof ZGetInputSchema>;
|
||||||
@@ -251,7 +251,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent, bookingRef.externalCalendarId);
|
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent, bookingRef.externalCalendarId);
|
||||||
} else if (bookingRef.type.endsWith("_video")) {
|
} else if (bookingRef.type.endsWith("_video") && bookingRef.type !== "daily_video") {
|
||||||
return deleteMeeting(
|
return deleteMeeting(
|
||||||
getDelegationCredentialOrRegularCredential({
|
getDelegationCredentialOrRegularCredential({
|
||||||
credentials,
|
credentials,
|
||||||
|
|||||||
Reference in new issue
Block a user