Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f4e51b859 | ||
|
|
4f5d0db792 | ||
|
|
8733bcaf7a | ||
|
|
5aaca2b7aa | ||
|
|
eacd05bf3b | ||
|
|
d51e0182b5 | ||
|
|
220897ba86 | ||
|
|
78c0203a45 | ||
|
|
720fd90066 | ||
|
|
ca8ee749a2 | ||
|
|
ce2d3a4b00 | ||
|
|
2923684c7c | ||
|
|
879ae597be | ||
|
|
d21f7ccbc8 |
Vendored
@@ -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 type { PageProps } from "app/_types";
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
||||
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
|
||||
@@ -25,43 +24,40 @@ export const generateMetadata = async () => {
|
||||
const Page = async ({ searchParams: _searchParams }: PageProps) => {
|
||||
const searchParams = await _searchParams;
|
||||
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 organizationId = me.organization?.id ?? me.profiles[0]?.organizationId;
|
||||
const isOrgPrivate = organizationId
|
||||
? await OrganizationRepository.checkIfPrivate({
|
||||
orgId: organizationId,
|
||||
})
|
||||
: false;
|
||||
const canViewTeamAvailability = me.organization?.isOrgAdmin || !isOrgPrivate;
|
||||
const [meCaller, availabilityCaller] = await Promise.all([
|
||||
createRouterCaller(meRouter),
|
||||
createRouterCaller(availabilityRouter),
|
||||
]);
|
||||
|
||||
return (
|
||||
<ShellMainAppDir
|
||||
heading={t("availability")}
|
||||
subtitle={t("configure_availability")}
|
||||
CTA={
|
||||
<AvailabilityCTA
|
||||
toggleGroupOptions={[
|
||||
{ value: "mine", label: t("my_availability") },
|
||||
...(canViewTeamAvailability ? [{ value: "team", label: t("team_availability") }] : []),
|
||||
]}
|
||||
/>
|
||||
}>
|
||||
{searchParams?.type === "team" && canViewTeamAvailability ? (
|
||||
<AvailabilitySliderTable userTimeFormat={me?.timeFormat ?? null} isOrg={!!organizationId} />
|
||||
) : (
|
||||
<AvailabilityList availabilities={availabilities ?? { schedules: [] }} me={me} />
|
||||
)}
|
||||
</ShellMainAppDir>
|
||||
);
|
||||
} catch (error) {
|
||||
notFound();
|
||||
}
|
||||
const [me, availabilities] = await Promise.all([meCaller.get(), availabilityCaller.list()]);
|
||||
const organizationId = me.organization?.id ?? me.profiles[0]?.organizationId;
|
||||
const isOrgPrivate = organizationId
|
||||
? await OrganizationRepository.checkIfPrivate({
|
||||
orgId: organizationId,
|
||||
})
|
||||
: false;
|
||||
const canViewTeamAvailability = me.organization?.isOrgAdmin || !isOrgPrivate;
|
||||
|
||||
return (
|
||||
<ShellMainAppDir
|
||||
heading={t("availability")}
|
||||
subtitle={t("configure_availability")}
|
||||
CTA={
|
||||
<AvailabilityCTA
|
||||
toggleGroupOptions={[
|
||||
{ value: "mine", label: t("my_availability") },
|
||||
...(canViewTeamAvailability ? [{ value: "team", label: t("team_availability") }] : []),
|
||||
]}
|
||||
/>
|
||||
}>
|
||||
{searchParams?.type === "team" && canViewTeamAvailability ? (
|
||||
<AvailabilitySliderTable userTimeFormat={me?.timeFormat ?? null} isOrg={!!organizationId} />
|
||||
) : (
|
||||
<AvailabilityList availabilities={availabilities ?? { schedules: [] }} me={me} />
|
||||
)}
|
||||
</ShellMainAppDir>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getServerSideProps } from "@lib/apps/getServerSideProps";
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
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";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
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 context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
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 <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;
|
||||
|
||||
@@ -13,7 +13,8 @@ export function AppHomePageSkeleton() {
|
||||
isPublic
|
||||
heading={t("app_store")}
|
||||
subtitle={t("app_store_description")}
|
||||
headerClassName="sm:hidden lg:block hidden">
|
||||
headerClassName="sm:hidden lg:block hidden"
|
||||
isAdmin={false}>
|
||||
<SkeletonContainer>
|
||||
<div className="flex flex-col gap-y-8">
|
||||
{/* 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 { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
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 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 t = await getTranslate();
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={`${t("editing_org")}: ${org.name}`}
|
||||
description={t("admin_orgs_edit_description")}>
|
||||
<LicenseRequired>
|
||||
<OrgForm org={org} />
|
||||
</LicenseRequired>
|
||||
</SettingsHeader>
|
||||
);
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
const org = await OrganizationRepository.adminFindById({ id: input.data.id });
|
||||
const t = await getTranslate();
|
||||
return (
|
||||
<SettingsHeader title={`${t("editing_org")}: ${org.name}`} description={t("admin_orgs_edit_description")}>
|
||||
<LicenseRequired>
|
||||
<OrgForm org={org} />
|
||||
</LicenseRequired>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
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 { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
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 input = userIdSchema.safeParse(await params);
|
||||
|
||||
if (!input.success) {
|
||||
notFound();
|
||||
}
|
||||
if (!input.success) throw new Error("Invalid access");
|
||||
|
||||
try {
|
||||
const user = await UserRepository.adminFindById(input.data.id);
|
||||
const t = await getTranslate();
|
||||
const user = await UserRepository.adminFindById(input.data.id);
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("editing_user")} description={t("admin_users_edit_description")}>
|
||||
<LicenseRequired>
|
||||
<UsersEditView user={user} />
|
||||
</LicenseRequired>
|
||||
</SettingsHeader>
|
||||
);
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
return (
|
||||
<SettingsHeader title={t("editing_user")} description={t("admin_users_edit_description")}>
|
||||
<LicenseRequired>
|
||||
<UsersEditView user={user} />
|
||||
</LicenseRequired>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
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 user = session?.user;
|
||||
const parsed = querySchema.safeParse(await params);
|
||||
if (!parsed.success) {
|
||||
notFound();
|
||||
}
|
||||
if (!parsed.success) throw new Error("Invalid workflow id");
|
||||
|
||||
// const workflow = await WorkflowRepository.getById({ id: +parsed.data.workflow });
|
||||
// let verifiedEmails, verifiedNumbers;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { ComponentProps } from "react";
|
||||
import React from "react";
|
||||
@@ -9,15 +8,14 @@ import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
|
||||
type AppsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
isAdmin: boolean;
|
||||
actions?: (className?: string) => JSX.Element;
|
||||
emptyStore?: boolean;
|
||||
} & 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 session = useSession();
|
||||
const router = useRouter();
|
||||
const isAdmin = session.data?.user.role === "ADMIN";
|
||||
|
||||
return (
|
||||
<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 { RecentAppsSlider } from "@calcom/features/apps/components/RecentAppsSlider";
|
||||
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 { TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import type { HorizontalTabItemProps } 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";
|
||||
|
||||
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 [searchText, setSearchText] = useState<string | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<AppsLayout
|
||||
isPublic
|
||||
isAdmin={isAdmin}
|
||||
heading={t("app_store")}
|
||||
subtitle={t("app_store_description")}
|
||||
actions={(className) => (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.2.6",
|
||||
"version": "5.2.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { expect, test, beforeEach, vi, describe } from "vitest";
|
||||
import "vitest-fetch-mock";
|
||||
|
||||
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 type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
|
||||
|
||||
@@ -187,8 +188,8 @@ test("Calendar Cache is being read on cache HIT", async () => {
|
||||
await calendarCache.upsertCachedAvailability(
|
||||
credentialInDb1.id,
|
||||
{
|
||||
timeMin: dateFrom1,
|
||||
timeMax: dateTo1,
|
||||
timeMin: getTimeMin(dateFrom1),
|
||||
timeMax: getTimeMax(dateTo1),
|
||||
items: [{ id: testSelectedCalendar.externalId }],
|
||||
},
|
||||
JSON.parse(
|
||||
|
||||
@@ -663,7 +663,13 @@ export default class GoogleCalendarService implements Calendar {
|
||||
): Promise<calendar_v3.Schema$FreeBusyResponse> {
|
||||
if (shouldServeCache === false) return await this.fetchAvailability(args);
|
||||
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;
|
||||
return await this.fetchAvailability(args);
|
||||
}
|
||||
@@ -1055,9 +1061,9 @@ export default class GoogleCalendarService implements Calendar {
|
||||
|
||||
for (const [_eventTypeId, selectedCalendars] of Array.from(selectedCalendarsPerEventType.entries())) {
|
||||
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(),
|
||||
/** 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(),
|
||||
// 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||
@@ -14,7 +14,7 @@ export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
|
||||
}),
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: getReplyToEmail(this.calEvent),
|
||||
...getReplyToHeader(this.calEvent),
|
||||
subject: `${this.t("guests_added_event_type_subject", {
|
||||
eventType: this.calEvent.type,
|
||||
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 AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||
@@ -8,7 +8,7 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: getReplyToEmail(this.calEvent),
|
||||
...getReplyToHeader(this.calEvent),
|
||||
subject: `${this.attendee.language.translate("complete_your_booking_subject", {
|
||||
title: this.calEvent.title,
|
||||
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 { renderEmail } from "../";
|
||||
@@ -15,7 +15,7 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
|
||||
}),
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: getReplyToEmail(this.calEvent),
|
||||
...getReplyToHeader(this.calEvent),
|
||||
subject: `${this.t("event_cancelled_subject", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||
@@ -8,7 +8,7 @@ export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail {
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: getReplyToEmail(this.calEvent),
|
||||
...getReplyToHeader(this.calEvent),
|
||||
subject: `${this.t("event_no_longer_attending_subject", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// TODO: We should find a way to keep App specific email templates within the App itself
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -26,12 +26,10 @@ export default class AttendeeDailyVideoDownloadRecordingEmail extends BaseEmail
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: [
|
||||
...this.calEvent.attendees
|
||||
.filter(({ email }) => email !== this.attendee.email)
|
||||
.map(({ email }) => email),
|
||||
getReplyToEmail(this.calEvent),
|
||||
],
|
||||
...getReplyToHeader(
|
||||
this.calEvent,
|
||||
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||
),
|
||||
subject: `${this.t("download_recording_subject", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -37,12 +37,10 @@ export default class AttendeeDailyVideoDownloadTranscriptEmail extends BaseEmail
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: [
|
||||
...this.calEvent.attendees
|
||||
.filter(({ email }) => email !== this.attendee.email)
|
||||
.map(({ email }) => email),
|
||||
getReplyToEmail(this.calEvent),
|
||||
],
|
||||
...getReplyToHeader(
|
||||
this.calEvent,
|
||||
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||
),
|
||||
subject: `${this.t("download_transcript_email_subject", {
|
||||
title: this.calEvent.title,
|
||||
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 { renderEmail } from "../";
|
||||
@@ -9,7 +9,7 @@ export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: getReplyToEmail(this.calEvent),
|
||||
...getReplyToHeader(this.calEvent),
|
||||
subject: `${this.t("event_declined_subject", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||
@@ -14,7 +14,7 @@ export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail
|
||||
}),
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: getReplyToEmail(this.calEvent),
|
||||
...getReplyToHeader(this.calEvent),
|
||||
subject: `${this.t("location_changed_event_type_subject", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { renderEmail } from "../";
|
||||
@@ -8,16 +8,13 @@ import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||
export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
const toAddresses = this.calEvent.attendees.map((attendee) => attendee.email);
|
||||
|
||||
return {
|
||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||
to: toAddresses.join(","),
|
||||
replyTo: [
|
||||
...this.calEvent.attendees
|
||||
.filter(({ email }) => email !== this.attendee.email)
|
||||
.map(({ email }) => email),
|
||||
getReplyToEmail(this.calEvent),
|
||||
],
|
||||
...getReplyToHeader(
|
||||
this.calEvent,
|
||||
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||
),
|
||||
subject: `${this.calEvent.attendees[0].language.translate("booking_submitted_subject", {
|
||||
title: this.calEvent.title,
|
||||
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 { renderEmail } from "../";
|
||||
@@ -15,12 +15,10 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
||||
}),
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: [
|
||||
...this.calEvent.attendees
|
||||
.filter(({ email }) => email !== this.attendee.email)
|
||||
.map(({ email }) => email),
|
||||
getReplyToEmail(this.calEvent),
|
||||
],
|
||||
...getReplyToHeader(
|
||||
this.calEvent,
|
||||
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||
),
|
||||
subject: `${this.attendee.language.translate("event_type_has_been_rescheduled_on_time_date", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
|
||||
import { default as cloneDeep } from "lodash/cloneDeep";
|
||||
|
||||
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 type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -40,12 +40,10 @@ export default class AttendeeScheduledEmail extends BaseEmail {
|
||||
}),
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: [
|
||||
...this.calEvent.attendees
|
||||
.filter(({ email }) => email !== this.attendee.email)
|
||||
.map(({ email }) => email),
|
||||
getReplyToEmail(this.calEvent),
|
||||
],
|
||||
...getReplyToHeader(
|
||||
this.calEvent,
|
||||
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||
),
|
||||
subject: `${this.calEvent.title}`,
|
||||
html: await this.getHtml(clonedCalEvent, this.attendee),
|
||||
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 { renderEmail } from "../";
|
||||
@@ -15,12 +15,10 @@ export default class AttendeeUpdatedEmail extends AttendeeScheduledEmail {
|
||||
}),
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: [
|
||||
...this.calEvent.attendees
|
||||
.filter(({ email }) => email !== this.attendee.email)
|
||||
.map(({ email }) => email),
|
||||
getReplyToEmail(this.calEvent),
|
||||
],
|
||||
...getReplyToHeader(
|
||||
this.calEvent,
|
||||
this.calEvent.attendees.filter(({ email }) => email !== this.attendee.email).map(({ email }) => email)
|
||||
),
|
||||
subject: `${this.attendee.language.translate("event_type_has_been_updated", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getReplyToEmail } from "@calcom/lib/getReplyToEmail";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||
@@ -9,7 +9,7 @@ export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail {
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
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", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||
@@ -16,7 +17,10 @@ export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
|
||||
}),
|
||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||
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", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.attendees[0].name,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -23,7 +24,10 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
|
||||
return {
|
||||
to: `${this.calEvent.organizer.email}>`,
|
||||
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", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -35,7 +36,10 @@ export default class OrganizerDailyVideoDownloadTranscriptEmail extends BaseEmai
|
||||
return {
|
||||
to: `${this.calEvent.organizer.email}>`,
|
||||
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", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||
@@ -16,7 +17,10 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai
|
||||
}),
|
||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||
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", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.attendees[0].name,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { checkIfUserHasFeatureController } from "@calcom/features/flags/operations/check-if-user-has-feature.controller";
|
||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
@@ -21,7 +22,10 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
|
||||
return {
|
||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||
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}`,
|
||||
html: await this.getHtmlRequestEmail(template, this.calEvent, this.calEvent.organizer),
|
||||
text: this.getTextBody("event_awaiting_approval"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import OrganizerRequestEmail from "./organizer-request-email";
|
||||
@@ -10,7 +11,10 @@ export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail
|
||||
return {
|
||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||
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", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { renderEmail } from "../";
|
||||
@@ -9,7 +9,6 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||
|
||||
return {
|
||||
icalEvent: generateIcsFile({
|
||||
calEvent: this.calEvent,
|
||||
@@ -18,7 +17,10 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
||||
}),
|
||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||
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", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { default as cloneDeep } from "lodash/cloneDeep";
|
||||
|
||||
import { getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
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}>`,
|
||||
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}`,
|
||||
html: await this.getHtml(
|
||||
clonedCalEvent,
|
||||
|
||||
@@ -14,7 +14,7 @@ export type WorkflowEmailData = {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
replyTo: string;
|
||||
replyTo?: string;
|
||||
sender?: string | null;
|
||||
attachments?: Attachment[];
|
||||
};
|
||||
@@ -31,7 +31,7 @@ export default class WorkflowEmail extends BaseEmail {
|
||||
return {
|
||||
to: this.mailData.to,
|
||||
from: `${this.mailData.sender || SENDER_NAME} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.mailData.replyTo,
|
||||
...(this.mailData.replyTo && { replyTo: this.mailData.replyTo }),
|
||||
subject: this.mailData.subject,
|
||||
html: addHTMLStyles(this.mailData.html),
|
||||
attachments: this.mailData.attachments,
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock("@calcom/lib/webstorage", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Last Booking Response", () => {
|
||||
describe.skip("Last Booking Response", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
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) => {
|
||||
if (!responses) return;
|
||||
const prevResponse = Object.fromEntries(
|
||||
Object.entries(responses).filter(([key]) => responsesToStore.includes(key))
|
||||
);
|
||||
localStorage.setItem("lastBookingResponse", JSON.stringify(prevResponse));
|
||||
return;
|
||||
// if (!responses) return;
|
||||
// const prevResponse = Object.fromEntries(
|
||||
// Object.entries(responses).filter(([key]) => responsesToStore.includes(key))
|
||||
// );
|
||||
// localStorage.setItem("lastBookingResponse", JSON.stringify(prevResponse));
|
||||
};
|
||||
|
||||
export const getLastBookingResponse = () => {
|
||||
|
||||
@@ -73,6 +73,9 @@ async function getBooking(prisma: PrismaClient, uid: string, isSeatedEvent?: boo
|
||||
name: true,
|
||||
bookingSeat: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
|
||||
@@ -7,7 +7,6 @@ import prisma from "@calcom/prisma";
|
||||
import type { Calendar, SelectedCalendarEventTypeIds } from "@calcom/types/Calendar";
|
||||
|
||||
import type { ICalendarCacheRepository } from "./calendar-cache.repository.interface";
|
||||
import { getTimeMax, getTimeMin } from "./lib/datesForCache";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] });
|
||||
|
||||
@@ -19,8 +18,8 @@ function parseKeyForCache(args: FreeBusyArgs): string {
|
||||
// Ensure that calendarIds are unique
|
||||
const uniqueItems = uniqueBy(args.items, ["id"]);
|
||||
const key = JSON.stringify({
|
||||
timeMin: getTimeMin(args.timeMin),
|
||||
timeMax: getTimeMax(args.timeMax),
|
||||
timeMin: args.timeMin,
|
||||
timeMax: args.timeMax,
|
||||
items: uniqueItems,
|
||||
});
|
||||
return key;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getTimeMin, getTimeMax } from "./datesForCache";
|
||||
|
||||
describe("getTimeMin", () => {
|
||||
// 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", () => {
|
||||
const result = getTimeMin();
|
||||
const expected = new Date();
|
||||
@@ -74,4 +75,23 @@ describe("getTimeMax", () => {
|
||||
const result = getTimeMax(testDate);
|
||||
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 currentYear = now.getUTCFullYear();
|
||||
|
||||
// If no date is passed, return start of the month two months from *now*.
|
||||
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));
|
||||
result.setUTCHours(0, 0, 0, 0);
|
||||
return result.toISOString();
|
||||
}
|
||||
|
||||
// If a date is passed, determine the base month/year for calculation.
|
||||
const date = new Date(timeMax);
|
||||
const dateMonth = date.getUTCMonth();
|
||||
const dateYear = date.getUTCFullYear();
|
||||
|
||||
// If the date is within current month or next month, return start of overnext month
|
||||
// Otherwise, return start of overnext month from the date
|
||||
let targetYear = dateYear;
|
||||
let targetMonth = dateMonth;
|
||||
let baseYear = currentYear;
|
||||
let baseMonth = currentMonth;
|
||||
|
||||
// Check if date is within current month or next month
|
||||
const isWithinCurrentOrNextMonth =
|
||||
(dateYear === currentYear && dateMonth <= currentMonth + 1) ||
|
||||
(dateYear === currentYear + 1 && currentMonth === 11 && dateMonth === 0);
|
||||
// Check if date is within the current month or the next two months relative to *now*.
|
||||
const isWithinCurrentOrNextTwoMonths =
|
||||
(dateYear === currentYear && dateMonth <= currentMonth + 2) ||
|
||||
(dateYear === currentYear + 1 &&
|
||||
((currentMonth === 10 && dateMonth === 0) || (currentMonth === 11 && dateMonth <= 1)));
|
||||
|
||||
if (isWithinCurrentOrNextMonth) {
|
||||
// For dates within current month or next month, return start of overnext month
|
||||
targetMonth = dateMonth + 2;
|
||||
if (targetMonth > 11) {
|
||||
targetMonth = 0;
|
||||
targetYear++;
|
||||
}
|
||||
} else {
|
||||
// For dates beyond overnext month, return start of overnext month from the date
|
||||
targetMonth = dateMonth + 2;
|
||||
if (targetMonth > 11) {
|
||||
targetMonth = 0;
|
||||
targetYear++;
|
||||
}
|
||||
// If the input date is beyond the next two months relative to *now*,
|
||||
// use the *input date's* month/year as the base.
|
||||
if (!isWithinCurrentOrNextTwoMonths) {
|
||||
baseYear = dateYear;
|
||||
baseMonth = dateMonth;
|
||||
}
|
||||
// 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);
|
||||
return result.toISOString();
|
||||
}
|
||||
|
||||
@@ -332,10 +332,12 @@ export async function handler(req: NextRequest) {
|
||||
html: emailContent.emailBody,
|
||||
batchId: batchId,
|
||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||
replyTo:
|
||||
reminder.booking?.eventType?.customReplyToEmail ??
|
||||
reminder.booking?.userPrimaryEmail ??
|
||||
reminder.booking.user?.email,
|
||||
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||
replyTo:
|
||||
reminder.booking?.eventType?.customReplyToEmail ??
|
||||
reminder.booking?.userPrimaryEmail ??
|
||||
reminder.booking.user?.email,
|
||||
}),
|
||||
attachments: reminder.workflowStep.includeCalendarEvent
|
||||
? [
|
||||
{
|
||||
@@ -410,10 +412,12 @@ export async function handler(req: NextRequest) {
|
||||
html: emailContent.emailBody,
|
||||
batchId: batchId,
|
||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||
replyTo:
|
||||
reminder.booking?.eventType?.customReplyToEmail ||
|
||||
reminder.booking?.userPrimaryEmail ||
|
||||
reminder.booking.user?.email,
|
||||
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||
replyTo:
|
||||
reminder.booking?.eventType?.customReplyToEmail ||
|
||||
reminder.booking?.userPrimaryEmail ||
|
||||
reminder.booking.user?.email,
|
||||
}),
|
||||
sender: reminder.workflowStep?.sender,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -251,7 +251,7 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) =>
|
||||
return {
|
||||
subject: emailContent.emailSubject,
|
||||
html: emailContent.emailBody,
|
||||
replyTo: evt?.eventType?.customReplyToEmail || evt.organizer.email,
|
||||
...(!evt.hideOrganizerEmail && { replyTo: evt?.eventType?.customReplyToEmail || evt.organizer.email }),
|
||||
attachments,
|
||||
sender,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,18 @@ import type { AppFlags } from "./config";
|
||||
import type { IFeaturesRepository } from "./features.repository.interface";
|
||||
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 {
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
return await getFeatureFlag(db, slug);
|
||||
@@ -15,6 +26,15 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
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) {
|
||||
try {
|
||||
/**
|
||||
@@ -39,6 +59,15 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
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) {
|
||||
try {
|
||||
const user = await db.user.findUnique({
|
||||
@@ -65,10 +94,34 @@ export class FeaturesRepository implements IFeaturesRepository {
|
||||
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) {
|
||||
try {
|
||||
const teamFeature = await db.teamFeatures.findUnique({
|
||||
where: { teamId_featureId: { teamId, featureId } },
|
||||
const teamFeature = await db.teamFeatures.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ teamId, featureId },
|
||||
{
|
||||
team: {
|
||||
parent: {
|
||||
features: {
|
||||
some: {
|
||||
featureId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
return !!teamFeature;
|
||||
} catch (err) {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const ZSendWorkflowEmailsSchema = z.object({
|
||||
to: z.array(z.string()),
|
||||
subject: z.string(),
|
||||
html: z.string(),
|
||||
replyTo: z.string(),
|
||||
replyTo: z.string().optional(),
|
||||
sender: z.string().nullable().optional(),
|
||||
attachments: z
|
||||
.array(
|
||||
|
||||
@@ -169,7 +169,7 @@ export const getBusyCalendarTimes = async (
|
||||
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
||||
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
|
||||
// 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) {
|
||||
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,
|
||||
periodLimits,
|
||||
}: {
|
||||
time: dayjs.ConfigType;
|
||||
time: string | Date | number;
|
||||
periodLimits: PeriodLimits;
|
||||
}) {
|
||||
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 dateInSystemTz = dayjs(time);
|
||||
|
||||
const dateObj = new Date(time);
|
||||
if (periodLimits.endOfRollingPeriodEndDayInBookerTz) {
|
||||
const isAfterRollingEndDay = dateInSystemTz.isAfter(periodLimits.endOfRollingPeriodEndDayInBookerTz);
|
||||
log.silly("rollingEndDayCheck", {
|
||||
formattedDate: dateInSystemTz.format(),
|
||||
isAfterRollingEndDay,
|
||||
endOfRollingPeriodEndDayInBookerTz: periodLimits.endOfRollingPeriodEndDayInBookerTz.format(),
|
||||
});
|
||||
const isAfterRollingEndDay =
|
||||
dateObj.valueOf() > periodLimits.endOfRollingPeriodEndDayInBookerTz.valueOf();
|
||||
|
||||
if (isAfterRollingEndDay)
|
||||
log.warn(
|
||||
"Booking is out of bounds due to rolling period end day.",
|
||||
safeStringify({
|
||||
formattedDate: dateInSystemTz.format(),
|
||||
formattedDate: dateObj.toISOString(),
|
||||
isAfterRollingEndDay,
|
||||
endOfRollingPeriodEndDayInBookerTz: periodLimits.endOfRollingPeriodEndDayInBookerTz.format(),
|
||||
endOfRollingPeriodEndDayTs: periodLimits.endOfRollingPeriodEndDayInBookerTz.valueOf(),
|
||||
})
|
||||
);
|
||||
return isAfterRollingEndDay;
|
||||
}
|
||||
|
||||
if (periodLimits.startOfRangeStartDayInEventTz && periodLimits.endOfRangeEndDayInEventTz) {
|
||||
const isBeforeRangeStart = dateInSystemTz.isBefore(periodLimits.startOfRangeStartDayInEventTz);
|
||||
const isAfterRangeEnd = dateInSystemTz.isAfter(periodLimits.endOfRangeEndDayInEventTz);
|
||||
log.silly("rangeCheck", {
|
||||
formattedDate: dateInSystemTz.format(),
|
||||
isAfterRangeEnd,
|
||||
isBeforeRangeStart,
|
||||
startOfRangeStartDayInEventTz: periodLimits.startOfRangeStartDayInEventTz.format(),
|
||||
endOfRangeEndDayInEventTz: periodLimits.endOfRangeEndDayInEventTz.format(),
|
||||
});
|
||||
const isBeforeRangeStart = dateObj.valueOf() < periodLimits.startOfRangeStartDayInEventTz.valueOf();
|
||||
const isAfterRangeEnd = dateObj.valueOf() > periodLimits.endOfRangeEndDayInEventTz.valueOf();
|
||||
if (isBeforeRangeStart || isAfterRangeEnd)
|
||||
log.warn(
|
||||
"Booking is out of bounds due to range start and end.",
|
||||
safeStringify({
|
||||
formattedDate: dateInSystemTz.format(),
|
||||
formattedDate: dateObj.toISOString(),
|
||||
isBeforeRangeStart,
|
||||
isAfterRangeEnd,
|
||||
startOfRangeStartDayInEventTz: periodLimits.startOfRangeStartDayInEventTz.format(),
|
||||
endOfRangeEndDayInEventTz: periodLimits.endOfRangeEndDayInEventTz.format(),
|
||||
startOfRangeStartDayInEventTz: periodLimits.startOfRangeStartDayInEventTz.toDate().toISOString(),
|
||||
endOfRangeEndDayInEventTz: periodLimits.endOfRangeEndDayInEventTz.toDate().toISOString(),
|
||||
})
|
||||
);
|
||||
return isBeforeRangeStart || isAfterRangeEnd;
|
||||
@@ -321,7 +310,7 @@ export function isTimeViolatingFutureLimit({
|
||||
}
|
||||
|
||||
export default function isOutOfBounds(
|
||||
time: dayjs.ConfigType,
|
||||
time: NonNullable<dayjs.ConfigType>,
|
||||
{
|
||||
periodType,
|
||||
periodDays,
|
||||
@@ -354,7 +343,7 @@ export default function isOutOfBounds(
|
||||
});
|
||||
|
||||
const isOutOfBoundsByPeriod = isTimeViolatingFutureLimit({
|
||||
time,
|
||||
time: dayjs.isDayjs(time) ? time.toDate() : time,
|
||||
periodLimits,
|
||||
});
|
||||
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SelectedCalendar_credentialId_idx" ON "SelectedCalendar"("credentialId");
|
||||
@@ -790,6 +790,7 @@ model SelectedCalendar {
|
||||
@@index([integration])
|
||||
@@index([externalId])
|
||||
@@index([eventTypeId])
|
||||
@@index([credentialId])
|
||||
}
|
||||
|
||||
enum EventTypeCustomInputType {
|
||||
|
||||
@@ -8,10 +8,6 @@ export const bookingMinimalSelect = Prisma.validator<Prisma.BookingSelect>()({
|
||||
customInputs: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
attendees: {
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
},
|
||||
},
|
||||
attendees: true,
|
||||
metadata: true,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ZGetInputSchema = z.object({
|
||||
beforeCreatedDate: z.string().optional(),
|
||||
}),
|
||||
limit: z.number().min(1).max(100),
|
||||
offset: z.number(),
|
||||
offset: z.number().default(0),
|
||||
});
|
||||
|
||||
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);
|
||||
} else if (bookingRef.type.endsWith("_video")) {
|
||||
} else if (bookingRef.type.endsWith("_video") && bookingRef.type !== "daily_video") {
|
||||
return deleteMeeting(
|
||||
getDelegationCredentialOrRegularCredential({
|
||||
credentials,
|
||||
|
||||
Reference in New Issue
Block a user