refactor: Setting redesign (#11124)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
1 parent
85a1713897
commit
685be1663e
33 files changed
+1184
-782
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { MoreHorizontal, Edit2, Trash } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -34,22 +35,31 @@ const ApiKeyListItem = ({
|
||||
const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation({
|
||||
async onSuccess() {
|
||||
await utils.viewer.apiKeys.list.invalidate();
|
||||
showToast(t("api_key_deleted"), "success");
|
||||
},
|
||||
onError(err) {
|
||||
console.log(err);
|
||||
showToast(t("something_went_wrong"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={apiKey.id}
|
||||
className={classNames("flex w-full justify-between p-4", lastItem ? "" : "border-subtle border-b")}>
|
||||
className={classNames(
|
||||
"flex w-full justify-between px-4 py-4 sm:px-6",
|
||||
lastItem ? "" : "border-subtle border-b"
|
||||
)}>
|
||||
<div>
|
||||
<p className="font-medium"> {apiKey?.note ? apiKey.note : t("api_key_no_note")}</p>
|
||||
<div className="flex items-center space-x-3.5">
|
||||
<div className="flex gap-1">
|
||||
<p className="text-sm font-semibold"> {apiKey?.note ? apiKey.note : t("api_key_no_note")}</p>
|
||||
{!neverExpires && isExpired && <Badge variant="red">{t("expired")}</Badge>}
|
||||
{!isExpired && <Badge variant="green">{t("active")}</Badge>}
|
||||
<p className="text-default text-xs">
|
||||
{" "}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center space-x-3.5">
|
||||
<p className="text-default text-sm">
|
||||
{neverExpires ? (
|
||||
<div className="text-subtle flex flex-row space-x-3">{t("api_key_never_expires")}</div>
|
||||
<div className="flex flex-row space-x-3">{t("api_key_never_expires")}</div>
|
||||
) : (
|
||||
`${isExpired ? t("expired") : t("expires")} ${dayjs(apiKey?.expiresAt?.toString()).fromNow()}`
|
||||
)}
|
||||
@@ -71,6 +81,8 @@ const ApiKeyListItem = ({
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="destructive"
|
||||
disabled={deleteApiKey.isLoading}
|
||||
onClick={() =>
|
||||
deleteApiKey.mutate({
|
||||
id: apiKey.id,
|
||||
|
||||
@@ -145,7 +145,7 @@ const CreateConnectionDialog = ({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter showDivider className="mt-10">
|
||||
<DialogFooter showDivider className="relative mt-10">
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
|
||||
@@ -6,7 +6,20 @@ import OIDCConnection from "@calcom/features/ee/sso/components/OIDCConnection";
|
||||
import SAMLConnection from "@calcom/features/ee/sso/components/SAMLConnection";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { AppSkeletonLoader as SkeletonLoader, Meta, Alert } from "@calcom/ui";
|
||||
import { Meta, Alert, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={true} />
|
||||
<div className="divide-subtle border-subtle space-y-6 rounded-b-xl border border-t-0 px-6 py-4">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default function SSOConfiguration({ teamId }: { teamId: number | null }) {
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
@@ -22,14 +35,18 @@ export default function SSOConfiguration({ teamId }: { teamId: number | null })
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <SkeletonLoader />;
|
||||
return <SkeletonLoader title={t("sso_configuration")} description={t("sso_configuration_description")} />;
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("sso_configuration")} description={t("saml_description")} />
|
||||
<Alert severity="warning" message={t(errorMessage)} className="mb-4 " />
|
||||
<Meta
|
||||
title={t("sso_configuration")}
|
||||
description={t("sso_configuration_description")}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<Alert severity="warning" message={t(errorMessage)} className="mt-4" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +55,7 @@ export default function SSOConfiguration({ teamId }: { teamId: number | null })
|
||||
if (!connection) {
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<div className="flex flex-col space-y-10">
|
||||
<div className="[&>*]:border-subtle flex flex-col [&>*:last-child]:rounded-b-xl [&>*]:border [&>*]:border-t-0 [&>*]:px-4 [&>*]:py-6 [&>*]:sm:px-6">
|
||||
<SAMLConnection teamId={teamId} connection={null} />
|
||||
<OIDCConnection teamId={teamId} connection={null} />
|
||||
</div>
|
||||
@@ -48,7 +65,7 @@ export default function SSOConfiguration({ teamId }: { teamId: number | null })
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<div className="[&>*]:border-subtle flex flex-col [&>*:last-child]:rounded-b-xl [&>*]:border [&>*]:border-t-0 [&>*]:px-4 [&>*]:py-6 [&>*]:sm:px-6">
|
||||
{connection.type === "saml" ? (
|
||||
<SAMLConnection teamId={teamId} connection={connection} />
|
||||
) : (
|
||||
|
||||
@@ -19,8 +19,12 @@ const SAMLSSO = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-default w-full sm:mx-0 xl:mt-0">
|
||||
<Meta title={t("sso_configuration")} description={t("sso_configuration_description")} />
|
||||
<div className="bg-default w-full sm:mx-0">
|
||||
<Meta
|
||||
title={t("sso_configuration")}
|
||||
description={t("sso_configuration_description")}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<SSOConfiguration teamId={null} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,8 @@ import { bookerLayoutOptions, type BookerLayoutSettings } from "@calcom/prisma/z
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Label, CheckboxField, Button } from "@calcom/ui";
|
||||
|
||||
import SectionBottomActions from "./SectionBottomActions";
|
||||
|
||||
type BookerLayoutSelectorProps = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
@@ -30,6 +32,8 @@ type BookerLayoutSelectorProps = {
|
||||
* to this boolean.
|
||||
*/
|
||||
isDark?: boolean;
|
||||
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const defaultFieldName = "metadata.bookerLayouts";
|
||||
@@ -40,6 +44,7 @@ export const BookerLayoutSelector = ({
|
||||
name,
|
||||
fallbackToUserSettings,
|
||||
isDark,
|
||||
isDisabled = false,
|
||||
}: BookerLayoutSelectorProps) => {
|
||||
const { control, getValues } = useFormContext();
|
||||
const { t } = useLocale();
|
||||
@@ -51,10 +56,12 @@ export const BookerLayoutSelector = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label className="mb-0">{title ? title : t("layout")}</Label>
|
||||
<p className="text-subtle max-w-full break-words py-1 text-sm">
|
||||
{description ? description : t("bookerlayout_description")}
|
||||
</p>
|
||||
<div className="border-subtle rounded-t-xl border p-6">
|
||||
<Label className="mb-0 text-base font-semibold">{title ? title : t("layout")}</Label>
|
||||
<p className="text-subtle max-w-full break-words py-1 text-sm">
|
||||
{description ? description : t("bookerlayout_description")}
|
||||
</p>
|
||||
</div>
|
||||
<Controller
|
||||
// If the event does not have any settings, we don't want to register this field in the form.
|
||||
// That way the settings won't get saved into the event on save, but remain null. Thus keep using
|
||||
@@ -62,12 +69,19 @@ export const BookerLayoutSelector = ({
|
||||
control={shouldShowUserSettings ? undefined : control}
|
||||
name={name || defaultFieldName}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<BookerLayoutFields
|
||||
showUserSettings={shouldShowUserSettings}
|
||||
settings={value}
|
||||
onChange={onChange}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<>
|
||||
<BookerLayoutFields
|
||||
showUserSettings={shouldShowUserSettings}
|
||||
settings={value}
|
||||
onChange={onChange}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<SectionBottomActions align="end">
|
||||
<Button type="submit" disabled={isDisabled} color="primary">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
@@ -141,7 +155,7 @@ const BookerLayoutFields = ({ settings, onChange, showUserSettings, isDark }: Bo
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-4 space-y-5">
|
||||
<div className="border-subtle space-y-5 border-x px-6 py-8">
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-col gap-5 transition-opacity sm:flex-row sm:gap-3",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
const SectionBottomActions = ({
|
||||
align = "start",
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
align?: "start" | "end";
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle bg-muted flex rounded-b-xl border px-6 py-4",
|
||||
align === "end" && "justify-end",
|
||||
className
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionBottomActions;
|
||||
@@ -633,7 +633,7 @@ export default function SettingsLayout({
|
||||
<MobileSettingsContainer onSideContainerOpen={() => setSideContainerOpen(!sideContainerOpen)} />
|
||||
}>
|
||||
<div className="flex flex-1 [&>*]:flex-1">
|
||||
<div className="mx-auto max-w-full justify-center md:max-w-4xl">
|
||||
<div className="mx-auto max-w-full justify-center lg:max-w-4xl">
|
||||
<ShellHeader />
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||
@@ -676,33 +676,40 @@ type SidebarContainerElementProps = {
|
||||
|
||||
export const getLayout = (page: React.ReactElement) => <SettingsLayout>{page}</SettingsLayout>;
|
||||
|
||||
function ShellHeader() {
|
||||
export function ShellHeader() {
|
||||
const { meta } = useMeta();
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
return (
|
||||
<header className="mx-auto block justify-between pt-8 sm:flex">
|
||||
<div className="border-subtle mb-8 flex w-full items-center border-b pb-6">
|
||||
{meta.backButton && (
|
||||
<a href="javascript:history.back()">
|
||||
<ArrowLeft className="mr-7" />
|
||||
</a>
|
||||
)}
|
||||
<div>
|
||||
{meta.title && isLocaleReady ? (
|
||||
<h1 className="font-cal text-emphasis mb-1 text-xl font-bold leading-5 tracking-wide">
|
||||
{t(meta.title)}
|
||||
</h1>
|
||||
) : (
|
||||
<div className="bg-emphasis mb-1 h-5 w-24 animate-pulse rounded-md" />
|
||||
)}
|
||||
{meta.description && isLocaleReady ? (
|
||||
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">{t(meta.description)}</p>
|
||||
) : (
|
||||
<div className="bg-emphasis h-5 w-32 animate-pulse rounded-md" />
|
||||
<>
|
||||
<header
|
||||
className={classNames(
|
||||
"border-subtle mx-auto block justify-between sm:flex",
|
||||
meta.borderInShellHeader && "rounded-t-xl border px-4 py-6 sm:px-6",
|
||||
meta.borderInShellHeader === undefined && "mb-8 border-b pb-8"
|
||||
)}>
|
||||
<div className="flex w-full items-center">
|
||||
{meta.backButton && (
|
||||
<a href="javascript:history.back()">
|
||||
<ArrowLeft className="mr-7" />
|
||||
</a>
|
||||
)}
|
||||
<div>
|
||||
{meta.title && isLocaleReady ? (
|
||||
<h1 className="font-cal text-emphasis mb-1 text-xl font-bold leading-5 tracking-wide">
|
||||
{t(meta.title)}
|
||||
</h1>
|
||||
) : (
|
||||
<div className="bg-emphasis mb-1 h-5 w-24 animate-pulse rounded-md" />
|
||||
)}
|
||||
{meta.description && isLocaleReady ? (
|
||||
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">{t(meta.description)}</p>
|
||||
) : (
|
||||
<div className="bg-emphasis h-5 w-32 animate-pulse rounded-md" />
|
||||
)}
|
||||
</div>
|
||||
<div className="ms-auto flex-shrink-0">{meta.CTA}</div>
|
||||
</div>
|
||||
<div className="ms-auto flex-shrink-0">{meta.CTA}</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1008,7 +1008,7 @@ function MainContainer({
|
||||
<main className="bg-default relative z-0 flex-1 focus:outline-none">
|
||||
{/* show top navigation for md and smaller (tablet and phones) */}
|
||||
{TopNavContainerProp}
|
||||
<div className="max-w-full px-4 py-4 md:py-8 lg:px-12">
|
||||
<div className="max-w-full px-2 py-4 md:py-12 lg:px-6">
|
||||
<ErrorBoundary>
|
||||
{!props.withoutMain ? <ShellMain {...props}>{props.children}</ShellMain> : props.children}
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -5,18 +5,9 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Label,
|
||||
Select,
|
||||
Switch,
|
||||
TextArea,
|
||||
TextField,
|
||||
ToggleGroup,
|
||||
DialogFooter,
|
||||
} from "@calcom/ui";
|
||||
import { Button, Form, Label, Select, Switch, TextArea, TextField, ToggleGroup } from "@calcom/ui";
|
||||
|
||||
import SectionBottomActions from "../../settings/SectionBottomActions";
|
||||
import customTemplate, { hasTemplateIntegration } from "../lib/integrationTemplate";
|
||||
import WebhookTestDisclosure from "./WebhookTestDisclosure";
|
||||
|
||||
@@ -87,7 +78,7 @@ const WebhookForm = (props: {
|
||||
|
||||
const [useCustomTemplate, setUseCustomTemplate] = useState(false);
|
||||
const [newSecret, setNewSecret] = useState("");
|
||||
const [changeSecret, setChangeSecret] = useState(false);
|
||||
const [changeSecret, setChangeSecret] = useState<boolean>(false);
|
||||
const hasSecretKey = !!props?.webhook?.secret;
|
||||
// const currentSecret = props?.webhook?.secret;
|
||||
|
||||
@@ -98,10 +89,10 @@ const WebhookForm = (props: {
|
||||
}, [changeSecret, formMethods]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => props.onSubmit({ ...values, changeSecret, newSecret })}>
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => props.onSubmit({ ...values, changeSecret, newSecret })}>
|
||||
<div className="border-subtle border-x p-6">
|
||||
<Controller
|
||||
name="subscriberUrl"
|
||||
control={formMethods.control}
|
||||
@@ -115,10 +106,12 @@ const WebhookForm = (props: {
|
||||
required
|
||||
type="url"
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("subscriberUrl", e?.target.value);
|
||||
formMethods.setValue("subscriberUrl", e?.target.value, { shouldDirty: true });
|
||||
if (hasTemplateIntegration({ url: e.target.value })) {
|
||||
setUseCustomTemplate(true);
|
||||
formMethods.setValue("payloadTemplate", customTemplate({ url: e.target.value }));
|
||||
formMethods.setValue("payloadTemplate", customTemplate({ url: e.target.value }), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -129,13 +122,13 @@ const WebhookForm = (props: {
|
||||
name="active"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<div className="font-sm text-emphasis mt-8 font-medium">
|
||||
<div className="font-sm text-emphasis mt-6 font-medium">
|
||||
<Switch
|
||||
label={t("enable_webhook")}
|
||||
checked={value}
|
||||
// defaultChecked={props?.webhook?.active ? props?.webhook?.active : true}
|
||||
onCheckedChange={(value) => {
|
||||
formMethods.setValue("active", value);
|
||||
formMethods.setValue("active", value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -147,8 +140,8 @@ const WebhookForm = (props: {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const selectValue = translatedTriggerOptions.filter((option) => value.includes(option.value));
|
||||
return (
|
||||
<div className="mt-8">
|
||||
<Label className="font-sm text-emphasis mt-8 font-medium">
|
||||
<div className="mt-6">
|
||||
<Label className="font-sm text-emphasis font-medium">
|
||||
<>{t("event_triggers")}</>
|
||||
</Label>
|
||||
<Select
|
||||
@@ -167,7 +160,7 @@ const WebhookForm = (props: {
|
||||
name="secret"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<div className="mt-8 ">
|
||||
<div className="mt-6">
|
||||
{!!hasSecretKey && !changeSecret && (
|
||||
<>
|
||||
<Label className="font-sm text-emphasis font-medium">Secret</Label>
|
||||
@@ -218,7 +211,7 @@ const WebhookForm = (props: {
|
||||
labelClassName="font-medium text-emphasis font-sm"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("secret", e?.target.value);
|
||||
formMethods.setValue("secret", e?.target.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -231,7 +224,7 @@ const WebhookForm = (props: {
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="font-sm text-emphasis mt-8">
|
||||
<Label className="font-sm text-emphasis mt-6">
|
||||
<>{t("payload_template")}</>
|
||||
</Label>
|
||||
<div className="mb-2">
|
||||
@@ -239,7 +232,7 @@ const WebhookForm = (props: {
|
||||
onValueChange={(val) => {
|
||||
if (val === "default") {
|
||||
setUseCustomTemplate(false);
|
||||
formMethods.setValue("payloadTemplate", undefined);
|
||||
formMethods.setValue("payloadTemplate", undefined, { shouldDirty: true });
|
||||
} else {
|
||||
setUseCustomTemplate(true);
|
||||
}
|
||||
@@ -258,33 +251,34 @@ const WebhookForm = (props: {
|
||||
rows={3}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("payloadTemplate", e?.target.value);
|
||||
formMethods.setValue("payloadTemplate", e?.target.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<div className="bg-subtle mt-8 rounded-md p-6">
|
||||
<WebhookTestDisclosure />
|
||||
</div>
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
onClick={props.onCancel}
|
||||
{...(!props.onCancel ? { href: `${WEBAPP_URL}/settings/developer/webhooks` } : {})}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!formMethods.formState.isDirty && !changeSecret}
|
||||
loading={formMethods.formState.isSubmitting || formMethods.formState.isSubmitted}>
|
||||
{props?.webhook?.id ? t("save") : t("create_webhook")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
|
||||
<DialogFooter showDivider>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
onClick={props.onCancel}
|
||||
{...(!props.onCancel ? { href: `${WEBAPP_URL}/settings/developer/webhooks` } : {})}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={formMethods.formState.isSubmitting || formMethods.formState.isSubmitted}>
|
||||
{props?.webhook?.id ? t("save") : t("create_webhook")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</>
|
||||
<div className="mt-6 rounded-md">
|
||||
<WebhookTestDisclosure />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ export default function WebhookTestDisclosure() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<div className="border-subtle flex justify-between rounded-t-xl border p-6">
|
||||
<div>
|
||||
<p className="text-emphasis text-sm font-semibold leading-5">{t("webhook_test")}</p>
|
||||
<p className="text-default mb-4 text-sm">{t("test_webhook")}</p>
|
||||
<p className="text-default text-sm">{t("test_webhook")}</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -31,8 +31,8 @@ export default function WebhookTestDisclosure() {
|
||||
{t("ping_test")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="border-subtle space-y-0 rounded-md border sm:mx-0">
|
||||
<div className="flex justify-between border-b p-4">
|
||||
<div className="border-subtle space-y-0 rounded-b-xl border border-t-0 px-6 py-8 sm:mx-0">
|
||||
<div className="border-subtle flex justify-between rounded-t-xl border p-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<h3 className="text-emphasis self-center text-sm font-semibold leading-4">
|
||||
{t("webhook_response")}
|
||||
@@ -44,10 +44,10 @@ export default function WebhookTestDisclosure() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-inverted bg-inverted rounded-b-md p-4 font-mono text-[13px] leading-4">
|
||||
<div className="bg-muted border-subtle rounded-b-xl border border-t-0 p-4 font-mono text-[13px] leading-4 ">
|
||||
{!mutation.data && <p>{t("no_data_yet")}</p>}
|
||||
{mutation.status === "success" && (
|
||||
<div className="text-inverted overflow-x-auto">{JSON.stringify(mutation.data, null, 4)}</div>
|
||||
<div className="overflow-x-auto">{JSON.stringify(mutation.data, null, 4)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@ function Component({ webhookId }: { webhookId: string }) {
|
||||
<Meta
|
||||
title={t("edit_webhook")}
|
||||
description={t("add_webhook_description", { appName: APP_NAME })}
|
||||
backButton
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<WebhookForm
|
||||
noRoutingFormTriggers={false}
|
||||
@@ -80,7 +80,7 @@ function Component({ webhookId }: { webhookId: string }) {
|
||||
}
|
||||
|
||||
if (values.changeSecret) {
|
||||
values.secret = values.newSecret.length ? values.newSecret : null;
|
||||
values.secret = values.newSecret.trim().length ? values.newSecret : null;
|
||||
}
|
||||
|
||||
if (!values.payloadTemplate) {
|
||||
|
||||
@@ -4,13 +4,24 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Meta, showToast, SkeletonContainer } from "@calcom/ui";
|
||||
import { Meta, showToast, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import { getLayout } from "../../settings/layouts/SettingsLayout";
|
||||
import type { WebhookFormSubmitData } from "../components/WebhookForm";
|
||||
import WebhookForm from "../components/WebhookForm";
|
||||
import { subscriberUrlReserved } from "../lib/subscriberUrlReserved";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={true} />
|
||||
<div className="divide-subtle border-subtle space-y-6 rounded-b-xl border border-t-0 px-6 py-4">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
const NewWebhookView = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useLocale();
|
||||
@@ -71,7 +82,13 @@ const NewWebhookView = () => {
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) return <SkeletonContainer />;
|
||||
if (isLoading)
|
||||
return (
|
||||
<SkeletonLoader
|
||||
title={t("add_webhook")}
|
||||
description={t("add_webhook_description", { appName: APP_NAME })}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -79,6 +96,7 @@ const NewWebhookView = () => {
|
||||
title={t("add_webhook")}
|
||||
description={t("add_webhook_description", { appName: APP_NAME })}
|
||||
backButton
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<WebhookForm
|
||||
noRoutingFormTriggers={false}
|
||||
|
||||
@@ -1,36 +1,73 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { WebhooksByViewer } from "@calcom/trpc/server/routers/viewer/webhook/getByViewer.handler";
|
||||
import { Avatar, CreateButtonWithTeamsList, EmptyScreen, Meta } from "@calcom/ui";
|
||||
import {
|
||||
Avatar,
|
||||
CreateButtonWithTeamsList,
|
||||
EmptyScreen,
|
||||
Meta,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
} from "@calcom/ui";
|
||||
import { Link as LinkIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
import { getLayout } from "../../settings/layouts/SettingsLayout";
|
||||
import { WebhookListItem, WebhookListSkeleton } from "../components";
|
||||
import { WebhookListItem } from "../components";
|
||||
|
||||
const SkeletonLoader = ({
|
||||
title,
|
||||
description,
|
||||
borderInShellHeader,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
borderInShellHeader: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={borderInShellHeader} />
|
||||
<div className="divide-subtle border-subtle space-y-6 rounded-b-xl border border-t-0 px-6 py-4">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const WebhooksView = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
|
||||
const { data } = trpc.viewer.webhook.getByViewer.useQuery(undefined, {
|
||||
suspense: true,
|
||||
const { data, isLoading } = trpc.viewer.webhook.getByViewer.useQuery(undefined, {
|
||||
enabled: session.status === "authenticated",
|
||||
});
|
||||
|
||||
if (isLoading || !data) {
|
||||
return (
|
||||
<SkeletonLoader
|
||||
title={t("webhooks")}
|
||||
description={t("add_webhook_description", { appName: APP_NAME })}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title="Webhooks"
|
||||
title={t("webhooks")}
|
||||
description={t("add_webhook_description", { appName: APP_NAME })}
|
||||
CTA={
|
||||
data && data.webhookGroups.length > 0 ? (
|
||||
<CreateButtonWithTeamsList
|
||||
color="secondary"
|
||||
subtitle={t("create_for").toUpperCase()}
|
||||
createFunction={(teamId?: number) => {
|
||||
router.push(`webhooks/new${teamId ? `?teamId=${teamId}` : ""}`);
|
||||
@@ -41,11 +78,10 @@ const WebhooksView = () => {
|
||||
<></>
|
||||
)
|
||||
}
|
||||
borderInShellHeader={!(data && data.webhookGroups.length > 0)}
|
||||
/>
|
||||
<div>
|
||||
<Suspense fallback={<WebhookListSkeleton />}>
|
||||
{data && <WebhooksList webhooksByViewer={data} />}
|
||||
</Suspense>
|
||||
<WebhooksList webhooksByViewer={data} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -65,11 +101,11 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
|
||||
{webhookGroups && (
|
||||
<>
|
||||
{!!webhookGroups.length && (
|
||||
<>
|
||||
<div className={classNames("mt-0", hasTeams && "mt-6")}>
|
||||
{webhookGroups.map((group) => (
|
||||
<div key={group.teamId}>
|
||||
{hasTeams && (
|
||||
<div className="items-centers flex ">
|
||||
<div className="items-centers flex">
|
||||
<Avatar
|
||||
alt={group.profile.image || ""}
|
||||
imageSrc={group.profile.image || `${bookerUrl}/${group.profile.name}/avatar.png`}
|
||||
@@ -82,7 +118,11 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col" key={group.profile.slug}>
|
||||
<div className="border-subtle mb-8 mt-3 rounded-md border">
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle rounded-md rounded-t-none border border-t-0",
|
||||
hasTeams && "mb-8 mt-3 rounded-t-md border-t"
|
||||
)}>
|
||||
{group.webhooks.map((webhook, index) => (
|
||||
<WebhookListItem
|
||||
key={webhook.id}
|
||||
@@ -98,13 +138,14 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{!webhookGroups.length && (
|
||||
<EmptyScreen
|
||||
Icon={LinkIcon}
|
||||
headline={t("create_your_first_webhook")}
|
||||
description={t("create_your_first_webhook_description", { appName: APP_NAME })}
|
||||
className="rounded-b-md rounded-t-none border-t-0"
|
||||
buttonRaw={
|
||||
<CreateButtonWithTeamsList
|
||||
subtitle={t("create_for").toUpperCase()}
|
||||
|
||||
Reference in new issue
Block a user