perf: server-fetch data for all pages in /settings/my-account (#20712)
* wip * fix type check * refactor "/settings/my-account/profile" * refactor "settings/my-account/general" * refactor "/settings/my-accouunt/calendars" * refactor "/settings/my-account/appearance" * fix: type-check --------- Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: Amit Sharma <samit91848@gmail.com>
This commit is contained in:
co-authored by
Amit Sharma
Amit Sharma
parent
6259b0b3cb
commit
6b9115dfde
@@ -1,9 +1,12 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||
|
||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||
|
||||
@@ -28,7 +31,24 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
||||
redirect("/apps/installed/calendar");
|
||||
}
|
||||
|
||||
return <InstalledApps category={parsedParams.data.category} />;
|
||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||
createRouterCaller(calendarsRouter),
|
||||
createRouterCaller(appsRouter),
|
||||
]);
|
||||
|
||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
||||
const installedCalendars = await appsCaller.integrations({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<InstalledApps
|
||||
connectedCalendars={connectedCalendars}
|
||||
installedCalendars={installedCalendars}
|
||||
category={parsedParams.data.category}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstalledAppsWrapper;
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function revalidateSettingsAppearance() {
|
||||
revalidatePath("/settings/my-account/appearance");
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { SkeletonLoader } from "~/settings/my-account/appearance-skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
+32
-8
@@ -1,7 +1,15 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getTranslate } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
import { getCachedHasTeamPlan } from "@calcom/web/app/cache/membership";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import AppearancePage from "~/settings/my-account/appearance-view";
|
||||
|
||||
@@ -15,13 +23,29 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const userId = session?.user?.id;
|
||||
const redirectUrl = "/auth/login?callbackUrl=/settings/my-account/appearance";
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("appearance")} description={t("appearance_description")}>
|
||||
<AppearancePage />
|
||||
</SettingsHeader>
|
||||
);
|
||||
if (!userId) {
|
||||
redirect(redirectUrl);
|
||||
}
|
||||
|
||||
const [meCaller, hasTeamPlan] = await Promise.all([
|
||||
createRouterCaller(meRouter),
|
||||
getCachedHasTeamPlan(userId),
|
||||
]);
|
||||
|
||||
const user = await meCaller.get();
|
||||
|
||||
if (!user) {
|
||||
redirect(redirectUrl);
|
||||
}
|
||||
const isCurrentUsernamePremium =
|
||||
user && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
|
||||
const hasPaidPlan = IS_SELF_HOSTED ? true : hasTeamPlan?.hasTeamPlan || isCurrentUsernamePremium;
|
||||
|
||||
return <AppearancePage user={user} hasPaidPlan={hasPaidPlan} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { CalendarListContainerSkeletonLoader } from "@components/apps/CalendarListContainer";
|
||||
|
||||
export default function Loading() {
|
||||
return <CalendarListContainerSkeletonLoader />;
|
||||
}
|
||||
+13
-20
@@ -1,8 +1,8 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getTranslate } from "app/_utils";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||
|
||||
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||
|
||||
@@ -16,25 +16,18 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
|
||||
const AddCalendarButton = () => {
|
||||
return (
|
||||
<>
|
||||
<Button color="secondary" StartIcon="plus" href="/apps/categories/calendar">
|
||||
{t("add_calendar")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||
createRouterCaller(calendarsRouter),
|
||||
createRouterCaller(appsRouter),
|
||||
]);
|
||||
|
||||
const connectedCalendars = await calendarsCaller.connectedCalendars();
|
||||
const installedCalendars = await appsCaller.integrations({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("calendars")}
|
||||
description={t("calendars_description")}
|
||||
CTA={<AddCalendarButton />}>
|
||||
<CalendarListContainer />
|
||||
</SettingsHeader>
|
||||
<CalendarListContainer connectedCalendars={connectedCalendars} installedCalendars={installedCalendars} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+1
-10
@@ -1,5 +1,4 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getTranslate } from "app/_utils";
|
||||
|
||||
import { ConferencingAppsViewWebWrapper } from "@calcom/atoms/connect/conferencing-apps/ConferencingAppsViewWebWrapper";
|
||||
|
||||
@@ -13,15 +12,7 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<ConferencingAppsViewWebWrapper
|
||||
title={t("conferencing")}
|
||||
description={t("conferencing_description")}
|
||||
add={t("add")}
|
||||
/>
|
||||
);
|
||||
return <ConferencingAppsViewWebWrapper />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function revalidateSettingsGeneral() {
|
||||
revalidatePath("/settings/my-account/general");
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { SkeletonLoader } from "~/settings/my-account/general-skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
+22
-14
@@ -1,10 +1,15 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getTranslate } from "app/_utils";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
import { getTravelSchedule } from "@calcom/web/app/cache/travelSchedule";
|
||||
|
||||
import GeneralQueryView from "~/settings/my-account/general-view";
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import GeneralView from "~/settings/my-account/general-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -16,17 +21,20 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
const revalidatePage = async () => {
|
||||
"use server";
|
||||
revalidatePath("settings/my-account/general");
|
||||
};
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const userId = session?.user?.id;
|
||||
const redirectUrl = "/auth/login?callbackUrl=/settings/my-account/general";
|
||||
|
||||
return (
|
||||
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
|
||||
<GeneralQueryView revalidatePage={revalidatePage} />
|
||||
</SettingsHeader>
|
||||
);
|
||||
if (!userId) {
|
||||
return redirect(redirectUrl);
|
||||
}
|
||||
|
||||
const meCaller = await createRouterCaller(meRouter);
|
||||
const [user, travelSchedules] = await Promise.all([meCaller.get(), getTravelSchedule(userId)]);
|
||||
if (!user) {
|
||||
redirect(redirectUrl);
|
||||
}
|
||||
return <GeneralView user={user} travelSchedules={travelSchedules ?? []} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
+2
-20
@@ -1,10 +1,6 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getTranslate } from "app/_utils";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import CreateNewOutOfOfficeEntryButton from "@calcom/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton";
|
||||
import OutOfOfficeEntriesList from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
|
||||
import { OutOfOfficeToggleGroup } from "@calcom/features/settings/outOfOffice/OutOfOfficeToggleGroup";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -15,22 +11,8 @@ export const generateMetadata = async () =>
|
||||
"/settings/my-account/out-of-office"
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("out_of_office")}
|
||||
description={t("out_of_office_description")}
|
||||
CTA={
|
||||
<div className="flex gap-2">
|
||||
<OutOfOfficeToggleGroup />
|
||||
<CreateNewOutOfOfficeEntryButton data-testid="add_entry_ooo" />
|
||||
</div>
|
||||
}>
|
||||
<OutOfOfficeEntriesList />
|
||||
</SettingsHeader>
|
||||
);
|
||||
const Page = () => {
|
||||
return <OutOfOfficeEntriesList />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { SkeletonLoader } from "~/settings/my-account/profile-skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
+9
-12
@@ -1,8 +1,8 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { getTranslate } from "app/_utils";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
|
||||
|
||||
import ProfileView from "~/settings/my-account/profile-view";
|
||||
|
||||
@@ -16,16 +16,13 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
const meCaller = await createRouterCaller(meRouter);
|
||||
const user = await meCaller.get({ includePasswordAdded: true });
|
||||
if (!user) {
|
||||
redirect("/auth/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("profile")}
|
||||
description={t("profile_description", { appName: APP_NAME })}
|
||||
borderInShellHeader={true}>
|
||||
<ProfileView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
return <ProfileView user={user} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
+2
-14
@@ -1,8 +1,5 @@
|
||||
import { getTranslate } from "app/_utils";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
|
||||
import PushNotificationsView from "~/settings/my-account/push-notifications-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -14,17 +11,8 @@ export const generateMetadata = async () =>
|
||||
"/settings/my-account/push-notifications"
|
||||
);
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("push_notifications")}
|
||||
description={t("push_notifications_description")}
|
||||
borderInShellHeader={true}>
|
||||
<PushNotificationsView />
|
||||
</SettingsHeader>
|
||||
);
|
||||
const Page = () => {
|
||||
return <PushNotificationsView />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
"use server";
|
||||
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
|
||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
|
||||
const CACHE_TAGS = {
|
||||
HAS_TEAM_PLAN: "MembershipRepository.findFirstAcceptedMembershipByUserId",
|
||||
} as const;
|
||||
|
||||
export const getCachedHasTeamPlan = unstable_cache(
|
||||
async (userId: number) => {
|
||||
const hasTeamPlan = await MembershipRepository.findFirstAcceptedMembershipByUserId(userId);
|
||||
|
||||
return { hasTeamPlan: !!hasTeamPlan };
|
||||
},
|
||||
["getCachedHasTeamPlan"],
|
||||
{
|
||||
revalidate: NEXTJS_CACHE_TTL,
|
||||
tags: [CACHE_TAGS.HAS_TEAM_PLAN],
|
||||
}
|
||||
);
|
||||
|
||||
export const revalidateHasTeamPlan = async () => {
|
||||
revalidateTag(CACHE_TAGS.HAS_TEAM_PLAN);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function revalidateSettingsProfile() {
|
||||
revalidatePath("/settings/my-account/profile");
|
||||
}
|
||||
|
||||
export async function revalidateSettingsCalendars() {
|
||||
revalidatePath("/settings/my-account/calendars");
|
||||
}
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
"use server";
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||
import { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule";
|
||||
import { unstable_cache } from "@calcom/lib/unstable_cache";
|
||||
|
||||
const CACHE_TAGS = {
|
||||
TRAVEL_SCHEDULES: "TravelRepository.findTravelSchedulesByUserId",
|
||||
} as const;
|
||||
|
||||
export const getTravelSchedule = unstable_cache(
|
||||
async (userId: number) => {
|
||||
return await TravelScheduleRepository.findTravelSchedulesByUserId(userId);
|
||||
},
|
||||
["getTravelSchedule"],
|
||||
{
|
||||
revalidate: NEXTJS_CACHE_TTL,
|
||||
tags: [CACHE_TAGS.TRAVEL_SCHEDULES],
|
||||
}
|
||||
);
|
||||
|
||||
export const revalidateTravelSchedules = async () => {
|
||||
revalidateTag(CACHE_TAGS.TRAVEL_SCHEDULES);
|
||||
};
|
||||
@@ -7,13 +7,16 @@ import { DestinationCalendarSettingsWebWrapper } from "@calcom/atoms/destination
|
||||
import { SelectedCalendarsSettingsWebWrapper } from "@calcom/atoms/selected-calendars/wrappers/SelectedCalendarsSettingsWebWrapper";
|
||||
import AppListCard from "@calcom/features/apps/components/AppListCard";
|
||||
import { SkeletonLoader } from "@calcom/features/apps/components/SkeletonLoader";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { ShellSubHeading } from "@calcom/ui/components/layout";
|
||||
import { List } from "@calcom/ui/components/list";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateSettingsCalendars } from "@calcom/web/app/cache/path/settings/my-account";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
@@ -63,9 +66,43 @@ function CalendarList(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarListContainer(props: { heading?: boolean; fromOnboarding?: boolean }) {
|
||||
const AddCalendarButton = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<Button color="secondary" StartIcon="plus" href="/apps/categories/calendar">
|
||||
{t("add_calendar")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CalendarListContainerSkeletonLoader = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("calendars")}
|
||||
description={t("calendars_description")}
|
||||
CTA={<AddCalendarButton />}>
|
||||
<SkeletonLoader />
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
type CalendarListContainerProps = {
|
||||
connectedCalendars: RouterOutputs["viewer"]["calendars"]["connectedCalendars"];
|
||||
installedCalendars: RouterOutputs["viewer"]["apps"]["integrations"];
|
||||
heading?: boolean;
|
||||
fromOnboarding?: boolean;
|
||||
};
|
||||
|
||||
export function CalendarListContainer({
|
||||
connectedCalendars: data,
|
||||
installedCalendars,
|
||||
heading = true,
|
||||
fromOnboarding,
|
||||
}: CalendarListContainerProps) {
|
||||
const { t } = useLocale();
|
||||
const { heading = true, fromOnboarding } = props;
|
||||
const { error, setQuery: setError } = useRouterQuery("error");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,70 +122,61 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
}
|
||||
),
|
||||
utils.viewer.calendars.connectedCalendars.invalidate(),
|
||||
revalidateSettingsCalendars(),
|
||||
]);
|
||||
const query = trpc.viewer.calendars.connectedCalendars.useQuery();
|
||||
const installedCalendars = trpc.viewer.apps.integrations.useQuery({
|
||||
variant: "calendar",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
|
||||
const mutation = trpc.viewer.calendars.setDestinationCalendar.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.calendars.connectedCalendars.invalidate();
|
||||
revalidateSettingsCalendars();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryCell
|
||||
query={query}
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => {
|
||||
return (
|
||||
<>
|
||||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||
<>
|
||||
{heading && (
|
||||
<>
|
||||
<DestinationCalendarSettingsWebWrapper />
|
||||
<Suspense fallback={<SkeletonLoader />}>
|
||||
<SelectedCalendarsSettingsWebWrapper
|
||||
onChanged={onChanged}
|
||||
fromOnboarding={fromOnboarding}
|
||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : fromOnboarding ? (
|
||||
<>
|
||||
{!!query.data?.connectedCalendars.length && (
|
||||
<ShellSubHeading
|
||||
className="mt-4"
|
||||
title={<SubHeadingTitleWithConnections title={t("connect_additional_calendar")} />}
|
||||
/>
|
||||
)}
|
||||
<CalendarList onChanged={onChanged} />
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon="calendar"
|
||||
headline={t("no_category_apps", {
|
||||
category: t("calendar").toLowerCase(),
|
||||
})}
|
||||
description={t(`no_category_apps_description_calendar`)}
|
||||
buttonRaw={
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="connect-calendar-apps"
|
||||
href="/apps/categories/calendar">
|
||||
{t(`connect_calendar_apps`)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<SettingsHeader
|
||||
title={t("calendars")}
|
||||
description={t("calendars_description")}
|
||||
CTA={<AddCalendarButton />}>
|
||||
{!!data.connectedCalendars.length || !!installedCalendars?.items.length ? (
|
||||
<>
|
||||
{heading && (
|
||||
<>
|
||||
<DestinationCalendarSettingsWebWrapper />
|
||||
<Suspense fallback={<SkeletonLoader />}>
|
||||
<SelectedCalendarsSettingsWebWrapper
|
||||
onChanged={onChanged}
|
||||
fromOnboarding={fromOnboarding}
|
||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : fromOnboarding ? (
|
||||
<>
|
||||
{!!data?.connectedCalendars.length && (
|
||||
<ShellSubHeading
|
||||
className="mt-4"
|
||||
title={<SubHeadingTitleWithConnections title={t("connect_additional_calendar")} />}
|
||||
/>
|
||||
)}
|
||||
<CalendarList onChanged={onChanged} />
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon="calendar"
|
||||
headline={t("no_category_apps", {
|
||||
category: t("calendar").toLowerCase(),
|
||||
})}
|
||||
description={t(`no_category_apps_description_calendar`)}
|
||||
buttonRaw={
|
||||
<Button color="secondary" data-testid="connect-calendar-apps" href="/apps/categories/calendar">
|
||||
{t(`connect_calendar_apps`)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SettingsHeader>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/Bul
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import type { Icon } from "@calcom/ui/components/icon";
|
||||
@@ -181,9 +182,11 @@ type ModalState = {
|
||||
|
||||
type PageProps = {
|
||||
category: AppCategories;
|
||||
connectedCalendars: RouterOutputs["viewer"]["calendars"]["connectedCalendars"];
|
||||
installedCalendars: RouterOutputs["viewer"]["apps"]["integrations"];
|
||||
};
|
||||
|
||||
export default function InstalledApps({ category }: PageProps) {
|
||||
export default function InstalledApps({ category, connectedCalendars, installedCalendars }: PageProps) {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
|
||||
@@ -233,7 +236,12 @@ export default function InstalledApps({ category }: PageProps) {
|
||||
{categoryList.includes(category) && (
|
||||
<IntegrationsContainer handleDisconnect={handleDisconnect} variant={category} />
|
||||
)}
|
||||
{category === "calendar" && <CalendarListContainer />}
|
||||
{category === "calendar" && (
|
||||
<CalendarListContainer
|
||||
connectedCalendars={connectedCalendars}
|
||||
installedCalendars={installedCalendars}
|
||||
/>
|
||||
)}
|
||||
{category === "other" && (
|
||||
<IntegrationsContainer
|
||||
handleDisconnect={handleDisconnect}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
|
||||
export const SkeletonLoader = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<SettingsHeader title={t("appearance")} description={t("appearance_description")}>
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle mt-6 flex items-center rounded-t-xl border p-6 text-sm">
|
||||
<SkeletonText className="h-8 w-1/3" />
|
||||
</div>
|
||||
<div className="border-subtle space-y-6 border-x px-4 py-6 sm:px-6">
|
||||
<div className="[&>*]:bg-emphasis flex w-full items-center justify-center gap-x-2 [&>*]:animate-pulse">
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<SkeletonText className="h-8 w-1/3" />
|
||||
<SkeletonText className="h-8 w-1/3" />
|
||||
</div>
|
||||
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
<div className="rounded-b-lg">
|
||||
<SectionBottomActions align="end">
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</SectionBottomActions>
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { revalidateSettingsAppearance } from "app/(use-page-wrapper)/settings/(settings-layout)/my-account/appearance/actions";
|
||||
import { revalidateHasTeamPlan } from "app/cache/membership";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@@ -8,11 +10,11 @@ import type { z } from "zod";
|
||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import ThemeLabel from "@calcom/features/settings/ThemeLabel";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
|
||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
@@ -23,38 +25,9 @@ import { Alert } from "@calcom/ui/components/alert";
|
||||
import { UpgradeTeamsBadge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { SettingsToggle, ColorPicker, Form } from "@calcom/ui/components/form";
|
||||
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { useCalcomTheme } from "@calcom/ui/styles";
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle mt-6 flex items-center rounded-t-xl border p-6 text-sm">
|
||||
<SkeletonText className="h-8 w-1/3" />
|
||||
</div>
|
||||
<div className="border-subtle space-y-6 border-x px-4 py-6 sm:px-6">
|
||||
<div className="[&>*]:bg-emphasis flex w-full items-center justify-center gap-x-2 [&>*]:animate-pulse">
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<SkeletonText className="h-8 w-1/3" />
|
||||
<SkeletonText className="h-8 w-1/3" />
|
||||
</div>
|
||||
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
<div className="rounded-b-lg">
|
||||
<SectionBottomActions align="end">
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</SectionBottomActions>
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const useBrandColors = (
|
||||
currentTheme: string | null,
|
||||
{
|
||||
@@ -158,6 +131,8 @@ const AppearanceView = ({
|
||||
const mutation = trpc.viewer.me.updateProfile.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
await utils.viewer.me.invalidate();
|
||||
revalidateSettingsAppearance();
|
||||
revalidateHasTeamPlan();
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
resetBrandColorsThemeReset({ brandColor: data.brandColor, darkBrandColor: data.darkBrandColor });
|
||||
resetBookerLayoutThemeReset({ metadata: data.metadata });
|
||||
@@ -173,11 +148,13 @@ const AppearanceView = ({
|
||||
},
|
||||
onSettled: async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
revalidateSettingsAppearance();
|
||||
revalidateHasTeamPlan();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader title={t("appearance")} description={t("appearance_description")}>
|
||||
<div className="border-subtle mt-6 flex items-center rounded-t-lg border p-6 text-sm">
|
||||
<div>
|
||||
<p className="text-default text-base font-semibold">{t("app_theme")}</p>
|
||||
@@ -425,17 +402,8 @@ const AppearanceView = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearancePage = () => {
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery();
|
||||
const { isPending: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
|
||||
|
||||
if (isPending || isTeamPlanStatusLoading || !user) return <SkeletonLoader />;
|
||||
|
||||
return <AppearanceView user={user} hasPaidPlan={hasPaidPlan} />;
|
||||
};
|
||||
|
||||
export default AppearancePage;
|
||||
export default AppearanceView;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
|
||||
export const SkeletonLoader = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle space-y-6 rounded-b-xl border border-t-0 px-4 py-8 sm:px-6">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
|
||||
<SkeletonButton className="ml-auto h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { revalidateSettingsGeneral } from "app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/actions";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { TimezoneSelect } from "@calcom/features/components/timezone-select";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { formatLocalizedDateTime } from "@calcom/lib/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { localeOptions } from "@calcom/lib/i18n";
|
||||
@@ -18,8 +20,8 @@ import { Form } from "@calcom/ui/components/form";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateTravelSchedules } from "@calcom/web/app/cache/travelSchedule";
|
||||
|
||||
import TravelScheduleModal from "@components/settings/TravelScheduleModal";
|
||||
|
||||
@@ -45,55 +47,13 @@ export type FormValues = {
|
||||
}[];
|
||||
};
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle space-y-6 rounded-b-xl border border-t-0 px-4 py-8 sm:px-6">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
|
||||
<SkeletonButton className="ml-auto h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
interface GeneralViewProps {
|
||||
localeProp: string;
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
travelSchedules: RouterOutputs["viewer"]["travelSchedules"]["get"];
|
||||
revalidatePage: GeneralQueryViewProps["revalidatePage"];
|
||||
}
|
||||
|
||||
type GeneralQueryViewProps = {
|
||||
revalidatePage: () => Promise<void>;
|
||||
};
|
||||
|
||||
const GeneralQueryView = ({ revalidatePage }: GeneralQueryViewProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery();
|
||||
|
||||
const { data: travelSchedules, isPending: isPendingTravelSchedules } =
|
||||
trpc.viewer.travelSchedules.get.useQuery();
|
||||
|
||||
if (isPending || isPendingTravelSchedules) return <SkeletonLoader />;
|
||||
if (!user) {
|
||||
throw new Error(t("something_went_wrong"));
|
||||
}
|
||||
return (
|
||||
<GeneralView
|
||||
user={user}
|
||||
travelSchedules={travelSchedules || []}
|
||||
localeProp={user.locale}
|
||||
revalidatePage={revalidatePage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: GeneralViewProps) => {
|
||||
const GeneralView = ({ user, travelSchedules }: GeneralViewProps) => {
|
||||
const localeProp = user.locale ?? "en";
|
||||
const utils = trpc.useContext();
|
||||
const {
|
||||
t,
|
||||
@@ -106,6 +66,8 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
|
||||
const mutation = trpc.viewer.me.updateProfile.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await utils.viewer.me.invalidate();
|
||||
revalidateSettingsGeneral();
|
||||
revalidateTravelSchedules();
|
||||
reset(getValues());
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
await update(res);
|
||||
@@ -114,13 +76,14 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
|
||||
window.calNewLocale = res.locale;
|
||||
document.cookie = `calNewLocale=${res.locale}; path=/`;
|
||||
}
|
||||
await revalidatePage();
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
},
|
||||
onSettled: async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
revalidateSettingsGeneral();
|
||||
revalidateTravelSchedules();
|
||||
setIsUpdateBtnLoading(false);
|
||||
},
|
||||
});
|
||||
@@ -188,215 +151,217 @@ const GeneralView = ({ localeProp, user, travelSchedules, revalidatePage }: Gene
|
||||
const watchedTzSchedules = formMethods.watch("travelSchedules");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={async (values) => {
|
||||
setIsUpdateBtnLoading(true);
|
||||
mutation.mutate({
|
||||
...values,
|
||||
locale: values.locale.value,
|
||||
timeFormat: values.timeFormat.value,
|
||||
weekStart: values.weekStart.value,
|
||||
});
|
||||
}}>
|
||||
<div className="border-subtle border-x border-y-0 px-4 py-8 sm:px-6">
|
||||
<Controller
|
||||
name="locale"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis">
|
||||
<>{t("language")}</>
|
||||
</Label>
|
||||
<Select<{ label: string; value: string }>
|
||||
className="capitalize"
|
||||
options={localeOptions}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("timezone")}</>
|
||||
</Label>
|
||||
<TimezoneSelect
|
||||
id="timezone"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{!watchedTzSchedules.length ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
className="mt-2"
|
||||
StartIcon="calendar"
|
||||
onClick={() => setIsTZScheduleOpen(true)}>
|
||||
{t("schedule_timezone_change")}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="bg-muted border-subtle mt-2 rounded-md border p-4">
|
||||
<Label>{t("travel_schedule")}</Label>
|
||||
<div className="border-subtle bg-default mt-4 rounded-md border text-sm">
|
||||
{watchedTzSchedules.map((schedule, index) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex items-center p-4",
|
||||
index !== 0 ? "border-subtle border-t" : ""
|
||||
)}
|
||||
key={index}>
|
||||
<div>
|
||||
<div className="text-emphasis font-semibold">{`${formatLocalizedDateTime(
|
||||
schedule.startDate,
|
||||
{ day: "numeric", month: "long" },
|
||||
language
|
||||
)} ${
|
||||
schedule.endDate
|
||||
? `- ${formatLocalizedDateTime(
|
||||
schedule.endDate,
|
||||
{ day: "numeric", month: "long" },
|
||||
language
|
||||
)}`
|
||||
: ``
|
||||
}`}</div>
|
||||
<div className="text-subtle">{schedule.timeZone.replace(/_/g, " ")}</div>
|
||||
</div>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="ml-auto"
|
||||
variant="icon"
|
||||
StartIcon="trash-2"
|
||||
onClick={() => {
|
||||
const updatedSchedules = watchedTzSchedules.filter(
|
||||
(s, filterIndex) => filterIndex !== index
|
||||
);
|
||||
formMethods.setValue("travelSchedules", updatedSchedules, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<SettingsHeader title={t("general")} description={t("general_description")} borderInShellHeader={true}>
|
||||
<div>
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={async (values) => {
|
||||
setIsUpdateBtnLoading(true);
|
||||
mutation.mutate({
|
||||
...values,
|
||||
locale: values.locale.value,
|
||||
timeFormat: values.timeFormat.value,
|
||||
weekStart: values.weekStart.value,
|
||||
});
|
||||
}}>
|
||||
<div className="border-subtle border-x border-y-0 px-4 py-8 sm:px-6">
|
||||
<Controller
|
||||
name="locale"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis">
|
||||
<>{t("language")}</>
|
||||
</Label>
|
||||
<Select<{ label: string; value: string }>
|
||||
className="capitalize"
|
||||
options={localeOptions}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("timezone")}</>
|
||||
</Label>
|
||||
<TimezoneSelect
|
||||
id="timezone"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{!watchedTzSchedules.length ? (
|
||||
<Button
|
||||
StartIcon="plus"
|
||||
color="secondary"
|
||||
className="mt-4"
|
||||
className="mt-2"
|
||||
StartIcon="calendar"
|
||||
onClick={() => setIsTZScheduleOpen(true)}>
|
||||
{t("add")}
|
||||
{t("schedule_timezone_change")}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="bg-muted border-subtle mt-2 rounded-md border p-4">
|
||||
<Label>{t("travel_schedule")}</Label>
|
||||
<div className="border-subtle bg-default mt-4 rounded-md border text-sm">
|
||||
{watchedTzSchedules.map((schedule, index) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex items-center p-4",
|
||||
index !== 0 ? "border-subtle border-t" : ""
|
||||
)}
|
||||
key={index}>
|
||||
<div>
|
||||
<div className="text-emphasis font-semibold">{`${formatLocalizedDateTime(
|
||||
schedule.startDate,
|
||||
{ day: "numeric", month: "long" },
|
||||
language
|
||||
)} ${
|
||||
schedule.endDate
|
||||
? `- ${formatLocalizedDateTime(
|
||||
schedule.endDate,
|
||||
{ day: "numeric", month: "long" },
|
||||
language
|
||||
)}`
|
||||
: ``
|
||||
}`}</div>
|
||||
<div className="text-subtle">{schedule.timeZone.replace(/_/g, " ")}</div>
|
||||
</div>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="ml-auto"
|
||||
variant="icon"
|
||||
StartIcon="trash-2"
|
||||
onClick={() => {
|
||||
const updatedSchedules = watchedTzSchedules.filter(
|
||||
(s, filterIndex) => filterIndex !== index
|
||||
);
|
||||
formMethods.setValue("travelSchedules", updatedSchedules, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
StartIcon="plus"
|
||||
color="secondary"
|
||||
className="mt-4"
|
||||
onClick={() => setIsTZScheduleOpen(true)}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="timeFormat"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("time_format")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={timeFormatOptions}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("timeFormat", { ...event }, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<div className="text-gray text-subtle mt-2 flex items-center text-xs">
|
||||
{t("timeformat_profile_hint")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="timeFormat"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("time_format")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={timeFormatOptions}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("timeFormat", { ...event }, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<div className="text-gray text-subtle mt-2 flex items-center text-xs">
|
||||
{t("timeformat_profile_hint")}
|
||||
<Controller
|
||||
name="weekStart"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("start_of_week")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={weekStartOptions}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
name="weekStart"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("start_of_week")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={weekStartOptions}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
loading={isUpdateBtnLoading}
|
||||
disabled={isDisabled}
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-testid="general-submit-button">
|
||||
<>{t("update")}</>
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</Form>
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
loading={isUpdateBtnLoading}
|
||||
disabled={isDisabled}
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-testid="general-submit-button">
|
||||
<>{t("update")}</>
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</Form>
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("dynamic_booking")}
|
||||
description={t("allow_dynamic_booking")}
|
||||
disabled={mutation.isPending}
|
||||
checked={isAllowDynamicBookingChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsAllowDynamicBookingChecked(checked);
|
||||
mutation.mutate({ allowDynamicBooking: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("dynamic_booking")}
|
||||
description={t("allow_dynamic_booking")}
|
||||
disabled={mutation.isPending}
|
||||
checked={isAllowDynamicBookingChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsAllowDynamicBookingChecked(checked);
|
||||
mutation.mutate({ allowDynamicBooking: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
data-testid="my-seo-indexing-switch"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("seo_indexing")}
|
||||
description={t("allow_seo_indexing")}
|
||||
disabled={mutation.isPending || user.organizationSettings?.allowSEOIndexing === false}
|
||||
checked={isAllowSEOIndexingChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsAllowSEOIndexingChecked(checked);
|
||||
mutation.mutate({ allowSEOIndexing: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
<SettingsToggle
|
||||
data-testid="my-seo-indexing-switch"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("seo_indexing")}
|
||||
description={t("allow_seo_indexing")}
|
||||
disabled={mutation.isPending || user.organizationSettings?.allowSEOIndexing === false}
|
||||
checked={isAllowSEOIndexingChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsAllowSEOIndexingChecked(checked);
|
||||
mutation.mutate({ allowSEOIndexing: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("monthly_digest_email")}
|
||||
description={t("monthly_digest_email_for_teams")}
|
||||
disabled={mutation.isPending}
|
||||
checked={isReceiveMonthlyDigestEmailChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsReceiveMonthlyDigestEmailChecked(checked);
|
||||
mutation.mutate({ receiveMonthlyDigestEmail: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
<TravelScheduleModal
|
||||
open={isTZScheduleOpen}
|
||||
onOpenChange={() => setIsTZScheduleOpen(false)}
|
||||
setValue={formMethods.setValue}
|
||||
existingSchedules={formMethods.getValues("travelSchedules") ?? []}
|
||||
/>
|
||||
</div>
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("monthly_digest_email")}
|
||||
description={t("monthly_digest_email_for_teams")}
|
||||
disabled={mutation.isPending}
|
||||
checked={isReceiveMonthlyDigestEmailChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsReceiveMonthlyDigestEmailChecked(checked);
|
||||
mutation.mutate({ receiveMonthlyDigestEmail: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
<TravelScheduleModal
|
||||
open={isTZScheduleOpen}
|
||||
onOpenChange={() => setIsTZScheduleOpen(false)}
|
||||
setValue={formMethods.setValue}
|
||||
existingSchedules={formMethods.getValues("travelSchedules") ?? []}
|
||||
/>
|
||||
</div>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralQueryView;
|
||||
export default GeneralView;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import {
|
||||
SkeletonButton,
|
||||
SkeletonAvatar,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
} from "@calcom/ui/components/skeleton";
|
||||
|
||||
export const SkeletonLoader = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={t("profile")}
|
||||
description={t("profile_description", { appName: APP_NAME })}
|
||||
borderInShellHeader={true}>
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle space-y-6 rounded-b-lg border border-t-0 px-4 py-8">
|
||||
<div className="flex items-center">
|
||||
<SkeletonAvatar className="me-4 mt-0 h-16 w-16 px-4" />
|
||||
<SkeletonButton className="h-6 w-32 rounded-md p-5" />
|
||||
</div>
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { revalidateSettingsProfile } from "app/cache/path/settings/my-account";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { get, pick } from "lodash";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
@@ -12,6 +13,7 @@ import { z } from "zod";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { DisplayInfo } from "@calcom/features/users/components/UserTable/EditSheet/DisplayInfo";
|
||||
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||
import { emailSchema } from "@calcom/lib/emailSchema";
|
||||
@@ -34,12 +36,6 @@ import { Label } from "@calcom/ui/components/form";
|
||||
import { TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { ImageUploader } from "@calcom/ui/components/image-uploader";
|
||||
import {
|
||||
SkeletonButton,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
SkeletonAvatar,
|
||||
} from "@calcom/ui/components/skeleton";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
@@ -50,24 +46,6 @@ import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
|
||||
import type { TRPCClientErrorLike } from "@trpc/client";
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle space-y-6 rounded-b-lg border border-t-0 px-4 py-8">
|
||||
<div className="flex items-center">
|
||||
<SkeletonAvatar className="me-4 mt-0 h-16 w-16 px-4" />
|
||||
<SkeletonButton className="h-6 w-32 rounded-md p-5" />
|
||||
</div>
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeleteAccountValues {
|
||||
totpCode: string;
|
||||
}
|
||||
@@ -87,18 +65,20 @@ export type FormValues = {
|
||||
bio: string;
|
||||
secondaryEmails: Email[];
|
||||
};
|
||||
type Props = {
|
||||
user: RouterOutputs["viewer"]["me"]["get"];
|
||||
};
|
||||
|
||||
const ProfileView = () => {
|
||||
const ProfileView = ({ user }: Props) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const { update } = useSession();
|
||||
const { data: user, isPending } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
|
||||
|
||||
const updateProfileMutation = trpc.viewer.me.updateProfile.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await update(res);
|
||||
utils.viewer.me.invalidate();
|
||||
utils.viewer.me.shouldVerifyEmail.invalidate();
|
||||
revalidateSettingsProfile();
|
||||
|
||||
if (res.hasEmailBeenChanged && res.sendEmailVerification) {
|
||||
showToast(t("change_of_email_toast", { email: tempFormValues?.email }), "success");
|
||||
@@ -125,6 +105,7 @@ const ProfileView = () => {
|
||||
onSuccess: async (res) => {
|
||||
showToast(t(res.message), "success");
|
||||
utils.viewer.me.invalidate();
|
||||
revalidateSettingsProfile();
|
||||
},
|
||||
onError: (e) => {
|
||||
showToast(t(e.message), "error");
|
||||
@@ -136,6 +117,7 @@ const ProfileView = () => {
|
||||
setShowSecondaryEmailModalOpen(false);
|
||||
setNewlyAddedSecondaryEmail(res?.data?.email);
|
||||
utils.viewer.me.invalidate();
|
||||
revalidateSettingsProfile();
|
||||
},
|
||||
onError: (error) => {
|
||||
setSecondaryEmailAddErrorMessage(error?.message || "");
|
||||
@@ -160,6 +142,8 @@ const ProfileView = () => {
|
||||
|
||||
const onDeleteMeSuccessMutation = async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
revalidateSettingsProfile();
|
||||
|
||||
showToast(t("Your account was deleted"), "success");
|
||||
|
||||
setHasDeleteErrors(false); // dismiss any open errors
|
||||
@@ -189,6 +173,7 @@ const ProfileView = () => {
|
||||
onError: onDeleteMeErrorMutation,
|
||||
async onSettled() {
|
||||
await utils.viewer.me.invalidate();
|
||||
revalidateSettingsProfile();
|
||||
},
|
||||
});
|
||||
const deleteMeWithoutPasswordMutation = trpc.viewer.me.deleteMeWithoutPassword.useMutation({
|
||||
@@ -196,6 +181,7 @@ const ProfileView = () => {
|
||||
onError: onDeleteMeErrorMutation,
|
||||
async onSettled() {
|
||||
await utils.viewer.me.invalidate();
|
||||
revalidateSettingsProfile();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -241,10 +227,6 @@ const ProfileView = () => {
|
||||
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
||||
};
|
||||
|
||||
if (isPending || !user) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
const userEmail = user.email || "";
|
||||
const defaultValues = {
|
||||
username: user.username || "",
|
||||
@@ -269,7 +251,10 @@ const ProfileView = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsHeader
|
||||
title={t("profile")}
|
||||
description={t("profile_description", { appName: APP_NAME })}
|
||||
borderInShellHeader={true}>
|
||||
<ProfileForm
|
||||
key={JSON.stringify(defaultValues)}
|
||||
defaultValues={defaultValues}
|
||||
@@ -305,6 +290,7 @@ const ProfileView = () => {
|
||||
onSuccessMutation={async () => {
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
await utils.viewer.me.invalidate();
|
||||
revalidateSettingsProfile();
|
||||
}}
|
||||
onErrorMutation={() => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
@@ -473,7 +459,7 @@ const ProfileView = () => {
|
||||
onCancel={() => setNewlyAddedSecondaryEmail(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useWebPush } from "@calcom/features/notifications/WebPushContext";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
@@ -9,11 +10,16 @@ const PushNotificationsView = () => {
|
||||
const { subscribe, unsubscribe, isSubscribed, isLoading } = useWebPush();
|
||||
|
||||
return (
|
||||
<div className="border-subtle rounded-b-xl border-x border-b px-4 pb-10 pt-8 sm:px-6">
|
||||
<Button color="primary" onClick={isSubscribed ? unsubscribe : subscribe} disabled={isLoading}>
|
||||
{isSubscribed ? t("disable_browser_notifications") : t("allow_browser_notifications")}
|
||||
</Button>
|
||||
</div>
|
||||
<SettingsHeader
|
||||
title={t("push_notifications")}
|
||||
description={t("push_notifications_description")}
|
||||
borderInShellHeader={true}>
|
||||
<div className="border-subtle rounded-b-xl border-x border-b px-4 pb-10 pt-8 sm:px-6">
|
||||
<Button color="primary" onClick={isSubscribed ? unsubscribe : subscribe} disabled={isLoading}>
|
||||
{isSubscribed ? t("disable_browser_notifications") : t("allow_browser_notifications")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DataTableSegment,
|
||||
} from "@calcom/features/data-table";
|
||||
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import ServerTrans from "@calcom/lib/components/ServerTrans";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
@@ -39,7 +40,7 @@ import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import CreateNewOutOfOfficeEntryButton from "./CreateNewOutOfOfficeEntryButton";
|
||||
import { CreateOrEditOutOfOfficeEntryModal } from "./CreateOrEditOutOfOfficeModal";
|
||||
import type { BookingRedirectForm } from "./CreateOrEditOutOfOfficeModal";
|
||||
import { OutOfOfficeTab } from "./OutOfOfficeToggleGroup";
|
||||
import { OutOfOfficeTab, OutOfOfficeToggleGroup } from "./OutOfOfficeToggleGroup";
|
||||
|
||||
interface OutOfOfficeEntry {
|
||||
id: number;
|
||||
@@ -63,10 +64,22 @@ interface OutOfOfficeEntry {
|
||||
}
|
||||
|
||||
export default function OutOfOfficeEntriesList() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<DataTableProvider useSegments={useSegments}>
|
||||
<OutOfOfficeEntriesListContent />
|
||||
</DataTableProvider>
|
||||
<SettingsHeader
|
||||
title={t("out_of_office")}
|
||||
description={t("out_of_office_description")}
|
||||
CTA={
|
||||
<div className="flex gap-2">
|
||||
<OutOfOfficeToggleGroup />
|
||||
<CreateNewOutOfOfficeEntryButton data-testid="add_entry_ooo" />
|
||||
</div>
|
||||
}>
|
||||
<DataTableProvider useSegments={useSegments}>
|
||||
<OutOfOfficeEntriesListContent />
|
||||
</DataTableProvider>
|
||||
</SettingsHeader>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,20 @@ export class MembershipRepository {
|
||||
});
|
||||
}
|
||||
|
||||
static async findFirstAcceptedMembershipByUserId(userId: number) {
|
||||
return await prisma.membership.findFirst({
|
||||
where: {
|
||||
accepted: true,
|
||||
userId,
|
||||
team: {
|
||||
slug: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async createMany(data: IMembership[]) {
|
||||
return await prisma.membership.createMany({
|
||||
data: data.map((item) => ({
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { cache as unstable_cache } from "./unstable_cache";
|
||||
|
||||
export { unstable_cache };
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This implementation is adapted from https://github.com/vercel/next.js/issues/51613#issuecomment-1892644565.
|
||||
* It is a wrapper around `unstable_cache` that adds serialization and deserialization
|
||||
*/
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { parse, stringify } from "superjson";
|
||||
|
||||
export const cache = <T, P extends unknown[]>(
|
||||
fn: (...params: P) => Promise<T>,
|
||||
keys: Parameters<typeof unstable_cache>[1],
|
||||
opts: Parameters<typeof unstable_cache>[2]
|
||||
) => {
|
||||
const wrap = async (params: unknown[]): Promise<string> => {
|
||||
const result = await fn(...(params as P));
|
||||
return stringify(result);
|
||||
};
|
||||
|
||||
const cachedFn = unstable_cache(wrap, keys, opts);
|
||||
|
||||
return async (...params: P): Promise<T> => {
|
||||
const result = await cachedFn(params);
|
||||
return parse(result);
|
||||
};
|
||||
};
|
||||
+13
-21
@@ -12,12 +12,6 @@ import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { SkeletonText, SkeletonContainer } from "@calcom/ui/components/skeleton";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
type ConferencingAppsViewWebWrapperProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
add: string;
|
||||
};
|
||||
|
||||
export type UpdateUsersDefaultConferencingAppParams = {
|
||||
appSlug: string;
|
||||
appLink?: string;
|
||||
@@ -175,11 +169,17 @@ const useDisconnectIntegrationModalController = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const ConferencingAppsViewWebWrapper = ({
|
||||
title,
|
||||
description,
|
||||
add,
|
||||
}: ConferencingAppsViewWebWrapperProps) => {
|
||||
const AddConferencingButton = () => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Button color="secondary" StartIcon="plus" href="/apps/categories/conferencing">
|
||||
{t("add")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConferencingAppsViewWebWrapper = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
@@ -203,20 +203,12 @@ export const ConferencingAppsViewWebWrapper = ({
|
||||
);
|
||||
};
|
||||
|
||||
const AddConferencingButton = () => {
|
||||
return (
|
||||
<Button color="secondary" StartIcon="plus" href="/apps/categories/conferencing">
|
||||
{add}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const disconnectIntegrationModalCtrl = useDisconnectIntegrationModalController();
|
||||
|
||||
return (
|
||||
<SettingsHeader
|
||||
title={title}
|
||||
description={description}
|
||||
title={t("conferencing")}
|
||||
description={t("conferencing_description")}
|
||||
CTA={<AddConferencingButton />}
|
||||
borderInShellHeader={true}>
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
type HasTeamPlanOptions = {
|
||||
ctx: {
|
||||
@@ -6,18 +7,11 @@ type HasTeamPlanOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
export const hasTeamPlanHandler = async ({ ctx: { user } }: HasTeamPlanOptions) => {
|
||||
const hasTeamPlan = await prisma.membership.findFirst({
|
||||
where: {
|
||||
accepted: true,
|
||||
userId: user.id,
|
||||
team: {
|
||||
slug: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export const hasTeamPlanHandler = async ({ ctx }: HasTeamPlanOptions) => {
|
||||
const userId = ctx.user.id;
|
||||
|
||||
const hasTeamPlan = await MembershipRepository.findFirstAcceptedMembershipByUserId(userId);
|
||||
|
||||
return { hasTeamPlan: !!hasTeamPlan };
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user