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:
Anik Dhabal Babu
2024-07-05 00:35:46 +05:30
committed by GitHub
co-authored by Udit Takkar Udit Takkar
parent 785ab8b74a
commit 7d06d9f865
5 changed files with 136 additions and 143 deletions
@@ -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")}