refactor: org settings (#11682)

This commit is contained in:
Udit Takkar 2023-10-11 14:34:54 +05:30 committed by GitHub
parent 226ac889c7
commit ebc70feef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 653 additions and 402 deletions

View File

@ -45,7 +45,7 @@ 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">
<div className="border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-8 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")}

View File

@ -25,7 +25,7 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
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">
<div className="divide-subtle border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-4">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
</div>
@ -79,7 +79,7 @@ const ApiKeysView = () => {
<div>
{data?.length ? (
<>
<div className="border-subtle rounded-b-md border border-t-0">
<div className="border-subtle rounded-b-lg border border-t-0">
{data.map((apiKey, index) => (
<ApiKeyListItem
key={apiKey.id}
@ -98,7 +98,7 @@ const ApiKeysView = () => {
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"
className="rounded-b-lg rounded-t-none border-t-0"
buttonRaw={<NewApiKeyButton />}
/>
)}

View File

@ -6,8 +6,8 @@ import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSele
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 { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -35,7 +35,10 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
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="border-subtle mt-6 flex items-center rounded-t-xl border p-6 text-sm">
<SkeletonText className="h-8 w-1/3" />
</div>
<div className="border-subtle space-y-6 border-x px-4 py-6 sm:px-6">
<div className="flex items-center justify-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" />
@ -48,7 +51,7 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
<SkeletonText className="h-8 w-full" />
</div>
<div className="rounded-b-xl">
<div className="rounded-b-lg">
<SectionBottomActions align="end">
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</SectionBottomActions>
@ -57,9 +60,6 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
);
};
const DEFAULT_LIGHT_BRAND_COLOR = "#292929";
const DEFAULT_DARK_BRAND_COLOR = "#fafafa";
const AppearanceView = ({
user,
hasPaidPlan,
@ -137,7 +137,7 @@ const AppearanceView = ({
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">
<div className="border-subtle mt-6 flex items-center rounded-t-lg border p-6 text-sm">
<div>
<p className="text-default text-base font-semibold">{t("theme")}</p>
<p className="text-default">{t("theme_applies_note")}</p>
@ -149,13 +149,13 @@ const AppearanceView = ({
mutation.mutate({
// Radio values don't support null as values, therefore we convert an empty string
// back to null here.
theme: values.theme || null,
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}
value={undefined}
label={t("theme_system")}
defaultChecked={user.theme === null}
register={userThemeFormMethods.register}
@ -226,11 +226,7 @@ const AppearanceView = ({
});
}
}}
childrenClassName="lg:ml-0"
switchContainerClassName={classNames(
"py-6 px-4 sm:px-6 border-subtle rounded-xl border",
isCustomBrandColorChecked && "rounded-b-none"
)}>
childrenClassName="lg:ml-0">
<div className="border-subtle flex flex-col gap-6 border-x p-6">
<Controller
name="brandColor"
@ -241,7 +237,7 @@ const AppearanceView = ({
<p className="text-default mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
<ColorPicker
defaultValue={user.brandColor}
resetDefaultValue="#292929"
resetDefaultValue={DEFAULT_LIGHT_BRAND_COLOR}
onChange={(value) => {
try {
checkWCAGContrastColor("#ffffff", value);
@ -273,7 +269,7 @@ const AppearanceView = ({
<p className="text-default mb-2 block text-sm font-medium">{t("dark_brand_color")}</p>
<ColorPicker
defaultValue={user.darkBrandColor}
resetDefaultValue="#fafafa"
resetDefaultValue={DEFAULT_DARK_BRAND_COLOR}
onChange={(value) => {
try {
checkWCAGContrastColor("#101010", value);
@ -328,7 +324,7 @@ const AppearanceView = ({
setHideBrandingValue(checked);
mutation.mutate({ hideBranding: checked });
}}
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
switchContainerClassName="mt-6"
/>
</div>
);

View File

@ -35,7 +35,7 @@ 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="border-subtle mt-8 space-y-6 rounded-lg border px-4 py-6 sm:px-6">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
@ -109,11 +109,11 @@ const CalendarsView = () => {
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">
<div className="border-subtle mt-8 rounded-t-lg 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>
<p className="text-default text-sm leading-tight">{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
@ -137,15 +137,15 @@ const CalendarsView = () => {
</Button>
</SectionBottomActions>
<div className="border-subtle mt-8 rounded-t-xl border px-4 py-6 sm:px-6">
<div className="border-subtle mt-8 rounded-t-lg 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>
<p className="text-default text-sm leading-tight">{t("select_calendars")}</p>
</div>
<List
className="border-subtle flex flex-col gap-6 rounded-b-xl border border-t-0 p-6"
className="border-subtle flex flex-col gap-6 rounded-b-lg border border-t-0 p-6"
noBorderTreatment>
{data.connectedCalendars.map((item) => (
<Fragment key={item.credentialId}>
@ -173,7 +173,7 @@ const CalendarsView = () => {
/>
)}
{item?.error === undefined && item.calendars && (
<ListItem className="flex-col rounded-md">
<ListItem className="flex-col rounded-lg">
<div className="flex w-full flex-1 items-center space-x-3 p-4 rtl:space-x-reverse">
{
// eslint-disable-next-line @next/next/no-img-element

View File

@ -16,7 +16,7 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
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">
<div className="divide-subtle border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-4">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
</div>
@ -100,7 +100,7 @@ const ConferencingLayout = () => {
}
return (
<AppList
listClassName="rounded-xl rounded-t-none border-t-0"
listClassName="rounded-lg rounded-t-none border-t-0"
handleDisconnect={handleDisconnect}
data={data}
variant="conferencing"

View File

@ -237,7 +237,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
setIsAllowDynamicBookingChecked(checked);
mutation.mutate({ allowDynamicBooking: checked });
}}
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
switchContainerClassName="mt-6"
/>
<SettingsToggle
@ -250,7 +250,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
setIsAllowSEOIndexingChecked(checked);
mutation.mutate({ allowSEOIndexing: checked });
}}
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
switchContainerClassName="mt-6"
/>
<SettingsToggle
@ -263,7 +263,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
setIsReceiveMonthlyDigestEmailChecked(checked);
mutation.mutate({ receiveMonthlyDigestEmail: checked });
}}
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
switchContainerClassName="mt-6"
/>
</div>
);

View File

@ -50,7 +50,7 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
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">
<div className="border-subtle space-y-6 rounded-b-lg border border-t-0 px-4 py-8">
<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" />
@ -279,7 +279,7 @@ const ProfileView = () => {
}
/>
<div className="border-subtle mt-6 rounded-xl rounded-b-none border border-b-0 p-6">
<div className="border-subtle mt-6 rounded-lg 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>

View File

@ -1,9 +1,9 @@
import TeamBillingView from "@calcom/features/ee/teams/pages/team-billing-view";
import type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper";
const Page = TeamBillingView as CalPageWrapper;
import BillingPage from "../../settings/billing/index";
const Page = BillingPage as CalPageWrapper;
Page.PageWrapper = PageWrapper;
export default Page;

View File

@ -66,8 +66,8 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
onCheckedChange={(checked) => {
mutation.mutate({ disableImpersonation: !checked });
}}
switchContainerClassName="rounded-t-none border-t-0"
disabled={mutation.isLoading}
switchContainerClassName="py-6 px-4 sm:px-6 border-subtle rounded-b-xl border border-t-0"
/>
</div>
</>

View File

@ -63,7 +63,7 @@ const TwoFactorAuthView = () => {
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
</Badge>
}
switchContainerClassName="border-subtle rounded-b-xl border border-t-0 px-5 py-6 sm:px-6"
switchContainerClassName="rounded-t-none border-t-0"
/>
<EnableTwoFactorModal

View File

@ -1,9 +1,9 @@
import TeamBillingView from "@calcom/features/ee/teams/pages/team-billing-view";
import type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper";
const Page = TeamBillingView as CalPageWrapper;
import BillingPage from "../../billing";
const Page = BillingPage as CalPageWrapper;
Page.PageWrapper = PageWrapper;
export default Page;

View File

@ -290,6 +290,7 @@
"where": "Where",
"add_to_calendar": "Add to calendar",
"add_to_calendar_description":"Select where to add events when youre booked.",
"add_events_to":"Add events to",
"add_another_calendar": "Add another calendar",
"other": "Other",
"email_sign_in_subject": "Your sign-in link for {{appName}}",
@ -459,6 +460,7 @@
"no_event_types_have_been_setup": "This user hasn't set up any event types yet.",
"edit_logo": "Edit logo",
"upload_a_logo": "Upload a logo",
"upload_logo": "Upload logo",
"remove_logo": "Remove logo",
"enable": "Enable",
"code": "Code",
@ -571,6 +573,7 @@
"your_team_name": "Your team name",
"team_updated_successfully": "Team updated successfully",
"your_team_updated_successfully": "Your team has been updated successfully.",
"your_org_updated_successfully": "Your Org has been updated successfully.",
"about": "About",
"team_description": "A few sentences about your team. This will appear on your team's url page.",
"org_description": "A few sentences about your organization. This will appear on your organization's url page.",
@ -2080,6 +2083,7 @@
"allow": "Allow",
"view_only_edit_availability_not_onboarded":"This user has not completed onboarding. You will not be able to set their availability until they have completed onboarding.",
"view_only_edit_availability":"You are viewing this user's availability. You can only edit your own availability.",
"you_can_override_calendar_in_advanced_tab":"You can override this on a per-event basis in Advanced settings in each event type.",
"edit_users_availability":"Edit user's availability: {{username}}",
"resend_invitation": "Resend invitation",
"invitation_resent": "The invitation was resent.",

View File

@ -126,6 +126,7 @@ const DestinationCalendarSelector = ({
return (
<div className="relative" title={`${t("create_events_on")}: ${selectedOption?.label || ""}`}>
<p className="text-sm font-medium leading-none">{t("add_events_to")}</p>
<Select
name="primarySelectedCalendar"
placeholder={
@ -155,7 +156,7 @@ const DestinationCalendarSelector = ({
}}
isSearchable={false}
className={classNames(
"border-default mb-2 mt-1 block w-full min-w-0 flex-1 rounded-none rounded-r-sm text-sm"
"border-default my-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm text-sm"
)}
onChange={(newValue) => {
setSelectedOption(newValue);
@ -176,6 +177,7 @@ const DestinationCalendarSelector = ({
components={{ SingleValue: SingleValueComponent, Option: OptionComponent }}
isMulti={false}
/>
<p className="text-sm leading-tight">{t("you_can_override_calendar_in_advanced_tab")}</p>
</div>
);
};

View File

@ -1,10 +1,19 @@
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
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 { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
import { APP_NAME } from "@calcom/lib/constants";
import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import {
Button,
ColorPicker,
@ -14,17 +23,19 @@ import {
SkeletonButton,
SkeletonContainer,
SkeletonText,
SettingsToggle,
Alert,
} from "@calcom/ui";
import ThemeLabel from "../../../../settings/ThemeLabel";
import { getLayout } from "../../../../settings/layouts/SettingsLayout";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<div className="mb-8 mt-6 space-y-6">
<div className="flex items-center">
<Meta title={title} description={description} borderInShellHeader={false} />
<div className="border-subtle mt-6 flex items-center rounded-t-xl border p-6 text-sm">
<SkeletonText className="h-8 w-1/3" />
</div>
<div className="border-subtle space-y-6 border-x px-4 py-6 sm:px-6">
<div className="flex items-center justify-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" />
@ -35,144 +46,153 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
</div>
<SkeletonText className="h-8 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</div>
<div className="rounded-b-xl">
<SectionBottomActions align="end">
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</SectionBottomActions>
</div>
</SkeletonContainer>
);
};
interface OrgAppearanceValues {
hideBranding: boolean;
hideBookATeamMember: boolean;
type BrandColorsFormValues = {
brandColor: string;
darkBrandColor: string;
theme: string | null | undefined;
}
};
const OrgAppearanceView = () => {
const OrgAppearanceView = ({
currentOrg,
isAdminOrOwner,
}: {
currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"];
isAdminOrOwner: boolean;
}) => {
const { t } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const themeForm = useForm<{ theme: string | null | undefined }>({
defaultValues: {
theme: currentOrg?.theme,
},
});
const {
formState: { isSubmitting: isOrgThemeSubmitting, isDirty: isOrgThemeDirty },
reset: resetOrgThemeReset,
} = themeForm;
const [hideBrandingValue, setHideBrandingValue] = useState(currentOrg?.hideBranding ?? false);
const brandColorsFormMethods = useForm<BrandColorsFormValues>({
defaultValues: {
brandColor: currentOrg?.brandColor || DEFAULT_LIGHT_BRAND_COLOR,
darkBrandColor: currentOrg?.darkBrandColor || DEFAULT_DARK_BRAND_COLOR,
},
});
const mutation = trpc.viewer.organizations.update.useMutation({
onError: (err) => {
showToast(err.message, "error");
},
async onSuccess() {
async onSuccess(res) {
await utils.viewer.teams.get.invalidate();
await utils.viewer.organizations.listCurrent.invalidate();
showToast(t("your_team_updated_successfully"), "success");
brandColorsFormMethods.reset({
brandColor: res.data.brandColor as string,
darkBrandColor: res.data.darkBrandColor as string,
});
resetOrgThemeReset({ theme: res.data.theme as string | undefined });
},
});
const { data: currentOrg, isLoading } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
onError: () => {
router.push("/settings");
},
});
const onBrandColorsFormSubmit = (values: BrandColorsFormValues) => {
mutation.mutate(values);
};
const form = useForm<OrgAppearanceValues>({
defaultValues: {
theme: currentOrg?.theme,
brandColor: currentOrg?.brandColor,
darkBrandColor: currentOrg?.darkBrandColor,
hideBranding: currentOrg?.hideBranding,
},
});
const isAdmin =
currentOrg &&
(currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN);
if (isLoading) {
return <SkeletonLoader title={t("booking_appearance")} description={t("appearance_team_description")} />;
}
return (
<LicenseRequired>
<Meta title={t("booking_appearance")} description={t("appearance_team_description")} />
{isAdmin ? (
<Form
form={form}
handleSubmit={(values) => {
mutation.mutate({
...values,
theme: values.theme || null,
});
}}>
<div className="mb-6 flex items-center text-sm">
<div>
<p className="font-semibold">{t("theme")}</p>
<p className="text-default">{t("theme_applies_note")}</p>
<Meta
title={t("appearance")}
description={t("appearance_team_description")}
borderInShellHeader={false}
/>
{isAdminOrOwner ? (
<div>
<Form
form={themeForm}
handleSubmit={(value) => {
mutation.mutate({
theme: value.theme ?? null,
});
}}>
<div className="border-subtle mt-6 flex items-center rounded-t-xl border p-6 text-sm">
<div>
<p className="text-default text-base font-semibold">{t("theme")}</p>
<p className="text-default">{t("theme_applies_note")}</p>
</div>
</div>
</div>
<div className="flex flex-col justify-between sm:flex-row">
<ThemeLabel
variant="system"
value={null}
label={t("theme_system")}
defaultChecked={currentOrg.theme === null}
register={form.register}
/>
<ThemeLabel
variant="light"
value="light"
label={t("theme_light")}
defaultChecked={currentOrg.theme === "light"}
register={form.register}
/>
<ThemeLabel
variant="dark"
value="dark"
label={t("theme_dark")}
defaultChecked={currentOrg.theme === "dark"}
register={form.register}
/>
</div>
<hr className="border-subtle my-8" />
<div className="text-default mb-6 flex items-center text-sm">
<div>
<p className="font-semibold">{t("custom_brand_colors")}</p>
<p className="mt-0.5 leading-5">{t("customize_your_brand_colors")}</p>
<div className="border-subtle flex flex-col justify-between border-x px-6 py-8 sm:flex-row">
<ThemeLabel
variant="system"
value={undefined}
label={t("theme_system")}
defaultChecked={currentOrg.theme === null}
register={themeForm.register}
/>
<ThemeLabel
variant="light"
value="light"
label={t("light")}
defaultChecked={currentOrg.theme === "light"}
register={themeForm.register}
/>
<ThemeLabel
variant="dark"
value="dark"
label={t("dark")}
defaultChecked={currentOrg.theme === "dark"}
register={themeForm.register}
/>
</div>
</div>
<SectionBottomActions className="mb-6" align="end">
<Button
disabled={isOrgThemeSubmitting || !isOrgThemeDirty}
type="submit"
data-testid="update-org-theme-btn"
color="primary">
{t("update")}
</Button>
</SectionBottomActions>
</Form>
<div className="block justify-between sm:flex">
<Controller
name="brandColor"
control={form.control}
defaultValue={currentOrg.brandColor}
render={() => (
<div>
<p className="text-emphasis mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
<ColorPicker
defaultValue={currentOrg.brandColor || "#292929"}
resetDefaultValue="#292929"
onChange={(value) => form.setValue("brandColor", value, { shouldDirty: true })}
/>
</div>
)}
<Form
form={brandColorsFormMethods}
handleSubmit={(values) => {
onBrandColorsFormSubmit(values);
}}>
<BrandColorsForm
onSubmit={onBrandColorsFormSubmit}
orgBrandColor={currentOrg?.brandColor}
orgDarkBrandColor={currentOrg?.darkBrandColor}
/>
<Controller
name="darkBrandColor"
control={form.control}
defaultValue={currentOrg.darkBrandColor}
render={() => (
<div className="mt-6 sm:mt-0">
<p className="text-emphasis mb-2 block text-sm font-medium">{t("dark_brand_color")}</p>
<ColorPicker
defaultValue={currentOrg.darkBrandColor || "#fafafa"}
resetDefaultValue="#fafafa"
onChange={(value) => form.setValue("darkBrandColor", value, { shouldDirty: true })}
/>
</div>
)}
/>
</div>
<Button color="primary" className="mt-8" type="submit" loading={mutation.isLoading}>
{t("update")}
</Button>
</Form>
</Form>
<SettingsToggle
toggleSwitchAtTheEnd={true}
title={t("disable_cal_branding", { appName: APP_NAME })}
disabled={mutation?.isLoading}
description={t("removes_cal_branding", { appName: APP_NAME })}
checked={hideBrandingValue}
onCheckedChange={(checked) => {
setHideBrandingValue(checked);
mutation.mutate({ hideBranding: checked });
}}
switchContainerClassName="border-subtle mt-6 rounded-xl border py-6 px-4 sm:px-6"
/>
</div>
) : (
<div className="border-subtle rounded-md border p-5">
<span className="text-default text-sm">{t("only_owner_change")}</span>
@ -182,6 +202,148 @@ const OrgAppearanceView = () => {
);
};
OrgAppearanceView.getLayout = getLayout;
const BrandColorsForm = ({
onSubmit,
orgBrandColor,
orgDarkBrandColor,
}: {
onSubmit: (values: BrandColorsFormValues) => void;
orgBrandColor: string | undefined;
orgDarkBrandColor: string | undefined;
}) => {
const { t } = useLocale();
const brandColorsFormMethods = useFormContext();
const {
formState: { isSubmitting: isBrandColorsFormSubmitting, isDirty: isBrandColorsFormDirty },
handleSubmit,
} = brandColorsFormMethods;
export default OrgAppearanceView;
const [isCustomBrandColorChecked, setIsCustomBrandColorChecked] = useState(
orgBrandColor !== DEFAULT_LIGHT_BRAND_COLOR || orgDarkBrandColor !== DEFAULT_DARK_BRAND_COLOR
);
const [darkModeError, setDarkModeError] = useState(false);
const [lightModeError, setLightModeError] = useState(false);
return (
<div className="mt-6">
<SettingsToggle
toggleSwitchAtTheEnd={true}
title={t("custom_brand_colors")}
description={t("customize_your_brand_colors")}
checked={isCustomBrandColorChecked}
onCheckedChange={(checked) => {
setIsCustomBrandColorChecked(checked);
if (!checked) {
onSubmit({
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}
defaultValue={orgBrandColor}
render={() => (
<div>
<p className="text-default mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
<ColorPicker
defaultValue={orgBrandColor || DEFAULT_LIGHT_BRAND_COLOR}
resetDefaultValue={DEFAULT_LIGHT_BRAND_COLOR}
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={orgDarkBrandColor}
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={orgDarkBrandColor || DEFAULT_DARK_BRAND_COLOR}
resetDefaultValue={DEFAULT_DARK_BRAND_COLOR}
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>
)}
/>
</div>
<SectionBottomActions align="end">
<Button
disabled={isBrandColorsFormSubmitting || !isBrandColorsFormDirty}
color="primary"
type="submit">
{t("update")}
</Button>
</SectionBottomActions>
</SettingsToggle>
</div>
);
};
const OrgAppearanceViewWrapper = () => {
const router = useRouter();
const { t } = useLocale();
const { data: currentOrg, isLoading } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
onError: () => {
router.push("/settings");
},
});
if (isLoading) {
return <SkeletonLoader title={t("appearance")} description={t("appearance_team_description")} />;
}
if (!currentOrg) return null;
const isAdminOrOwner =
currentOrg &&
(currentOrg.user.role === MembershipRole.OWNER || currentOrg.user.role === MembershipRole.ADMIN);
return <OrgAppearanceView currentOrg={currentOrg} isAdminOrOwner={isAdminOrOwner} />;
};
OrgAppearanceViewWrapper.getLayout = getLayout;
export default OrgAppearanceViewWrapper;

View File

@ -2,6 +2,7 @@ import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { nameOfDay } from "@calcom/lib/weekday";
@ -24,7 +25,7 @@ import {
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<Meta title={title} description={description} borderInShellHeader={true} />
<div className="mb-8 mt-6 space-y-6">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
@ -131,68 +132,76 @@ const GeneralView = ({ currentOrg, isAdminOrOwner, localeProp }: GeneralViewProp
weekStart: values.weekStart.value,
});
}}>
<Meta title={t("general")} description={t("organization_general_description")} />
<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 });
}}
/>
</>
)}
<Meta
title={t("general")}
description={t("organization_general_description")}
borderInShellHeader={true}
/>
<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 className="text-gray text-default mt-2 flex items-center text-sm">
{t("timeformat_profile_hint")}
<div className="border-subtle border-x border-y-0 px-4 py-8 sm:px-6">
<Controller
name="timeZone"
control={formMethods.control}
render={({ field: { value } }) => (
<>
<Label className="text-emphasis">
<>{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>
<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 });
}}
/>
</>
)}
/>
<Button disabled={isDisabled} color="primary" type="submit" className="mt-8">
<>{t("update")}</>
</Button>
<SectionBottomActions align="end">
<Button disabled={isDisabled} color="primary" type="submit">
{t("update")}
</Button>
</SectionBottomActions>
</Form>
);
};

View File

@ -8,6 +8,7 @@ import { z } from "zod";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@ -25,6 +26,10 @@ import {
TextField,
Editor,
LinkIconButton,
SkeletonAvatar,
SkeletonButton,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { getLayout } from "../../../../settings/layouts/SettingsLayout";
@ -32,191 +37,112 @@ import { useOrgBranding } from "../../../organizations/context/provider";
const orgProfileFormSchema = z.object({
name: z.string(),
logo: z.string(),
logo: z.string().nullable(),
bio: z.string(),
});
type FormValues = {
name: string;
logo: string | null;
bio: string;
slug: string;
};
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">
<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" />
</div>
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
</div>
</SkeletonContainer>
);
};
const OrgProfileView = () => {
const { t } = useLocale();
const router = useRouter();
const utils = trpc.useContext();
const [firstRender, setFirstRender] = useState(true);
const orgBranding = useOrgBranding();
useLayoutEffect(() => {
document.body.focus();
}, []);
const form = useForm({
resolver: zodResolver(orgProfileFormSchema),
});
const mutation = trpc.viewer.organizations.update.useMutation({
onError: (err) => {
showToast(err.message, "error");
},
async onSuccess() {
await utils.viewer.teams.get.invalidate();
showToast(t("your_organization_updated_sucessfully"), "success");
},
});
const { data: currentOrganisation, isLoading } = trpc.viewer.organizations.listCurrent.useQuery(undefined, {
onError: () => {
router.push("/settings");
},
onSuccess: (org) => {
if (org) {
form.setValue("name", org.name || "");
form.setValue("slug", org.slug || org.metadata?.requestedSlug || "");
form.setValue("logo", org.logo || "");
form.setValue("bio", org.bio || "");
if (org.slug === null && (org?.metadata as Prisma.JsonObject)?.requestedSlug) {
form.setValue("slug", ((org?.metadata as Prisma.JsonObject)?.requestedSlug as string) || "");
}
}
},
});
if (isLoading || !orgBranding || !currentOrganisation) {
return <SkeletonLoader title={t("profile")} description={t("profile_org_description")} />;
}
const isOrgAdminOrOwner =
currentOrganisation &&
(currentOrganisation.user.role === MembershipRole.OWNER ||
currentOrganisation.user.role === MembershipRole.ADMIN);
currentOrganisation.user.role === MembershipRole.OWNER ||
currentOrganisation.user.role === MembershipRole.ADMIN;
const isBioEmpty =
!currentOrganisation ||
!currentOrganisation.bio ||
!currentOrganisation.bio.replace("<p><br></p>", "").length;
if (!orgBranding) return null;
const defaultValues: FormValues = {
name: currentOrganisation?.name || "",
logo: currentOrganisation?.logo || "",
bio: currentOrganisation?.bio || "",
slug:
currentOrganisation?.slug ||
((currentOrganisation?.metadata as Prisma.JsonObject)?.requestedSlug as string) ||
"",
};
return (
<LicenseRequired>
<Meta title={t("profile")} description={t("profile_org_description")} />
{!isLoading && (
<>
{isOrgAdminOrOwner ? (
<Form
form={form}
handleSubmit={(values) => {
if (currentOrganisation) {
const variables = {
logo: values.logo,
name: values.name,
slug: values.slug,
bio: values.bio,
};
mutation.mutate(variables);
}
}}>
<div className="flex items-center">
<Controller
control={form.control}
name="logo"
render={({ field: { value } }) => (
<>
<Avatar
alt=""
imageSrc={getPlaceholderAvatar(value, currentOrganisation?.name as string)}
size="lg"
/>
<div className="ms-4">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("update")}
handleAvatarChange={(newLogo) => {
form.setValue("logo", newLogo);
}}
imageSrc={value}
/>
</div>
</>
)}
/>
</div>
<hr className="border-subtle my-8" />
<Controller
control={form.control}
name="name"
render={({ field: { value } }) => (
<div className="mt-8">
<TextField
name="name"
label={t("org_name")}
value={value}
onChange={(e) => {
form.setValue("name", e?.target.value);
}}
/>
</div>
)}
/>
<Controller
control={form.control}
name="slug"
render={({ field: { value } }) => (
<div className="mt-8">
<TextField
name="slug"
label={t("org_url")}
value={value}
disabled
addOnSuffix={`.${subdomainSuffix()}`}
/>
</div>
)}
/>
<div className="mt-8">
<Label>{t("about")}</Label>
<Editor
getText={() => md.render(form.getValues("bio") || "")}
setText={(value: string) => form.setValue("bio", turndown(value))}
excludedToolbarItems={["blockType"]}
disableLists
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
</div>
<p className="text-default mt-2 text-sm">{t("org_description")}</p>
<Button color="primary" className="mt-8" type="submit" loading={mutation.isLoading}>
{t("update")}
</Button>
</Form>
) : (
<div className="flex">
<div className="flex-grow">
<div>
<Label className="text-emphasis">{t("org_name")}</Label>
<p className="text-default text-sm">{currentOrganisation?.name}</p>
</div>
{currentOrganisation && !isBioEmpty && (
<>
<Label className="text-emphasis mt-5">{t("about")}</Label>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(currentOrganisation.bio || "") }}
/>
</>
)}
</div>
<div className="">
<LinkIconButton
Icon={LinkIcon}
onClick={() => {
navigator.clipboard.writeText(orgBranding.fullDomain);
showToast("Copied to clipboard", "success");
}}>
{t("copy_link_org")}
</LinkIconButton>
<Meta title={t("profile")} description={t("profile_org_description")} borderInShellHeader={true} />
<>
{isOrgAdminOrOwner ? (
<OrgProfileForm defaultValues={defaultValues} />
) : (
<div className="flex">
<div className="flex-grow">
<div>
<Label className="text-emphasis">{t("org_name")}</Label>
<p className="text-default text-sm">{currentOrganisation?.name}</p>
</div>
{!isBioEmpty && (
<>
<Label className="text-emphasis mt-5">{t("about")}</Label>
<div
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(currentOrganisation.bio || "") }}
/>
</>
)}
</div>
)}
{/* Disable Org disbanding */}
{/* <hr className="border-subtle my-8 border" />
<div className="">
<LinkIconButton
Icon={LinkIcon}
onClick={() => {
navigator.clipboard.writeText(orgBranding.fullDomain);
showToast("Copied to clipboard", "success");
}}>
{t("copy_link_org")}
</LinkIconButton>
</div>
</div>
)}
{/* Disable Org disbanding */}
{/* <hr className="border-subtle my-8 border" />
<div className="text-default mb-3 text-base font-semibold">{t("danger_zone")}</div>
{currentOrganisation?.user.role === "OWNER" ? (
<Dialog>
@ -234,13 +160,156 @@ const OrgProfileView = () => {
</ConfirmationDialogContent>
</Dialog>
) : null} */}
{/* LEAVE ORG should go above here ^ */}
</>
)}
{/* LEAVE ORG should go above here ^ */}
</>
</LicenseRequired>
);
};
const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
const utils = trpc.useContext();
const { t } = useLocale();
const [firstRender, setFirstRender] = useState(true);
const form = useForm({
defaultValues,
resolver: zodResolver(orgProfileFormSchema),
});
const mutation = trpc.viewer.organizations.update.useMutation({
onError: (err) => {
showToast(err.message, "error");
},
onSuccess: async (res) => {
reset({
logo: (res.data?.logo || "") as string,
name: (res.data?.name || "") as string,
bio: (res.data?.bio || "") as string,
slug: defaultValues["slug"],
});
await utils.viewer.teams.get.invalidate();
await utils.viewer.organizations.listCurrent.invalidate();
showToast(t("your_organization_updated_sucessfully"), "success");
},
});
const {
formState: { isSubmitting, isDirty },
reset,
} = form;
const isDisabled = isSubmitting || !isDirty;
return (
<Form
form={form}
handleSubmit={(values) => {
const variables = {
logo: values.logo,
name: values.name,
slug: values.slug,
bio: values.bio,
};
mutation.mutate(variables);
}}>
<div className="border-subtle border-x px-4 py-8 sm:px-6">
<div className="flex items-center">
<Controller
control={form.control}
name="logo"
render={({ field: { value } }) => {
const showRemoveLogoButton = !!value;
return (
<>
<Avatar
alt={defaultValues.name || ""}
imageSrc={getPlaceholderAvatar(value, defaultValues.name as string)}
size="lg"
/>
<div className="ms-4">
<div className="flex gap-2">
<ImageUploader
target="avatar"
id="avatar-upload"
buttonMsg={t("upload_logo")}
handleAvatarChange={(newLogo) => {
form.setValue("logo", newLogo, { shouldDirty: true });
}}
imageSrc={value || undefined}
triggerButtonColor={showRemoveLogoButton ? "secondary" : "primary"}
/>
{showRemoveLogoButton && (
<Button
color="secondary"
onClick={() => {
form.setValue("logo", null, { shouldDirty: true });
}}>
{t("remove")}
</Button>
)}
</div>
</div>
</>
);
}}
/>
</div>
<Controller
control={form.control}
name="name"
render={({ field: { value } }) => (
<div className="mt-8">
<TextField
name="name"
label={t("org_name")}
value={value}
onChange={(e) => {
form.setValue("name", e?.target.value, { shouldDirty: true });
}}
/>
</div>
)}
/>
<Controller
control={form.control}
name="slug"
render={({ field: { value } }) => (
<div className="mt-8">
<TextField
name="slug"
label={t("org_url")}
value={value}
disabled
addOnSuffix={`.${subdomainSuffix()}`}
/>
</div>
)}
/>
<div className="mt-8">
<Label>{t("about")}</Label>
<Editor
getText={() => md.render(form.getValues("bio") || "")}
setText={(value: string) => form.setValue("bio", turndown(value), { shouldDirty: true })}
excludedToolbarItems={["blockType"]}
disableLists
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
</div>
<p className="text-default mt-2 text-sm">{t("org_description")}</p>
</div>
<SectionBottomActions align="end">
<Button color="primary" type="submit" loading={mutation.isLoading} disabled={isDisabled}>
{t("update")}
</Button>
</SectionBottomActions>
</Form>
);
};
OrgProfileView.getLayout = getLayout;
export default OrgProfileView;

View File

@ -14,7 +14,7 @@ const BillingView = () => {
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
return (
<>
<Meta title={t("team_billing")} description={t("team_billing_description")} />
<Meta title={t("billing")} description={t("team_billing_description")} borderInShellHeader={true} />
<div className="text-default flex flex-col text-sm sm:flex-row">
<div>
<h2 className="font-medium">{t("view_and_manage_billing_details")}</h2>

View File

@ -14,7 +14,7 @@ const SectionBottomActions = ({
return (
<div
className={classNames(
"border-subtle bg-muted flex rounded-b-xl border px-6 py-4",
"border-subtle bg-muted flex rounded-b-lg border px-6 py-4",
align === "end" && "justify-end",
className
)}>

View File

@ -382,7 +382,7 @@ const SettingsSidebarContainer = ({
alt={team.name || "Team logo"}
/>
)}
<p className="w-1/2 truncate">{team.name}</p>
<p className="w-1/2 truncate leading-normal">{team.name}</p>
{!team.accepted && (
<Badge className="ms-3" variant="orange">
Inv.
@ -526,7 +526,7 @@ const SettingsSidebarContainer = ({
alt={otherTeam.name || "Team logo"}
/>
)}
<p className="w-1/2 truncate">{otherTeam.name}</p>
<p className="w-1/2 truncate leading-normal">{otherTeam.name}</p>
</div>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-0.5">
@ -688,7 +688,7 @@ export function ShellHeader() {
<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 && "rounded-t-lg border px-4 py-6 sm:px-6",
meta.borderInShellHeader === undefined && "mb-8 border-b pb-8"
)}>
<div className="flex w-full items-center">
@ -703,12 +703,12 @@ export function ShellHeader() {
{t(meta.title)}
</h1>
) : (
<div className="bg-emphasis mb-1 h-5 w-24 animate-pulse rounded-md" />
<div className="bg-emphasis mb-1 h-5 w-24 animate-pulse rounded-lg" />
)}
{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 className="bg-emphasis h-5 w-32 animate-pulse rounded-lg" />
)}
</div>
<div className="ms-auto flex-shrink-0">{meta.CTA}</div>

View File

@ -17,7 +17,7 @@ export default function WebhookTestDisclosure() {
return (
<>
<div className="border-subtle flex justify-between rounded-t-xl border p-6">
<div className="border-subtle flex justify-between rounded-t-lg border p-6">
<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>
@ -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-b-lg border border-t-0 px-6 py-8 sm:mx-0">
<div className="border-subtle flex justify-between rounded-t-lg border p-4">
<div className="flex items-center space-x-1">
<h3 className="text-emphasis self-center text-sm font-semibold leading-4">
{t("webhook_response")}
@ -44,7 +44,7 @@ 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="bg-muted border-subtle rounded-b-lg border border-t-0 p-4 font-mono text-[13px] leading-4">
{!mutation.data && <p>{t("no_data_yet")}</p>}
{mutation.status === "success" && (
<div className="overflow-x-auto">{JSON.stringify(mutation.data, null, 4)}</div>

View File

@ -61,6 +61,7 @@ function Component({ webhookId }: { webhookId: string }) {
title={t("edit_webhook")}
description={t("add_webhook_description", { appName: APP_NAME })}
borderInShellHeader={true}
backButton
/>
<WebhookForm
noRoutingFormTriggers={false}

View File

@ -15,7 +15,7 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
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">
<div className="divide-subtle border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-4">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
</div>

View File

@ -32,7 +32,7 @@ const SkeletonLoader = ({
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">
<div className="divide-subtle border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-4">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
</div>
@ -120,8 +120,8 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
<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"
"border-subtle rounded-lg rounded-t-none border border-t-0",
hasTeams && "mb-8 mt-3 rounded-t-lg border-t"
)}>
{group.webhooks.map((webhook, index) => (
<WebhookListItem
@ -145,7 +145,7 @@ const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer
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"
className="rounded-b-lg rounded-t-none border-t-0"
buttonRaw={
<CreateButtonWithTeamsList
subtitle={t("create_for").toUpperCase()}

View File

@ -103,3 +103,6 @@ export const CALCOM_VERSION = process.env.NEXT_PUBLIC_CALCOM_VERSION as string;
export const APP_CREDENTIAL_SHARING_ENABLED =
process.env.CALCOM_WEBHOOK_SECRET && process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
export const DEFAULT_LIGHT_BRAND_COLOR = "#292929";
export const DEFAULT_DARK_BRAND_COLOR = "#fafafa";

View File

@ -99,7 +99,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
// Sync Services: Close.com
if (prevOrganisation) closeComUpdateTeam(prevOrganisation, updatedOrganisation);
return { update: true, userId: ctx.user.id };
return { update: true, userId: ctx.user.id, data };
};
export default updateHandler;

View File

@ -79,7 +79,7 @@ export const Select = <
cx("text-emphasis placeholder:text-muted flex gap-1", innerClassNames?.valueContainer),
multiValue: () =>
cx(
"bg-subtle text-default rounded-md py-1.5 px-2 flex items-center text-sm leading-none",
"bg-subtle text-default rounded-md py-1.5 px-2 flex items-center text-sm leading-tight",
innerClassNames?.multiValue
),
menu: () =>

View File

@ -46,7 +46,12 @@ function SettingsToggle({
<div className="flex w-full flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
<fieldset className="block w-full flex-col sm:flex">
{toggleSwitchAtTheEnd ? (
<div className={classNames("flex justify-between space-x-3", switchContainerClassName)}>
<div
className={classNames(
"border-subtle flex justify-between space-x-3 rounded-lg border px-4 py-6 sm:px-6",
checked && children && "rounded-b-none",
switchContainerClassName
)}>
<div>
<div className="flex items-center">
<Label