diff --git a/apps/web/components/AppListCard.tsx b/apps/web/components/AppListCard.tsx index 7252a8ffc66823..b41cddd1bef7bb 100644 --- a/apps/web/components/AppListCard.tsx +++ b/apps/web/components/AppListCard.tsx @@ -79,8 +79,8 @@ export default function AppListCard(props: AppListCardProps) { }, []); return ( -
-
+
+
{logo ? ( 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 ( <> - + {cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))} {data.items .filter((item) => item.invalidCredentialIds) diff --git a/apps/web/pages/settings/billing/index.tsx b/apps/web/pages/settings/billing/index.tsx index acf3b30578aa72..2f3f2a9a6faa4e 100644 --- a/apps/web/pages/settings/billing/index.tsx +++ b/apps/web/pages/settings/billing/index.tsx @@ -22,11 +22,12 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => { <>
-

{title}

+

{title}

{description}

{children}
+
); }; @@ -44,16 +45,14 @@ const BillingView = () => { return ( <> - -
+ +
-
- ); }; - if (isLoading || !data) { - return ( - - ); - } - return ( <> } - borderInShellHeader={true} /> -
- {data?.length ? ( - <> -
- {data.map((apiKey, index) => ( - { - setApiKeyToEdit(apiKey); - setApiKeyModal(true); - }} - /> - ))} -
- - ) : ( - } - /> - )} -
+ <> + {isLoading && } +
+ {isLoading ? null : data?.length ? ( + <> +
+ {data.map((apiKey, index) => ( + { + setApiKeyToEdit(apiKey); + setApiKeyModal(true); + }} + /> + ))} +
+ + + ) : ( + } + /> + )} +
+
diff --git a/apps/web/pages/settings/my-account/appearance.tsx b/apps/web/pages/settings/my-account/appearance.tsx index 43ec340a979ab4..1b2269824b57f8 100644 --- a/apps/web/pages/settings/my-account/appearance.tsx +++ b/apps/web/pages/settings/my-account/appearance.tsx @@ -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 ( - -
-
+ +
+
@@ -47,83 +44,49 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
-
-
- - - + +
); }; -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({ - defaultValues: { - theme: user.theme, - }, - }); - - const { - formState: { isSubmitting: isUserThemeSubmitting, isDirty: isUserThemeDirty }, - reset: resetUserThemeReset, - } = userThemeFormMethods; - - const bookerLayoutFormMethods = useForm({ - defaultValues: { - metadata: user.metadata as z.infer, - }, - }); - const { - formState: { isSubmitting: isBookerLayoutFormSubmitting, isDirty: isBookerLayoutFormDirty }, - reset: resetBookerLayoutThemeReset, - } = bookerLayoutFormMethods; + const { isLoading: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan(); - const brandColorsFormMethods = useForm({ + const formMethods = useForm({ defaultValues: { - brandColor: user.brandColor || DEFAULT_LIGHT_BRAND_COLOR, - darkBrandColor: user.darkBrandColor || DEFAULT_DARK_BRAND_COLOR, + theme: user?.theme, + brandColor: user?.brandColor || "#292929", + darkBrandColor: user?.darkBrandColor || "#fafafa", + hideBranding: user?.hideBranding, + metadata: user?.metadata as z.infer, }, }); - 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 ; + + if (!user) return null; + + const isDisabled = isSubmitting || !isDirty; + return ( -
- -
+
{ + 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, + }); + }}> + +
-

{t("theme")}

+

{t("theme")}

{t("theme_applies_note")}

- { - mutation.mutate({ - // Radio values don't support null as values, therefore we convert an empty string - // back to null here. - theme: values.theme || null, - }); - }}> -
- - - -
- - - -
- -
{ - const layoutError = validateBookerLayouts(values?.metadata?.defaultBookerLayouts || null); - if (layoutError) { - showToast(t(layoutError), "error"); - return; - } else { - mutation.mutate(values); - } - }}> - + + - + +
-
{ - mutation.mutate(values); - }}> -
- { - 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" - )}> -
- + + +
+
+
+

{t("custom_brand_colors")}

+

{t("customize_your_brand_colors")}

+
+
+ +
+ ( +
+

{t("light_brand_color")}

+ ( -
-

{t("light_brand_color")}

- { - try { - checkWCAGContrastColor("#ffffff", value); - setLightModeError(false); - brandColorsFormMethods.setValue("brandColor", value, { shouldDirty: true }); - } catch (err) { - setLightModeError(false); - } - }} - /> - {lightModeError ? ( -
- -
- ) : null} -
- )} + resetDefaultValue="#292929" + onChange={(value) => { + if (!checkWCAGContrastColor("#ffffff", value)) { + setLightModeError(true); + } else { + setLightModeError(false); + } + formMethods.setValue("brandColor", value, { shouldDirty: true }); + }} /> - - + )} + /> + ( +
+

{t("dark_brand_color")}

+ ( -
-

{t("dark_brand_color")}

- { - try { - checkWCAGContrastColor("#101010", value); - setDarkModeError(false); - brandColorsFormMethods.setValue("darkBrandColor", value, { shouldDirty: true }); - } catch (err) { - setDarkModeError(true); - } - }} - /> - {darkModeError ? ( -
- -
- ) : null} -
- )} + resetDefaultValue="#fafafa" + onChange={(value) => { + if (!checkWCAGContrastColor("#101010", value)) { + setDarkModeError(true); + } else { + setDarkModeError(false); + } + formMethods.setValue("darkBrandColor", value, { shouldDirty: true }); + }} />
- - - - + )} + /> +
+ {darkModeError ? ( +
+
- - + ) : null} + {lightModeError ? ( +
+ +
+ ) : null} {/* TODO future PR to preview brandColors */} {/* */} - - } - onCheckedChange={(checked) => { - setHideBrandingValue(checked); - mutation.mutate({ hideBranding: checked }); - }} - switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6" +
+ ( + <> +
+
+
+

+ {t("disable_cal_branding", { appName: APP_NAME })} +

+ +
+

{t("removes_cal_branding", { appName: APP_NAME })}

+
+
+ + formMethods.setValue("hideBranding", checked, { shouldDirty: true }) + } + checked={hasPaidPlan ? value : false} + /> +
+
+ + )} /> -
+ + ); }; -const AppearanceViewWrapper = () => { - const { data: user, isLoading } = trpc.viewer.me.useQuery(); - const { isLoading: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan(); - - const { t } = useLocale(); - - if (isLoading || isTeamPlanStatusLoading || !user) - return ; - - return ; -}; - -AppearanceViewWrapper.getLayout = getLayout; -AppearanceViewWrapper.PageWrapper = PageWrapper; +AppearanceView.getLayout = getLayout; +AppearanceView.PageWrapper = PageWrapper; -export default AppearanceViewWrapper; +export default AppearanceView; diff --git a/apps/web/pages/settings/my-account/calendars.tsx b/apps/web/pages/settings/my-account/calendars.tsx index 71ca36af79fc2e..f9c630d4738daf 100644 --- a/apps/web/pages/settings/my-account/calendars.tsx +++ b/apps/web/pages/settings/my-account/calendars.tsx @@ -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 ( -
+
- +
); @@ -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 ( <> - } - borderInShellHeader={false} - /> + } /> } success={({ data }) => { - const isDestinationUpdateBtnDisabled = - selectedDestinationCalendarOption?.externalId === query?.data?.destinationCalendar?.externalId; return data.connectedCalendars.length ? (
-
-

- {t("add_to_calendar")} -

-

{t("add_to_calendar_description")}

+
+
+ +
+ +
+
+

+ {t("add_to_calendar")} +

+

+ + Where to add events when you re booked. You can override this on a per-event basis in + advanced settings in the event type. + +

+
+ +
-
- { - setSelectedDestinationCalendar(option); - }} - isLoading={mutation.isLoading} - /> -
- - - - -
-

- {t("check_for_conflicts")} -

-

{t("select_calendars")}

-
- - +

+ {t("check_for_conflicts")} +

+

{t("select_calendars")}

+ {data.connectedCalendars.map((item) => ( {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" /> ); }} diff --git a/apps/web/pages/settings/my-account/conferencing.tsx b/apps/web/pages/settings/my-account/conferencing.tsx index 96173690377aa1..be48afc6ca352c 100644 --- a/apps/web/pages/settings/my-account/conferencing.tsx +++ b/apps/web/pages/settings/my-account/conferencing.tsx @@ -15,8 +15,8 @@ import { AppList } from "@components/apps/AppList"; const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { return ( - -
+ +
@@ -28,9 +28,11 @@ const AddConferencingButton = () => { const { t } = useLocale(); return ( - + <> + + ); }; @@ -70,7 +72,6 @@ const ConferencingLayout = () => { title={t("conferencing")} description={t("conferencing_description")} CTA={} - borderInShellHeader={true} /> { color="secondary" data-testid="connect-conferencing-apps" href="/apps/categories/conferencing"> - {t("connect_conference_apps")} + {t("connect_conferencing_apps")} } /> ); } - return ( - - ); + return ; }} />
diff --git a/apps/web/pages/settings/my-account/general.tsx b/apps/web/pages/settings/my-account/general.tsx index 131ce856b47b86..688a79ff6a1c41 100644 --- a/apps/web/pages/settings/my-account/general.tsx +++ b/apps/web/pages/settings/my-account/general.tsx @@ -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 ( - -
+ +
- +
); @@ -61,7 +59,6 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => { const utils = trpc.useContext(); const { t } = useLocale(); const { update } = useSession(); - const [isUpdateBtnLoading, setIsUpdateBtnLoading] = useState(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 ( -
-
{ - setIsUpdateBtnLoading(true); - mutation.mutate({ - ...values, - locale: values.locale.value, - timeFormat: values.timeFormat.value, - weekStart: values.weekStart.value, - }); - }}> - -
- ( - <> - - - className="capitalize" - options={localeOptions} - value={value} - onChange={onChange} - /> - - )} - /> - ( - <> - - { - if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true }); - }} - /> - - )} - /> - ( - <> - - { - if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true }); - }} - /> - - )} - /> -
- - - - -
- - { - setIsAllowDynamicBookingChecked(checked); - mutation.mutate({ allowDynamicBooking: checked }); - }} - switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6" +
{ + mutation.mutate({ + ...values, + locale: values.locale.value, + timeFormat: values.timeFormat.value, + weekStart: values.weekStart.value, + }); + }}> + + ( + <> + + + className="capitalize" + options={localeOptions} + value={value} + onChange={onChange} + /> + + )} /> - - { - setIsAllowSEOIndexingChecked(checked); - mutation.mutate({ allowSEOIndexing: checked }); - }} - switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6" + ( + <> + + { + if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true }); + }} + /> + + )} /> - - { - setIsReceiveMonthlyDigestEmailChecked(checked); - mutation.mutate({ receiveMonthlyDigestEmail: checked }); - }} - switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6" + ( + <> + + { + if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true }); + }} + /> + + )} + /> +
+ ( + { + formMethods.setValue("allowDynamicBooking", checked, { shouldDirty: true }); + }} + /> + )} + /> +
+ +
+ ( + { + formMethods.setValue("allowSEOIndexing", checked, { shouldDirty: true }); + }} + /> + )} + /> +
+ +
+ ( + { + formMethods.setValue("receiveMonthlyDigestEmail", checked, { shouldDirty: true }); + }} + /> + )} + /> +
+ + + ); }; diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx index f2c86de11b7a03..2b738408e5434b 100644 --- a/apps/web/pages/settings/my-account/profile.tsx +++ b/apps/web/pages/settings/my-account/profile.tsx @@ -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 ( - -
+ +
@@ -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(); - - 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 ( ); @@ -233,17 +219,11 @@ const ProfileView = () => { return ( <> - + { if (values.email !== user.email && isCALIdentityProvider) { @@ -258,7 +238,7 @@ const ProfileView = () => { } }} extraField={ -
+
{ showToast(t("settings_updated_successfully"), "success"); @@ -272,19 +252,16 @@ const ProfileView = () => { } /> -
- -

{t("account_deletion_cannot_be_undone")}

-
+
+ + {/* Delete account Dialog */} - - - - - + + + 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 (
-
-
- { - const showRemoveAvatarButton = !isFallbackImg || (value && userAvatar !== value); - return ( - <> - -
-

{t("profile_picture")}

-
- { - formMethods.setValue("avatar", newAvatar, { shouldDirty: true }); - }} - imageSrc={value || undefined} - triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"} - /> - - {showRemoveAvatarButton && ( - - )} -
-
- - ); - }} - /> -
- {extraField} -
- -
-
- -
-
- - md.render(formMethods.getValues("bio") || "")} - setText={(value: string) => { - formMethods.setValue("bio", turndown(value), { shouldDirty: true }); - }} - excludedToolbarItems={["blockType"]} - disableLists - firstRender={firstRender} - setFirstRender={setFirstRender} - /> -
+
+ ( + <> + +
+ { + formMethods.setValue("avatar", newAvatar, { shouldDirty: true }); + }} + imageSrc={value || undefined} + /> +
+ + )} + /> +
+ {extraField} +
+ +
+
+ +
+
+ + md.render(formMethods.getValues("bio") || "")} + setText={(value: string) => { + formMethods.setValue("bio", turndown(value), { shouldDirty: true }); + }} + excludedToolbarItems={["blockType"]} + disableLists + firstRender={firstRender} + setFirstRender={setFirstRender} + />
- - - + ); }; diff --git a/apps/web/pages/settings/security/impersonation.tsx b/apps/web/pages/settings/security/impersonation.tsx index d3afab267ee364..fbe31be02ba233 100644 --- a/apps/web/pages/settings/security/impersonation.tsx +++ b/apps/web/pages/settings/security/impersonation.tsx @@ -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 ( - - -
- -
-
- ); -}; +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( - 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 ( <> - -
- { - mutation.mutate({ disableImpersonation: !checked }); - }} - disabled={mutation.isLoading} - switchContainerClassName="py-6 px-4 sm:px-6 border-subtle rounded-b-xl border border-t-0" - /> -
+ +
{ + mutation.mutate({ disableImpersonation }); + }}> +
+ { + setValue("disableImpersonation", !e, { shouldDirty: true }); + }} + fitToHeight={true} + checked={!watch("disableImpersonation")} + /> +
+ + {t("user_impersonation_heading")} + + + {t("user_impersonation_description")} + +
+
+ +
); }; -const ProfileImpersonationViewWrapper = () => { - const { data: user, isLoading } = trpc.viewer.me.useQuery(); - const { t } = useLocale(); +ProfileImpersonationView.getLayout = getLayout; +ProfileImpersonationView.PageWrapper = PageWrapper; - if (isLoading || !user) - return ; - - return ; +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; diff --git a/apps/web/pages/settings/security/password.tsx b/apps/web/pages/settings/security/password.tsx index 71077c944725ac..6da897b4301cb4 100644 --- a/apps/web/pages/settings/security/password.tsx +++ b/apps/web/pages/settings/security/password.tsx @@ -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 ( - - -
- - - -
-
- - - -
-
- ); -}; - -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(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 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 sessionTimeoutWatch = formMethods.watch("sessionTimeout"); + const handleSubmit = (values: ChangePasswordSessionFormValues) => { + 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 ( <> - + {user && user.identityProvider !== IdentityProvider.CAL ? (
@@ -180,127 +130,87 @@ const PasswordView = ({ user }: PasswordViewProps) => {
) : (
-
- {formMethods.formState.errors.apiError && ( -
- -
- )} -
-
- -
-
- -
+ {formMethods.formState.errors.apiError && ( +
+ +
+ )} + +
+
+ +
+
+
-

- {t("invalid_password_hint", { passwordLength: passwordMinLength })} -

- - - -
+

+ {t("invalid_password_hint", { passwordLength: passwordMinLength })} +

+
{ 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" - )}> - <> -
-
-

{t("session_timeout_after")}

- 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")} - - - - + /> +
+
+ )}
+ {/* TODO: Why is this Form not submitting? Hacky fix but works */} + )} ); }; -const PasswordViewWrapper = () => { - const { data: user, isLoading } = trpc.viewer.me.useQuery(); - const { t } = useLocale(); - if (isLoading || !user) - return ; - - return ; -}; - -PasswordViewWrapper.getLayout = getLayout; -PasswordViewWrapper.PageWrapper = PageWrapper; +PasswordView.getLayout = getLayout; +PasswordView.PageWrapper = PageWrapper; -export default PasswordViewWrapper; +export default PasswordView; diff --git a/apps/web/pages/settings/security/two-factor-auth.tsx b/apps/web/pages/settings/security/two-factor-auth.tsx index 7cd47f33175a37..fd5c1a7e202d69 100644 --- a/apps/web/pages/settings/security/two-factor-auth.tsx +++ b/apps/web/pages/settings/security/two-factor-auth.tsx @@ -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 ( -
@@ -37,34 +28,36 @@ const TwoFactorAuthView = () => { const { t } = useLocale(); const { data: user, isLoading } = trpc.viewer.me.useQuery(); - const [enableModalOpen, setEnableModalOpen] = useState(false); - const [disableModalOpen, setDisableModalOpen] = useState(false); + const [enableModalOpen, setEnableModalOpen] = useState(false); + const [disableModalOpen, setDisableModalOpen] = useState(false); - if (isLoading) - return ; + if (isLoading) return ; const isCalProvider = user?.identityProvider === "CAL"; const canSetupTwoFactor = !isCalProvider && !user?.twoFactorEnabled; return ( <> - + {canSetupTwoFactor && } - - user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true) - } - Badge={ - - {user?.twoFactorEnabled ? t("enabled") : t("disabled")} - - } - switchContainerClassName="border-subtle rounded-b-xl border border-t-0 px-5 py-6 sm:px-6" - /> +
+ + user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true) + } + /> +
+
+

{t("two_factor_auth")}

+ + {user?.twoFactorEnabled ? t("enabled") : t("disabled")} + +
+

{t("add_an_extra_layer_of_security")}

+
+
+ className={classNames("flex w-full justify-between p-4", lastItem ? "" : "border-subtle border-b")}>
-
-

{apiKey?.note ? apiKey.note : t("api_key_no_note")}

+

{apiKey?.note ? apiKey.note : t("api_key_no_note")}

+
{!neverExpires && isExpired && {t("expired")}} {!isExpired && {t("active")}} -
-
-

+

+ {" "} {neverExpires ? ( -

{t("api_key_never_expires")}
+
{t("api_key_never_expires")}
) : ( `${isExpired ? t("expired") : t("expires")} ${dayjs(apiKey?.expiresAt?.toString()).fromNow()}` )} @@ -81,8 +71,6 @@ const ApiKeyListItem = ({ deleteApiKey.mutate({ id: apiKey.id, diff --git a/packages/features/ee/sso/components/OIDCConnection.tsx b/packages/features/ee/sso/components/OIDCConnection.tsx index daef6902a77ba2..245b651c19e308 100644 --- a/packages/features/ee/sso/components/OIDCConnection.tsx +++ b/packages/features/ee/sso/components/OIDCConnection.tsx @@ -145,7 +145,7 @@ const CreateConnectionDialog = ({ )} />
- + - - + )} /> @@ -155,7 +141,7 @@ const BookerLayoutFields = ({ settings, onChange, showUserSettings, isDark }: Bo }; return ( -
+
{ - return ( -
- {children} -
- ); -}; - -export default SectionBottomActions; diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx index 755300bc99b431..823a1c0ec3695a 100644 --- a/packages/features/settings/layouts/SettingsLayout.tsx +++ b/packages/features/settings/layouts/SettingsLayout.tsx @@ -633,7 +633,7 @@ export default function SettingsLayout({ setSideContainerOpen(!sideContainerOpen)} /> }>
-
+
}>{children} @@ -676,40 +676,33 @@ type SidebarContainerElementProps = { export const getLayout = (page: React.ReactElement) => {page}; -export function ShellHeader() { +function ShellHeader() { const { meta } = useMeta(); const { t, isLocaleReady } = useLocale(); return ( - <> -
-
- {meta.backButton && ( - - - +
+
+ {meta.backButton && ( + + + + )} +
+ {meta.title && isLocaleReady ? ( +

+ {t(meta.title)} +

+ ) : ( +
+ )} + {meta.description && isLocaleReady ? ( +

{t(meta.description)}

+ ) : ( +
)} -
- {meta.title && isLocaleReady ? ( -

- {t(meta.title)} -

- ) : ( -
- )} - {meta.description && isLocaleReady ? ( -

{t(meta.description)}

- ) : ( -
- )} -
-
{meta.CTA}
-
- +
{meta.CTA}
+
+
); } diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index ab32c2a5fc46b0..773a93c39d398e 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -1008,7 +1008,7 @@ function MainContainer({
{/* show top navigation for md and smaller (tablet and phones) */} {TopNavContainerProp} -
+
{!props.withoutMain ? {props.children} : props.children} diff --git a/packages/features/webhooks/components/WebhookForm.tsx b/packages/features/webhooks/components/WebhookForm.tsx index ca3e924fb066f6..3f34feabb819b9 100644 --- a/packages/features/webhooks/components/WebhookForm.tsx +++ b/packages/features/webhooks/components/WebhookForm.tsx @@ -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(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 ( -
props.onSubmit({ ...values, changeSecret, newSecret })}> -
+ <> + props.onSubmit({ ...values, changeSecret, newSecret })}> { - 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 } }) => ( -
+
{ - formMethods.setValue("active", value, { shouldDirty: true }); + formMethods.setValue("active", value); }} />
@@ -140,8 +147,8 @@ const WebhookForm = (props: { render={({ field: { onChange, value } }) => { const selectValue = translatedTriggerOptions.filter((option) => value.includes(option.value)); return ( -
-