Compare commits
8 Commits
main
...
new-app-in
Author | SHA1 | Date |
---|---|---|
Morgan Vernay | d5b34b86c5 | |
Morgan Vernay | 4fba8af57c | |
Morgan Vernay | 4e4a8b69f0 | |
Morgan Vernay | 905bc23f87 | |
Morgan Vernay | a4d20e7d65 | |
Morgan Vernay | ca0edf1ee7 | |
Morgan Vernay | 550a1fe6c0 | |
Morgan Vernay | 2585d62007 |
|
@ -0,0 +1,82 @@
|
|||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
import { Avatar, StepCard } from "@calcom/ui";
|
||||
|
||||
type AccountSelectorProps = {
|
||||
avatar?: string;
|
||||
name: string;
|
||||
alreadyInstalled: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const AccountSelector: FC<AccountSelectorProps> = ({ avatar, alreadyInstalled, name, onClick }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"hover:bg-muted flex cursor-pointer flex-row items-center gap-2 p-1",
|
||||
alreadyInstalled && "cursor-not-allowed"
|
||||
)}
|
||||
onClick={onClick}>
|
||||
<Avatar
|
||||
alt={avatar || ""}
|
||||
imageSrc={avatar || `${CAL_URL}/${avatar}`} // if no image, use default avatar
|
||||
size="sm"
|
||||
/>
|
||||
<div className="text-md pt-0.5 font-medium text-gray-500">
|
||||
{name}
|
||||
{alreadyInstalled ? <span className="ml-1 text-sm text-gray-400">(already installed)</span> : ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export type PersonalAccountProps = Pick<User, "id" | "avatar" | "name"> & { alreadyInstalled: boolean };
|
||||
|
||||
export type TeamsProp = (Pick<Team, "id" | "name" | "logo"> & {
|
||||
accepted: true;
|
||||
alreadyInstalled: boolean;
|
||||
})[];
|
||||
|
||||
type onSelectPersonalAccParams = { type: "personal"; id?: undefined };
|
||||
type onSelectPersonalTeamParams = { type: "team"; id: number };
|
||||
export type onSelectParams = onSelectPersonalAccParams | onSelectPersonalTeamParams;
|
||||
|
||||
export type onSelectProp = (params: onSelectParams) => void;
|
||||
|
||||
type AccountStepCardProps = {
|
||||
teams: TeamsProp;
|
||||
personalAccount: PersonalAccountProps;
|
||||
onSelect: onSelectProp;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export const AccountsStepCard: FC<AccountStepCardProps> = ({ teams, personalAccount, onSelect, loading }) => {
|
||||
return (
|
||||
<StepCard>
|
||||
<div className="text-sm font-medium text-gray-400">Install app on</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-2 flex flex-col gap-2 ",
|
||||
loading && "bg-muted pointer-events-none animate-pulse"
|
||||
)}>
|
||||
<AccountSelector
|
||||
avatar={personalAccount.avatar ?? ""}
|
||||
name={personalAccount.name ?? ""}
|
||||
alreadyInstalled={personalAccount.alreadyInstalled}
|
||||
onClick={() => !personalAccount.alreadyInstalled && onSelect({ type: "personal" })}
|
||||
/>
|
||||
{teams.map((team) => (
|
||||
<AccountSelector
|
||||
key={team.id}
|
||||
alreadyInstalled={team.alreadyInstalled}
|
||||
avatar={team.logo ?? ""}
|
||||
name={team.name}
|
||||
onClick={() => !team.alreadyInstalled && onSelect({ type: "team", id: team.id })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</StepCard>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { EventTypeAppSettings } from "@calcom/app-store/_components/EventTypeAppSettingsInterface";
|
||||
import type { EventTypeAppSettingsComponentProps, EventTypeModel } from "@calcom/app-store/types";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, StepCard } from "@calcom/ui";
|
||||
|
||||
import useAppsData from "@lib/hooks/useAppsData";
|
||||
|
||||
export type ConfigureEventTypeProp = EventTypeAppSettingsComponentProps["eventType"] &
|
||||
Pick<EventTypeModel, "metadata" | "schedulingType">;
|
||||
|
||||
type ConfigureStepCardProps = {
|
||||
slug: string;
|
||||
eventType: ConfigureEventTypeProp;
|
||||
onSave: (data: Record<string, unknown>) => void;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export const ConfigureStepCard: FC<ConfigureStepCardProps> = ({ slug, eventType, onSave, loading }) => {
|
||||
const { t } = useLocale();
|
||||
const { getAppDataGetter, getAppDataSetter } = useAppsData();
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
|
||||
const data = getAppDataGetter(slug as EventTypeAppsList)("") as Record<string, unknown>;
|
||||
return (
|
||||
<StepCard>
|
||||
<EventTypeAppSettings
|
||||
slug={slug}
|
||||
disabled={shouldLockDisableProps("apps").disabled}
|
||||
eventType={eventType}
|
||||
getAppData={getAppDataGetter(slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(slug as EventTypeAppsList)}
|
||||
/>
|
||||
<Button
|
||||
className="text-md mt-6 w-full justify-center"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
onSave(data);
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
</StepCard>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,92 @@
|
|||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
|
||||
import type { EventType, Team } from "@calcom/prisma/client";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { ScrollableArea, Badge } from "@calcom/ui";
|
||||
import { Clock } from "@calcom/ui/components/icon";
|
||||
|
||||
export type EventTypeProp = Pick<
|
||||
EventType,
|
||||
| "description"
|
||||
| "id"
|
||||
| "metadata"
|
||||
| "length"
|
||||
| "title"
|
||||
| "position"
|
||||
| "requiresConfirmation"
|
||||
| "recurringEvent"
|
||||
| "slug"
|
||||
> & { team: Pick<Team, "slug"> | null };
|
||||
|
||||
type EventTypesCardProps = {
|
||||
eventTypes: EventTypeProp[];
|
||||
onSelect: (id: number) => void;
|
||||
userName: string;
|
||||
};
|
||||
|
||||
export const EventTypesStepCard: FC<EventTypesCardProps> = ({ eventTypes, onSelect, userName }) => {
|
||||
return (
|
||||
<div className="sm:border-subtle bg-default mt-10 border dark:bg-black sm:rounded-md ">
|
||||
<ScrollableArea className="rounded-md">
|
||||
<ul className="border-subtle max-h-97 !static w-full divide-y">
|
||||
{eventTypes.map((eventType) => (
|
||||
<EventTypeCard
|
||||
key={eventType.id}
|
||||
{...eventType}
|
||||
onClick={() => onSelect(eventType.id)}
|
||||
userName={userName}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type EventTypeCardProps = EventTypeProp & { onClick: () => void; userName: string };
|
||||
|
||||
const EventTypeCard: FC<EventTypeCardProps> = ({
|
||||
title,
|
||||
description,
|
||||
id,
|
||||
metadata,
|
||||
length,
|
||||
onClick,
|
||||
slug,
|
||||
team,
|
||||
userName,
|
||||
}) => {
|
||||
const parsedMetaData = EventTypeMetaDataSchema.safeParse(metadata);
|
||||
const durations =
|
||||
parsedMetaData.success &&
|
||||
parsedMetaData.data?.multipleDuration &&
|
||||
Boolean(parsedMetaData.data?.multipleDuration.length)
|
||||
? [length, ...parsedMetaData.data?.multipleDuration?.filter((duration) => duration !== length)].sort()
|
||||
: [length];
|
||||
return (
|
||||
<li
|
||||
className="hover:bg-muted min-h-20 box-border flex w-full cursor-pointer flex-col px-4 py-3"
|
||||
onClick={onClick}>
|
||||
<div>
|
||||
<span className="text-default font-semibold ltr:mr-1 rtl:ml-1">{title}</span>{" "}
|
||||
<small className="text-subtle hidden font-normal sm:inline">
|
||||
/{team ? team.slug : userName}/{slug}
|
||||
</small>
|
||||
</div>
|
||||
{Boolean(description) && (
|
||||
<div className="text-subtle line-clamp-4 break-words text-sm sm:max-w-[650px] [&>*:not(:first-child)]:hidden [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2 flex flex-row flex-wrap gap-2">
|
||||
{Boolean(durations.length) &&
|
||||
durations.map((duration) => (
|
||||
<Badge key={`event-type-${id}-duration-${duration}`} variant="gray" startIcon={Clock}>
|
||||
{duration}m
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
import type { FC } from "react";
|
||||
import React from "react";
|
||||
|
||||
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
||||
import { Button, StepCard } from "@calcom/ui";
|
||||
|
||||
type OAuthCardProps = {
|
||||
description: string;
|
||||
name: string;
|
||||
logo: string;
|
||||
onClick: () => void;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const Logo: SVGComponent = ({ href, className }) => {
|
||||
return <img src={href} alt="app logo" className={className} />;
|
||||
};
|
||||
|
||||
export const OAuthStepCard: FC<OAuthCardProps> = ({ description, name, logo, onClick, isLoading }) => {
|
||||
const Logo: SVGComponent = () => <img src={logo} alt="app logo" className="mr-2 h-6 w-6" />;
|
||||
return (
|
||||
<StepCard>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>{description}</p>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
className="min-w-20 w-fit justify-center self-center"
|
||||
StartIcon={Logo}
|
||||
onClick={onClick}>
|
||||
Connect With {name}
|
||||
</Button>
|
||||
</div>
|
||||
</StepCard>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import type { FC } from "react";
|
||||
|
||||
export const StepFooter: FC = () => {
|
||||
return (
|
||||
<div className="text-subtle mt-1 flex flex-row justify-between px-1 text-sm ">
|
||||
{/* <div className="cursor-pointer hover:underline">Go Back</div>{" "}
|
||||
<div className="cursor-pointer hover:underline">Skip</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import type { FC, ReactNode } from "react";
|
||||
|
||||
type StepHeaderProps = {
|
||||
children?: ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
};
|
||||
export const StepHeader: FC<StepHeaderProps> = ({ children, title, subtitle }) => {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<p className="font-cal mb-3 text-[28px] font-medium capitalize leading-7">{title}</p>
|
||||
|
||||
<p className="text-subtle font-sans text-sm font-normal">{subtitle}</p>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,9 +1,7 @@
|
|||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
|
||||
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
|
@ -13,6 +11,8 @@ import { trpc } from "@calcom/trpc/react";
|
|||
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||
import { Grid, Lock } from "@calcom/ui/components/icon";
|
||||
|
||||
import useAppsData from "@lib/hooks/useAppsData";
|
||||
|
||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||
EventTypeAppCardComponentProps["eventType"];
|
||||
|
||||
|
@ -23,44 +23,12 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||
teamId: eventType.team?.id || eventType.parent?.teamId,
|
||||
});
|
||||
|
||||
const methods = useFormContext<FormValues>();
|
||||
const installedApps =
|
||||
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
|
||||
const notInstalledApps =
|
||||
eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
|
||||
const allAppsData = methods.watch("metadata")?.apps || {};
|
||||
|
||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||
methods.setValue("metadata", {
|
||||
...methods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
});
|
||||
};
|
||||
|
||||
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
|
||||
return function (key) {
|
||||
const appData = allAppsData[appId as keyof typeof allAppsData] || {};
|
||||
if (key) {
|
||||
return appData[key as keyof typeof appData];
|
||||
}
|
||||
return appData;
|
||||
};
|
||||
};
|
||||
|
||||
const getAppDataSetter = (appId: EventTypeAppsList): SetAppData => {
|
||||
return function (key, value) {
|
||||
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
|
||||
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
|
||||
const appData = allAppsDataFromForm[appId];
|
||||
setAllAppsData({
|
||||
...allAppsDataFromForm,
|
||||
[appId]: {
|
||||
...appData,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
const { getAppDataGetter, getAppDataSetter } = useAppsData();
|
||||
|
||||
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
eventType,
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import type { FormValues } from "pages/event-types/[type]";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
|
||||
const useAppsData = () => {
|
||||
const methods = useFormContext<FormValues>();
|
||||
const allAppsData = methods.watch("metadata")?.apps || {};
|
||||
|
||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||
methods.setValue("metadata", {
|
||||
...methods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
});
|
||||
};
|
||||
|
||||
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
|
||||
return function (key?: string) {
|
||||
const appData = allAppsData[appId as keyof typeof allAppsData] || {};
|
||||
if (key) {
|
||||
return appData[key as keyof typeof appData];
|
||||
}
|
||||
return appData;
|
||||
};
|
||||
};
|
||||
|
||||
const getAppDataSetter = (appId: EventTypeAppsList): SetAppData => {
|
||||
return function (key, value) {
|
||||
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
|
||||
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
|
||||
const appData = allAppsDataFromForm[appId];
|
||||
setAllAppsData({
|
||||
...allAppsDataFromForm,
|
||||
[appId]: {
|
||||
...appData,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return { getAppDataGetter, getAppDataSetter };
|
||||
};
|
||||
|
||||
export default useAppsData;
|
|
@ -0,0 +1,506 @@
|
|||
import type { GetServerSidePropsContext } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Head from "next/head";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useState, type CSSProperties } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
import { Steps } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import type {
|
||||
PersonalAccountProps,
|
||||
TeamsProp,
|
||||
onSelectParams,
|
||||
} from "@components/apps/onboarding/AccountsStepCard";
|
||||
import { AccountsStepCard } from "@components/apps/onboarding/AccountsStepCard";
|
||||
import type { ConfigureEventTypeProp } from "@components/apps/onboarding/ConfigureStepCard";
|
||||
import { ConfigureStepCard } from "@components/apps/onboarding/ConfigureStepCard";
|
||||
import type { EventTypeProp } from "@components/apps/onboarding/EventTypesStepCard";
|
||||
import { EventTypesStepCard } from "@components/apps/onboarding/EventTypesStepCard";
|
||||
import { OAuthStepCard } from "@components/apps/onboarding/OAuthStepCard";
|
||||
import { StepFooter } from "@components/apps/onboarding/StepFooter";
|
||||
import { StepHeader } from "@components/apps/onboarding/StepHeader";
|
||||
|
||||
const ACCOUNTS_STEP = "accounts";
|
||||
const OAUTH_STEP = "connect";
|
||||
const EVENT_TYPES_STEP = "event-types";
|
||||
const CONFIGURE_STEP = "configure";
|
||||
|
||||
const STEPS = [ACCOUNTS_STEP, OAUTH_STEP, EVENT_TYPES_STEP, CONFIGURE_STEP] as const;
|
||||
const MAX_NUMBER_OF_STEPS = STEPS.length;
|
||||
|
||||
type StepType = (typeof STEPS)[number];
|
||||
|
||||
type StepObj = Record<
|
||||
StepType,
|
||||
{
|
||||
getTitle: (appName: string) => string;
|
||||
getDescription: (appName: string) => string;
|
||||
getStepNumber: (hasTeams: boolean, isOAuth: boolean) => number;
|
||||
}
|
||||
>;
|
||||
|
||||
const STEPS_MAP: StepObj = {
|
||||
[ACCOUNTS_STEP]: {
|
||||
getTitle: () => "Select Account",
|
||||
getDescription: (appName) => `Install ${appName} on your personal account or on a team account.`,
|
||||
getStepNumber: (hasTeams) => (hasTeams ? 1 : 0),
|
||||
},
|
||||
[OAUTH_STEP]: {
|
||||
getTitle: (appName) => `Install ${appName}`,
|
||||
getDescription: (appName) => `Give permissions to connect your Cal.com to ${appName}.`,
|
||||
getStepNumber: (hasTeams, isOAuth) => (hasTeams ? 1 : 0) + (isOAuth ? 1 : 0),
|
||||
},
|
||||
[EVENT_TYPES_STEP]: {
|
||||
getTitle: () => "Select Event Type",
|
||||
getDescription: (appName) => `On which event type do you want to install ${appName}?`,
|
||||
getStepNumber: (hasTeams, isOAuth) => 1 + (hasTeams ? 1 : 0) + (isOAuth ? 1 : 0),
|
||||
},
|
||||
[CONFIGURE_STEP]: {
|
||||
getTitle: (appName) => `Configure ${appName}`,
|
||||
getDescription: () => "Finalise the App setup. You can change these settings later.",
|
||||
getStepNumber: (hasTeams, isOAuth) => 2 + (hasTeams ? 1 : 0) + (isOAuth ? 1 : 0),
|
||||
},
|
||||
} as const;
|
||||
|
||||
type OnboardingPageProps = {
|
||||
hasTeams: boolean;
|
||||
appMetadata: AppMeta;
|
||||
step: StepType;
|
||||
teams: TeamsProp;
|
||||
personalAccount: PersonalAccountProps;
|
||||
eventTypes?: EventTypeProp[];
|
||||
teamId?: number;
|
||||
userName: string;
|
||||
hasEventTypes: boolean;
|
||||
configureEventType: ConfigureEventTypeProp | null;
|
||||
};
|
||||
|
||||
const getRedirectUrl = (slug: string, step: StepType, teamId?: number, eventTypeId?: number) => {
|
||||
return `/apps/onboarding/${step}?slug=${slug}${teamId ? `&teamId=${teamId}` : ""}${
|
||||
eventTypeId ? `&eventTypeId=${eventTypeId}` : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
const OnboardingPage = ({
|
||||
hasTeams,
|
||||
step,
|
||||
teams,
|
||||
personalAccount,
|
||||
appMetadata,
|
||||
eventTypes,
|
||||
teamId,
|
||||
userName,
|
||||
hasEventTypes,
|
||||
configureEventType,
|
||||
}: OnboardingPageProps) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const stepObj = STEPS_MAP[step];
|
||||
const nbOfSteps =
|
||||
MAX_NUMBER_OF_STEPS - (hasTeams ? 0 : 1) - (appMetadata.isOAuth ? 0 : 1) - (hasEventTypes ? 0 : 1);
|
||||
const { t } = useLocale();
|
||||
const [isLoadingOAuth, setIsLoadingOAuth] = useState(false);
|
||||
const [isSelectingAccount, setIsSelectingAccount] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const methods = useForm();
|
||||
|
||||
const handleSelectAccount = ({ id: teamId }: onSelectParams) => {
|
||||
setIsSelectingAccount(true);
|
||||
if (appMetadata.isOAuth) {
|
||||
router.push(getRedirectUrl(appMetadata.slug, OAUTH_STEP, teamId));
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/integrations/${appMetadata.slug}/add${teamId ? `?teamId=${teamId}` : ""}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
router.push(
|
||||
!hasEventTypes
|
||||
? getInstalledAppPath({ slug: appMetadata.slug, variant: appMetadata.variant })
|
||||
: getRedirectUrl(appMetadata.slug, EVENT_TYPES_STEP, teamId)
|
||||
);
|
||||
})
|
||||
.catch(() => setIsSelectingAccount(false));
|
||||
};
|
||||
|
||||
const handleSelectEventType = (id: number) => {
|
||||
if (hasEventTypes) {
|
||||
router.push(getRedirectUrl(appMetadata.slug, CONFIGURE_STEP, teamId, id));
|
||||
return;
|
||||
}
|
||||
router.push(`/apps/installed`);
|
||||
return;
|
||||
};
|
||||
|
||||
const handleOAuth = async () => {
|
||||
try {
|
||||
setIsLoadingOAuth(true);
|
||||
const state = JSON.stringify({
|
||||
returnToOnboarding: hasEventTypes,
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
const res = await fetch(`/api/integrations/${appMetadata.slug}/add?state=${state}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const oAuthUrl = (await res.json())?.url;
|
||||
router.push(oAuthUrl);
|
||||
} catch (err) {
|
||||
setIsLoadingOAuth(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveSettings = (data: Record<string, unknown>) => {
|
||||
setIsSaving(true);
|
||||
console.log("SAVE THIS DATA IN EVENT TYPE", data);
|
||||
// redirect to event type settings, advanced tab -> apps
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={pathname}
|
||||
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen"
|
||||
data-testid="onboarding"
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
"--cal-brand-emphasis": "#101010",
|
||||
"--cal-brand-text": "white",
|
||||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}>
|
||||
<Head>
|
||||
<title>Install {appMetadata?.name ?? ""}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="mx-auto py-6 sm:px-4 md:py-24">
|
||||
<div className="relative">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-[600px]">
|
||||
<StepHeader
|
||||
title={stepObj.getTitle(appMetadata.name)}
|
||||
subtitle={stepObj.getDescription(appMetadata.name)}>
|
||||
<Steps
|
||||
maxSteps={nbOfSteps}
|
||||
currentStep={stepObj.getStepNumber(hasTeams, appMetadata.isOAuth ?? false)}
|
||||
disableNavigation
|
||||
/>
|
||||
</StepHeader>
|
||||
{step === ACCOUNTS_STEP && (
|
||||
<AccountsStepCard
|
||||
teams={teams}
|
||||
personalAccount={personalAccount}
|
||||
onSelect={handleSelectAccount}
|
||||
loading={isSelectingAccount}
|
||||
/>
|
||||
)}
|
||||
{step === OAUTH_STEP && (
|
||||
<OAuthStepCard
|
||||
description={appMetadata.description}
|
||||
name={appMetadata.name}
|
||||
logo={appMetadata.logo}
|
||||
onClick={handleOAuth}
|
||||
isLoading={isLoadingOAuth}
|
||||
/>
|
||||
)}
|
||||
{step === EVENT_TYPES_STEP && eventTypes && Boolean(eventTypes?.length) && (
|
||||
<EventTypesStepCard
|
||||
eventTypes={eventTypes}
|
||||
onSelect={handleSelectEventType}
|
||||
userName={userName}
|
||||
/>
|
||||
)}
|
||||
{step === CONFIGURE_STEP && configureEventType && (
|
||||
// Find solution for this, should not have to use FormProvider
|
||||
<FormProvider {...methods}>
|
||||
<ConfigureStepCard
|
||||
slug={appMetadata.slug}
|
||||
eventType={configureEventType}
|
||||
onSave={handleSaveSettings}
|
||||
loading={isSaving}
|
||||
/>
|
||||
</FormProvider>
|
||||
)}
|
||||
<StepFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Redirect Error map to give context on edge cases, this is for the devs, never shown to users
|
||||
const ERROR_MESSAGES = {
|
||||
appNotFound: "App not found",
|
||||
userNotAuthed: "User is not logged in",
|
||||
userNotFound: "User from session not found",
|
||||
userWithoutTeams: "User has no teams on team step",
|
||||
noEventTypesFound: "User or teams does not have any event types",
|
||||
appNotOAuth: "App does not use OAuth",
|
||||
appNotEventType: "App does not have EventTypes",
|
||||
appNotExtendsEventType: "App does not extend EventTypes",
|
||||
userNotInTeam: "User is not in provided team",
|
||||
} as const;
|
||||
|
||||
const getUser = async (userId: number) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
avatar: true,
|
||||
name: true,
|
||||
username: true,
|
||||
teams: {
|
||||
select: {
|
||||
accepted: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error(ERROR_MESSAGES.userNotFound);
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
const getAppBySlug = async (appSlug: string) => {
|
||||
const app = await prisma.app.findUnique({
|
||||
where: { slug: appSlug, enabled: true },
|
||||
select: { slug: true, keys: true, enabled: true, dirName: true },
|
||||
});
|
||||
if (!app) throw new Error(ERROR_MESSAGES.appNotFound);
|
||||
return app;
|
||||
};
|
||||
|
||||
const getEventTypes = async (userId: number, teamId?: number) => {
|
||||
const eventTypes = (
|
||||
await prisma.eventType.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
description: true,
|
||||
durationLimits: true,
|
||||
metadata: true,
|
||||
length: true,
|
||||
title: true,
|
||||
position: true,
|
||||
recurringEvent: true,
|
||||
requiresConfirmation: true,
|
||||
team: { select: { slug: true } },
|
||||
slug: true,
|
||||
},
|
||||
where: teamId ? { teamId } : { userId },
|
||||
})
|
||||
).sort((eventTypeA, eventTypeB) => {
|
||||
return eventTypeB.position - eventTypeA.position;
|
||||
});
|
||||
if (eventTypes.length === 0) {
|
||||
throw new Error(ERROR_MESSAGES.noEventTypesFound);
|
||||
}
|
||||
return eventTypes;
|
||||
};
|
||||
|
||||
const getEventTypeById = async (eventTypeId: number) => {
|
||||
const eventTypeDB = await prisma.eventType.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
users: { select: { username: true } },
|
||||
length: true,
|
||||
title: true,
|
||||
teamId: true,
|
||||
seatsPerTimeSlot: true,
|
||||
recurringEvent: true,
|
||||
team: { select: { slug: true } },
|
||||
schedulingType: true,
|
||||
metadata: true,
|
||||
},
|
||||
where: { id: eventTypeId },
|
||||
});
|
||||
|
||||
if (!eventTypeDB) {
|
||||
throw new Error(ERROR_MESSAGES.noEventTypesFound);
|
||||
}
|
||||
return {
|
||||
...eventTypeDB,
|
||||
URL: `${CAL_URL}/${
|
||||
eventTypeDB.team ? `team/${eventTypeDB.team.slug}` : eventTypeDB?.users?.[0]?.username
|
||||
}/${eventTypeDB.slug}`,
|
||||
} as ConfigureEventTypeProp;
|
||||
};
|
||||
|
||||
const getAppInstallsBySlug = async (appSlug: string, userId: number, teamIds?: number[]) => {
|
||||
const appInstalls = await prisma.credential.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
appId: appSlug,
|
||||
userId: userId,
|
||||
},
|
||||
teamIds && Boolean(teamIds.length)
|
||||
? {
|
||||
appId: appSlug,
|
||||
teamId: { in: teamIds },
|
||||
}
|
||||
: {},
|
||||
],
|
||||
},
|
||||
});
|
||||
return appInstalls;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
try {
|
||||
let eventTypes: EventTypeProp[] = [];
|
||||
let eventType: ConfigureEventTypeProp | null = null;
|
||||
const { req, res, query, params } = context;
|
||||
const stepsEnum = z.enum(STEPS);
|
||||
const parsedAppSlug = z.coerce.string().parse(query?.slug);
|
||||
const parsedStepParam = z.coerce.string().parse(params?.step);
|
||||
const parsedTeamIdParam = z.coerce.number().optional().parse(query?.teamId);
|
||||
const parsedEventTypeIdParam = z.coerce.number().optional().parse(query?.eventTypeId);
|
||||
const _ = stepsEnum.parse(parsedStepParam);
|
||||
const session = await getServerSession({ req, res });
|
||||
const locale = await getLocale(context.req);
|
||||
const app = await getAppBySlug(parsedAppSlug);
|
||||
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
|
||||
const hasEventTypes = appMetadata.extendsFeature === "EventType";
|
||||
|
||||
if (!session?.user?.id) throw new Error(ERROR_MESSAGES.userNotAuthed);
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
|
||||
const userAcceptedTeams = user.teams
|
||||
.filter((team) => team.accepted)
|
||||
.map((team) => ({ ...team.team, accepted: team.accepted }));
|
||||
const hasTeams = Boolean(userAcceptedTeams.length);
|
||||
|
||||
const appInstalls = await getAppInstallsBySlug(
|
||||
parsedAppSlug,
|
||||
user.id,
|
||||
userAcceptedTeams.map(({ id }) => id)
|
||||
);
|
||||
|
||||
if (parsedTeamIdParam) {
|
||||
const isUserMemberOfTeam = userAcceptedTeams.some((team) => team.id === parsedTeamIdParam);
|
||||
if (!isUserMemberOfTeam) {
|
||||
throw new Error(ERROR_MESSAGES.userNotInTeam);
|
||||
}
|
||||
}
|
||||
|
||||
switch (parsedStepParam) {
|
||||
case ACCOUNTS_STEP:
|
||||
if (!hasTeams) {
|
||||
throw new Error(ERROR_MESSAGES.userWithoutTeams);
|
||||
}
|
||||
break;
|
||||
|
||||
case EVENT_TYPES_STEP:
|
||||
if (!hasEventTypes) {
|
||||
throw new Error(ERROR_MESSAGES.appNotExtendsEventType);
|
||||
}
|
||||
eventTypes = await getEventTypes(user.id, parsedTeamIdParam);
|
||||
break;
|
||||
|
||||
case CONFIGURE_STEP:
|
||||
if (!hasEventTypes) {
|
||||
throw new Error(ERROR_MESSAGES.appNotExtendsEventType);
|
||||
}
|
||||
if (!parsedEventTypeIdParam) {
|
||||
throw new Error(ERROR_MESSAGES.appNotEventType);
|
||||
}
|
||||
eventType = await getEventTypeById(parsedEventTypeIdParam);
|
||||
break;
|
||||
|
||||
case OAUTH_STEP:
|
||||
if (!appMetadata.isOAuth) {
|
||||
throw new Error(ERROR_MESSAGES.appNotOAuth);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const personalAccount = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
alreadyInstalled: appInstalls.some((install) => !Boolean(install.teamId) && install.userId === user.id),
|
||||
};
|
||||
|
||||
const teamsWithIsAppInstalled = hasTeams
|
||||
? userAcceptedTeams.map((team) => ({
|
||||
...team,
|
||||
alreadyInstalled: appInstalls.some(
|
||||
(install) => Boolean(install.teamId) && install.teamId === team.id
|
||||
),
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
hasTeams,
|
||||
app,
|
||||
appMetadata,
|
||||
step: parsedStepParam,
|
||||
teams: teamsWithIsAppInstalled,
|
||||
personalAccount,
|
||||
eventTypes,
|
||||
teamId: parsedTeamIdParam ?? null,
|
||||
userName: user.username,
|
||||
hasEventTypes,
|
||||
configureEventType: eventType,
|
||||
} as OnboardingPageProps,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
console.info("Zod Parse Error", err.message);
|
||||
return { redirect: { permanent: false, destination: "/apps" } };
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
console.info("Redirect Error", err.message);
|
||||
switch (err.message) {
|
||||
case ERROR_MESSAGES.userNotAuthed:
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
case ERROR_MESSAGES.userNotFound:
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
default:
|
||||
return { redirect: { permanent: false, destination: "/apps" } };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
OnboardingPage.isThemeSupported = false;
|
||||
OnboardingPage.PageWrapper = PageWrapper;
|
||||
|
||||
export default OnboardingPage;
|
|
@ -104,10 +104,9 @@ function generateFiles() {
|
|||
* If a file has index.ts or index.tsx, it can be imported after removing the index.ts* part
|
||||
*/
|
||||
function getModulePath(path: string, moduleName: string) {
|
||||
return (
|
||||
`./${path.replace(/\\/g, "/")}/` +
|
||||
moduleName.replace(/\/index\.ts|\/index\.tsx/, "").replace(/\.tsx$|\.ts$/, "")
|
||||
);
|
||||
return `./${path.replace(/\\/g, "/")}/${moduleName
|
||||
.replace(/\/index\.ts|\/index\.tsx/, "")
|
||||
.replace(/\.tsx$|\.ts$/, "")}`;
|
||||
}
|
||||
|
||||
type ImportConfig =
|
||||
|
@ -329,6 +328,14 @@ function generateFiles() {
|
|||
lazyImport: true,
|
||||
})
|
||||
);
|
||||
browserOutput.push(
|
||||
...getExportedObject("EventTypeSettingsMap", {
|
||||
importConfig: {
|
||||
fileToBeImported: "components/EventTypeAppSettingsInterface.tsx",
|
||||
},
|
||||
lazyImport: true,
|
||||
})
|
||||
);
|
||||
|
||||
const banner = `/**
|
||||
This file is autogenerated using the command \`yarn app-store:build --watch\`.
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { EventTypeSettingsMap } from "@calcom/app-store/apps.browser.generated";
|
||||
|
||||
import type { EventTypeAppSettingsComponentProps } from "../types";
|
||||
import { DynamicComponent } from "./DynamicComponent";
|
||||
|
||||
export const EventTypeAppSettings = (props: EventTypeAppSettingsComponentProps) => {
|
||||
const { slug, ...rest } = props;
|
||||
return <DynamicComponent slug={slug} componentMap={EventTypeSettingsMap} {...rest} />;
|
||||
};
|
|
@ -14,5 +14,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-app-card",
|
||||
"dirName": "alby"
|
||||
"dirName": "alby",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -12,5 +12,6 @@
|
|||
"description": "The joyful productivity app\r\r",
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"],
|
||||
"dirName": "amie"
|
||||
"dirName": "amie",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
dirName: "applecalendar",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -39,3 +39,6 @@ export const EventTypeAddonMap = {
|
|||
import("./templates/event-type-app-card/components/EventTypeAppCardInterface")
|
||||
),
|
||||
};
|
||||
export const EventTypeSettingsMap = {
|
||||
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppSettingsInterface")),
|
||||
};
|
||||
|
|
|
@ -20,5 +20,6 @@
|
|||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?around.co\\/[a-zA-Z0-9]*",
|
||||
"organizerInputPlaceholder": "https://www.around.co/rick"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getAppOnboardingRedirectUrl } from "@calcom/lib/getAppOnboardingRedirectUrl";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
@ -86,5 +88,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
},
|
||||
});
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
|
||||
if (state?.returnToOnboarding) {
|
||||
return res.redirect(getAppOnboardingRedirectUrl(appConfig.slug, state.teamId));
|
||||
}
|
||||
|
||||
res.redirect(getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug }));
|
||||
}
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-app-card",
|
||||
"dirName": "basecamp3"
|
||||
"dirName": "basecamp3",
|
||||
"isOAuth": true
|
||||
}
|
||||
|
|
|
@ -14,5 +14,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"dirName": "cal-ai"
|
||||
"dirName": "cal-ai",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
email: "ali@cal.com",
|
||||
dirName: "caldavcalendar",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -19,5 +19,6 @@
|
|||
"organizerInputPlaceholder": "https://party.campfire.to/your-team",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?party.campfire.to\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
"description": "Close is the inside sales CRM of choice for startups and SMBs. Make more calls, send more emails and close more deals starting today.",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "link-as-an-app",
|
||||
"dependencies": ["google-calendar"]
|
||||
"dependencies": ["google-calendar"],
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export const metadata = {
|
|||
},
|
||||
key: { apikey: process.env.DAILY_API_KEY },
|
||||
dirName: "dailyvideo",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
"description": "Copy your server invite link and start scheduling calls in Discord! Discord is a VoIP and instant messaging social platform. Users have the ability to communicate with voice calls, video calls, text messaging, media and files in private chats or as part of communities.",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-location-video-static"
|
||||
"__template": "event-type-location-video-static",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-location-video-static",
|
||||
"dirName": "eightxeight"
|
||||
"dirName": "eightxeight",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-location-video-static",
|
||||
"dirName": "element-call"
|
||||
"dirName": "element-call",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
dirName: "exchange2013calendar",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -18,6 +18,7 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
dirName: "exchange2016calendar",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -12,5 +12,6 @@
|
|||
"publisher": "Cal.com",
|
||||
"email": "help@cal.com",
|
||||
"description": "Fetch Microsoft Exchange calendars and availabilities using Exchange Web Services (EWS).",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
"urlRegExp": "^https?:\\/\\/facetime\\.apple\\.com\\/join.+$"
|
||||
}
|
||||
},
|
||||
"isTemplate": false
|
||||
"isTemplate": false,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -23,5 +23,6 @@
|
|||
}
|
||||
},
|
||||
"description": "Fathom Analytics provides simple, privacy-focused website analytics. We're a GDPR-compliant, Google Analytics alternative.",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -24,5 +24,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export const metadata = {
|
|||
extendsFeature: "EventType",
|
||||
email: "help@cal.com",
|
||||
dirName: "giphy",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -18,6 +18,7 @@ export const metadata = {
|
|||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
dirName: "googlecalendar",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -27,6 +27,7 @@ export const metadata = {
|
|||
},
|
||||
dirName: "googlevideo",
|
||||
dependencies: ["google-calendar"],
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
},
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "booking-pages-tag"
|
||||
"__template": "booking-pages-tag",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export const metadata = {
|
|||
title: "HubSpot CRM",
|
||||
email: "help@cal.com",
|
||||
dirName: "hubspot",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -28,6 +28,7 @@ export const metadata = {
|
|||
key: { apikey: randomString(12) },
|
||||
dirName: "huddle01video",
|
||||
concurrentMeetings: true,
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"dirName": "intercom"
|
||||
"dirName": "intercom",
|
||||
"isOAuth": true
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export const metadata = {
|
|||
},
|
||||
dirName: "jitsivideo",
|
||||
concurrentMeetings: true,
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -16,6 +16,7 @@ export const metadata = {
|
|||
url: "https://larksuite.com/",
|
||||
email: "alan@larksuite.com",
|
||||
dirName: "larkcalendar",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -14,5 +14,6 @@
|
|||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"imageSrc": "icon.svg",
|
||||
"dirName": "make"
|
||||
"dirName": "make",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-location-video-static",
|
||||
"dirName": "mirotalk"
|
||||
"dirName": "mirotalk",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
"description": "Automate without limits. The workflow automation platform that doesn't box you in, that you never outgrow",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ export const metadata = {
|
|||
dirName: "office365calendar",
|
||||
url: "https://cal.com/",
|
||||
email: "help@cal.com",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
}
|
||||
},
|
||||
"dirName": "office365video",
|
||||
"concurrentMeetings": true
|
||||
"concurrentMeetings": true,
|
||||
"isOAuth": true
|
||||
}
|
||||
|
|
|
@ -14,5 +14,6 @@
|
|||
"__createdUsingCli": true,
|
||||
"imageSrc": "icon.svg",
|
||||
"__template": "event-type-app-card",
|
||||
"dirName": "paypal"
|
||||
"dirName": "paypal",
|
||||
"isOAuth": true
|
||||
}
|
||||
|
|
|
@ -20,5 +20,6 @@
|
|||
"organizerInputPlaceholder": "https://www.ping.gg/call/theo",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?ping.gg\\/call\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Pipedream, Inc.",
|
||||
"email": "support@pipedream.com",
|
||||
"description": "Connect APIs, remarkably fast. Stop writing boilerplate code, struggling with authentication and managing infrastructure. Start connecting APIs with code-level control when you need it — and no code when you don't",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -23,5 +23,6 @@
|
|||
}
|
||||
},
|
||||
"description": "Simple, privacy-friendly Google Analytics alternative.",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -1,39 +1,14 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Tooltip, TextField } from "@calcom/ui";
|
||||
|
||||
import type { appDataSchema } from "../zod";
|
||||
import EventTypeAppSettingsInterface from "./EventTypeAppSettingsInterface";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
|
||||
const { t } = useLocale();
|
||||
const { disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const [additionalParameters, setAdditionalParameters] = useState("");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
const query = additionalParameters !== "" ? `?${additionalParameters}` : "";
|
||||
const eventTypeURL = eventType.URL + query;
|
||||
|
||||
function QRCode({ size, data }: { size: number; data: string }) {
|
||||
const QR_URL = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&data=${data}`;
|
||||
return (
|
||||
<Tooltip content={eventTypeURL}>
|
||||
<a download href={QR_URL} target="_blank" rel="noreferrer">
|
||||
<img
|
||||
className="hover:bg-muted border-default border hover:shadow-sm"
|
||||
style={{ padding: size / 16, borderRadius: size / 20 }}
|
||||
width={size}
|
||||
src={QR_URL}
|
||||
alt={eventTypeURL}
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
const { disabled, getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
|
@ -43,24 +18,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
}}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<div className="flex w-full flex-col gap-5 text-sm">
|
||||
<div className="flex w-full">
|
||||
<TextField
|
||||
name="hello"
|
||||
disabled={disabled}
|
||||
value={additionalParameters}
|
||||
onChange={(e) => setAdditionalParameters(e.target.value)}
|
||||
label={t("additional_url_parameters")}
|
||||
containerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-60 flex items-baseline gap-2">
|
||||
<QRCode size={256} data={eventTypeURL} />
|
||||
<QRCode size={128} data={eventTypeURL} />
|
||||
<QRCode size={64} data={eventTypeURL} />
|
||||
</div>
|
||||
</div>
|
||||
<EventTypeAppSettingsInterface
|
||||
eventType={eventType}
|
||||
slug={app.slug}
|
||||
disabled={disabled}
|
||||
getAppData={getAppData}
|
||||
setAppData={setAppData}
|
||||
/>
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import type { EventTypeAppSettingsComponent } from "@calcom/app-store/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { TextField, Tooltip } from "@calcom/ui";
|
||||
|
||||
const EventTypeAppSettingsInterface: EventTypeAppSettingsComponent = ({ eventType, disabled }) => {
|
||||
const { t } = useLocale();
|
||||
const [additionalParameters, setAdditionalParameters] = useState("");
|
||||
const query = additionalParameters !== "" ? `?${additionalParameters}` : "";
|
||||
const eventTypeURL = eventType.URL + query;
|
||||
|
||||
function QRCode({ size, data }: { size: number; data: string }) {
|
||||
const QR_URL = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&data=${data}`;
|
||||
return (
|
||||
<Tooltip content={eventTypeURL}>
|
||||
<a download href={QR_URL} target="_blank" rel="noreferrer">
|
||||
<img
|
||||
className={classNames(
|
||||
"hover:bg-muted border-default border hover:shadow-sm",
|
||||
size >= 256 && "min-h-32"
|
||||
)}
|
||||
style={{ padding: size / 16, borderRadius: size / 20 }}
|
||||
width={size}
|
||||
src={QR_URL}
|
||||
alt={eventTypeURL}
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5 text-sm">
|
||||
<div className="flex w-full">
|
||||
<TextField
|
||||
name="hello"
|
||||
disabled={disabled}
|
||||
value={additionalParameters}
|
||||
onChange={(e) => setAdditionalParameters(e.target.value)}
|
||||
label={t("additional_url_parameters")}
|
||||
containerClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-60 flex items-baseline gap-2">
|
||||
<QRCode size={256} data={eventTypeURL} />
|
||||
<QRCode size={128} data={eventTypeURL} />
|
||||
<QRCode size={64} data={eventTypeURL} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppSettingsInterface;
|
|
@ -11,5 +11,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "support@cal.com",
|
||||
"description": "Easily generate a QR code for your links to print, share, or embed.",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Eric Luce",
|
||||
"email": "info@restlessmindstech.com",
|
||||
"description": "Quickly share your Cal.com meeting links with Raycast",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -18,5 +18,6 @@
|
|||
"type": "integrations:riverside_video",
|
||||
"linkType": "static"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
"upgradeUrl": "/routing-forms/forms"
|
||||
},
|
||||
"description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
"description": "Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": true
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
"description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -19,5 +19,6 @@
|
|||
"organizerInputPlaceholder": "https://signal.me/#p/+11234567890",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?signal.me\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -19,5 +19,6 @@
|
|||
"organizerInputPlaceholder": "https://sirius.video/sebastian",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?sirius.video\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "link-as-an-app",
|
||||
"dirName": "skiff"
|
||||
"dirName": "skiff",
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export const metadata = {
|
|||
extendsFeature: "EventType",
|
||||
email: "help@cal.com",
|
||||
dirName: "stripepayment",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -2,8 +2,11 @@ import type { Prisma } from "@prisma/client";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import { getAppOnboardingRedirectUrl } from "@calcom/lib/getAppOnboardingRedirectUrl";
|
||||
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import type { StripeData } from "../lib/server";
|
||||
import stripe from "../lib/server";
|
||||
|
||||
|
@ -47,6 +50,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
req
|
||||
);
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
|
||||
if (state?.returnToOnboarding) {
|
||||
return res.redirect(getAppOnboardingRedirectUrl("stripe", state.teamId));
|
||||
}
|
||||
|
||||
const returnTo = getReturnToValueFromQueryState(req);
|
||||
res.redirect(returnTo || getInstalledAppPath({ variant: "payment", slug: "stripe" }));
|
||||
}
|
||||
|
|
|
@ -18,5 +18,6 @@
|
|||
"type": "integrations:sylaps_video",
|
||||
"label": "Sylaps"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export const metadata = {
|
|||
},
|
||||
},
|
||||
dirName: "tandemvideo",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
"description": "Adds a link to copy Typeform Redirect URL",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export type IntegrationOAuthCallbackState = {
|
|||
returnTo: string;
|
||||
installGoogleVideo?: boolean;
|
||||
teamId?: number;
|
||||
returnToOnboarding?: boolean;
|
||||
};
|
||||
|
||||
export type CredentialOwner = {
|
||||
|
@ -52,4 +53,23 @@ export type EventTypeAppCardComponentProps = {
|
|||
disabled?: boolean;
|
||||
LockedIcon?: JSX.Element | false;
|
||||
};
|
||||
|
||||
export type EventTypeAppSettingsComponentProps = {
|
||||
// Limit what data should be accessible to apps\
|
||||
eventType: Pick<
|
||||
z.infer<typeof EventTypeModel>,
|
||||
"id" | "title" | "description" | "teamId" | "length" | "recurringEvent" | "seatsPerTimeSlot" | "team"
|
||||
> & {
|
||||
URL: string;
|
||||
};
|
||||
getAppData: GetAppData;
|
||||
setAppData: SetAppData;
|
||||
disabled?: boolean;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type EventTypeAppCardComponent = React.FC<EventTypeAppCardComponentProps>;
|
||||
|
||||
export type EventTypeAppSettingsComponent = React.FC<EventTypeAppSettingsComponentProps>;
|
||||
|
||||
export type EventTypeModel = z.infer<typeof EventTypeModel>;
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
"email": "support@cal.com",
|
||||
"description": "The world's fastest calendar, beautifully designed for a remote world\r",
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"]
|
||||
"dependencies": ["google-calendar"],
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export const metadata = {
|
|||
variant: "other",
|
||||
email: "support@tryvital.io",
|
||||
dirName: "vital",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Andreas Vejnø Andersen",
|
||||
"email": "info@vejnoe.dk",
|
||||
"description": "Get the local weather forecast with icons in your calendar\r\r",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"concurrentMeetings": true
|
||||
"concurrentMeetings": true,
|
||||
"isAuth": true
|
||||
}
|
||||
|
|
|
@ -19,5 +19,6 @@
|
|||
"organizerInputPlaceholder": "https://wa.me/send?phone=1234567890",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?wa.me\\/[a-zA-Z0-9]*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isAuth": false
|
||||
}
|
||||
|
|
|
@ -20,5 +20,6 @@
|
|||
"organizerInputPlaceholder": "https://www.whereby.com/cal",
|
||||
"urlRegExp": "^(?:https?://)?(?:(?!.*-\\.)(?:\\w+(-\\w+)*\\.))*whereby\\.com(/[\\w\\-._~:?#\\[\\]@!$&'()*+,;%=]+)*$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isAuth": false
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export const metadata = {
|
|||
variant: "other",
|
||||
email: "help@cal.com",
|
||||
dirName: "wipemycalother",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "support@cal.com",
|
||||
"description": "Embedded booking pages right into your wordpress page",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"isAuth": false
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export const metadata = {
|
|||
variant: "automation",
|
||||
email: "help@cal.com",
|
||||
dirName: "zapier",
|
||||
isOAuth: false,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic",
|
||||
"scope": "ZohoBigin.modules.events.ALL,ZohoBigin.modules.contacts.ALL"
|
||||
"scope": "ZohoBigin.modules.events.ALL,ZohoBigin.modules.contacts.ALL",
|
||||
"isOAuth": true
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
"logo": "icon.svg",
|
||||
"publisher": "Cal.com",
|
||||
"url": "https://cal.com/",
|
||||
"email": "help@cal.com"
|
||||
"email": "help@cal.com",
|
||||
"isAuth": true
|
||||
}
|
||||
|
|
|
@ -12,5 +12,6 @@
|
|||
"description": "Zoho CRM is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "basic"
|
||||
"__template": "basic",
|
||||
"isOAuth": true
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export const metadata = {
|
|||
},
|
||||
},
|
||||
dirName: "zoomvideo",
|
||||
isOAuth: true,
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const getAppOnboardingRedirectUrl = (slug: string, teamId?: number) => {
|
||||
return `/apps/onboarding/event-types?slug=${slug}${teamId ? `&teamId=${teamId}` : ""}`;
|
||||
};
|
|
@ -155,6 +155,8 @@ export interface App {
|
|||
concurrentMeetings?: boolean;
|
||||
|
||||
createdAt?: string;
|
||||
/** Specifies if the App uses an OAuth flow */
|
||||
isOAuth?: boolean;
|
||||
}
|
||||
|
||||
export type AppFrontendPayload = Omit<App, "key"> & {
|
||||
|
|
|
@ -1,17 +1,30 @@
|
|||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
interface ISteps {
|
||||
type StepWithNav = {
|
||||
maxSteps: number;
|
||||
currentStep: number;
|
||||
navigateToStep: (step: number) => void;
|
||||
disableNavigation?: false;
|
||||
stepLabel?: (currentStep: number, maxSteps: number) => string;
|
||||
}
|
||||
};
|
||||
|
||||
const Steps = (props: ISteps) => {
|
||||
type StepWithoutNav = {
|
||||
maxSteps: number;
|
||||
currentStep: number;
|
||||
navigateToStep?: undefined;
|
||||
disableNavigation: true;
|
||||
stepLabel?: (currentStep: number, maxSteps: number) => string;
|
||||
};
|
||||
|
||||
// Discriminative union on disableNavigation prop
|
||||
type StepsProps = StepWithNav | StepWithoutNav;
|
||||
|
||||
const Steps = (props: StepsProps) => {
|
||||
const {
|
||||
maxSteps,
|
||||
currentStep,
|
||||
navigateToStep,
|
||||
disableNavigation = false,
|
||||
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
|
||||
} = props;
|
||||
return (
|
||||
|
@ -22,10 +35,10 @@ const Steps = (props: ISteps) => {
|
|||
return index <= currentStep - 1 ? (
|
||||
<div
|
||||
key={`step-${index}`}
|
||||
onClick={() => navigateToStep(index)}
|
||||
onClick={() => navigateToStep?.(index)}
|
||||
className={classNames(
|
||||
"bg-inverted h-1 w-full rounded-[1px]",
|
||||
index < currentStep - 1 ? "cursor-pointer" : ""
|
||||
index < currentStep - 1 && !disableNavigation ? "cursor-pointer" : ""
|
||||
)}
|
||||
data-testid={`step-indicator-${index}`}
|
||||
/>
|
||||
|
|
|
@ -35,7 +35,7 @@ const ScrollableArea = ({ children, className }: PropsWithChildren<{ className?:
|
|||
className // Pass in your max-w / max-h
|
||||
)}>
|
||||
{children}
|
||||
{isOverflowingY && <div style={overflowIndicatorStyles} data-testid="overflow-indicator" />}
|
||||
{isOverflowingY && <div data-testid="overflow-indicator" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue