2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
import { zodResolver } from "@hookform/resolvers/zod";
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import type { FC } from "react";
import { useReducer, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
import { classNames as cs } from "@calcom/lib";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { AppCategories } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import {
Button,
ConfirmationDialogContent,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
EmptyScreen,
Form,
Icon,
List,
showToast,
SkeletonButton,
SkeletonContainer,
SkeletonText,
Switch,
TextField,
} from "@calcom/ui";
import AppListCard from "../../../apps/web/components/AppListCard";
type App = RouterOutputs["viewer"]["appsRouter"]["listLocal"][number];
const IntegrationContainer = ({
app,
category,
handleModelOpen,
}: {
app: App;
category: string;
handleModelOpen: (data: EditModalState) => void;
}) => {
const { t } = useLocale();
const utils = trpc.useUtils();
const [disableDialog, setDisableDialog] = useState(false);
const showKeyModal = (fromEnabled?: boolean) => {
// FIXME: This is preventing the modal from opening for apps that has null keys
if (app.keys) {
handleModelOpen({
dirName: app.dirName,
keys: app.keys,
slug: app.slug,
type: app.type,
isOpen: "editKeys",
fromEnabled,
appName: app.name,
});
}
};
const enableAppMutation = trpc.viewer.appsRouter.toggle.useMutation({
onSuccess: (enabled) => {
utils.viewer.appsRouter.listLocal.invalidate({ category });
setDisableDialog(false);
showToast(
enabled ? t("app_is_enabled", { appName: app.name }) : t("app_is_disabled", { appName: app.name }),
"success"
);
if (enabled) {
showKeyModal();
}
},
onError: (error) => {
showToast(error.message, "error");
},
});
return (
<li>
<AppListCard
logo={app.logo}
description={app.description}
title={app.name}
isTemplate={app.isTemplate}
actions={
<div className="flex items-center justify-self-end">
{app.keys && (
<Button color="secondary" className="mr-2" onClick={() => showKeyModal()}>
<Icon name="pencil" />
</Button>
)}
<Switch
checked={app.enabled}
onClick={() => {
if (app.enabled) {
setDisableDialog(true);
} else if (app.keys) {
showKeyModal(true);
} else {
enableAppMutation.mutate({ slug: app.slug, enabled: !app.enabled });
}
}}
/>
</div>
}
/>
<Dialog open={disableDialog} onOpenChange={setDisableDialog}>
<ConfirmationDialogContent
title={t("disable_app")}
variety="danger"
onConfirm={() => {
enableAppMutation.mutate({ slug: app.slug, enabled: !app.enabled });
}}>
{t("disable_app_description")}
</ConfirmationDialogContent>
</Dialog>
</li>
);
};
const querySchema = z.object({
category: z
.nativeEnum({ ...AppCategories, conferencing: "conferencing" })
.optional()
.default(AppCategories.calendar),
});
const AdminAppsList = ({
baseURL,
className,
useQueryParam = false,
classNames,
onSubmit = noop,
...rest
}: {
baseURL: string;
classNames?: {
form?: string;
appCategoryNavigationRoot?: string;
appCategoryNavigationContainer?: string;
verticalTabsItem?: string;
};
className?: string;
useQueryParam?: boolean;
onSubmit?: () => void;
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">) => {
return (
<form
{...rest}
className={
classNames?.form ?? "max-w-80 bg-default mb-4 rounded-md px-0 pt-0 md:max-w-full md:px-8 md:pt-10"
}
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}>
<AppCategoryNavigation
baseURL={baseURL}
useQueryParam={useQueryParam}
classNames={{
root: className,
verticalTabsItem: classNames?.verticalTabsItem,
container: cs("min-w-0 w-full", classNames?.appCategoryNavigationContainer ?? "max-w-[500px]"),
}}>
<AdminAppsListContainer />
</AppCategoryNavigation>
</form>
);
};
const EditKeysModal: FC<{
dirName: string;
slug: string;
type: string;
isOpen: boolean;
keys: App["keys"];
handleModelClose: () => void;
fromEnabled?: boolean;
appName?: string;
}> = (props) => {
const utils = trpc.useUtils();
const { t } = useLocale();
const { dirName, slug, type, isOpen, keys, handleModelClose, fromEnabled, appName } = props;
const appKeySchema = appKeysSchemas[dirName as keyof typeof appKeysSchemas];
const formMethods = useForm({
resolver: zodResolver(appKeySchema),
});
const saveKeysMutation = trpc.viewer.appsRouter.saveKeys.useMutation({
onSuccess: () => {
showToast(fromEnabled ? t("app_is_enabled", { appName }) : t("keys_have_been_saved"), "success");
utils.viewer.appsRouter.listLocal.invalidate();
handleModelClose();
},
onError: (error) => {
showToast(error.message, "error");
},
});
return (
<Dialog open={isOpen} onOpenChange={handleModelClose}>
<DialogContent title={t("edit_keys")} type="creation">
{!!keys && typeof keys === "object" && (
<Form
id="edit-keys"
form={formMethods}
handleSubmit={(values) =>
saveKeysMutation.mutate({
slug,
type,
keys: values,
dirName,
fromEnabled,
})
}
className="px-4 pb-4">
{Object.keys(keys).map((key) => (
<Controller
name={key}
key={key}
control={formMethods.control}
defaultValue={keys && keys[key] ? keys?.[key] : ""}
render={({ field: { value } }) => (
<TextField
label={key}
key={key}
name={key}
value={value}
onChange={(e) => {
formMethods.setValue(key, e?.target.value);
}}
/>
)}
/>
))}
</Form>
)}
<DialogFooter showDivider className="mt-8">
<DialogClose onClick={handleModelClose} />
<Button form="edit-keys" type="submit">
{t("save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
interface EditModalState extends Pick<App, "keys"> {
isOpen: "none" | "editKeys" | "disableKeys";
dirName: string;
type: string;
slug: string;
fromEnabled?: boolean;
appName?: string;
}
const AdminAppsListContainer = () => {
const searchParams = useCompatSearchParams();
const { t } = useLocale();
const category = searchParams?.get("category") || AppCategories.calendar;
const { data: apps, isPending } = trpc.viewer.appsRouter.listLocal.useQuery(
{ category },
{ enabled: searchParams !== null }
);
const [modalState, setModalState] = useReducer(
(data: EditModalState, partialData: Partial<EditModalState>) => ({ ...data, ...partialData }),
{
keys: null,
isOpen: "none",
dirName: "",
type: "",
slug: "",
}
);
const handleModelClose = () =>
setModalState({ keys: null, isOpen: "none", dirName: "", slug: "", type: "" });
const handleModelOpen = (data: EditModalState) => setModalState({ ...data });
if (isPending) return <SkeletonLoader />;
if (!apps || apps.length === 0) {
return (
<EmptyScreen
Icon="circle-alert"
headline={t("no_available_apps")}
description={t("no_available_apps_description")}
/>
);
}
return (
<>
<List>
{apps.map((app) => (
<IntegrationContainer
handleModelOpen={handleModelOpen}
app={app}
key={app.name}
category={category}
/>
))}
</List>
<EditKeysModal
keys={modalState.keys}
dirName={modalState.dirName}
handleModelClose={handleModelClose}
isOpen={modalState.isOpen === "editKeys"}
slug={modalState.slug}
type={modalState.type}
fromEnabled={modalState.fromEnabled}
appName={modalState.appName}
/>
</>
);
};
export default AdminAppsList;
const SkeletonLoader = () => {
return (
<SkeletonContainer className="w-[30rem] pr-10">
<div className="mb-8 mt-6 space-y-6">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</div>
</SkeletonContainer>
);
};

View File

@@ -0,0 +1,89 @@
import { zodResolver } from "@hookform/resolvers/zod";
import type { Dispatch, SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType } from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
Form,
showToast,
TextField,
} from "@calcom/ui";
type LocationTypeSetLinkDialogFormProps = {
link?: string;
type: EventLocationType["type"];
};
export function AppSetDefaultLinkDialog({
locationType,
setLocationType,
onSuccess,
}: {
locationType: EventLocationType & { slug: string };
setLocationType: Dispatch<SetStateAction<(EventLocationType & { slug: string }) | undefined>>;
onSuccess: () => void;
}) {
const { t } = useLocale();
const eventLocationTypeOptions = getEventLocationType(locationType.type);
const form = useForm<LocationTypeSetLinkDialogFormProps>({
resolver: zodResolver(
z.object({ link: z.string().regex(new RegExp(eventLocationTypeOptions?.urlRegExp ?? "")) })
),
});
const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
onSuccess: () => {
onSuccess();
},
onError: () => {
showToast(`Invalid App Link Format`, "error");
},
});
return (
<Dialog open={!!locationType} onOpenChange={() => setLocationType(undefined)}>
<DialogContent
title={t("default_app_link_title")}
description={t("default_app_link_description")}
type="creation"
Icon="circle-alert">
<Form
form={form}
handleSubmit={(values) => {
updateDefaultAppMutation.mutate({
appSlug: locationType.slug,
appLink: values.link,
});
setLocationType(undefined);
}}>
<>
<TextField
type="text"
required
{...form.register("link")}
placeholder={locationType.organizerInputPlaceholder ?? ""}
label={locationType.label ?? ""}
/>
<DialogFooter showDivider className="mt-8">
<DialogClose />
<Button color="primary" type="submit">
{t("save")}
</Button>
</DialogFooter>
</>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,69 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { ButtonProps } from "@calcom/ui";
import { Button, ConfirmationDialogContent, Dialog, DialogTrigger, showToast } from "@calcom/ui";
export default function DisconnectIntegration({
credentialId,
label,
trashIcon,
isGlobal,
onSuccess,
buttonProps,
}: {
credentialId: number;
label?: string;
trashIcon?: boolean;
isGlobal?: boolean;
onSuccess?: () => void;
buttonProps?: ButtonProps;
}) {
const { t } = useLocale();
const [modalOpen, setModalOpen] = useState(false);
const utils = trpc.useUtils();
const mutation = trpc.viewer.deleteCredential.useMutation({
onSuccess: () => {
showToast(t("app_removed_successfully"), "success");
setModalOpen(false);
onSuccess && onSuccess();
},
onError: () => {
showToast(t("error_removing_app"), "error");
setModalOpen(false);
},
async onSettled() {
await utils.viewer.connectedCalendars.invalidate();
await utils.viewer.integrations.invalidate();
},
});
return (
<>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogTrigger asChild>
<Button
color={buttonProps?.color || "destructive"}
StartIcon={!trashIcon ? undefined : "trash"}
size="base"
variant={trashIcon && !label ? "icon" : "button"}
disabled={isGlobal}
{...buttonProps}>
{label && label}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("remove_app")}
confirmBtnText={t("yes_remove_app")}
onConfirm={() => {
mutation.mutate({ id: credentialId });
}}>
<p className="mt-5">{t("are_you_sure_you_want_to_remove_this_app")}</p>
</ConfirmationDialogContent>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,49 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Dialog, showToast, ConfirmationDialogContent } from "@calcom/ui";
interface DisconnectIntegrationModalProps {
credentialId: number | null;
isOpen: boolean;
handleModelClose: () => void;
teamId?: number;
}
export default function DisconnectIntegrationModal({
credentialId,
isOpen,
handleModelClose,
teamId,
}: DisconnectIntegrationModalProps) {
const { t } = useLocale();
const utils = trpc.useUtils();
const mutation = trpc.viewer.deleteCredential.useMutation({
onSuccess: () => {
showToast(t("app_removed_successfully"), "success");
handleModelClose();
utils.viewer.integrations.invalidate();
utils.viewer.connectedCalendars.invalidate();
},
onError: () => {
showToast(t("error_removing_app"), "error");
handleModelClose();
},
});
return (
<Dialog open={isOpen} onOpenChange={handleModelClose}>
<ConfirmationDialogContent
variety="danger"
title={t("remove_app")}
confirmBtnText={t("yes_remove_app")}
onConfirm={() => {
if (credentialId) {
mutation.mutate({ id: credentialId, teamId });
}
}}>
<p className="mt-5">{t("are_you_sure_you_want_to_remove_this_app")}</p>
</ConfirmationDialogContent>
</Dialog>
);
}