Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
98 changes: 47 additions & 51 deletions apps/web/modules/apps/installation/[[...step]]/step-view.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { usePathname, useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { Toaster } from "sonner";
import type { z } from "zod";
Expand All @@ -22,6 +22,7 @@ import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import type { AppMeta } from "@calcom/types/App";
import { Form, Steps } from "@calcom/ui/components/form";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";

import { HttpError } from "@lib/core/http/error";
Expand All @@ -32,7 +33,9 @@ import { ConfigureStepCard } from "@components/apps/installation/ConfigureStepCa
import { EventTypesStepCard } from "@components/apps/installation/EventTypesStepCard";
import { StepHeader } from "@components/apps/installation/StepHeader";

import { STEPS } from "~/apps/installation/[[...step]]/constants";
import type { STEPS } from "~/apps/installation/[[...step]]/constants";

import { useStepManager } from "./useStepManager";

export type TEventType = EventTypeAppSettingsComponentProps["eventType"] &
Pick<
Expand Down Expand Up @@ -66,15 +69,6 @@ export type TEventTypesForm = {

type StepType = (typeof STEPS)[number];

type StepObj = Record<
StepType,
{
getTitle: (appName: string) => string;
getDescription: (appName: string) => string;
stepNumber: number;
}
>;

export type TTeams = (Pick<Team, "id" | "name" | "logoUrl" | "isOrganization"> & {
alreadyInstalled: boolean;
})[];
Expand Down Expand Up @@ -116,42 +110,17 @@ const OnboardingPage = ({
const pathname = usePathname();
const router = useRouter();

const STEPS_MAP: StepObj = {
[AppOnboardingSteps.ACCOUNTS_STEP]: {
getTitle: () => `${t("select_account_header")}`,
getDescription: (appName) =>
`${t("select_account_description", { appName, interpolation: { escapeValue: false } })}`,
stepNumber: 1,
},
[AppOnboardingSteps.EVENT_TYPES_STEP]: {
getTitle: () => `${t("select_event_types_header")}`,
getDescription: (appName) =>
`${t("select_event_types_description", { appName, interpolation: { escapeValue: false } })}`,
stepNumber: installableOnTeams ? 2 : 1,
},
[AppOnboardingSteps.CONFIGURE_STEP]: {
getTitle: (appName) =>
`${t("configure_app_header", { appName, interpolation: { escapeValue: false } })}`,
getDescription: () => `${t("configure_app_description")}`,
stepNumber: installableOnTeams ? 3 : 2,
},
} as const;
const [configureStep, setConfigureStep] = useState(false);

const currentStep: AppOnboardingSteps = useMemo(() => {
if (step == AppOnboardingSteps.EVENT_TYPES_STEP && configureStep) {
return AppOnboardingSteps.CONFIGURE_STEP;
}
return step;
}, [step, configureStep]);
const stepObj = STEPS_MAP[currentStep];
const isOnlySingleAccountToSelect = !teams?.length || !installableOnTeams;

const maxSteps = useMemo(() => {
if (!showEventTypesStep) {
return 1;
}
return installableOnTeams ? STEPS.length : STEPS.length - 1;
}, [showEventTypesStep, installableOnTeams]);
const stepManager = useStepManager({
step,
configureStep,
showEventTypesStep,
isOnlySingleAccountToSelect,
appName: appMetadata.name,
});

const utils = trpc.useContext();

Expand Down Expand Up @@ -208,6 +177,10 @@ const OnboardingPage = ({
});

const handleSelectAccount = async (teamId?: number) => {
if (mutation.isPending) {
return;
}

mutation.mutate({
type: appMetadata.type,
variant: appMetadata.variant,
Expand All @@ -230,6 +203,27 @@ const OnboardingPage = ({
router.push(`/apps/installed/${appMetadata.categories[0]}?hl=${appMetadata.slug}`);
};

async function skipAccountsStep() {
await handleSelectAccount();
// Replace URL to make it look like user came from app page
router.replace(`/apps/${appMetadata.slug}`);
}

useEffect(() => {
if (isOnlySingleAccountToSelect && !mutation.isPending && step === AppOnboardingSteps.ACCOUNTS_STEP) {
skipAccountsStep();
}
}, [isOnlySingleAccountToSelect, step]);

// Show loading when mutation is pending OR when auto-skip conditions are met
if (mutation.isPending) {
return (
<div className="absolute inset-0 z-10 flex items-center justify-center">
<SkeletonText className="h-10 w-10 animate-spin" />
</div>
);
}

return (
<div
key={pathname}
Expand Down Expand Up @@ -286,12 +280,14 @@ const OnboardingPage = ({
console.error(err);
}
}}>
<StepHeader
title={stepObj.getTitle(appMetadata.name)}
subtitle={stepObj.getDescription(appMetadata.name)}>
<Steps maxSteps={maxSteps} currentStep={stepObj.stepNumber} disableNavigation />
<StepHeader title={stepManager.title} subtitle={stepManager.description}>
<Steps
maxSteps={stepManager.maxSteps}
currentStep={stepManager.stepNumber}
disableNavigation
/>
</StepHeader>
{currentStep === AppOnboardingSteps.ACCOUNTS_STEP && (
{stepManager.currentStep === AppOnboardingSteps.ACCOUNTS_STEP && (
<AccountsStepCard
teams={teams}
personalAccount={personalAccount}
Expand All @@ -300,7 +296,7 @@ const OnboardingPage = ({
installableOnTeams={installableOnTeams}
/>
)}
{currentStep === AppOnboardingSteps.EVENT_TYPES_STEP &&
{stepManager.currentStep === AppOnboardingSteps.EVENT_TYPES_STEP &&
eventTypeGroups &&
Boolean(eventTypeGroups?.length) && (
<EventTypesStepCard
Expand All @@ -309,7 +305,7 @@ const OnboardingPage = ({
handleSetUpLater={handleSetUpLater}
/>
)}
{currentStep === AppOnboardingSteps.CONFIGURE_STEP &&
{stepManager.currentStep === AppOnboardingSteps.CONFIGURE_STEP &&
formPortalRef.current &&
eventTypeGroups && (
<ConfigureStepCard
Expand Down
95 changes: 95 additions & 0 deletions apps/web/modules/apps/installation/[[...step]]/useStepManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useMemo } from "react";

import { AppOnboardingSteps } from "@calcom/lib/apps/appOnboardingSteps";
import { useLocale } from "@calcom/lib/hooks/useLocale";

import { STEPS } from "./constants";

type StepConfig = {
getTitle: (appName?: string) => string;
getDescription: (appName?: string) => string;
stepNumber: number;
};

type UseStepManagerProps = {
step: AppOnboardingSteps;
configureStep: boolean;
showEventTypesStep: boolean;
isOnlySingleAccountToSelect: boolean;
appName: string;
};

type StepManagerReturn = {
currentStep: AppOnboardingSteps;
maxSteps: number;
title: string;
description: string;
stepNumber: number;
};

export function useStepManager({
step,
configureStep,
showEventTypesStep,
isOnlySingleAccountToSelect,
appName,
}: UseStepManagerProps): StepManagerReturn {
const { t } = useLocale();

const excludeSteps: AppOnboardingSteps[] = [];

if (isOnlySingleAccountToSelect) {
excludeSteps.push(AppOnboardingSteps.ACCOUNTS_STEP);
}

if (!showEventTypesStep) {
excludeSteps.push(AppOnboardingSteps.EVENT_TYPES_STEP);
excludeSteps.push(AppOnboardingSteps.CONFIGURE_STEP);
}

const visibleSteps = STEPS.filter((step) => !excludeSteps.includes(step));

const getStepNumber = (step: AppOnboardingSteps): number => {
const stepIndex = visibleSteps.indexOf(step);
return stepIndex === -1 ? 0 : stepIndex + 1;
};

const stepConfigs: Record<AppOnboardingSteps, StepConfig> = {
[AppOnboardingSteps.ACCOUNTS_STEP]: {
getTitle: () => t("select_account_header"),
getDescription: (appName) =>
t("select_account_description", { appName, interpolation: { escapeValue: false } }),
stepNumber: getStepNumber(AppOnboardingSteps.ACCOUNTS_STEP),
},
[AppOnboardingSteps.EVENT_TYPES_STEP]: {
getTitle: () => t("select_event_types_header"),
getDescription: (appName) =>
t("select_event_types_description", { appName, interpolation: { escapeValue: false } }),
stepNumber: getStepNumber(AppOnboardingSteps.EVENT_TYPES_STEP),
},
[AppOnboardingSteps.CONFIGURE_STEP]: {
getTitle: (appName) => t("configure_app_header", { appName, interpolation: { escapeValue: false } }),
getDescription: () => t("configure_app_description"),
stepNumber: getStepNumber(AppOnboardingSteps.CONFIGURE_STEP),
},
};

const currentStep: AppOnboardingSteps = useMemo(() => {
if (step === AppOnboardingSteps.EVENT_TYPES_STEP && configureStep) {
return AppOnboardingSteps.CONFIGURE_STEP;
}
return step;
}, [step, configureStep]);

const maxSteps = visibleSteps.length;

const currentStepConfig = stepConfigs[currentStep];

return {
currentStep,
maxSteps,
title: currentStepConfig.getTitle(appName),
description: currentStepConfig.getDescription(appName),
stepNumber: currentStepConfig.stepNumber,
};
}
Loading