Revamp Google Cal warning for Meet, Amie, and Vimcal (#7308)
* Create requires Google Cal component * Create installed GCal message * Move requires GCal component to App * Clean up * Abstract prerequisite component * Add requires message on app card * Refactor to dependency * Clean up * Change typeform dep & remove app card dep component * Clean up * Change dependency to dependencies * Pass disableInstall to default install button for AppCard * Refactor app page to dependencies * Type fix * More type fixes * Update apps/web/components/apps/App.tsx * Apply suggestions from code review --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
parent
c32aadf297
commit
58b439ca65
|
@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||
import React, { useState } from "react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
@ -24,6 +24,8 @@ import {
|
|||
FiShield,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
/* These app slugs all require Google Cal to be installed */
|
||||
|
||||
const Component = ({
|
||||
name,
|
||||
type,
|
||||
|
@ -45,6 +47,7 @@ const Component = ({
|
|||
isProOnly,
|
||||
images,
|
||||
isTemplate,
|
||||
dependencies,
|
||||
}: Parameters<typeof App>[0]) => {
|
||||
const { t } = useLocale();
|
||||
const hasImages = images && images.length > 0;
|
||||
|
@ -76,6 +79,15 @@ const Component = ({
|
|||
}
|
||||
);
|
||||
|
||||
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
|
||||
enabled: !!dependencies,
|
||||
});
|
||||
|
||||
const disableInstall =
|
||||
dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
|
||||
|
||||
// const disableInstall = requiresGCal && !gCalInstalled.data;
|
||||
|
||||
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
|
||||
// Such apps, can only be installed once.
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
|
||||
|
@ -137,6 +149,7 @@ const Component = ({
|
|||
<InstallAppButton
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
disableInstall={disableInstall}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
|
@ -176,6 +189,7 @@ const Component = ({
|
|||
<InstallAppButton
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
disableInstall={disableInstall}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
|
@ -203,6 +217,16 @@ const Component = ({
|
|||
) : (
|
||||
<SkeletonButton className="h-10 w-24" />
|
||||
)}
|
||||
|
||||
{dependencies &&
|
||||
(!dependencyData.isLoading ? (
|
||||
<div className="mt-6">
|
||||
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
|
||||
</div>
|
||||
) : (
|
||||
<SkeletonButton className="mt-6 h-20 grow" />
|
||||
))}
|
||||
|
||||
{price !== 0 && (
|
||||
<span className="block text-right">
|
||||
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
|
||||
|
@ -332,6 +356,8 @@ export default function App(props: {
|
|||
isProOnly: AppType["isProOnly"];
|
||||
images?: string[];
|
||||
isTemplate?: boolean;
|
||||
disableInstall?: boolean;
|
||||
dependencies?: string[];
|
||||
}) {
|
||||
return (
|
||||
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
|||
import path from "path";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
@ -36,11 +35,11 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
|||
isProOnly={data.isProOnly}
|
||||
images={source.data?.items as string[] | undefined}
|
||||
isTemplate={data.isTemplate}
|
||||
dependencies={data.dependencies}
|
||||
// tos="https://zoom.us/terms"
|
||||
// privacy="https://zoom.us/privacy"
|
||||
body={
|
||||
<>
|
||||
{data.slug === "google-meet" && <ExisitingGoogleCal />}
|
||||
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -1635,6 +1635,10 @@
|
|||
"add_a_new_route": "Add a new Route",
|
||||
"no_responses_yet": "No responses yet",
|
||||
"this_will_be_the_placeholder": "This will be the placeholder",
|
||||
"this_app_requires_connected_account": "{{appName}} requires a connected {{dependencyName}} account",
|
||||
"connect_app": "Connect {{dependencyName}}",
|
||||
"app_is_connected": "{{dependencyName}} is connected",
|
||||
"requires_app": "Requires {{dependencyName}}",
|
||||
"verification_code": "Verification code",
|
||||
"verify": "Verify",
|
||||
"select_all": "Select All",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import prisma, { safeAppSelect, safeCredentialSelect } from "@calcom/prisma";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { AppFrontendPayload as App } from "@calcom/types/App";
|
||||
|
@ -82,6 +83,21 @@ export async function getAppRegistryWithCredentials(userId: number) {
|
|||
// Skip if app isn't installed
|
||||
/* This is now handled from the DB */
|
||||
// if (!app.installed) return apps;
|
||||
let dependencyData: {
|
||||
name?: string;
|
||||
installed?: boolean;
|
||||
}[] = [];
|
||||
if (app.dependencies) {
|
||||
dependencyData = app.dependencies.map((dependency) => {
|
||||
const dependencyInstalled = dbApps.some(
|
||||
(dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency
|
||||
);
|
||||
// If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app.
|
||||
const dependencyName = getAppFromSlug(dependency)?.name;
|
||||
return { name: dependencyName, installed: dependencyInstalled };
|
||||
});
|
||||
}
|
||||
|
||||
const { rating, reviews, trending, verified, ...remainingAppProps } = app;
|
||||
apps.push({
|
||||
rating: rating || 0,
|
||||
|
@ -93,7 +109,9 @@ export async function getAppRegistryWithCredentials(userId: number) {
|
|||
credentials: dbapp.credentials,
|
||||
installed: true,
|
||||
isDefault: usersDefaultApp === dbapp.slug,
|
||||
...(app.dependencies && { dependencyData }),
|
||||
});
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "support@cal.com",
|
||||
"description": "The joyful productivity app\r\r",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"]
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ export const InstallAppButtonMap = {
|
|||
exchange2016calendar: dynamic(() => import("./exchange2016calendar/components/InstallAppButton")),
|
||||
exchangecalendar: dynamic(() => import("./exchangecalendar/components/InstallAppButton")),
|
||||
googlecalendar: dynamic(() => import("./googlecalendar/components/InstallAppButton")),
|
||||
googlevideo: dynamic(() => import("./googlevideo/components/InstallAppButton")),
|
||||
hubspot: dynamic(() => import("./hubspot/components/InstallAppButton")),
|
||||
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
||||
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { FiAlertCircle, FiArrowRight, FiCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
import { InstallAppButtonMap } from "./apps.browser.generated";
|
||||
import type { InstallAppButtonProps } from "./types";
|
||||
|
@ -16,9 +22,16 @@ export const InstallAppButtonWithoutPlanCheck = (
|
|||
) => {
|
||||
const key = deriveAppDictKeyFromType(props.type, InstallAppButtonMap);
|
||||
const InstallAppButtonComponent = InstallAppButtonMap[key as keyof typeof InstallAppButtonMap];
|
||||
if (!InstallAppButtonComponent) return <>{props.render({ useDefaultComponent: true })}</>;
|
||||
if (!InstallAppButtonComponent)
|
||||
return <>{props.render({ useDefaultComponent: true, disabled: props.disableInstall })}</>;
|
||||
|
||||
return <InstallAppButtonComponent render={props.render} onChanged={props.onChanged} />;
|
||||
return (
|
||||
<InstallAppButtonComponent
|
||||
render={props.render}
|
||||
onChanged={props.onChanged}
|
||||
disableInstall={props.disableInstall}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InstallAppButton = (
|
||||
|
@ -26,6 +39,7 @@ export const InstallAppButton = (
|
|||
isProOnly?: App["isProOnly"];
|
||||
type: App["type"];
|
||||
wrapperClassName?: string;
|
||||
disableInstall?: boolean;
|
||||
} & InstallAppButtonProps
|
||||
) => {
|
||||
const { isLoading, data: user } = trpc.viewer.me.useQuery();
|
||||
|
@ -63,3 +77,79 @@ export const InstallAppButton = (
|
|||
};
|
||||
|
||||
export { AppConfiguration } from "./_components/AppConfiguration";
|
||||
|
||||
export const AppDependencyComponent = ({
|
||||
appName,
|
||||
dependencyData,
|
||||
}: {
|
||||
appName: string;
|
||||
dependencyData: RouterOutputs["viewer"]["appsRouter"]["queryForDependencies"];
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"rounded-md py-3 px-4",
|
||||
dependencyData && dependencyData.some((dependency) => !dependency.installed)
|
||||
? "bg-blue-100"
|
||||
: "bg-gray-100"
|
||||
)}>
|
||||
{dependencyData &&
|
||||
dependencyData.map((dependency) => {
|
||||
return dependency.installed ? (
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="flex items-start">
|
||||
<div>
|
||||
<FiCheck className="mt-1 mr-2 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">
|
||||
{t("app_is_connected", { dependencyName: dependency.name })}
|
||||
</span>
|
||||
<div>
|
||||
<div>
|
||||
<span>
|
||||
{t("this_app_requires_connected_account", {
|
||||
appName,
|
||||
dependencyName: dependency.name,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="flex items-start text-blue-900">
|
||||
<div>
|
||||
<FiAlertCircle className="mt-1 mr-2 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">
|
||||
{t("this_app_requires_connected_account", { appName, dependencyName: dependency.name })}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
<Link
|
||||
href={`${CAL_URL}/apps/${dependency.slug}`}
|
||||
className="flex items-center text-blue-900 underline">
|
||||
<span className="mr-1">
|
||||
{t("connect_app", { dependencyName: dependency.name })}
|
||||
</span>
|
||||
<FiArrowRight />
|
||||
</Link>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { FiAlertCircle, FiArrowRight, FiCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
const ExistingGoogleCal = ({ gCalInstalled, appName }: { gCalInstalled?: boolean; appName: string }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return gCalInstalled ? (
|
||||
<div className="rounded-md bg-gray-100 py-3 px-4">
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="flex items-start">
|
||||
<div>
|
||||
<FiCheck className="mt-1 mr-2 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("google_calendar_is_connected")}</span>
|
||||
<div>
|
||||
<div>
|
||||
<span>{t("requires_google_calendar")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md bg-blue-100 py-3 px-4 text-blue-900">
|
||||
<div className="items-start space-x-2.5">
|
||||
<div className="flex items-start">
|
||||
<div>
|
||||
<FiAlertCircle className="mt-1 mr-2 font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("this_app_requires_google_calendar", { appName })}</span>
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
<Link
|
||||
href={`${CAL_URL}/apps/google-calendar`}
|
||||
className="flex items-center text-blue-900 underline">
|
||||
<span className="mr-1">{t("connect_google_calendar")}</span>
|
||||
<FiArrowRight />
|
||||
</Link>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExistingGoogleCal;
|
|
@ -31,6 +31,7 @@ export const metadata = {
|
|||
},
|
||||
},
|
||||
dirName: "googlevideo",
|
||||
dependencies: ["google-calendar"],
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
const ExistingGoogleCal = () => {
|
||||
const { t } = useLocale();
|
||||
const { isLoading, data: hasGoogleCal } = trpc.viewer.appsRouter.checkForGCal.useQuery();
|
||||
|
||||
return (
|
||||
<div className="rounded-md bg-blue-100 py-3 px-4 text-blue-900">
|
||||
<div className="flex items-start space-x-2.5">
|
||||
<div>
|
||||
<FiAlertCircle className="font-semibold" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">{t("requires_google_calendar")}</span>
|
||||
<div>
|
||||
<>
|
||||
{isLoading ? (
|
||||
<SkeletonText className="h-4 w-full" />
|
||||
) : hasGoogleCal ? (
|
||||
t("connected_google_calendar")
|
||||
) : (
|
||||
<Trans i18nKey="no_google_calendar">
|
||||
Please connect your Google Calendar account{" "}
|
||||
<Link href={`${CAL_URL}/apps/google-calendar`} className="font-semibold text-blue-900">
|
||||
here
|
||||
</Link>
|
||||
</Trans>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExistingGoogleCal;
|
|
@ -1,64 +0,0 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
import useApp from "@calcom/lib/hooks/useApp";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { DialogProps } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const [showWarningDialog, setShowWarningDialog] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
setShowWarningDialog(true);
|
||||
// mutation.mutate("");
|
||||
},
|
||||
disabled: showWarningDialog,
|
||||
})}
|
||||
<WarningDialog open={showWarningDialog} onOpenChange={setShowWarningDialog} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function WarningDialog(props: DialogProps) {
|
||||
const { t } = useLocale();
|
||||
const googleCalendarData = useApp("google-calendar");
|
||||
const googleCalendarPresent = googleCalendarData.data?.isInstalled;
|
||||
|
||||
const mutation = useAddAppMutation(googleCalendarPresent ? "google_video" : "google_calendar", {
|
||||
installGoogleVideo: !googleCalendarPresent,
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog name="Account check" open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent
|
||||
type="creation"
|
||||
title={t("using_meet_requires_calendar")}
|
||||
description={googleCalendarPresent ? "" : t("continue_to_install_google_calendar")}>
|
||||
<DialogFooter>
|
||||
<>
|
||||
<DialogClose
|
||||
type="button"
|
||||
color="secondary"
|
||||
tabIndex={-1}
|
||||
onClick={() => {
|
||||
props.onOpenChange?.(false);
|
||||
}}>
|
||||
{t("cancel")}
|
||||
</DialogClose>
|
||||
|
||||
<Button type="button" onClick={() => mutation.mutate("")}>
|
||||
{googleCalendarPresent ? t("install_google_meet") : t("install_google_calendar")}
|
||||
</Button>
|
||||
</>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { default as InstallAppButton } from "./InstallAppButton";
|
|
@ -10,5 +10,6 @@
|
|||
"publisher": "Cal.com",
|
||||
"email": "help@cal.com",
|
||||
"description": "Adds a link to copy Typeform Redirect URL",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["routing-forms"]
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ export interface InstallAppButtonProps {
|
|||
}
|
||||
) => JSX.Element;
|
||||
onChanged?: () => unknown;
|
||||
disableInstall?: boolean;
|
||||
}
|
||||
export type EventTypeAppCardComponentProps = {
|
||||
// Limit what data should be accessible to apps
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
"publisher": "Cal.com, Inc.",
|
||||
"email": "support@cal.com",
|
||||
"description": "The world's fastest calendar, beautifully designed for a remote world\r",
|
||||
"__createdUsingCli": true
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"]
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import type { Prisma } from "@prisma/client";
|
|||
import z from "zod";
|
||||
|
||||
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
||||
import { getLocalAppMetadata } from "@calcom/app-store/utils";
|
||||
import { getLocalAppMetadata, getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import { sendDisabledAppEmail } from "@calcom/emails";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
|
@ -320,4 +320,26 @@ export const appsRouter = router({
|
|||
|
||||
return !!updated;
|
||||
}),
|
||||
queryForDependencies: authedProcedure.input(z.string().array().optional()).query(async ({ ctx, input }) => {
|
||||
if (!input) return;
|
||||
|
||||
const dependencyData: { name: string; slug: string; installed: boolean }[] = [];
|
||||
|
||||
await Promise.all(
|
||||
input.map(async (dependency) => {
|
||||
const appInstalled = await ctx.prisma.credential.findFirst({
|
||||
where: {
|
||||
appId: dependency,
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const app = await getAppFromSlug(dependency);
|
||||
|
||||
dependencyData.push({ name: app?.name || dependency, slug: dependency, installed: !!appInstalled });
|
||||
})
|
||||
);
|
||||
|
||||
return dependencyData;
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import { Tag } from "@calcom/app-store/types";
|
||||
import type { Tag } from "@calcom/app-store/types";
|
||||
|
||||
import { Optional } from "./utils";
|
||||
import type { Optional } from "./utils";
|
||||
|
||||
type CommonProperties = {
|
||||
default?: false;
|
||||
|
@ -138,12 +138,18 @@ export interface App {
|
|||
dirName?: string;
|
||||
isTemplate?: boolean;
|
||||
__template?: string;
|
||||
/** Slug of an app needed to be installed before the current app can be added */
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
export type AppFrontendPayload = Omit<App, "key"> & {
|
||||
/** We should type error if keys are leaked to the frontend */
|
||||
isDefault?: boolean;
|
||||
key?: never;
|
||||
dependencyData?: {
|
||||
name?: string;
|
||||
installed?: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type AppMeta = Optional<App, "rating" | "trending" | "reviews" | "verified">;
|
||||
|
|
|
@ -88,6 +88,7 @@ export function AppCard({ app, credentials, searchText }: AppCardProps) {
|
|||
<InstallAppButton
|
||||
type={app.type}
|
||||
isProOnly={app.isProOnly}
|
||||
disableInstall={!!app.dependencies && !app.dependencyData?.some((data) => !data.installed)}
|
||||
wrapperClassName="[@media(max-width:260px)]:w-full"
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
|
@ -116,6 +117,7 @@ export function AppCard({ app, credentials, searchText }: AppCardProps) {
|
|||
type={app.type}
|
||||
isProOnly={app.isProOnly}
|
||||
wrapperClassName="[@media(max-width:260px)]:w-full"
|
||||
disableInstall={!!app.dependencies && app.dependencyData?.some((data) => !data.installed)}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
|
@ -123,6 +125,7 @@ export function AppCard({ app, credentials, searchText }: AppCardProps) {
|
|||
onClick: () => {
|
||||
mutation.mutate({ type: app.type, variant: app.variant, slug: app.slug });
|
||||
},
|
||||
disabled: !!props.disabled,
|
||||
};
|
||||
}
|
||||
return (
|
||||
|
|
Loading…
Reference in New Issue
Block a user