Files
calendar/packages/features/apps/AdminAppsList.tsx
T
GitStart-Cal.comandGitHub e731c33235 feat: Add divider and set spacing on modals with the type=creation prop (feat-add-divider)
## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

- Add the prop customDividerClassName to customize the CSS Divider if needed
- Add a divider on all modals with type="creation" prop, following the [figma](https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/1.-Cal-DS?type=design&node-id=29898%3A100590&t=62LvZCZAEZm5zDdw-1) design
- Some type="creation" modals seem not to be this type, we pushed all changes with screenshots, if you want us to revert the changes on some modals we will revert

![image](https://github.com/calcom/cal.com/assets/121884634/6674016e-819d-49da-9d44-14eefb4160ee)


- Inline Embed: 
![image](https://github.com/calcom/cal.com/assets/121884634/191c94ea-257f-44ba-8c7a-3815550e36f1)

- Enable Two-Factor Authentication:
![image](https://github.com/calcom/cal.com/assets/121884634/3b9246be-52ab-4c9d-af12-eda82541e837)

-Enable Two-Factor Authentication (step 2):
![image](https://github.com/calcom/cal.com/assets/121884634/a3b7934b-fd66-441d-b9d7-c5ba2fc7fa91)

-Enable Two-Factor Authentication (step 3):
![image](https://github.com/calcom/cal.com/assets/121884634/77367ae7-0967-497a-858a-f18a91b62077)

- Disable Two-Factor Authentication:
![image](https://github.com/calcom/cal.com/assets/121884634/8fc31cc2-e3ca-4c74-8ac6-c4832f9116d9)

- Delete Account:
![image](https://github.com/calcom/cal.com/assets/121884634/5146f605-523e-4c49-aeeb-7c5dd533844d)

- Update Timezone:
![image](https://github.com/calcom/cal.com/assets/121884634/f5acb9f0-2550-4d48-8c02-5cf1b94aaf33)

- Duplicate Event Type:
![image](https://github.com/calcom/cal.com/assets/121884634/078d40a5-5e35-4d4a-8d0e-77d7b8c21d6d)

- Add a New Form:
![image](https://github.com/calcom/cal.com/assets/121884634/5b6352e0-a1c1-46e3-af18-dc84e83fdecd)

- Change Team Member Role:
![image](https://github.com/calcom/cal.com/assets/121884634/dc8b8ba1-7986-4980-ab78-6027aca350ca)

- Impersonate:
![image](https://github.com/calcom/cal.com/assets/121884634/ffd28b83-0c9b-47a1-8894-29d35add9c27)

- Bulk Update Event Types:
![image](https://github.com/calcom/cal.com/assets/121884634/ba864dcc-0c73-426e-bc49-3ac81a55d3ea)

- Connecting with MS Teams:
![image](https://github.com/calcom/cal.com/assets/121884634/d6666cbf-cc36-4fe5-90bb-9d69de26248d)

- Set a Default App Link:
![image](https://github.com/calcom/cal.com/assets/121884634/f4f7093c-839c-4ae1-a4f3-ecd266238115)

- OIDC configuration:
![image](https://github.com/calcom/cal.com/assets/121884634/a689acd7-2488-423e-a57d-5610d683d11d)

- SAML Configuration:
![image](https://github.com/calcom/cal.com/assets/121884634/61480037-1733-4ba9-8dfd-429819e7eac9)

- How to use booking questions as variables?
![image](https://github.com/calcom/cal.com/assets/121884634/2982ace4-41b0-4849-878b-b2ceabf59469)
![image](https://github.com/calcom/cal.com/assets/121884634/65888a13-d9a7-4df5-a8b7-56becc61882e)

- Invite Link Settings:
![image](https://github.com/calcom/cal.com/assets/121884634/3f81fcd1-f862-4106-a79c-70808090f055)

- Add Action:
![image](https://github.com/calcom/cal.com/assets/121884634/ec5f9490-dccc-4c8e-bc8e-b48f8a29144b)

- Edit Keys:
![245192220-c669e6fe-01ba-4a66-9b03-96ab4e8f1809](https://github.com/calcom/cal.com/assets/121884634/bfff69b3-831a-4a5c-9835-0569109b8386)

- Create an Api Key:
![image](https://github.com/calcom/cal.com/assets/121884634/fbdf7494-6da4-49da-ba41-5f9a6393bb72)

- API key created successfully:
![image](https://github.com/calcom/cal.com/assets/121884634/81e6a5c2-5d9d-40ad-a78e-ed1ab2128a26)

- Invite Team Member:
![image](https://github.com/calcom/cal.com/assets/121884634/9e4ceea9-f708-4958-a6d7-e00304beacbf)

- Add a Question:
![image](https://github.com/calcom/cal.com/assets/121884634/8329c593-8203-484e-a4dd-c9bd5210d8db)

- Add a new event type:
![image](https://github.com/calcom/cal.com/assets/121884634/19a72734-aa79-4828-81bb-f534d46a5fc5)
2023-06-29 08:47:16 +01:00

347 lines
9.3 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import { noop } from "lodash";
import { useRouter } from "next/router";
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 { 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,
List,
showToast,
SkeletonButton,
SkeletonContainer,
SkeletonText,
TextField,
Switch,
} from "@calcom/ui";
import { AlertCircle, Edit } from "@calcom/ui/components/icon";
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.useContext();
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()}>
<Edit />
</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.useContext();
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 { t } = useLocale();
const router = useRouter();
const { category } = querySchema.parse(router.query);
const { data: apps, isLoading } = trpc.viewer.appsRouter.listLocal.useQuery(
{ category },
{ enabled: router.isReady }
);
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 (isLoading) return <SkeletonLoader />;
if (!apps || apps.length === 0) {
return (
<EmptyScreen
Icon={AlertCircle}
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>
);
};