Revert "refactor: Setting redesign (#11124)"

This reverts commit 4f3cf4f948.
bugfix/reinstate-settings-redesign
Alex van Andel 2023-09-29 13:37:06 +01:00
parent c22d406d12
commit 85a1713897
33 changed files with 783 additions and 1185 deletions

View File

@ -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")}

View File

@ -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)

View File

@ -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")}

View File

@ -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}>

View File

@ -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;

View File

@ -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"
/>
);
}}

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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;

View File

@ -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;

View File

@ -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}

View File

@ -288,7 +288,6 @@
"when": "When",
"where": "Where",
"add_to_calendar": "Add to calendar",
"add_to_calendar_description":"Select where to add events when youre 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",

View File

@ -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,

View File

@ -145,7 +145,7 @@ const CreateConnectionDialog = ({
)}
/>
</div>
<DialogFooter showDivider className="relative mt-10">
<DialogFooter showDivider className="mt-10">
<Button
type="button"
color="secondary"

View File

@ -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} />
) : (

View File

@ -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>
);

View File

@ -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",

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>
</>
);
};

View File

@ -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>

View File

@ -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) {

View File

@ -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}

View File

@ -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()}

View File

@ -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;

View File

@ -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(),

View File

@ -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;
};
/**

View File

@ -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>

View File

@ -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>

View File

@ -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 &&

View File

@ -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]);