Skip to content

perf: fetch server side data for workflow details page #20778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
32fde0b
perf:fetch-server-side-data-for-dynamic-webflow-page
TusharBhatt1 Apr 20, 2025
4f1b698
skeleton
TusharBhatt1 Apr 20, 2025
87a241e
suggested-changes
TusharBhatt1 Apr 20, 2025
b189e51
type check
TusharBhatt1 Apr 20, 2025
fbae058
skeleton
TusharBhatt1 Apr 20, 2025
9ceafca
type-check
TusharBhatt1 Apr 20, 2025
e2eb382
dynamic shell name
TusharBhatt1 Apr 20, 2025
7663a7f
type-check
TusharBhatt1 Apr 20, 2025
62da080
type-check
TusharBhatt1 Apr 20, 2025
efcd593
one more call to ss
TusharBhatt1 Apr 21, 2025
2ca0c44
Merge branch 'calcom:main' into perf/fetch-server-side-data-for-dynam…
TusharBhatt1 Apr 21, 2025
6dc25e0
skeleton
TusharBhatt1 Apr 21, 2025
1ffde98
Merge branch 'perf/fetch-server-side-data-for-dynamic-webflow-page' o…
TusharBhatt1 Apr 21, 2025
dc7f475
Merge branch 'main' into perf/fetch-server-side-data-for-dynamic-webf…
hbjORbj Apr 21, 2025
c15e43f
optimie queries
TusharBhatt1 Apr 21, 2025
38651fe
moved actionOptions call back to client side
TusharBhatt1 Apr 21, 2025
30f93d8
Merge branch 'perf/fetch-server-side-data-for-dynamic-webflow-page' o…
TusharBhatt1 Apr 21, 2025
af4424e
final-change
TusharBhatt1 Apr 21, 2025
76b65aa
fix: ts error
TusharBhatt1 Jun 29, 2025
1014bf1
Merge branch 'main' into perf/fetch-server-side-data-for-dynamic-webf…
TusharBhatt1 Jun 29, 2025
9998a15
adding revalidate and cache
TusharBhatt1 Jun 29, 2025
e390d98
Merge branch 'main' into perf/fetch-server-side-data-for-dynamic-webf…
TusharBhatt1 Jun 29, 2025
49b5bfe
Merge branch 'perf/fetch-server-side-data-for-dynamic-webflow-page' o…
TusharBhatt1 Jun 29, 2025
cb04804
indentation
TusharBhatt1 Jun 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 71 additions & 24 deletions apps/web/app/(use-page-wrapper)/workflows/[workflow]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { createRouterCaller } from "app/_trpc/context";
import type { PageProps } from "app/_types";
import { _generateMetadata } from "app/_utils";
import type { Metadata } from "next";
import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import { redirect } from "next/navigation";
import { z } from "zod";

// import { cookies, headers } from "next/headers";
// import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
// import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import LegacyPage from "@calcom/features/ee/workflows/pages/workflow";
import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router";
import {
getCachedUser,
getCachedWorkflowById,
getCachedWorkflowVerifiedNumber,
getCachedWorkflowVerifiedEmails,
} from "@calcom/web/cache/workflows";

import { buildLegacyRequest } from "@lib/buildLegacyCtx";

const querySchema = z.object({
workflow: z
Expand All @@ -15,32 +28,66 @@ const querySchema = z.object({
.transform((val) => Number(val)),
});

export const generateMetadata = async ({ params }: PageProps): Promise<Metadata | null> => {
const parsed = querySchema.safeParse(await params);
if (!parsed.success) {
notFound();
}
const workflow = await getCachedWorkflowById(parsed.data.workflow);
if (!workflow) {
notFound();
}
return await _generateMetadata(
(t) => (workflow && workflow.name ? workflow.name : t("untitled")),
() => "",
undefined,
undefined,
`/workflows/${parsed.data.workflow}`
);
};

const Page = async ({ params }: PageProps) => {
// const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
// const user = session?.user;
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });

if (!session?.user?.id) {
redirect("/auth/login");
}

if (!session?.user?.email) {
throw new Error("User email not found");
}

const parsed = querySchema.safeParse(await params);
if (!parsed.success) throw new Error("Invalid workflow id");

// const workflow = await WorkflowRepository.getById({ id: +parsed.data.workflow });
// let verifiedEmails, verifiedNumbers;
// try {
// verifiedEmails = await WorkflowRepository.getVerifiedEmails({
// userEmail: user?.email ?? null,
// userId: user?.id ?? null,
// teamId: workflow?.team?.id,
// });
// } catch (err) {}
// try {
// verifiedNumbers = await WorkflowRepository.getVerifiedNumbers({
// userId: user?.id ?? null,
// teamId: workflow?.team?.id,
// });
// } catch (err) {}

if (!parsed.success) {
notFound();
}
const workFlowId = parsed.data.workflow;

const eventCaller = await createRouterCaller(eventTypesRouter);

const workflowData = await getCachedWorkflowById(workFlowId);

if (!workflowData) return notFound();

const isOrg = workflowData?.team?.isOrganization ?? false;
const teamId = workflowData?.teamId ?? undefined;

const [verifiedEmails, verifiedNumbers, eventsData, user] = await Promise.all([
getCachedWorkflowVerifiedEmails(session.user.email, session.user.id, teamId),
teamId ? getCachedWorkflowVerifiedNumber(teamId, session.user.id) : [],
eventCaller.getTeamAndEventTypeOptions({ teamId, isOrg }),
getCachedUser(session.user, session.upId),
]);

return (
<LegacyPage
workflow={parsed.data.workflow}
// workflowData={workflow} verifiedEmails={verifiedEmails} verifiedNumbers={verifiedNumbers}
user={user}
eventsData={eventsData}
workflowId={workFlowId}
workflow={workflowData}
verifiedNumbers={verifiedNumbers}
verifiedEmails={verifiedEmails}
/>
);
};
Expand Down
80 changes: 80 additions & 0 deletions apps/web/cache/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use server";

import { revalidateTag, unstable_cache } from "next/cache";

import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";

const CACHE_TAGS = {
WORKFLOWS_LIST: "WorkflowRepository.filteredList",
WORKFLOW_BY_ID: "WorkflowRepository.getById",
VERIFIED_NUMBERS: "WorkflowRepository.getVerifiedNumbers",
VERIFIED_EMAILS: "WorkflowRepository.getVerifiedEmails",
ENRICHED_USER: "UserRepository.enrichUserWithTheProfile",
} as const;

export const getCachedWorkflowsFilteredList = unstable_cache(
async (userId: number, filters: any) => {
return await WorkflowRepository.getFilteredList({ userId, input: { filters } });
},
["getCachedWorkflowsFilteredList"],
{
revalidate: NEXTJS_CACHE_TTL,
tags: [CACHE_TAGS.WORKFLOWS_LIST],
}
);

export async function revalidateWorkflowsList() {
revalidateTag(CACHE_TAGS.WORKFLOWS_LIST);
}

export const getCachedWorkflowById = unstable_cache(
async (id: number) => {
return await WorkflowRepository.getById({ id });
},
["getCachedWorkflowById"],
{
revalidate: NEXTJS_CACHE_TTL,
tags: [CACHE_TAGS.WORKFLOW_BY_ID],
}
);

export const getCachedWorkflowVerifiedNumber = unstable_cache(
async (teamId: number, userId: number) => {
return await WorkflowRepository.getVerifiedNumbers({ teamId, userId });
},
["getCachedWorkflowVerifiedNumber"],
{
revalidate: NEXTJS_CACHE_TTL,
tags: [CACHE_TAGS.VERIFIED_NUMBERS],
}
);
export const getCachedWorkflowVerifiedEmails = unstable_cache(
async (userEmail: string, userId: number, teamId?: number) => {
return await WorkflowRepository.getVerifiedEmails({ userEmail, userId, teamId });
},
["getCachedWorkflowVerifiedEmails"],
{
revalidate: NEXTJS_CACHE_TTL,
tags: [CACHE_TAGS.VERIFIED_EMAILS],
}
);

export const getCachedUser = unstable_cache(
async (user: any, upId: string) => {
return await UserRepository.enrichUserWithTheProfile({ user, upId });
},
["getCachedUser"],
{
revalidate: NEXTJS_CACHE_TTL,
tags: [CACHE_TAGS.ENRICHED_USER],
}
);

export async function revalidateWorkflowById() {
revalidateTag(CACHE_TAGS.WORKFLOW_BY_ID);
}
export async function revalidateWorkflowVerifiedNumbers() {
revalidateTag(CACHE_TAGS.VERIFIED_NUMBERS);
}
2 changes: 1 addition & 1 deletion apps/web/modules/settings/admin/components/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function UsersTable({ setSMSLockState }: Props) {
const { data: usersAndTeams } = trpc.viewer.admin.getSMSLockStateTeamsUsers.useQuery();

if (!usersAndTeams) {
return <></>;
return null;
}

const users = usersAndTeams.users.locked.concat(usersAndTeams.users.reviewNeeded);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false);
const [isSenderIdNeeded, setIsSenderIdNeeded] = useState(false);
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false);

const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();

const formSchema = z.object({
Expand Down
23 changes: 23 additions & 0 deletions packages/features/ee/workflows/components/WorkFlowFormSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SkeletonText } from "@calcom/ui/components/skeleton";

export default function WorkFlowFormSkeleton() {
return (
<div className="min-w-80 bg-default border-subtle w-full space-y-6 rounded-md border p-7">
<div className="flex items-center gap-4">
<SkeletonText className="w-6 rounded-full" />
<div className="flex w-full flex-col gap-2">
<SkeletonText className="w-28" />
<SkeletonText className="w-40" />
</div>
</div>
<div className="border-subtle border-t" />
{Array.from({ length: 2 }).map((_, idx) => (
<div key={idx} className="space-y-2">
<SkeletonText className="w-28" />
<SkeletonText className="w-full" />
</div>
))}
<SkeletonText className="w-full" />
</div>
);
}
42 changes: 32 additions & 10 deletions packages/features/ee/workflows/components/WorkflowDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { useState, useEffect } from "react";
import type { UseFormReturn } from "react-hook-form";
import { Controller } from "react-hook-form";

import WorkFlowFormSkeleton from "@calcom/features/ee/workflows/components/WorkFlowFormSkeleton";
import { SENDER_ID, SENDER_NAME, SCANNING_WORKFLOW_STEPS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { WorkflowActions } from "@calcom/prisma/enums";
import { WorkflowTemplates } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import { InfoBadge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
Expand All @@ -24,6 +26,8 @@ import WorkflowStepContainer from "./WorkflowStepContainer";
type User = RouterOutputs["viewer"]["me"]["get"];

interface Props {
verifiedNumbers: RouterOutputs["viewer"]["workflows"]["getVerifiedNumbers"] | undefined;
verifiedEmails: RouterOutputs["viewer"]["workflows"]["getVerifiedEmails"] | undefined;
form: UseFormReturn<FormValues>;
workflowId: number;
selectedOptions: Option[];
Expand All @@ -36,10 +40,22 @@ interface Props {
}

export default function WorkflowDetailsPage(props: Props) {
const { form, workflowId, selectedOptions, setSelectedOptions, teamId, isOrg, allOptions } = props;
const {
form,
workflowId,
verifiedEmails,
verifiedNumbers,
selectedOptions,
setSelectedOptions,
teamId,
isOrg,
allOptions,
} = props;
const { t } = useLocale();
const router = useRouter();

const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();

const [isAddActionDialogOpen, setIsAddActionDialogOpen] = useState(false);

const [reload, setReload] = useState(false);
Expand Down Expand Up @@ -175,21 +191,26 @@ export default function WorkflowDetailsPage(props: Props) {

{/* Workflow Trigger Event & Steps */}
<div className="bg-muted border-subtle w-full rounded-md border p-3 py-5 md:ml-3 md:p-8">
{form.getValues("trigger") && (
<div>
<WorkflowStepContainer
form={form}
user={props.user}
teamId={teamId}
readOnly={props.readOnly}
/>
</div>
{form.getValues("trigger") ? (
<WorkflowStepContainer
verifiedNumbers={verifiedNumbers}
verifiedEmails={verifiedEmails}
form={form}
user={props.user}
teamId={teamId}
readOnly={props.readOnly}
actionOptions={actionOptions}
/>
) : (
<WorkFlowFormSkeleton />
)}
{form.getValues("steps") && (
<>
{form.getValues("steps")?.map((step) => {
return (
<WorkflowStepContainer
verifiedNumbers={verifiedNumbers}
verifiedEmails={verifiedEmails}
key={step.id}
form={form}
user={props.user}
Expand All @@ -198,6 +219,7 @@ export default function WorkflowDetailsPage(props: Props) {
setReload={setReload}
teamId={teamId}
readOnly={props.readOnly}
actionOptions={actionOptions}
/>
);
})}
Expand Down
Loading
Loading