chore: remove personal branding if in an org and use org branding (#14872)
* chore: remove personal branding if in an org and use org branding * fix: minor fix * Update SettingsLayout.tsx * Update SettingsLayout.tsx * Update profile.tsx * some fixes and e2e test * fix: e2e test --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Udit Takkar <udit222001@gmail.com>
This commit is contained in:
co-authored by
Udit Takkar
Udit Takkar
parent
785ab8b74a
commit
7d06d9f865
@@ -155,7 +155,7 @@ test.describe("Organization Logo", async () => {
|
||||
|
||||
await page.getByTestId("upload-avatar").click();
|
||||
|
||||
await page.getByText("Update").click();
|
||||
await page.getByTestId("update-org-profile-button").click();
|
||||
await page.waitForSelector("text=Your organization updated successfully");
|
||||
|
||||
const response = await prisma.avatar.findUniqueOrThrow({
|
||||
|
||||
@@ -5,20 +5,17 @@ import { useRouter } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import BrandColorsForm from "@calcom/features/ee/components/BrandColorsForm";
|
||||
import { AppearanceSkeletonLoader } from "@calcom/features/ee/components/CommonSkeletonLoaders";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import ThemeLabel from "@calcom/features/settings/ThemeLabel";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
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, Form, Meta, showToast, SettingsToggle, Avatar, ImageUploader } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Button, Form, showToast, SettingsToggle } from "@calcom/ui";
|
||||
|
||||
type BrandColorsFormValues = {
|
||||
brandColor: string;
|
||||
@@ -27,7 +24,6 @@ type BrandColorsFormValues = {
|
||||
|
||||
const OrgAppearanceView = ({
|
||||
currentOrg,
|
||||
isAdminOrOwner,
|
||||
}: {
|
||||
currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"];
|
||||
isAdminOrOwner: boolean;
|
||||
@@ -79,134 +75,79 @@ const OrgAppearanceView = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta
|
||||
title={t("appearance")}
|
||||
description={t("appearance_org_description")}
|
||||
borderInShellHeader={false}
|
||||
/>
|
||||
{isAdminOrOwner ? (
|
||||
<div>
|
||||
<div className="my-6">
|
||||
<div className="flex items-center text-sm">
|
||||
<Avatar
|
||||
alt="calVideoLogo"
|
||||
imageSrc={currentOrg?.calVideoLogo}
|
||||
fallback={<Icon name="plus" className="text-subtle h-6 w-6" />}
|
||||
size="lg"
|
||||
/>
|
||||
<div className="ms-4">
|
||||
<div className="flex gap-2">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="cal-video-logo-upload"
|
||||
buttonMsg={
|
||||
currentOrg?.calVideoLogo ? t("update_cal_video_logo") : t("upload_cal_video_logo")
|
||||
}
|
||||
handleAvatarChange={(newLogo) => {
|
||||
mutation.mutate({
|
||||
calVideoLogo: newLogo,
|
||||
});
|
||||
}}
|
||||
disabled={mutation.isPending}
|
||||
imageSrc={currentOrg?.calVideoLogo ?? undefined}
|
||||
uploadInstruction={t("cal_video_logo_upload_instruction")}
|
||||
triggerButtonColor={currentOrg?.calVideoLogo ? "secondary" : "primary"}
|
||||
/>
|
||||
{currentOrg?.calVideoLogo && (
|
||||
<Button
|
||||
color="destructive"
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => {
|
||||
mutation.mutate({
|
||||
calVideoLogo: null,
|
||||
});
|
||||
}}>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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 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>
|
||||
<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>
|
||||
|
||||
<Form
|
||||
form={brandColorsFormMethods}
|
||||
handleSubmit={(values) => {
|
||||
onBrandColorsFormSubmit(values);
|
||||
}}>
|
||||
<BrandColorsForm
|
||||
onSubmit={onBrandColorsFormSubmit}
|
||||
brandColor={currentOrg?.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR}
|
||||
darkBrandColor={currentOrg?.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("disable_cal_branding", { appName: APP_NAME })}
|
||||
disabled={mutation?.isPending}
|
||||
description={t("removes_cal_branding", { appName: APP_NAME })}
|
||||
checked={hideBrandingValue}
|
||||
onCheckedChange={(checked) => {
|
||||
setHideBrandingValue(checked);
|
||||
mutation.mutate({ hideBranding: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
</div>
|
||||
<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 className="py-5">
|
||||
<span className="text-default text-sm">{t("only_owner_change")}</span>
|
||||
</div>
|
||||
)}
|
||||
</LicenseRequired>
|
||||
<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>
|
||||
|
||||
<Form
|
||||
form={brandColorsFormMethods}
|
||||
handleSubmit={(values) => {
|
||||
onBrandColorsFormSubmit(values);
|
||||
}}>
|
||||
<BrandColorsForm
|
||||
onSubmit={onBrandColorsFormSubmit}
|
||||
brandColor={currentOrg?.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR}
|
||||
darkBrandColor={currentOrg?.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
<SettingsToggle
|
||||
toggleSwitchAtTheEnd={true}
|
||||
title={t("disable_cal_branding", { appName: APP_NAME })}
|
||||
disabled={mutation?.isPending}
|
||||
description={t("removes_cal_branding", { appName: APP_NAME })}
|
||||
checked={hideBrandingValue}
|
||||
onCheckedChange={(checked) => {
|
||||
setHideBrandingValue(checked);
|
||||
mutation.mutate({ hideBranding: checked });
|
||||
}}
|
||||
switchContainerClassName="mt-6"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -237,6 +178,4 @@ const OrgAppearanceViewWrapper = () => {
|
||||
return <OrgAppearanceView currentOrg={currentOrg} isAdminOrOwner={isAdminOrOwner} />;
|
||||
};
|
||||
|
||||
OrgAppearanceViewWrapper.getLayout = getLayout;
|
||||
|
||||
export default OrgAppearanceViewWrapper;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { z } from "zod";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import OrgAppearanceViewWrapper from "@calcom/features/ee/organizations/pages/settings/appearance";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -16,6 +17,7 @@ import { md } from "@calcom/lib/markdownIt";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import {
|
||||
Avatar,
|
||||
BannerUploader,
|
||||
@@ -41,6 +43,7 @@ const orgProfileFormSchema = z.object({
|
||||
name: z.string(),
|
||||
logoUrl: z.string().nullable(),
|
||||
banner: z.string().nullable(),
|
||||
calVideoLogo: z.string().nullable(),
|
||||
bio: z.string(),
|
||||
});
|
||||
|
||||
@@ -50,6 +53,7 @@ type FormValues = {
|
||||
banner: string | null;
|
||||
bio: string;
|
||||
slug: string;
|
||||
calVideoLogo: string | null;
|
||||
};
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
@@ -114,6 +118,7 @@ const OrgProfileView = () => {
|
||||
logoUrl: currentOrganisation?.logoUrl,
|
||||
banner: currentOrganisation?.bannerUrl || "",
|
||||
bio: currentOrganisation?.bio || "",
|
||||
calVideoLogo: currentOrganisation?.calVideoLogo || "",
|
||||
slug:
|
||||
currentOrganisation?.slug ||
|
||||
((currentOrganisation?.metadata as Prisma.JsonObject)?.requestedSlug as string) ||
|
||||
@@ -125,7 +130,10 @@ const OrgProfileView = () => {
|
||||
<Meta title={t("profile")} description={t("profile_org_description")} borderInShellHeader={true} />
|
||||
<>
|
||||
{isOrgAdminOrOwner ? (
|
||||
<OrgProfileForm defaultValues={defaultValues} />
|
||||
<>
|
||||
<OrgProfileForm defaultValues={defaultValues} />
|
||||
<OrgAppearanceViewWrapper />
|
||||
</>
|
||||
) : (
|
||||
<div className="border-subtle flex rounded-b-md border border-t-0 px-4 py-8 sm:px-6">
|
||||
<div className="flex-grow">
|
||||
@@ -182,6 +190,7 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
|
||||
bio: (res.data?.bio || "") as string,
|
||||
slug: defaultValues["slug"],
|
||||
banner: (res.data?.bannerUrl || "") as string,
|
||||
calVideoLogo: (res.data?.calVideoLogo || "") as string,
|
||||
});
|
||||
await utils.viewer.teams.get.invalidate();
|
||||
await utils.viewer.organizations.listCurrent.invalidate();
|
||||
@@ -206,6 +215,7 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
|
||||
slug: values.slug,
|
||||
bio: values.bio,
|
||||
banner: values.banner,
|
||||
calVideoLogo: values.calVideoLogo,
|
||||
};
|
||||
|
||||
mutation.mutate(variables);
|
||||
@@ -288,6 +298,44 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="calVideoLogo"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const showRemoveLogoButton = !!value;
|
||||
return (
|
||||
<>
|
||||
<Avatar
|
||||
alt="calVideoLogo"
|
||||
imageSrc={value}
|
||||
fallback={<Icon name="plus" className="text-subtle h-6 w-6" />}
|
||||
size="lg"
|
||||
/>
|
||||
<div className="ms-4">
|
||||
<div className="flex gap-2">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="cal-video-logo-upload"
|
||||
buttonMsg={t("upload_cal_video_logo")}
|
||||
handleAvatarChange={onChange}
|
||||
imageSrc={value || undefined}
|
||||
uploadInstruction={t("cal_video_logo_upload_instruction")}
|
||||
triggerButtonColor={showRemoveLogoButton ? "secondary" : "primary"}
|
||||
testId="cal-video-logo"
|
||||
/>
|
||||
{showRemoveLogoButton && (
|
||||
<Button color="secondary" onClick={() => onChange(null)}>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
@@ -334,7 +382,12 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
|
||||
<p className="text-default mt-2 text-sm">{t("org_description")}</p>
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button color="primary" type="submit" loading={mutation.isPending} disabled={isDisabled}>
|
||||
<Button
|
||||
data-testid="update-org-profile-button"
|
||||
color="primary"
|
||||
type="submit"
|
||||
loading={mutation.isPending}
|
||||
disabled={isDisabled}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
|
||||
@@ -84,10 +84,6 @@ const tabs: VerticalTabItemProps[] = [
|
||||
name: "privacy",
|
||||
href: "/settings/organizations/privacy",
|
||||
},
|
||||
{
|
||||
name: "appearance",
|
||||
href: "/settings/organizations/appearance",
|
||||
},
|
||||
{
|
||||
name: "billing",
|
||||
href: "/settings/organizations/billing",
|
||||
@@ -148,7 +144,7 @@ tabs.find((tab) => {
|
||||
// The following keys are assigned to admin only
|
||||
const adminRequiredKeys = ["admin"];
|
||||
const organizationRequiredKeys = ["organization"];
|
||||
const organizationAdminKeys = ["privacy", "appearance", "billing", "OAuth Clients", "SSO", "directory_sync"];
|
||||
const organizationAdminKeys = ["privacy", "billing", "OAuth Clients", "SSO", "directory_sync"];
|
||||
|
||||
const useTabs = () => {
|
||||
const session = useSession();
|
||||
@@ -161,6 +157,9 @@ const useTabs = () => {
|
||||
const processTabsMemod = useMemo(() => {
|
||||
const processedTabs = tabs.map((tab) => {
|
||||
if (tab.href === "/settings/my-account") {
|
||||
if (!!session.data?.user?.org?.id) {
|
||||
tab.children = tab?.children?.filter((child) => child.href !== "/settings/my-account/appearance");
|
||||
}
|
||||
return {
|
||||
...tab,
|
||||
name: user?.name || "my_account",
|
||||
|
||||
@@ -21,6 +21,7 @@ type ImageUploaderProps = {
|
||||
triggerButtonColor?: ButtonColor;
|
||||
uploadInstruction?: string;
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
// This is separate to prevent loading the component until file upload
|
||||
@@ -73,6 +74,7 @@ export default function ImageUploader({
|
||||
imageSrc,
|
||||
uploadInstruction,
|
||||
disabled = false,
|
||||
testId,
|
||||
}: ImageUploaderProps) {
|
||||
const { t } = useLocale();
|
||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
|
||||
@@ -125,7 +127,7 @@ export default function ImageUploader({
|
||||
color={triggerButtonColor ?? "secondary"}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
data-testid="open-upload-avatar-dialog"
|
||||
data-testid={testId ? `open-upload-${testId}-dialog` : "open-upload-avatar-dialog"}
|
||||
className="cursor-pointer py-1 text-sm">
|
||||
{buttonMsg}
|
||||
</Button>
|
||||
@@ -147,7 +149,7 @@ export default function ImageUploader({
|
||||
)}
|
||||
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
||||
<label
|
||||
data-testid="open-upload-image-filechooser"
|
||||
data-testid={testId ? `open-upload-${testId}-filechooser` : "open-upload-image-filechooser"}
|
||||
className="bg-subtle hover:bg-muted hover:text-emphasis border-subtle text-default mt-8 cursor-pointer rounded-sm border px-3 py-1 text-xs font-medium leading-4 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1">
|
||||
<input
|
||||
onInput={onInputFile}
|
||||
@@ -167,7 +169,7 @@ export default function ImageUploader({
|
||||
<DialogFooter className="relative">
|
||||
<DialogClose color="minimal">{t("cancel")}</DialogClose>
|
||||
<DialogClose
|
||||
data-testid="upload-avatar"
|
||||
data-testid={testId ? `upload-${testId}` : "upload-avatar"}
|
||||
color="primary"
|
||||
onClick={() => showCroppedImage(croppedAreaPixels)}>
|
||||
{t("save")}
|
||||
|
||||
Reference in New Issue
Block a user