Feat/onboarding video step connection (#8838)

* UI work - WIP scrollable area

* Add translations - refactor some components

* Add installed text

* Disable if there is no currently installed

* Extract loader

* Fix conditional

* Fix E2E

* fix typo
This commit is contained in:
sean-brydon authored and GitHub committed 2023-05-11 13:20:39 +00:00
1 parent 738e0f3e50
commit bdf3e34ea1
11 files changed
+177 -30

No files matched your search

@@ -3,15 +3,16 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui";
interface ICalendarItem {
interface IAppConnectionItem {
title: string;
description?: string;
logo: string;
type: App["type"];
installed?: boolean;
}
const CalendarItem = (props: ICalendarItem) => {
const { title, logo, type } = props;
const AppConnectionItem = (props: IAppConnectionItem) => {
const { title, logo, type, installed } = props;
const { t } = useLocale();
return (
<div className="flex flex-row items-center justify-between p-5">
@@ -25,13 +26,14 @@ const CalendarItem = (props: ICalendarItem) => {
<Button
{...buttonProps}
color="secondary"
disabled={installed}
type="button"
onClick={(event) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
}}>
{t("connect")}
{installed ? t("installed") : t("connect")}
</Button>
)}
/>
@@ -39,4 +41,4 @@ const CalendarItem = (props: ICalendarItem) => {
);
};
export { CalendarItem };
export { AppConnectionItem };
@@ -0,0 +1,17 @@
import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui";
export function StepConnectionLoader() {
return (
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
{Array.from({ length: 4 }).map((_item, index) => {
return (
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</li>
);
})}
</ul>
);
}
@@ -3,11 +3,12 @@ import { ArrowRightIcon } from "@heroicons/react/solid";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { SkeletonAvatar, SkeletonText, SkeletonButton, List } from "@calcom/ui";
import { List } from "@calcom/ui";
import { CalendarItem } from "../components/CalendarItem";
import { AppConnectionItem } from "../components/AppConnectionItem";
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect";
import { StepConnectionLoader } from "../components/StepConnectionLoader";
interface IConnectCalendarsProps {
nextStep: () => void;
@@ -33,7 +34,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
firstCalendar.integration.title &&
firstCalendar.integration.logo && (
<>
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black">
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black ">
<ConnectedCalendarItem
key={firstCalendar.integration.title}
name={firstCalendar.integration.title}
@@ -60,7 +61,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
queryIntegrations.data.items.map((item) => (
<li key={item.title}>
{item.title && item.logo && (
<CalendarItem
<AppConnectionItem
type={item.type}
title={item.title}
description={item.description}
@@ -72,19 +73,8 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
)}
{queryIntegrations.isLoading && (
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
{[0, 0, 0, 0].map((_item, index) => {
return (
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</li>
);
})}
</ul>
)}
{queryIntegrations.isLoading && <StepConnectionLoader />}
<button
type="button"
data-testid="save-calendar-button"
@@ -0,0 +1,68 @@
import { ArrowRightIcon } from "@heroicons/react/solid";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { List } from "@calcom/ui";
import { AppConnectionItem } from "../components/AppConnectionItem";
import { StepConnectionLoader } from "../components/StepConnectionLoader";
interface ConnectedAppStepProps {
nextStep: () => void;
}
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
const { nextStep } = props;
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
variant: "conferencing",
onlyInstalled: false,
});
const { t } = useLocale();
const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
(item) => item.credentialIds.length > 0
);
return (
<>
{!isLoading && (
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
{queryConnectedVideoApps?.items &&
queryConnectedVideoApps?.items.map((item) => {
if (item.slug === "daily-video") return null; // we dont want to show daily here as it is installed by default
return (
<li key={item.name}>
{item.name && item.logo && (
<AppConnectionItem
type={item.type}
title={item.name}
description={item.description}
logo={item.logo}
installed={item.credentialIds.length > 0}
/>
)}
</li>
);
})}
</List>
)}
{isLoading && <StepConnectionLoader />}
<button
type="button"
data-testid="save-video-button"
className={classNames(
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
!hasAnyInstalledVideoApps ? "cursor-not-allowed opacity-20" : ""
)}
disabled={!hasAnyInstalledVideoApps}
onClick={() => nextStep()}>
{t("next_step_text")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</button>
</>
);
};
export { ConnectedVideoStep };
+18 -3
View File
@@ -15,6 +15,7 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
import { ConnectedVideoStep } from "@components/getting-started/steps-views/ConnectedVideoStep";
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
import UserProfile from "@components/getting-started/steps-views/UserProfile";
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
@@ -22,7 +23,13 @@ import { UserSettings } from "@components/getting-started/steps-views/UserSettin
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
const INITIAL_STEP = "user-settings";
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
const steps = [
"user-settings",
"connected-calendar",
"connected-video",
"setup-availability",
"user-profile",
] as const;
const stepTransform = (step: (typeof steps)[number]) => {
const stepIndex = steps.indexOf(step);
@@ -36,9 +43,9 @@ const stepRouteSchema = z.object({
step: z.array(z.enum(steps)).default([INITIAL_STEP]),
});
// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
const OnboardingPage = (props: IOnboardingPageProps) => {
const router = useRouter();
const { user } = props;
const { t } = useLocale();
@@ -55,6 +62,11 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
subtitle: [`${t("connect_your_calendar_instructions")}`],
skipText: `${t("connect_calendar_later")}`,
},
{
title: `${t("connect_your_video_app")}`,
subtitle: [`${t("connect_your_video_app_instructions")}`],
skipText: `${t("set_up_later")}`,
},
{
title: `${t("set_availability")}`,
subtitle: [
@@ -122,12 +134,15 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
{currentStep === "connected-calendar" && <ConnectedCalendars nextStep={() => goToIndex(2)} />}
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={() => goToIndex(3)} />}
{currentStep === "setup-availability" && (
<SetupAvailability nextStep={() => goToIndex(3)} defaultScheduleId={user.defaultScheduleId} />
<SetupAvailability nextStep={() => goToIndex(4)} defaultScheduleId={user.defaultScheduleId} />
)}
{currentStep === "user-profile" && <UserProfile user={user} />}
</StepCard>
{headers[currentStepIndex]?.skipText && (
<div className="flex w-full flex-row justify-center">
<Button
+11 -2
View File
@@ -47,10 +47,19 @@ test.describe("Onboarding", () => {
// tests skip button, we don't want to test entire flow.
await page.locator("button[data-testid=skip-step]").click();
await expect(page).toHaveURL(/.*setup-availability/);
await expect(page).toHaveURL(/.*connected-video/);
});
await test.step("step 3", async () => {
const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled();
await expect(isDisabled).toBe(true);
// tests skip button, we don't want to test entire flow.
await page.locator("button[data-testid=skip-step]").click();
await expect(page).toHaveURL(/.*setup-availability/);
});
await test.step("step 4", async () => {
const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
await expect(isDisabled).toBe(false);
// same here, skip this step.
@@ -59,7 +68,7 @@ test.describe("Onboarding", () => {
await expect(page).toHaveURL(/.*user-profile/);
});
await test.step("step 4", async () => {
await test.step("step 5", async () => {
await page.locator("button[type=submit]").click();
// should redirect to /event-types after onboarding
@@ -237,6 +237,8 @@
"set_availability": "Set your availability",
"continue_without_calendar": "Continue without calendar",
"connect_your_calendar": "Connect your calendar",
"connect_your_video_app":"Connect your video apps",
"connect_your_video_app_instructions":"Connect your video apps to use them on your event types.",
"connect_your_calendar_instructions": "Connect your calendar to automatically check for busy times and new events as they’re scheduled.",
"set_up_later": "Set up later",
"current_time": "Current time",