diff --git a/apps/web/modules/apps/installation/[[...step]]/step-view.tsx b/apps/web/modules/apps/installation/[[...step]]/step-view.tsx index 9b452f37bf6e1d..5d46003c5ab98f 100644 --- a/apps/web/modules/apps/installation/[[...step]]/step-view.tsx +++ b/apps/web/modules/apps/installation/[[...step]]/step-view.tsx @@ -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"; @@ -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"; @@ -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< @@ -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 & { alreadyInstalled: boolean; })[]; @@ -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(); @@ -208,6 +177,10 @@ const OnboardingPage = ({ }); const handleSelectAccount = async (teamId?: number) => { + if (mutation.isPending) { + return; + } + mutation.mutate({ type: appMetadata.type, variant: appMetadata.variant, @@ -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 ( +
+ +
+ ); + } + return (
- - + + - {currentStep === AppOnboardingSteps.ACCOUNTS_STEP && ( + {stepManager.currentStep === AppOnboardingSteps.ACCOUNTS_STEP && ( )} - {currentStep === AppOnboardingSteps.EVENT_TYPES_STEP && + {stepManager.currentStep === AppOnboardingSteps.EVENT_TYPES_STEP && eventTypeGroups && Boolean(eventTypeGroups?.length) && ( )} - {currentStep === AppOnboardingSteps.CONFIGURE_STEP && + {stepManager.currentStep === AppOnboardingSteps.CONFIGURE_STEP && formPortalRef.current && eventTypeGroups && ( 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.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, + }; +}