Revert "refactor: Setting redesign (#11124)"
This reverts commit 4f3cf4f948
.
bugfix/reinstate-settings-redesign
parent
c22d406d12
commit
85a1713897
|
@ -79,8 +79,8 @@ export default function AppListCard(props: AppListCardProps) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classNames(highlight && "dark:bg-muted bg-yellow-100")}>
|
||||
<div className="flex items-center gap-x-3 px-4 py-4 sm:px-6">
|
||||
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
|
||||
<div className="flex items-center gap-x-3 px-5 py-4">
|
||||
{logo ? (
|
||||
<img
|
||||
className={classNames(logo.includes("-dark") && "dark:invert", "h-10 w-10")}
|
||||
|
|
|
@ -29,10 +29,9 @@ interface AppListProps {
|
|||
variant?: AppCategories;
|
||||
data: RouterOutputs["viewer"]["integrations"];
|
||||
handleDisconnect: (credentialId: number) => void;
|
||||
listClassName?: string;
|
||||
}
|
||||
|
||||
export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppListProps) => {
|
||||
export const AppList = ({ data, handleDisconnect, variant }: AppListProps) => {
|
||||
const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
|
||||
const utils = trpc.useContext();
|
||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||
|
@ -156,7 +155,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
|||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<List className={listClassName}>
|
||||
<List>
|
||||
{cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))}
|
||||
{data.items
|
||||
.filter((item) => item.invalidCredentialIds)
|
||||
|
|
|
@ -22,11 +22,12 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
|
|||
<>
|
||||
<section className={classNames("text-default flex flex-col sm:flex-row", className)}>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">{title}</h2>
|
||||
<h2 className="font-medium">{title}</h2>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 pt-3 sm:ml-auto sm:pl-3 sm:pt-0">{children}</div>
|
||||
</section>
|
||||
<hr className="border-subtle" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -44,16 +45,14 @@ const BillingView = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("billing")} description={t("manage_billing_description")} borderInShellHeader={true} />
|
||||
<div className="border-subtle space-y-6 rounded-b-xl border border-t-0 px-6 py-8 text-sm sm:space-y-8">
|
||||
<Meta title={t("billing")} description={t("manage_billing_description")} />
|
||||
<div className="space-y-6 text-sm sm:space-y-8">
|
||||
<CtaRow title={t("view_and_manage_billing_details")} description={t("view_and_edit_billing_details")}>
|
||||
<Button color="primary" href={billingHref} target="_blank" EndIcon={ExternalLink}>
|
||||
{t("billing_portal")}
|
||||
</Button>
|
||||
</CtaRow>
|
||||
|
||||
<hr className="border-subtle" />
|
||||
|
||||
<CtaRow title={t("need_anything_else")} description={t("further_billing_help")}>
|
||||
<Button color="secondary" onClick={onContactSupportClick}>
|
||||
{t("contact_support")}
|
||||
|
|
|
@ -14,25 +14,12 @@ import {
|
|||
DialogContent,
|
||||
EmptyScreen,
|
||||
Meta,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
} from "@calcom/ui";
|
||||
import { Link as LinkIcon, Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
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 ApiKeysView = () => {
|
||||
const { t } = useLocale();
|
||||
|
||||
|
@ -52,57 +39,49 @@ const ApiKeysView = () => {
|
|||
setApiKeyToEdit(undefined);
|
||||
setApiKeyModal(true);
|
||||
}}>
|
||||
{t("add")}
|
||||
{t("new_api_key")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading || !data) {
|
||||
return (
|
||||
<SkeletonLoader
|
||||
title={t("api_keys")}
|
||||
description={t("create_first_api_key_description", { appName: APP_NAME })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={t("api_keys")}
|
||||
description={t("create_first_api_key_description", { appName: APP_NAME })}
|
||||
CTA={<NewApiKeyButton />}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
|
||||
<LicenseRequired>
|
||||
<div>
|
||||
{data?.length ? (
|
||||
<>
|
||||
<div className="border-subtle rounded-b-md border border-t-0">
|
||||
{data.map((apiKey, index) => (
|
||||
<ApiKeyListItem
|
||||
key={apiKey.id}
|
||||
apiKey={apiKey}
|
||||
lastItem={data.length === index + 1}
|
||||
onEditClick={() => {
|
||||
setApiKeyToEdit(apiKey);
|
||||
setApiKeyModal(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={LinkIcon}
|
||||
headline={t("create_first_api_key")}
|
||||
description={t("create_first_api_key_description", { appName: APP_NAME })}
|
||||
className="rounded-b-md rounded-t-none border-t-0"
|
||||
buttonRaw={<NewApiKeyButton />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
{isLoading && <SkeletonLoader />}
|
||||
<div>
|
||||
{isLoading ? null : data?.length ? (
|
||||
<>
|
||||
<div className="border-subtle mb-8 mt-6 rounded-md border">
|
||||
{data.map((apiKey, index) => (
|
||||
<ApiKeyListItem
|
||||
key={apiKey.id}
|
||||
apiKey={apiKey}
|
||||
lastItem={data.length === index + 1}
|
||||
onEditClick={() => {
|
||||
setApiKeyToEdit(apiKey);
|
||||
setApiKeyModal(true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<NewApiKeyButton />
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={LinkIcon}
|
||||
headline={t("create_first_api_key")}
|
||||
description={t("create_first_api_key_description", { appName: APP_NAME })}
|
||||
buttonRaw={<NewApiKeyButton />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</LicenseRequired>
|
||||
|
||||
<Dialog open={apiKeyModal} onOpenChange={setApiKeyModal}>
|
||||
|
|
|
@ -3,10 +3,8 @@ import { Controller, useForm } from "react-hook-form";
|
|||
import type { z } from "zod";
|
||||
|
||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import ThemeLabel from "@calcom/features/settings/ThemeLabel";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
|
||||
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
|
@ -14,7 +12,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
import type { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
|
@ -25,7 +22,7 @@ import {
|
|||
SkeletonButton,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
SettingsToggle,
|
||||
Switch,
|
||||
UpgradeTeamsBadge,
|
||||
} from "@calcom/ui";
|
||||
|
||||
|
@ -34,9 +31,9 @@ import PageWrapper from "@components/PageWrapper";
|
|||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={false} />
|
||||
<div className="border-subtle mt-6 space-y-6 rounded-t-xl border border-b-0 px-4 py-6 sm:px-6">
|
||||
<div className="flex items-center justify-center">
|
||||
<Meta title={title} description={description} />
|
||||
<div className="mb-8 mt-6 space-y-6">
|
||||
<div className="flex items-center">
|
||||
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
||||
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
||||
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
||||
|
@ -47,83 +44,49 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
|
|||
</div>
|
||||
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
<div className="rounded-b-xl">
|
||||
<SectionBottomActions align="end">
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</SectionBottomActions>
|
||||
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const DEFAULT_LIGHT_BRAND_COLOR = "#292929";
|
||||
const DEFAULT_DARK_BRAND_COLOR = "#fafafa";
|
||||
|
||||
const AppearanceView = ({
|
||||
user,
|
||||
hasPaidPlan,
|
||||
}: {
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
hasPaidPlan: boolean;
|
||||
}) => {
|
||||
const AppearanceView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
const [darkModeError, setDarkModeError] = useState(false);
|
||||
const [lightModeError, setLightModeError] = useState(false);
|
||||
const [isCustomBrandColorChecked, setIsCustomBranColorChecked] = useState(
|
||||
user?.brandColor !== DEFAULT_LIGHT_BRAND_COLOR || user?.darkBrandColor !== DEFAULT_DARK_BRAND_COLOR
|
||||
);
|
||||
const [hideBrandingValue, setHideBrandingValue] = useState(user?.hideBranding ?? false);
|
||||
|
||||
const userThemeFormMethods = useForm({
|
||||
const { isLoading: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
|
||||
|
||||
const formMethods = useForm({
|
||||
defaultValues: {
|
||||
theme: user.theme,
|
||||
theme: user?.theme,
|
||||
brandColor: user?.brandColor || "#292929",
|
||||
darkBrandColor: user?.darkBrandColor || "#fafafa",
|
||||
hideBranding: user?.hideBranding,
|
||||
metadata: user?.metadata as z.infer<typeof userMetadata>,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { isSubmitting: isUserThemeSubmitting, isDirty: isUserThemeDirty },
|
||||
reset: resetUserThemeReset,
|
||||
} = userThemeFormMethods;
|
||||
|
||||
const bookerLayoutFormMethods = useForm({
|
||||
defaultValues: {
|
||||
metadata: user.metadata as z.infer<typeof userMetadata>,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { isSubmitting: isBookerLayoutFormSubmitting, isDirty: isBookerLayoutFormDirty },
|
||||
reset: resetBookerLayoutThemeReset,
|
||||
} = bookerLayoutFormMethods;
|
||||
|
||||
const brandColorsFormMethods = useForm({
|
||||
defaultValues: {
|
||||
brandColor: user.brandColor || DEFAULT_LIGHT_BRAND_COLOR,
|
||||
darkBrandColor: user.darkBrandColor || DEFAULT_DARK_BRAND_COLOR,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { isSubmitting: isBrandColorsFormSubmitting, isDirty: isBrandColorsFormDirty },
|
||||
reset: resetBrandColorsThemeReset,
|
||||
} = brandColorsFormMethods;
|
||||
|
||||
const selectedTheme = userThemeFormMethods.watch("theme");
|
||||
const selectedTheme = formMethods.watch("theme");
|
||||
const selectedThemeIsDark =
|
||||
selectedTheme === "dark" ||
|
||||
(selectedTheme === "" &&
|
||||
typeof document !== "undefined" &&
|
||||
document.documentElement.classList.contains("dark"));
|
||||
|
||||
const {
|
||||
formState: { isSubmitting, isDirty },
|
||||
reset,
|
||||
} = formMethods;
|
||||
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
await utils.viewer.me.invalidate();
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
resetBrandColorsThemeReset({ brandColor: data.brandColor, darkBrandColor: data.darkBrandColor });
|
||||
resetBookerLayoutThemeReset({ metadata: data.metadata });
|
||||
resetUserThemeReset({ theme: data.theme });
|
||||
reset(data);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error.message) {
|
||||
|
@ -134,180 +97,136 @@ const AppearanceView = ({
|
|||
},
|
||||
});
|
||||
|
||||
if (isLoading || isTeamPlanStatusLoading)
|
||||
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Meta title={t("appearance")} description={t("appearance_description")} borderInShellHeader={false} />
|
||||
<div className="border-subtle mt-6 flex items-center rounded-t-xl border p-6 text-sm">
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => {
|
||||
const layoutError = validateBookerLayouts(values?.metadata?.defaultBookerLayouts || null);
|
||||
if (layoutError) throw new Error(t(layoutError));
|
||||
|
||||
mutation.mutate({
|
||||
...values,
|
||||
// Radio values don't support null as values, therefore we convert an empty string
|
||||
// back to null here.
|
||||
theme: values.theme || null,
|
||||
});
|
||||
}}>
|
||||
<Meta title={t("appearance")} description={t("appearance_description")} />
|
||||
<div className="mb-6 flex items-center text-sm">
|
||||
<div>
|
||||
<p className="text-default text-base font-semibold">{t("theme")}</p>
|
||||
<p className="text-default font-semibold">{t("theme")}</p>
|
||||
<p className="text-default">{t("theme_applies_note")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form
|
||||
form={userThemeFormMethods}
|
||||
handleSubmit={(values) => {
|
||||
mutation.mutate({
|
||||
// Radio values don't support null as values, therefore we convert an empty string
|
||||
// back to null here.
|
||||
theme: values.theme || null,
|
||||
});
|
||||
}}>
|
||||
<div className="border-subtle flex flex-col justify-between border-x px-6 py-8 sm:flex-row">
|
||||
<ThemeLabel
|
||||
variant="system"
|
||||
value={null}
|
||||
label={t("theme_system")}
|
||||
defaultChecked={user.theme === null}
|
||||
register={userThemeFormMethods.register}
|
||||
/>
|
||||
<ThemeLabel
|
||||
variant="light"
|
||||
value="light"
|
||||
label={t("light")}
|
||||
defaultChecked={user.theme === "light"}
|
||||
register={userThemeFormMethods.register}
|
||||
/>
|
||||
<ThemeLabel
|
||||
variant="dark"
|
||||
value="dark"
|
||||
label={t("dark")}
|
||||
defaultChecked={user.theme === "dark"}
|
||||
register={userThemeFormMethods.register}
|
||||
/>
|
||||
</div>
|
||||
<SectionBottomActions className="mb-6" align="end">
|
||||
<Button
|
||||
disabled={isUserThemeSubmitting || !isUserThemeDirty}
|
||||
type="submit"
|
||||
data-testid="update-theme-btn"
|
||||
color="primary">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</Form>
|
||||
|
||||
<Form
|
||||
form={bookerLayoutFormMethods}
|
||||
handleSubmit={(values) => {
|
||||
const layoutError = validateBookerLayouts(values?.metadata?.defaultBookerLayouts || null);
|
||||
if (layoutError) {
|
||||
showToast(t(layoutError), "error");
|
||||
return;
|
||||
} else {
|
||||
mutation.mutate(values);
|
||||
}
|
||||
}}>
|
||||
<BookerLayoutSelector
|
||||
isDark={selectedThemeIsDark}
|
||||
name="metadata.defaultBookerLayouts"
|
||||
title={t("bookerlayout_user_settings_title")}
|
||||
description={t("bookerlayout_user_settings_description")}
|
||||
isDisabled={isBookerLayoutFormSubmitting || !isBookerLayoutFormDirty}
|
||||
<div className="flex flex-col justify-between sm:flex-row">
|
||||
<ThemeLabel
|
||||
variant="system"
|
||||
value={null}
|
||||
label={t("theme_system")}
|
||||
defaultChecked={user.theme === null}
|
||||
register={formMethods.register}
|
||||
/>
|
||||
</Form>
|
||||
<ThemeLabel
|
||||
variant="light"
|
||||
value="light"
|
||||
label={t("light")}
|
||||
defaultChecked={user.theme === "light"}
|
||||
register={formMethods.register}
|
||||
/>
|
||||
<ThemeLabel
|
||||
variant="dark"
|
||||
value="dark"
|
||||
label={t("dark")}
|
||||
defaultChecked={user.theme === "dark"}
|
||||
register={formMethods.register}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={brandColorsFormMethods}
|
||||
handleSubmit={(values) => {
|
||||
mutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-6">
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("custom_brand_colors")}
|
||||
description={t("customize_your_brand_colors")}
|
||||
checked={isCustomBrandColorChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsCustomBranColorChecked(checked);
|
||||
if (!checked) {
|
||||
mutation.mutate({
|
||||
brandColor: DEFAULT_LIGHT_BRAND_COLOR,
|
||||
darkBrandColor: DEFAULT_DARK_BRAND_COLOR,
|
||||
});
|
||||
}
|
||||
}}
|
||||
childrenClassName="lg:ml-0"
|
||||
switchContainerClassName={classNames(
|
||||
"py-6 px-4 sm:px-6 border-subtle rounded-xl border",
|
||||
isCustomBrandColorChecked && "rounded-b-none"
|
||||
)}>
|
||||
<div className="border-subtle flex flex-col gap-6 border-x p-6">
|
||||
<Controller
|
||||
name="brandColor"
|
||||
control={brandColorsFormMethods.control}
|
||||
<hr className="border-subtle my-8 border [&:has(+hr)]:hidden" />
|
||||
<BookerLayoutSelector
|
||||
isDark={selectedThemeIsDark}
|
||||
name="metadata.defaultBookerLayouts"
|
||||
title={t("bookerlayout_user_settings_title")}
|
||||
description={t("bookerlayout_user_settings_description")}
|
||||
/>
|
||||
|
||||
<hr className="border-subtle my-8 border" />
|
||||
<div className="mb-6 flex items-center text-sm">
|
||||
<div>
|
||||
<p className="text-default font-semibold">{t("custom_brand_colors")}</p>
|
||||
<p className="text-default mt-0.5 leading-5">{t("customize_your_brand_colors")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="block justify-between sm:flex">
|
||||
<Controller
|
||||
name="brandColor"
|
||||
control={formMethods.control}
|
||||
defaultValue={user.brandColor}
|
||||
render={() => (
|
||||
<div>
|
||||
<p className="text-default mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
|
||||
<ColorPicker
|
||||
defaultValue={user.brandColor}
|
||||
render={() => (
|
||||
<div>
|
||||
<p className="text-default mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
|
||||
<ColorPicker
|
||||
defaultValue={user.brandColor}
|
||||
resetDefaultValue="#292929"
|
||||
onChange={(value) => {
|
||||
try {
|
||||
checkWCAGContrastColor("#ffffff", value);
|
||||
setLightModeError(false);
|
||||
brandColorsFormMethods.setValue("brandColor", value, { shouldDirty: true });
|
||||
} catch (err) {
|
||||
setLightModeError(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{lightModeError ? (
|
||||
<div className="mt-4">
|
||||
<Alert
|
||||
severity="warning"
|
||||
message="Light Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible."
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="darkBrandColor"
|
||||
control={brandColorsFormMethods.control}
|
||||
defaultValue={user.darkBrandColor}
|
||||
render={() => (
|
||||
<div className="mt-6 sm:mt-0">
|
||||
<p className="text-default mb-2 block text-sm font-medium">{t("dark_brand_color")}</p>
|
||||
<ColorPicker
|
||||
defaultValue={user.darkBrandColor}
|
||||
resetDefaultValue="#fafafa"
|
||||
onChange={(value) => {
|
||||
try {
|
||||
checkWCAGContrastColor("#101010", value);
|
||||
setDarkModeError(false);
|
||||
brandColorsFormMethods.setValue("darkBrandColor", value, { shouldDirty: true });
|
||||
} catch (err) {
|
||||
setDarkModeError(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{darkModeError ? (
|
||||
<div className="mt-4">
|
||||
<Alert
|
||||
severity="warning"
|
||||
message="Dark Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible."
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
resetDefaultValue="#292929"
|
||||
onChange={(value) => {
|
||||
if (!checkWCAGContrastColor("#ffffff", value)) {
|
||||
setLightModeError(true);
|
||||
} else {
|
||||
setLightModeError(false);
|
||||
}
|
||||
formMethods.setValue("brandColor", value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
disabled={isBrandColorsFormSubmitting || !isBrandColorsFormDirty}
|
||||
color="primary"
|
||||
type="submit">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="darkBrandColor"
|
||||
control={formMethods.control}
|
||||
defaultValue={user.darkBrandColor}
|
||||
render={() => (
|
||||
<div className="mt-6 sm:mt-0">
|
||||
<p className="text-default mb-2 block text-sm font-medium">{t("dark_brand_color")}</p>
|
||||
<ColorPicker
|
||||
defaultValue={user.darkBrandColor}
|
||||
resetDefaultValue="#fafafa"
|
||||
onChange={(value) => {
|
||||
if (!checkWCAGContrastColor("#101010", value)) {
|
||||
setDarkModeError(true);
|
||||
} else {
|
||||
setDarkModeError(false);
|
||||
}
|
||||
formMethods.setValue("darkBrandColor", value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{darkModeError ? (
|
||||
<div className="mt-4">
|
||||
<Alert
|
||||
severity="warning"
|
||||
message="Dark Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible."
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
) : null}
|
||||
{lightModeError ? (
|
||||
<div className="mt-4">
|
||||
<Alert
|
||||
severity="warning"
|
||||
message="Light Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible."
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{/* TODO future PR to preview brandColors */}
|
||||
{/* <Button
|
||||
color="secondary"
|
||||
|
@ -316,37 +235,51 @@ const AppearanceView = ({
|
|||
onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}>
|
||||
Preview
|
||||
</Button> */}
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("disable_cal_branding", { appName: APP_NAME })}
|
||||
disabled={!hasPaidPlan || mutation?.isLoading}
|
||||
description={t("removes_cal_branding", { appName: APP_NAME })}
|
||||
checked={hasPaidPlan ? hideBrandingValue : false}
|
||||
Badge={<UpgradeTeamsBadge />}
|
||||
onCheckedChange={(checked) => {
|
||||
setHideBrandingValue(checked);
|
||||
mutation.mutate({ hideBranding: checked });
|
||||
}}
|
||||
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
|
||||
<hr className="border-subtle my-8 border" />
|
||||
<Controller
|
||||
name="hideBranding"
|
||||
control={formMethods.control}
|
||||
defaultValue={user.hideBranding}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<div className="flex w-full text-sm">
|
||||
<div className="mr-1 flex-grow">
|
||||
<div className="flex items-center">
|
||||
<p className="text-default font-semibold ltr:mr-2 rtl:ml-2">
|
||||
{t("disable_cal_branding", { appName: APP_NAME })}
|
||||
</p>
|
||||
<UpgradeTeamsBadge />
|
||||
</div>
|
||||
<p className="text-default mt-0.5">{t("removes_cal_branding", { appName: APP_NAME })}</p>
|
||||
</div>
|
||||
<div className="flex-none">
|
||||
<Switch
|
||||
id="hideBranding"
|
||||
disabled={!hasPaidPlan}
|
||||
onCheckedChange={(checked) =>
|
||||
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
|
||||
}
|
||||
checked={hasPaidPlan ? value : false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
type="submit"
|
||||
loading={mutation.isLoading}
|
||||
color="primary"
|
||||
className="mt-8"
|
||||
data-testid="update-theme-btn">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceViewWrapper = () => {
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
const { isLoading: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
|
||||
AppearanceView.getLayout = getLayout;
|
||||
AppearanceView.PageWrapper = PageWrapper;
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
if (isLoading || isTeamPlanStatusLoading || !user)
|
||||
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
|
||||
|
||||
return <AppearanceView user={user} hasPaidPlan={hasPaidPlan} />;
|
||||
};
|
||||
|
||||
AppearanceViewWrapper.getLayout = getLayout;
|
||||
AppearanceViewWrapper.PageWrapper = PageWrapper;
|
||||
|
||||
export default AppearanceViewWrapper;
|
||||
export default AppearanceView;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Fragment, useState, useEffect } from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
@ -35,13 +34,13 @@ import PageWrapper from "@components/PageWrapper";
|
|||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle mt-8 space-y-6 rounded-xl border px-4 py-6 sm:px-6">
|
||||
<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="ml-auto h-8 w-20 rounded-md p-5" />
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
|
@ -66,21 +65,6 @@ const CalendarsView = () => {
|
|||
const utils = trpc.useContext();
|
||||
|
||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
||||
|
||||
const [selectedDestinationCalendarOption, setSelectedDestinationCalendar] = useState<{
|
||||
integration: string;
|
||||
externalId: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (query?.data?.destinationCalendar) {
|
||||
setSelectedDestinationCalendar({
|
||||
integration: query.data.destinationCalendar.integration,
|
||||
externalId: query.data.destinationCalendar.externalId,
|
||||
});
|
||||
}
|
||||
}, [query?.isLoading, query?.data?.destinationCalendar]);
|
||||
|
||||
const mutation = trpc.viewer.setDestinationCalendar.useMutation({
|
||||
async onSettled() {
|
||||
await utils.viewer.connectedCalendars.invalidate();
|
||||
|
@ -95,58 +79,43 @@ const CalendarsView = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={t("calendars")}
|
||||
description={t("calendars_description")}
|
||||
CTA={<AddCalendarButton />}
|
||||
borderInShellHeader={false}
|
||||
/>
|
||||
<Meta title={t("calendars")} description={t("calendars_description")} CTA={<AddCalendarButton />} />
|
||||
<QueryCell
|
||||
query={query}
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => {
|
||||
const isDestinationUpdateBtnDisabled =
|
||||
selectedDestinationCalendarOption?.externalId === query?.data?.destinationCalendar?.externalId;
|
||||
return data.connectedCalendars.length ? (
|
||||
<div>
|
||||
<div className="border-subtle mt-8 rounded-t-xl border px-4 py-6 sm:px-6">
|
||||
<h2 className="text-emphasis mb-1 text-base font-bold leading-5 tracking-wide">
|
||||
{t("add_to_calendar")}
|
||||
</h2>
|
||||
<p className="text-default text-sm">{t("add_to_calendar_description")}</p>
|
||||
</div>
|
||||
<div className="border-subtle flex w-full flex-col space-y-3 border border-x border-y-0 px-4 py-6 sm:px-6">
|
||||
<DestinationCalendarSelector
|
||||
hidePlaceholder
|
||||
value={selectedDestinationCalendarOption?.externalId}
|
||||
onChange={(option) => {
|
||||
setSelectedDestinationCalendar(option);
|
||||
}}
|
||||
isLoading={mutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
loading={mutation.isLoading}
|
||||
disabled={isDestinationUpdateBtnDisabled}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (selectedDestinationCalendarOption) mutation.mutate(selectedDestinationCalendarOption);
|
||||
}}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
<div className="bg-muted border-subtle mt-4 flex space-x-4 rounded-md p-2 sm:mx-0 sm:p-10 md:border md:p-6 xl:mt-0">
|
||||
<div className=" bg-default border-subtle flex h-9 w-9 items-center justify-center rounded-md border-2 p-[6px]">
|
||||
<Calendar className="text-default h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<div className="border-subtle mt-8 rounded-t-xl border px-4 py-6 sm:px-6">
|
||||
<h4 className="text-emphasis text-base font-semibold leading-5">
|
||||
{t("check_for_conflicts")}
|
||||
</h4>
|
||||
<p className="text-default pb-2 text-sm leading-5">{t("select_calendars")}</p>
|
||||
<div className="flex w-full flex-col space-y-3">
|
||||
<div>
|
||||
<h4 className=" text-emphasis pb-2 text-base font-semibold leading-5">
|
||||
{t("add_to_calendar")}
|
||||
</h4>
|
||||
<p className=" text-default text-sm leading-5">
|
||||
<Trans i18nKey="add_to_calendar_description">
|
||||
Where to add events when you re booked. You can override this on a per-event basis in
|
||||
advanced settings in the event type.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
<DestinationCalendarSelector
|
||||
hidePlaceholder
|
||||
value={data.destinationCalendar?.externalId}
|
||||
onChange={mutation.mutate}
|
||||
isLoading={mutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<List
|
||||
className="border-subtle flex flex-col gap-6 rounded-b-xl border border-t-0 p-6"
|
||||
noBorderTreatment>
|
||||
<h4 className="text-emphasis mt-12 text-base font-semibold leading-5">
|
||||
{t("check_for_conflicts")}
|
||||
</h4>
|
||||
<p className="text-default pb-2 text-sm leading-5">{t("select_calendars")}</p>
|
||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||
{data.connectedCalendars.map((item) => (
|
||||
<Fragment key={item.credentialId}>
|
||||
{item.error && item.error.message && (
|
||||
|
@ -238,7 +207,6 @@ const CalendarsView = () => {
|
|||
description={t("no_calendar_installed_description")}
|
||||
buttonText={t("add_a_calendar")}
|
||||
buttonOnClick={() => router.push("/apps/categories/calendar")}
|
||||
className="mt-6"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -15,8 +15,8 @@ import { AppList } from "@components/apps/AppList";
|
|||
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">
|
||||
<Meta title={title} description={description} />
|
||||
<div className="divide-subtle mb-8 mt-6 space-y-6">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
|
@ -28,9 +28,11 @@ const AddConferencingButton = () => {
|
|||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Button color="secondary" StartIcon={Plus} href="/apps/categories/conferencing">
|
||||
{t("add")}
|
||||
</Button>
|
||||
<>
|
||||
<Button color="secondary" StartIcon={Plus} href="/apps/categories/conferencing">
|
||||
{t("add_conferencing_app")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -70,7 +72,6 @@ const ConferencingLayout = () => {
|
|||
title={t("conferencing")}
|
||||
description={t("conferencing_description")}
|
||||
CTA={<AddConferencingButton />}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<QueryCell
|
||||
query={query}
|
||||
|
@ -92,20 +93,13 @@ const ConferencingLayout = () => {
|
|||
color="secondary"
|
||||
data-testid="connect-conferencing-apps"
|
||||
href="/apps/categories/conferencing">
|
||||
{t("connect_conference_apps")}
|
||||
{t("connect_conferencing_apps")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AppList
|
||||
listClassName="rounded-xl rounded-t-none border-t-0"
|
||||
handleDisconnect={handleDisconnect}
|
||||
data={data}
|
||||
variant="conferencing"
|
||||
/>
|
||||
);
|
||||
return <AppList handleDisconnect={handleDisconnect} data={data} variant="conferencing" />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { localeOptions } from "@calcom/lib/i18n";
|
||||
|
@ -15,12 +13,12 @@ import {
|
|||
Label,
|
||||
Meta,
|
||||
Select,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
SkeletonButton,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
TimezoneSelect,
|
||||
SettingsToggle,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
@ -28,14 +26,14 @@ import PageWrapper from "@components/PageWrapper";
|
|||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={true} />
|
||||
<div className="border-subtle space-y-6 rounded-b-xl border border-t-0 px-4 py-8 sm:px-6">
|
||||
<Meta title={title} description={description} />
|
||||
<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="ml-auto h-8 w-20 rounded-md p-5" />
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
|
@ -61,7 +59,6 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const { update } = useSession();
|
||||
const [isUpdateBtnLoading, setIsUpdateBtnLoading] = useState<boolean>(false);
|
||||
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
|
@ -75,7 +72,6 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||
},
|
||||
onSettled: async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
setIsUpdateBtnLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -109,6 +105,9 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||
value: user.weekStart,
|
||||
label: nameOfDay(localeProp, user.weekStart === "Sunday" ? 0 : 1),
|
||||
},
|
||||
allowDynamicBooking: user.allowDynamicBooking ?? true,
|
||||
allowSEOIndexing: user.allowSEOIndexing ?? true,
|
||||
receiveMonthlyDigestEmail: user.receiveMonthlyDigestEmail ?? true,
|
||||
},
|
||||
});
|
||||
const {
|
||||
|
@ -118,150 +117,151 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||
} = formMethods;
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
|
||||
const [isAllowDynamicBookingChecked, setIsAllowDynamicBookingChecked] = useState(
|
||||
!!user.allowDynamicBooking
|
||||
);
|
||||
const [isAllowSEOIndexingChecked, setIsAllowSEOIndexingChecked] = useState(!!user.allowSEOIndexing);
|
||||
const [isReceiveMonthlyDigestEmailChecked, setIsReceiveMonthlyDigestEmailChecked] = useState(
|
||||
!!user.receiveMonthlyDigestEmail
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => {
|
||||
setIsUpdateBtnLoading(true);
|
||||
mutation.mutate({
|
||||
...values,
|
||||
locale: values.locale.value,
|
||||
timeFormat: values.timeFormat.value,
|
||||
weekStart: values.weekStart.value,
|
||||
});
|
||||
}}>
|
||||
<Meta title={t("general")} description={t("general_description")} borderInShellHeader={true} />
|
||||
<div className="border-subtle border-x border-y-0 px-4 py-8 sm:px-6">
|
||||
<Controller
|
||||
name="locale"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis">
|
||||
<>{t("language")}</>
|
||||
</Label>
|
||||
<Select<{ label: string; value: string }>
|
||||
className="capitalize"
|
||||
options={localeOptions}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("timezone")}</>
|
||||
</Label>
|
||||
<TimezoneSelect
|
||||
id="timezone"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="timeFormat"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("time_format")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={timeFormatOptions}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("timeFormat", { ...event }, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<div className="text-gray text-default mt-2 flex items-center text-sm">
|
||||
{t("timeformat_profile_hint")}
|
||||
</div>
|
||||
<Controller
|
||||
name="weekStart"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-6">
|
||||
<>{t("start_of_week")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={weekStartOptions}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SectionBottomActions align="end">
|
||||
<Button loading={isUpdateBtnLoading} disabled={isDisabled} color="primary" type="submit">
|
||||
<>{t("update")}</>
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</Form>
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("dynamic_booking")}
|
||||
description={t("allow_dynamic_booking")}
|
||||
disabled={mutation.isLoading}
|
||||
checked={isAllowDynamicBookingChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsAllowDynamicBookingChecked(checked);
|
||||
mutation.mutate({ allowDynamicBooking: checked });
|
||||
}}
|
||||
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => {
|
||||
mutation.mutate({
|
||||
...values,
|
||||
locale: values.locale.value,
|
||||
timeFormat: values.timeFormat.value,
|
||||
weekStart: values.weekStart.value,
|
||||
});
|
||||
}}>
|
||||
<Meta title={t("general")} description={t("general_description")} />
|
||||
<Controller
|
||||
name="locale"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis">
|
||||
<>{t("language")}</>
|
||||
</Label>
|
||||
<Select<{ label: string; value: string }>
|
||||
className="capitalize"
|
||||
options={localeOptions}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("seo_indexing")}
|
||||
description={t("allow_seo_indexing")}
|
||||
disabled={mutation.isLoading}
|
||||
checked={isAllowSEOIndexingChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsAllowSEOIndexingChecked(checked);
|
||||
mutation.mutate({ allowSEOIndexing: checked });
|
||||
}}
|
||||
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
|
||||
<Controller
|
||||
name="timeZone"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-8">
|
||||
<>{t("timezone")}</>
|
||||
</Label>
|
||||
<TimezoneSelect
|
||||
id="timezone"
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("monthly_digest_email")}
|
||||
description={t("monthly_digest_email_for_teams")}
|
||||
disabled={mutation.isLoading}
|
||||
checked={isReceiveMonthlyDigestEmailChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
setIsReceiveMonthlyDigestEmailChecked(checked);
|
||||
mutation.mutate({ receiveMonthlyDigestEmail: checked });
|
||||
}}
|
||||
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
|
||||
<Controller
|
||||
name="timeFormat"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-8">
|
||||
<>{t("time_format")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={timeFormatOptions}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("timeFormat", { ...event }, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray text-default mt-2 flex items-center text-sm">
|
||||
{t("timeformat_profile_hint")}
|
||||
</div>
|
||||
<Controller
|
||||
name="weekStart"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="text-emphasis mt-8">
|
||||
<>{t("start_of_week")}</>
|
||||
</Label>
|
||||
<Select
|
||||
value={value}
|
||||
options={weekStartOptions}
|
||||
onChange={(event) => {
|
||||
if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8">
|
||||
<Controller
|
||||
name="allowDynamicBooking"
|
||||
control={formMethods.control}
|
||||
render={() => (
|
||||
<SettingsToggle
|
||||
title={t("dynamic_booking")}
|
||||
description={t("allow_dynamic_booking")}
|
||||
checked={formMethods.getValues("allowDynamicBooking")}
|
||||
onCheckedChange={(checked) => {
|
||||
formMethods.setValue("allowDynamicBooking", checked, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Controller
|
||||
name="allowSEOIndexing"
|
||||
control={formMethods.control}
|
||||
render={() => (
|
||||
<SettingsToggle
|
||||
title={t("seo_indexing")}
|
||||
description={t("allow_seo_indexing")}
|
||||
checked={formMethods.getValues("allowSEOIndexing")}
|
||||
onCheckedChange={(checked) => {
|
||||
formMethods.setValue("allowSEOIndexing", checked, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Controller
|
||||
name="receiveMonthlyDigestEmail"
|
||||
control={formMethods.control}
|
||||
render={() => (
|
||||
<SettingsToggle
|
||||
title={t("monthly_digest_email")}
|
||||
description={t("monthly_digest_email_for_teams")}
|
||||
checked={formMethods.getValues("receiveMonthlyDigestEmail")}
|
||||
onCheckedChange={(checked) => {
|
||||
formMethods.setValue("receiveMonthlyDigestEmail", checked, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
loading={mutation.isLoading}
|
||||
disabled={isDisabled}
|
||||
color="primary"
|
||||
type="submit"
|
||||
className="mt-8">
|
||||
<>{t("update")}</>
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,10 +7,8 @@ import { z } from "zod";
|
|||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import OrganizationAvatar from "@calcom/features/ee/organizations/components/OrganizationAvatar";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
|
@ -49,8 +47,8 @@ import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
|||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={true} />
|
||||
<div className="border-subtle space-y-6 rounded-b-xl border border-t-0 px-4 py-8">
|
||||
<Meta title={title} description={description} />
|
||||
<div className="mb-8 space-y-6">
|
||||
<div className="flex items-center">
|
||||
<SkeletonAvatar className="me-4 mt-0 h-16 w-16 px-4" />
|
||||
<SkeletonButton className="h-6 w-32 rounded-md p-5" />
|
||||
|
@ -71,30 +69,18 @@ interface DeleteAccountValues {
|
|||
|
||||
type FormValues = {
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
avatar: string;
|
||||
name: string;
|
||||
email: string;
|
||||
bio: string;
|
||||
};
|
||||
|
||||
const checkIfItFallbackImage = (fetchedImgSrc: string) => {
|
||||
return fetchedImgSrc.endsWith(AVATAR_FALLBACK);
|
||||
};
|
||||
|
||||
const ProfileView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { update } = useSession();
|
||||
|
||||
const [fetchedImgSrc, setFetchedImgSrc] = useState<string | undefined>();
|
||||
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery(undefined, {
|
||||
onSuccess: (userData) => {
|
||||
fetch(userData.avatar).then((res) => {
|
||||
if (res.url) setFetchedImgSrc(res.url);
|
||||
});
|
||||
},
|
||||
});
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await update(res);
|
||||
|
@ -218,7 +204,7 @@ const ProfileView = () => {
|
|||
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
||||
};
|
||||
|
||||
if (isLoading || !user || !fetchedImgSrc)
|
||||
if (isLoading || !user)
|
||||
return (
|
||||
<SkeletonLoader title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
|
||||
);
|
||||
|
@ -233,17 +219,11 @@ const ProfileView = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={t("profile")}
|
||||
description={t("profile_description", { appName: APP_NAME })}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<Meta title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
|
||||
<ProfileForm
|
||||
key={JSON.stringify(defaultValues)}
|
||||
defaultValues={defaultValues}
|
||||
isLoading={updateProfileMutation.isLoading}
|
||||
isFallbackImg={checkIfItFallbackImage(fetchedImgSrc)}
|
||||
userAvatar={user.avatar}
|
||||
userOrganization={user.organization}
|
||||
onSubmit={(values) => {
|
||||
if (values.email !== user.email && isCALIdentityProvider) {
|
||||
|
@ -258,7 +238,7 @@ const ProfileView = () => {
|
|||
}
|
||||
}}
|
||||
extraField={
|
||||
<div className="mt-6">
|
||||
<div className="mt-8">
|
||||
<UsernameAvailabilityField
|
||||
onSuccessMutation={async () => {
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
|
@ -272,19 +252,16 @@ const ProfileView = () => {
|
|||
}
|
||||
/>
|
||||
|
||||
<div className="border-subtle mt-6 rounded-xl rounded-b-none border border-b-0 p-6">
|
||||
<Label className="text-base font-semibold text-red-700">{t("danger_zone")}</Label>
|
||||
<p className="text-subtle">{t("account_deletion_cannot_be_undone")}</p>
|
||||
</div>
|
||||
<hr className="border-subtle my-6" />
|
||||
|
||||
<Label>{t("danger_zone")}</Label>
|
||||
{/* Delete account Dialog */}
|
||||
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
||||
<SectionBottomActions align="end">
|
||||
<DialogTrigger asChild>
|
||||
<Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={Trash2}>
|
||||
{t("delete_account")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</SectionBottomActions>
|
||||
<DialogTrigger asChild>
|
||||
<Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={Trash2}>
|
||||
{t("delete_account")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
title={t("delete_account_modal_title")}
|
||||
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
|
||||
|
@ -387,16 +364,12 @@ const ProfileForm = ({
|
|||
onSubmit,
|
||||
extraField,
|
||||
isLoading = false,
|
||||
isFallbackImg,
|
||||
userAvatar,
|
||||
userOrganization,
|
||||
}: {
|
||||
defaultValues: FormValues;
|
||||
onSubmit: (values: FormValues) => void;
|
||||
extraField?: React.ReactNode;
|
||||
isLoading: boolean;
|
||||
isFallbackImg: boolean;
|
||||
userAvatar: string;
|
||||
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
@ -404,7 +377,7 @@ const ProfileForm = ({
|
|||
|
||||
const profileFormSchema = z.object({
|
||||
username: z.string(),
|
||||
avatar: z.string().nullable(),
|
||||
avatar: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
|
@ -429,77 +402,56 @@ const ProfileForm = ({
|
|||
|
||||
return (
|
||||
<Form form={formMethods} handleSubmit={onSubmit}>
|
||||
<div className="border-subtle border-x px-4 pb-10 pt-8 sm:px-6">
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => {
|
||||
const showRemoveAvatarButton = !isFallbackImg || (value && userAvatar !== value);
|
||||
return (
|
||||
<>
|
||||
<OrganizationAvatar
|
||||
alt={formMethods.getValues("username")}
|
||||
imageSrc={value}
|
||||
size="lg"
|
||||
organizationSlug={userOrganization.slug}
|
||||
/>
|
||||
<div className="ms-4">
|
||||
<h2 className="mb-2 text-sm font-medium">{t("profile_picture")}</h2>
|
||||
<div className="flex gap-2">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("upload_avatar")}
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||
}}
|
||||
imageSrc={value || undefined}
|
||||
triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"}
|
||||
/>
|
||||
|
||||
{showRemoveAvatarButton && (
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
formMethods.setValue("avatar", null, { shouldDirty: true });
|
||||
}}>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{extraField}
|
||||
<div className="mt-6">
|
||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Label>{t("about")}</Label>
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("bio") || "")}
|
||||
setText={(value: string) => {
|
||||
formMethods.setValue("bio", turndown(value), { shouldDirty: true });
|
||||
}}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
disableLists
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<OrganizationAvatar
|
||||
alt={formMethods.getValues("username")}
|
||||
imageSrc={value}
|
||||
size="lg"
|
||||
organizationSlug={userOrganization.slug}
|
||||
/>
|
||||
<div className="ms-4">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("change_avatar")}
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||
}}
|
||||
imageSrc={value || undefined}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button loading={isLoading} disabled={isDisabled} color="primary" type="submit">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
{extraField}
|
||||
<div className="mt-8">
|
||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<Label>{t("about")}</Label>
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("bio") || "")}
|
||||
setText={(value: string) => {
|
||||
formMethods.setValue("bio", turndown(value), { shouldDirty: true });
|
||||
}}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
disableLists
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
/>
|
||||
</div>
|
||||
<Button loading={isLoading} disabled={isDisabled} color="primary" className="mt-8" type="submit">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,34 +1,23 @@
|
|||
import { useState } from "react";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Meta, showToast, SettingsToggle, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { Button, Form, Label, Meta, showToast, Skeleton, Switch } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={true} />
|
||||
<div className="border-subtle space-y-6 border border-t-0 px-4 py-8 sm:px-6">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"] }) => {
|
||||
const ProfileImpersonationView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const [disableImpersonation, setDisableImpersonation] = useState<boolean | undefined>(
|
||||
user?.disableImpersonation
|
||||
);
|
||||
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("profile_updated_successfully"), "success");
|
||||
reset(getValues());
|
||||
},
|
||||
onSettled: () => {
|
||||
utils.viewer.me.invalidate();
|
||||
|
@ -37,54 +26,83 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
|
|||
await utils.viewer.me.cancel();
|
||||
const previousValue = utils.viewer.me.getData();
|
||||
|
||||
setDisableImpersonation(disableImpersonation);
|
||||
|
||||
if (previousValue && disableImpersonation) {
|
||||
utils.viewer.me.setData(undefined, { ...previousValue, disableImpersonation });
|
||||
}
|
||||
return { previousValue };
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (context?.previousValue) {
|
||||
utils.viewer.me.setData(undefined, context.previousValue);
|
||||
setDisableImpersonation(context.previousValue?.disableImpersonation);
|
||||
}
|
||||
showToast(`${t("error")}, ${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const formMethods = useForm<{ disableImpersonation: boolean }>({
|
||||
defaultValues: {
|
||||
disableImpersonation: user?.disableImpersonation,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { isSubmitting, isDirty },
|
||||
setValue,
|
||||
reset,
|
||||
getValues,
|
||||
watch,
|
||||
} = formMethods;
|
||||
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={t("impersonation")}
|
||||
description={t("impersonation_description")}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<div>
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("user_impersonation_heading")}
|
||||
description={t("user_impersonation_description")}
|
||||
checked={!disableImpersonation}
|
||||
onCheckedChange={(checked) => {
|
||||
mutation.mutate({ disableImpersonation: !checked });
|
||||
}}
|
||||
disabled={mutation.isLoading}
|
||||
switchContainerClassName="py-6 px-4 sm:px-6 border-subtle rounded-b-xl border border-t-0"
|
||||
/>
|
||||
</div>
|
||||
<Meta title={t("impersonation")} description={t("impersonation_description")} />
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={({ disableImpersonation }) => {
|
||||
mutation.mutate({ disableImpersonation });
|
||||
}}>
|
||||
<div className="flex space-x-3">
|
||||
<Switch
|
||||
onCheckedChange={(e) => {
|
||||
setValue("disableImpersonation", !e, { shouldDirty: true });
|
||||
}}
|
||||
fitToHeight={true}
|
||||
checked={!watch("disableImpersonation")}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Skeleton as={Label} className="text-emphasis text-sm font-semibold leading-none">
|
||||
{t("user_impersonation_heading")}
|
||||
</Skeleton>
|
||||
<Skeleton as="p" className="text-default -mt-2 text-sm leading-normal">
|
||||
{t("user_impersonation_description")}
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
color="primary"
|
||||
loading={mutation.isLoading}
|
||||
className="mt-8"
|
||||
type="submit"
|
||||
disabled={isDisabled}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileImpersonationViewWrapper = () => {
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
const { t } = useLocale();
|
||||
ProfileImpersonationView.getLayout = getLayout;
|
||||
ProfileImpersonationView.PageWrapper = PageWrapper;
|
||||
|
||||
if (isLoading || !user)
|
||||
return <SkeletonLoader title={t("impersonation")} description={t("impersonation_description")} />;
|
||||
|
||||
return <ProfileImpersonationView user={user} />;
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
await ssr.viewer.me.prefetch();
|
||||
return {
|
||||
props: {
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
ProfileImpersonationViewWrapper.getLayout = getLayout;
|
||||
ProfileImpersonationViewWrapper.PageWrapper = PageWrapper;
|
||||
|
||||
export default ProfileImpersonationViewWrapper;
|
||||
export default ProfileImpersonationView;
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Form,
|
||||
Meta,
|
||||
PasswordField,
|
||||
Select,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
SkeletonButton,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
} from "@calcom/ui";
|
||||
import { Alert, Button, Form, Meta, PasswordField, Select, SettingsToggle, showToast } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
|
@ -34,58 +18,34 @@ type ChangePasswordSessionFormValues = {
|
|||
apiError: string;
|
||||
};
|
||||
|
||||
interface PasswordViewProps {
|
||||
user: RouterOutputs["viewer"]["me"];
|
||||
}
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={true} />
|
||||
<div className="border-subtle space-y-6 border-x px-4 py-8 sm:px-6">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
<div className="rounded-b-xl">
|
||||
<SectionBottomActions align="end">
|
||||
<SkeletonButton className="ml-auto h-8 w-20 rounded-md" />
|
||||
</SectionBottomActions>
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const PasswordView = ({ user }: PasswordViewProps) => {
|
||||
const PasswordView = () => {
|
||||
const { data } = useSession();
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const metadata = userMetadataSchema.safeParse(user?.metadata);
|
||||
const initialSessionTimeout = metadata.success ? metadata.data?.sessionTimeout : undefined;
|
||||
|
||||
const [sessionTimeout, setSessionTimeout] = useState<number | undefined>(initialSessionTimeout);
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const metadata = userMetadata.safeParse(user?.metadata);
|
||||
const sessionTimeout = metadata.success ? metadata.data?.sessionTimeout : undefined;
|
||||
|
||||
const sessionMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: (data) => {
|
||||
onSuccess: () => {
|
||||
showToast(t("session_timeout_changed"), "success");
|
||||
formMethods.reset(formMethods.getValues());
|
||||
setSessionTimeout(data.metadata?.sessionTimeout);
|
||||
},
|
||||
onSettled: () => {
|
||||
utils.viewer.me.invalidate();
|
||||
},
|
||||
onMutate: async () => {
|
||||
await utils.viewer.me.cancel();
|
||||
const previousValue = await utils.viewer.me.getData();
|
||||
const previousMetadata = userMetadataSchema.safeParse(previousValue?.metadata);
|
||||
const previousValue = utils.viewer.me.getData();
|
||||
const previousMetadata = userMetadata.parse(previousValue?.metadata);
|
||||
|
||||
if (previousValue && sessionTimeout && previousMetadata.success) {
|
||||
if (previousValue && sessionTimeout) {
|
||||
utils.viewer.me.setData(undefined, {
|
||||
...previousValue,
|
||||
metadata: { ...previousMetadata?.data, sessionTimeout: sessionTimeout },
|
||||
metadata: { ...previousMetadata, sessionTimeout: sessionTimeout },
|
||||
});
|
||||
return { previousValue };
|
||||
}
|
||||
return { previousValue };
|
||||
},
|
||||
onError: (error, _, context) => {
|
||||
if (context?.previousValue) {
|
||||
|
@ -124,30 +84,20 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||
defaultValues: {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
sessionTimeout,
|
||||
},
|
||||
});
|
||||
|
||||
const sessionTimeoutWatch = formMethods.watch("sessionTimeout");
|
||||
|
||||
const handleSubmit = (values: ChangePasswordSessionFormValues) => {
|
||||
const { oldPassword, newPassword } = values;
|
||||
|
||||
if (!oldPassword.length) {
|
||||
formMethods.setError(
|
||||
"oldPassword",
|
||||
{ type: "required", message: t("error_required_field") },
|
||||
{ shouldFocus: true }
|
||||
);
|
||||
}
|
||||
if (!newPassword.length) {
|
||||
formMethods.setError(
|
||||
"newPassword",
|
||||
{ type: "required", message: t("error_required_field") },
|
||||
{ shouldFocus: true }
|
||||
);
|
||||
}
|
||||
|
||||
const { oldPassword, newPassword, sessionTimeout: newSessionTimeout } = values;
|
||||
if (oldPassword && newPassword) {
|
||||
passwordMutation.mutate({ oldPassword, newPassword });
|
||||
}
|
||||
if (sessionTimeout !== newSessionTimeout) {
|
||||
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout: newSessionTimeout } });
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutOptions = [5, 10, 15].map((mins) => ({
|
||||
|
@ -162,7 +112,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("password")} description={t("password_description")} borderInShellHeader={true} />
|
||||
<Meta title={t("password")} description={t("password_description")} />
|
||||
{user && user.identityProvider !== IdentityProvider.CAL ? (
|
||||
<div>
|
||||
<div className="mt-6">
|
||||
|
@ -180,127 +130,87 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||
</div>
|
||||
) : (
|
||||
<Form form={formMethods} handleSubmit={handleSubmit}>
|
||||
<div className="border-x px-4 py-6 sm:px-6">
|
||||
{formMethods.formState.errors.apiError && (
|
||||
<div className="pb-6">
|
||||
<Alert severity="error" message={formMethods.formState.errors.apiError?.message} />
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full sm:grid sm:grid-cols-2 sm:gap-x-6">
|
||||
<div>
|
||||
<PasswordField {...formMethods.register("oldPassword")} label={t("old_password")} />
|
||||
</div>
|
||||
<div>
|
||||
<PasswordField
|
||||
{...formMethods.register("newPassword", {
|
||||
minLength: {
|
||||
message: t(isUser ? "password_hint_min" : "password_hint_admin_min"),
|
||||
value: passwordMinLength,
|
||||
},
|
||||
pattern: {
|
||||
message: "Should contain a number, uppercase and lowercase letters",
|
||||
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
|
||||
},
|
||||
})}
|
||||
label={t("new_password")}
|
||||
/>
|
||||
</div>
|
||||
{formMethods.formState.errors.apiError && (
|
||||
<div className="pb-6">
|
||||
<Alert severity="error" message={formMethods.formState.errors.apiError?.message} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-[38rem] sm:grid sm:grid-cols-2 sm:gap-x-4">
|
||||
<div>
|
||||
<PasswordField {...formMethods.register("oldPassword")} label={t("old_password")} />
|
||||
</div>
|
||||
<div>
|
||||
<PasswordField
|
||||
{...formMethods.register("newPassword", {
|
||||
minLength: {
|
||||
message: t(isUser ? "password_hint_min" : "password_hint_admin_min"),
|
||||
value: passwordMinLength,
|
||||
},
|
||||
pattern: {
|
||||
message: "Should contain a number, uppercase and lowercase letters",
|
||||
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
|
||||
},
|
||||
})}
|
||||
label={t("new_password")}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-default mt-4 w-full text-sm">
|
||||
{t("invalid_password_hint", { passwordLength: passwordMinLength })}
|
||||
</p>
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
loading={passwordMutation.isLoading}
|
||||
onClick={() => formMethods.clearErrors("apiError")}
|
||||
disabled={isDisabled || passwordMutation.isLoading || sessionMutation.isLoading}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
<div className="mt-6">
|
||||
<p className="text-default mt-4 max-w-[38rem] text-sm">
|
||||
{t("invalid_password_hint", { passwordLength: passwordMinLength })}
|
||||
</p>
|
||||
<div className="border-subtle mt-8 border-t py-8">
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("session_timeout")}
|
||||
description={t("session_timeout_description")}
|
||||
checked={sessionTimeout !== undefined}
|
||||
checked={sessionTimeoutWatch !== undefined}
|
||||
data-testid="session-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
setSessionTimeout(undefined);
|
||||
|
||||
if (metadata.success) {
|
||||
sessionMutation.mutate({
|
||||
metadata: { ...metadata.data, sessionTimeout: undefined },
|
||||
});
|
||||
}
|
||||
formMethods.setValue("sessionTimeout", undefined, { shouldDirty: true });
|
||||
} else {
|
||||
setSessionTimeout(10);
|
||||
formMethods.setValue("sessionTimeout", 10, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
childrenClassName="lg:ml-0"
|
||||
switchContainerClassName={classNames(
|
||||
"py-6 px-4 sm:px-6 border-subtle rounded-xl border",
|
||||
!!sessionTimeout && "rounded-b-none"
|
||||
)}>
|
||||
<>
|
||||
<div className="border-subtle border-x p-6 pb-8">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-default mb-2 font-medium">{t("session_timeout_after")}</p>
|
||||
<Select
|
||||
options={timeoutOptions}
|
||||
defaultValue={
|
||||
sessionTimeout
|
||||
? timeoutOptions.find((tmo) => tmo.value === sessionTimeout)
|
||||
: timeoutOptions[1]
|
||||
}
|
||||
isSearchable={false}
|
||||
className="block h-[36px] !w-auto min-w-0 flex-none rounded-md text-sm"
|
||||
onChange={(event) => {
|
||||
setSessionTimeout(event?.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
color="primary"
|
||||
loading={sessionMutation.isLoading}
|
||||
onClick={() => {
|
||||
sessionMutation.mutate({
|
||||
metadata: { ...metadata, sessionTimeout },
|
||||
});
|
||||
formMethods.clearErrors("apiError");
|
||||
/>
|
||||
{sessionTimeoutWatch && (
|
||||
<div className="mt-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<p className="text-default ltr:mr-2 rtl:ml-2">{t("session_timeout_after")}</p>
|
||||
<Select
|
||||
options={timeoutOptions}
|
||||
defaultValue={
|
||||
sessionTimeout
|
||||
? timeoutOptions.find((tmo) => tmo.value === sessionTimeout)
|
||||
: timeoutOptions[1]
|
||||
}
|
||||
isSearchable={false}
|
||||
className="block h-[36px] !w-auto min-w-0 flex-none rounded-md text-sm"
|
||||
onChange={(event) => {
|
||||
formMethods.setValue("sessionTimeout", event?.value, { shouldDirty: true });
|
||||
}}
|
||||
disabled={
|
||||
initialSessionTimeout === sessionTimeout ||
|
||||
passwordMutation.isLoading ||
|
||||
sessionMutation.isLoading
|
||||
}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</>
|
||||
</SettingsToggle>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* TODO: Why is this Form not submitting? Hacky fix but works */}
|
||||
<Button
|
||||
color="primary"
|
||||
className="mt-8"
|
||||
type="submit"
|
||||
loading={passwordMutation.isLoading || sessionMutation.isLoading}
|
||||
onClick={() => formMethods.clearErrors("apiError")}
|
||||
disabled={isDisabled || passwordMutation.isLoading || sessionMutation.isLoading}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PasswordViewWrapper = () => {
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
const { t } = useLocale();
|
||||
if (isLoading || !user)
|
||||
return <SkeletonLoader title={t("password")} description={t("password_description")} />;
|
||||
PasswordView.getLayout = getLayout;
|
||||
PasswordView.PageWrapper = PageWrapper;
|
||||
|
||||
return <PasswordView user={user} />;
|
||||
};
|
||||
|
||||
PasswordViewWrapper.getLayout = getLayout;
|
||||
PasswordViewWrapper.PageWrapper = PageWrapper;
|
||||
|
||||
export default PasswordViewWrapper;
|
||||
export default PasswordView;
|
||||
|
|
|
@ -3,24 +3,15 @@ import { useState } from "react";
|
|||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Badge,
|
||||
Meta,
|
||||
SkeletonButton,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
Alert,
|
||||
SettingsToggle,
|
||||
} from "@calcom/ui";
|
||||
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText, Alert } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
||||
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<Meta title={title} description={description} borderInShellHeader={true} />
|
||||
<div className="mb-8 mt-6 space-y-6">
|
||||
<div className="flex items-center">
|
||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||
|
@ -37,34 +28,36 @@ const TwoFactorAuthView = () => {
|
|||
const { t } = useLocale();
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
|
||||
const [enableModalOpen, setEnableModalOpen] = useState<boolean>(false);
|
||||
const [disableModalOpen, setDisableModalOpen] = useState<boolean>(false);
|
||||
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
||||
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||
|
||||
if (isLoading)
|
||||
return <SkeletonLoader title={t("2fa")} description={t("set_up_two_factor_authentication")} />;
|
||||
if (isLoading) return <SkeletonLoader />;
|
||||
|
||||
const isCalProvider = user?.identityProvider === "CAL";
|
||||
const canSetupTwoFactor = !isCalProvider && !user?.twoFactorEnabled;
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("2fa")} description={t("set_up_two_factor_authentication")} borderInShellHeader={true} />
|
||||
<Meta title={t("2fa")} description={t("set_up_two_factor_authentication")} />
|
||||
{canSetupTwoFactor && <Alert severity="neutral" message={t("2fa_disabled")} />}
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
data-testid="two-factor-switch"
|
||||
title={t("two_factor_auth")}
|
||||
description={t("add_an_extra_layer_of_security")}
|
||||
checked={user?.twoFactorEnabled ?? false}
|
||||
onCheckedChange={() =>
|
||||
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
||||
}
|
||||
Badge={
|
||||
<Badge className="mx-2 text-xs" variant={user?.twoFactorEnabled ? "success" : "gray"}>
|
||||
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
|
||||
</Badge>
|
||||
}
|
||||
switchContainerClassName="border-subtle rounded-b-xl border border-t-0 px-5 py-6 sm:px-6"
|
||||
/>
|
||||
<div className="mt-6 flex items-start space-x-4">
|
||||
<Switch
|
||||
data-testid="two-factor-switch"
|
||||
disabled={canSetupTwoFactor}
|
||||
checked={user?.twoFactorEnabled}
|
||||
onCheckedChange={() =>
|
||||
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
||||
}
|
||||
/>
|
||||
<div className="!mx-4">
|
||||
<div className="flex">
|
||||
<p className="text-default font-semibold">{t("two_factor_auth")}</p>
|
||||
<Badge className="mx-2 text-xs" variant={user?.twoFactorEnabled ? "success" : "gray"}>
|
||||
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-default text-sm">{t("add_an_extra_layer_of_security")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnableTwoFactorModal
|
||||
open={enableModalOpen}
|
||||
|
|
|
@ -288,7 +288,6 @@
|
|||
"when": "When",
|
||||
"where": "Where",
|
||||
"add_to_calendar": "Add to calendar",
|
||||
"add_to_calendar_description":"Select where to add events when you’re booked.",
|
||||
"add_another_calendar": "Add another calendar",
|
||||
"other": "Other",
|
||||
"email_sign_in_subject": "Your sign-in link for {{appName}}",
|
||||
|
@ -600,7 +599,6 @@
|
|||
"hide_book_a_team_member": "Hide Book a Team Member Button",
|
||||
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
|
||||
"danger_zone": "Danger zone",
|
||||
"account_deletion_cannot_be_undone":"Careful. Account deletion cannot be undone.",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"cancel_all_remaining": "Cancel all remaining",
|
||||
|
@ -690,7 +688,6 @@
|
|||
"people": "People",
|
||||
"your_email": "Your Email",
|
||||
"change_avatar": "Change Avatar",
|
||||
"upload_avatar": "Upload Avatar",
|
||||
"language": "Language",
|
||||
"timezone": "Timezone",
|
||||
"first_day_of_week": "First Day of Week",
|
||||
|
@ -1296,7 +1293,7 @@
|
|||
"customize_your_brand_colors": "Customize your own brand colour into your booking page.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}.'",
|
||||
"profile_picture": "Profile Picture",
|
||||
"profile_picture": "Profile picture",
|
||||
"upload": "Upload",
|
||||
"add_profile_photo": "Add profile photo",
|
||||
"web3": "Web3",
|
||||
|
@ -1883,7 +1880,6 @@
|
|||
"edit_invite_link": "Edit link settings",
|
||||
"invite_link_copied": "Invite link copied",
|
||||
"invite_link_deleted": "Invite link deleted",
|
||||
"api_key_deleted":"API Key deleted",
|
||||
"invite_link_updated": "Invite link settings saved",
|
||||
"link_expires_after": "Links set to expire after...",
|
||||
"one_day": "1 day",
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { MoreHorizontal, Edit2, Trash } from "@calcom/ui/components/icon";
|
||||
|
||||
|
@ -35,31 +34,22 @@ 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 px-4 py-4 sm:px-6",
|
||||
lastItem ? "" : "border-subtle border-b"
|
||||
)}>
|
||||
className={classNames("flex w-full justify-between p-4", lastItem ? "" : "border-subtle border-b")}>
|
||||
<div>
|
||||
<div className="flex gap-1">
|
||||
<p className="text-sm font-semibold"> {apiKey?.note ? apiKey.note : t("api_key_no_note")}</p>
|
||||
<p className="font-medium"> {apiKey?.note ? apiKey.note : t("api_key_no_note")}</p>
|
||||
<div className="flex items-center space-x-3.5">
|
||||
{!neverExpires && isExpired && <Badge variant="red">{t("expired")}</Badge>}
|
||||
{!isExpired && <Badge variant="green">{t("active")}</Badge>}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center space-x-3.5">
|
||||
<p className="text-default text-sm">
|
||||
<p className="text-default text-xs">
|
||||
{" "}
|
||||
{neverExpires ? (
|
||||
<div className="flex flex-row space-x-3">{t("api_key_never_expires")}</div>
|
||||
<div className="text-subtle flex flex-row space-x-3">{t("api_key_never_expires")}</div>
|
||||
) : (
|
||||
`${isExpired ? t("expired") : t("expires")} ${dayjs(apiKey?.expiresAt?.toString()).fromNow()}`
|
||||
)}
|
||||
|
@ -81,8 +71,6 @@ 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="relative mt-10">
|
||||
<DialogFooter showDivider className="mt-10">
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
|
|
|
@ -6,20 +6,7 @@ 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 { 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>
|
||||
);
|
||||
};
|
||||
import { AppSkeletonLoader as SkeletonLoader, Meta, Alert } from "@calcom/ui";
|
||||
|
||||
export default function SSOConfiguration({ teamId }: { teamId: number | null }) {
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
@ -35,18 +22,14 @@ export default function SSOConfiguration({ teamId }: { teamId: number | null })
|
|||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <SkeletonLoader title={t("sso_configuration")} description={t("sso_configuration_description")} />;
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<>
|
||||
<Meta
|
||||
title={t("sso_configuration")}
|
||||
description={t("sso_configuration_description")}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<Alert severity="warning" message={t(errorMessage)} className="mt-4" />
|
||||
<Meta title={t("sso_configuration")} description={t("saml_description")} />
|
||||
<Alert severity="warning" message={t(errorMessage)} className="mb-4 " />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -55,7 +38,7 @@ export default function SSOConfiguration({ teamId }: { teamId: number | null })
|
|||
if (!connection) {
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<div className="[&>*]:border-subtle flex flex-col [&>*:last-child]:rounded-b-xl [&>*]:border [&>*]:border-t-0 [&>*]:px-4 [&>*]:py-6 [&>*]:sm:px-6">
|
||||
<div className="flex flex-col space-y-10">
|
||||
<SAMLConnection teamId={teamId} connection={null} />
|
||||
<OIDCConnection teamId={teamId} connection={null} />
|
||||
</div>
|
||||
|
@ -65,7 +48,7 @@ export default function SSOConfiguration({ teamId }: { teamId: number | null })
|
|||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<div className="[&>*]:border-subtle flex flex-col [&>*:last-child]:rounded-b-xl [&>*]:border [&>*]:border-t-0 [&>*]:px-4 [&>*]:py-6 [&>*]:sm:px-6">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{connection.type === "saml" ? (
|
||||
<SAMLConnection teamId={teamId} connection={connection} />
|
||||
) : (
|
||||
|
|
|
@ -19,12 +19,8 @@ const SAMLSSO = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-default w-full sm:mx-0">
|
||||
<Meta
|
||||
title={t("sso_configuration")}
|
||||
description={t("sso_configuration_description")}
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<div className="bg-default w-full sm:mx-0 xl:mt-0">
|
||||
<Meta title={t("sso_configuration")} description={t("sso_configuration_description")} />
|
||||
<SSOConfiguration teamId={null} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,8 +12,6 @@ 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;
|
||||
|
@ -32,8 +30,6 @@ type BookerLayoutSelectorProps = {
|
|||
* to this boolean.
|
||||
*/
|
||||
isDark?: boolean;
|
||||
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const defaultFieldName = "metadata.bookerLayouts";
|
||||
|
@ -44,7 +40,6 @@ export const BookerLayoutSelector = ({
|
|||
name,
|
||||
fallbackToUserSettings,
|
||||
isDark,
|
||||
isDisabled = false,
|
||||
}: BookerLayoutSelectorProps) => {
|
||||
const { control, getValues } = useFormContext();
|
||||
const { t } = useLocale();
|
||||
|
@ -56,12 +51,10 @@ export const BookerLayoutSelector = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<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>
|
||||
<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
|
||||
|
@ -69,19 +62,12 @@ export const BookerLayoutSelector = ({
|
|||
control={shouldShowUserSettings ? undefined : control}
|
||||
name={name || defaultFieldName}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<BookerLayoutFields
|
||||
showUserSettings={shouldShowUserSettings}
|
||||
settings={value}
|
||||
onChange={onChange}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<SectionBottomActions align="end">
|
||||
<Button type="submit" disabled={isDisabled} color="primary">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</>
|
||||
<BookerLayoutFields
|
||||
showUserSettings={shouldShowUserSettings}
|
||||
settings={value}
|
||||
onChange={onChange}
|
||||
isDark={isDark}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
|
@ -155,7 +141,7 @@ const BookerLayoutFields = ({ settings, onChange, showUserSettings, isDark }: Bo
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="border-subtle space-y-5 border-x px-6 py-8">
|
||||
<div className="my-4 space-y-5">
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-col gap-5 transition-opacity sm:flex-row sm:gap-3",
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
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 lg:max-w-4xl">
|
||||
<div className="mx-auto max-w-full justify-center md:max-w-4xl">
|
||||
<ShellHeader />
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||
|
@ -676,40 +676,33 @@ type SidebarContainerElementProps = {
|
|||
|
||||
export const getLayout = (page: React.ReactElement) => <SettingsLayout>{page}</SettingsLayout>;
|
||||
|
||||
export function ShellHeader() {
|
||||
function ShellHeader() {
|
||||
const { meta } = useMeta();
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<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" />
|
||||
)}
|
||||
<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>
|
||||
</header>
|
||||
</>
|
||||
<div className="ms-auto flex-shrink-0">{meta.CTA}</div>
|
||||
</div>
|
||||
</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-2 py-4 md:py-12 lg:px-6">
|
||||
<div className="max-w-full px-4 py-4 md:py-8 lg:px-12">
|
||||
<ErrorBoundary>
|
||||
{!props.withoutMain ? <ShellMain {...props}>{props.children}</ShellMain> : props.children}
|
||||
</ErrorBoundary>
|
||||
|
|
|
@ -5,9 +5,18 @@ 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 } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Label,
|
||||
Select,
|
||||
Switch,
|
||||
TextArea,
|
||||
TextField,
|
||||
ToggleGroup,
|
||||
DialogFooter,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import SectionBottomActions from "../../settings/SectionBottomActions";
|
||||
import customTemplate, { hasTemplateIntegration } from "../lib/integrationTemplate";
|
||||
import WebhookTestDisclosure from "./WebhookTestDisclosure";
|
||||
|
||||
|
@ -78,7 +87,7 @@ const WebhookForm = (props: {
|
|||
|
||||
const [useCustomTemplate, setUseCustomTemplate] = useState(false);
|
||||
const [newSecret, setNewSecret] = useState("");
|
||||
const [changeSecret, setChangeSecret] = useState<boolean>(false);
|
||||
const [changeSecret, setChangeSecret] = useState(false);
|
||||
const hasSecretKey = !!props?.webhook?.secret;
|
||||
// const currentSecret = props?.webhook?.secret;
|
||||
|
||||
|
@ -89,10 +98,10 @@ const WebhookForm = (props: {
|
|||
}, [changeSecret, formMethods]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => props.onSubmit({ ...values, changeSecret, newSecret })}>
|
||||
<div className="border-subtle border-x p-6">
|
||||
<>
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => props.onSubmit({ ...values, changeSecret, newSecret })}>
|
||||
<Controller
|
||||
name="subscriberUrl"
|
||||
control={formMethods.control}
|
||||
|
@ -106,12 +115,10 @@ const WebhookForm = (props: {
|
|||
required
|
||||
type="url"
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("subscriberUrl", e?.target.value, { shouldDirty: true });
|
||||
formMethods.setValue("subscriberUrl", e?.target.value);
|
||||
if (hasTemplateIntegration({ url: e.target.value })) {
|
||||
setUseCustomTemplate(true);
|
||||
formMethods.setValue("payloadTemplate", customTemplate({ url: e.target.value }), {
|
||||
shouldDirty: true,
|
||||
});
|
||||
formMethods.setValue("payloadTemplate", customTemplate({ url: e.target.value }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -122,13 +129,13 @@ const WebhookForm = (props: {
|
|||
name="active"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<div className="font-sm text-emphasis mt-6 font-medium">
|
||||
<div className="font-sm text-emphasis mt-8 font-medium">
|
||||
<Switch
|
||||
label={t("enable_webhook")}
|
||||
checked={value}
|
||||
// defaultChecked={props?.webhook?.active ? props?.webhook?.active : true}
|
||||
onCheckedChange={(value) => {
|
||||
formMethods.setValue("active", value, { shouldDirty: true });
|
||||
formMethods.setValue("active", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -140,8 +147,8 @@ const WebhookForm = (props: {
|
|||
render={({ field: { onChange, value } }) => {
|
||||
const selectValue = translatedTriggerOptions.filter((option) => value.includes(option.value));
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<Label className="font-sm text-emphasis font-medium">
|
||||
<div className="mt-8">
|
||||
<Label className="font-sm text-emphasis mt-8 font-medium">
|
||||
<>{t("event_triggers")}</>
|
||||
</Label>
|
||||
<Select
|
||||
|
@ -160,7 +167,7 @@ const WebhookForm = (props: {
|
|||
name="secret"
|
||||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<div className="mt-6">
|
||||
<div className="mt-8 ">
|
||||
{!!hasSecretKey && !changeSecret && (
|
||||
<>
|
||||
<Label className="font-sm text-emphasis font-medium">Secret</Label>
|
||||
|
@ -211,7 +218,7 @@ const WebhookForm = (props: {
|
|||
labelClassName="font-medium text-emphasis font-sm"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("secret", e?.target.value, { shouldDirty: true });
|
||||
formMethods.setValue("secret", e?.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -224,7 +231,7 @@ const WebhookForm = (props: {
|
|||
control={formMethods.control}
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Label className="font-sm text-emphasis mt-6">
|
||||
<Label className="font-sm text-emphasis mt-8">
|
||||
<>{t("payload_template")}</>
|
||||
</Label>
|
||||
<div className="mb-2">
|
||||
|
@ -232,7 +239,7 @@ const WebhookForm = (props: {
|
|||
onValueChange={(val) => {
|
||||
if (val === "default") {
|
||||
setUseCustomTemplate(false);
|
||||
formMethods.setValue("payloadTemplate", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("payloadTemplate", undefined);
|
||||
} else {
|
||||
setUseCustomTemplate(true);
|
||||
}
|
||||
|
@ -251,34 +258,33 @@ const WebhookForm = (props: {
|
|||
rows={3}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
formMethods.setValue("payloadTemplate", e?.target.value, { shouldDirty: true });
|
||||
formMethods.setValue("payloadTemplate", e?.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
<div className="bg-subtle mt-8 rounded-md p-6">
|
||||
<WebhookTestDisclosure />
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-md">
|
||||
<WebhookTestDisclosure />
|
||||
</div>
|
||||
</Form>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -17,10 +17,10 @@ export default function WebhookTestDisclosure() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="border-subtle flex justify-between rounded-t-xl border p-6">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<p className="text-emphasis text-sm font-semibold leading-5">{t("webhook_test")}</p>
|
||||
<p className="text-default text-sm">{t("test_webhook")}</p>
|
||||
<p className="text-default mb-4 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-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="border-subtle space-y-0 rounded-md border sm:mx-0">
|
||||
<div className="flex justify-between border-b 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="bg-muted border-subtle rounded-b-xl border border-t-0 p-4 font-mono text-[13px] leading-4 ">
|
||||
<div className="text-inverted bg-inverted rounded-b-md p-4 font-mono text-[13px] leading-4">
|
||||
{!mutation.data && <p>{t("no_data_yet")}</p>}
|
||||
{mutation.status === "success" && (
|
||||
<div className="overflow-x-auto">{JSON.stringify(mutation.data, null, 4)}</div>
|
||||
<div className="text-inverted 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 })}
|
||||
borderInShellHeader={true}
|
||||
backButton
|
||||
/>
|
||||
<WebhookForm
|
||||
noRoutingFormTriggers={false}
|
||||
|
@ -80,7 +80,7 @@ function Component({ webhookId }: { webhookId: string }) {
|
|||
}
|
||||
|
||||
if (values.changeSecret) {
|
||||
values.secret = values.newSecret.trim().length ? values.newSecret : null;
|
||||
values.secret = values.newSecret.length ? values.newSecret : null;
|
||||
}
|
||||
|
||||
if (!values.payloadTemplate) {
|
||||
|
|
|
@ -4,24 +4,13 @@ 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, SkeletonText } from "@calcom/ui";
|
||||
import { Meta, showToast, SkeletonContainer } 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();
|
||||
|
@ -82,13 +71,7 @@ const NewWebhookView = () => {
|
|||
});
|
||||
};
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<SkeletonLoader
|
||||
title={t("add_webhook")}
|
||||
description={t("add_webhook_description", { appName: APP_NAME })}
|
||||
/>
|
||||
);
|
||||
if (isLoading) return <SkeletonContainer />;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -96,7 +79,6 @@ const NewWebhookView = () => {
|
|||
title={t("add_webhook")}
|
||||
description={t("add_webhook_description", { appName: APP_NAME })}
|
||||
backButton
|
||||
borderInShellHeader={true}
|
||||
/>
|
||||
<WebhookForm
|
||||
noRoutingFormTriggers={false}
|
||||
|
|
|
@ -1,73 +1,36 @@
|
|||
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,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
} from "@calcom/ui";
|
||||
import { Avatar, CreateButtonWithTeamsList, EmptyScreen, Meta } from "@calcom/ui";
|
||||
import { Link as LinkIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
import { getLayout } from "../../settings/layouts/SettingsLayout";
|
||||
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>
|
||||
);
|
||||
};
|
||||
import { WebhookListItem, WebhookListSkeleton } from "../components";
|
||||
|
||||
const WebhooksView = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
|
||||
const { data, isLoading } = trpc.viewer.webhook.getByViewer.useQuery(undefined, {
|
||||
const { data } = trpc.viewer.webhook.getByViewer.useQuery(undefined, {
|
||||
suspense: true,
|
||||
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={t("webhooks")}
|
||||
title="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}` : ""}`);
|
||||
|
@ -78,10 +41,11 @@ const WebhooksView = () => {
|
|||
<></>
|
||||
)
|
||||
}
|
||||
borderInShellHeader={!(data && data.webhookGroups.length > 0)}
|
||||
/>
|
||||
<div>
|
||||
<WebhooksList webhooksByViewer={data} />
|
||||
<Suspense fallback={<WebhookListSkeleton />}>
|
||||
{data && <WebhooksList webhooksByViewer={data} />}
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -101,11 +65,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`}
|
||||
|
@ -118,11 +82,7 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
|
|||
</div>
|
||||
)}
|
||||
<div className="flex flex-col" key={group.profile.slug}>
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle rounded-md rounded-t-none border border-t-0",
|
||||
hasTeams && "mb-8 mt-3 rounded-t-md border-t"
|
||||
)}>
|
||||
<div className="border-subtle mb-8 mt-3 rounded-md border">
|
||||
{group.webhooks.map((webhook, index) => (
|
||||
<WebhookListItem
|
||||
key={webhook.id}
|
||||
|
@ -138,14 +98,13 @@ 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()}
|
||||
|
|
|
@ -64,9 +64,6 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
|||
if (input.avatar) {
|
||||
data.avatar = await resizeBase64Image(input.avatar);
|
||||
}
|
||||
if (input.avatar === null) {
|
||||
data.avatar = null;
|
||||
}
|
||||
|
||||
if (isPremiumUsername) {
|
||||
const stripeCustomerId = userMetadata?.stripeCustomerId;
|
||||
|
|
|
@ -13,7 +13,7 @@ export const ZUpdateProfileInputSchema = z.object({
|
|||
name: z.string().max(FULL_NAME_LENGTH_MAX_LIMIT).optional(),
|
||||
email: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
avatar: z.string().optional(),
|
||||
timeZone: z.string().optional(),
|
||||
weekStart: z.string().optional(),
|
||||
hideBranding: z.boolean().optional(),
|
||||
|
|
|
@ -2,7 +2,6 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|||
|
||||
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { ButtonColor } from "@calcom/ui";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
|
@ -31,7 +30,6 @@ export type CreateBtnProps = {
|
|||
isLoading?: boolean;
|
||||
disableMobileButton?: boolean;
|
||||
"data-testid"?: string;
|
||||
color?: ButtonColor;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -51,7 +51,7 @@ function SettingsToggle({
|
|||
{title}
|
||||
{LockedIcon}
|
||||
</Label>
|
||||
{Badge && <div className="mb-2">{Badge}</div>}
|
||||
{Badge}
|
||||
</div>
|
||||
{description && <p className="text-default -mt-1.5 text-sm leading-normal">{description}</p>}
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,6 @@ import Cropper from "react-easy-crop";
|
|||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import type { ButtonColor } from "../..";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger, DialogFooter } from "../..";
|
||||
import { showToast } from "../toast";
|
||||
|
||||
|
@ -66,7 +65,6 @@ type ImageUploaderProps = {
|
|||
handleAvatarChange: (imageSrc: string) => void;
|
||||
imageSrc?: string;
|
||||
target: string;
|
||||
triggerButtonColor?: ButtonColor;
|
||||
};
|
||||
|
||||
interface FileEvent<T = Element> extends FormEvent<T> {
|
||||
|
@ -119,7 +117,6 @@ export default function ImageUploader({
|
|||
id,
|
||||
buttonMsg,
|
||||
handleAvatarChange,
|
||||
triggerButtonColor,
|
||||
...props
|
||||
}: ImageUploaderProps) {
|
||||
const { t } = useLocale();
|
||||
|
@ -172,7 +169,7 @@ export default function ImageUploader({
|
|||
(opened) => !opened && setFile(null) // unset file on close
|
||||
}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color={triggerButtonColor ?? "secondary"} type="button" className="py-1 text-sm">
|
||||
<Button color="secondary" type="button" className="py-1 text-sm">
|
||||
{buttonMsg}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
|
|
@ -18,7 +18,7 @@ export function List(props: ListProps) {
|
|||
data-testid="list"
|
||||
{...props}
|
||||
className={classNames(
|
||||
"mx-0 rounded-sm sm:overflow-hidden ",
|
||||
"-mx-4 rounded-sm sm:mx-0 sm:overflow-hidden ",
|
||||
// Add rounded top and bottome if roundContainer is true
|
||||
props.roundContainer && "[&>*:first-child]:rounded-t-md [&>*:last-child]:rounded-b-md ",
|
||||
!props.noBorderTreatment &&
|
||||
|
|
|
@ -9,7 +9,6 @@ type MetaType = {
|
|||
description: string;
|
||||
backButton?: boolean;
|
||||
CTA?: ReactNode;
|
||||
borderInShellHeader?: boolean;
|
||||
};
|
||||
|
||||
const initialMeta: MetaType = {
|
||||
|
@ -17,7 +16,6 @@ const initialMeta: MetaType = {
|
|||
description: "",
|
||||
backButton: false,
|
||||
CTA: null,
|
||||
borderInShellHeader: true,
|
||||
};
|
||||
|
||||
const MetaContext = createContext({
|
||||
|
@ -46,13 +44,13 @@ export function MetaProvider({ children }: { children: ReactNode }) {
|
|||
* elsewhere (ie. on a Heading, Title, Subtitle, etc.)
|
||||
* @example <Meta title="Password" description="Manage settings for your account passwords" />
|
||||
*/
|
||||
export default function Meta({ title, description, backButton, CTA, borderInShellHeader }: MetaType) {
|
||||
export default function Meta({ title, description, backButton, CTA }: MetaType) {
|
||||
const { setMeta, meta } = useMeta();
|
||||
|
||||
/* @TODO: maybe find a way to have this data on first render to prevent flicker */
|
||||
useEffect(() => {
|
||||
if (meta.title !== title || meta.description !== description || meta.CTA !== CTA) {
|
||||
setMeta({ title, description, backButton, CTA, borderInShellHeader });
|
||||
setMeta({ title, description, backButton, CTA });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [title, description, backButton, CTA]);
|
||||
|
|
Loading…
Reference in New Issue