diff --git a/apps/web/components/apps/AppList.tsx b/apps/web/components/apps/AppList.tsx index 0c3df3ba3c0a5..55206c8f0463e 100644 --- a/apps/web/components/apps/AppList.tsx +++ b/apps/web/components/apps/AppList.tsx @@ -139,7 +139,7 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL ...app, credentialOwner: { name: team.name, - avatar: team.logo, + avatar: team.logoUrl, teamId: team.teamId, credentialId: team.credentialId, readOnly: !team.isAdmin, diff --git a/apps/web/components/apps/InstallAppButtonChild.tsx b/apps/web/components/apps/InstallAppButtonChild.tsx index 640a6031605b9..274f9dfcf6c60 100644 --- a/apps/web/components/apps/InstallAppButtonChild.tsx +++ b/apps/web/components/apps/InstallAppButtonChild.tsx @@ -2,7 +2,7 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation"; import { doesAppSupportTeamInstall } from "@calcom/app-store/utils"; import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner"; import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams"; -import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { RouterOutputs } from "@calcom/trpc/react"; import type { AppFrontendPayload } from "@calcom/types/App"; @@ -124,8 +124,8 @@ export const InstallAppButtonChild = ({ disabled={isInstalled} CustomStartIcon={ } diff --git a/apps/web/components/eventtype/EventAppsTab.tsx b/apps/web/components/eventtype/EventAppsTab.tsx index 8d56dd00b10b2..10160ae34a403 100644 --- a/apps/web/components/eventtype/EventAppsTab.tsx +++ b/apps/web/components/eventtype/EventAppsTab.tsx @@ -115,7 +115,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { // credentialIds: team?.credentialId ? [team.credentialId] : [], credentialOwner: { name: team.name, - avatar: team.logo, + avatar: team.logoUrl, teamId: team.teamId, credentialId: team.credentialId, }, diff --git a/apps/web/components/getting-started/steps-views/UserProfile.tsx b/apps/web/components/getting-started/steps-views/UserProfile.tsx index f0331177bfd5c..637b409c6fa4f 100644 --- a/apps/web/components/getting-started/steps-views/UserProfile.tsx +++ b/apps/web/components/getting-started/steps-views/UserProfile.tsx @@ -33,7 +33,7 @@ const UserProfile = () => { const mutation = trpc.viewer.updateProfile.useMutation({ onSuccess: async (_data, context) => { - if (context.avatar) { + if (context.avatarUrl) { showToast(t("your_user_profile_updated_successfully"), "success"); await utils.viewer.me.refetch(); } else @@ -74,7 +74,7 @@ const UserProfile = () => { event.preventDefault(); const enteredAvatar = avatarRef.current?.value; mutation.mutate({ - avatar: enteredAvatar, + avatarUrl: enteredAvatar, }); } diff --git a/apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx b/apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx index 7163afa276fb4..f5f6ebea59ffa 100644 --- a/apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx +++ b/apps/web/lib/getting-started/[[...step]]/getServerSideProps.tsx @@ -33,7 +33,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => select: { id: true, name: true, - logo: true, + logoUrl: true, }, }, }, diff --git a/apps/web/lib/team/[slug]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/getServerSideProps.tsx index a094a923b7b9f..fac87f381bdb8 100644 --- a/apps/web/lib/team/[slug]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/getServerSideProps.tsx @@ -98,6 +98,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => isPrivate: true, isOrganization: true, metadata: true, + logoUrl: true, }, }, }, diff --git a/apps/web/modules/users/views/users-public-view.getServerSideProps.tsx b/apps/web/modules/users/views/users-public-view.getServerSideProps.tsx index 5590f0239d6d7..95d820689b778 100644 --- a/apps/web/modules/users/views/users-public-view.getServerSideProps.tsx +++ b/apps/web/modules/users/views/users-public-view.getServerSideProps.tsx @@ -46,6 +46,7 @@ export type UserPageProps = { markdownStrippedBio: string; safeBio: string; entity: { + logoUrl?: string | null; considerUnpublished: boolean; orgSlug?: string | null; name?: string | null; @@ -98,12 +99,7 @@ export const getServerSideProps: GetServerSideProps = async (cont orgSlug: isValidOrgDomain ? currentOrgDomain : null, }); - const usersWithoutAvatar = usersInOrgContext.map((user) => { - const { avatar: _1, ...rest } = user; - return rest; - }); - - const isDynamicGroup = usersWithoutAvatar.length > 1; + const isDynamicGroup = usersInOrgContext.length > 1; log.debug(safeStringify({ usersInOrgContext, isValidOrgDomain, currentOrgDomain, isDynamicGroup })); if (isDynamicGroup) { @@ -114,40 +110,27 @@ export const getServerSideProps: GetServerSideProps = async (cont permanent: false, destination: destinationUrl, }, - } as { - redirect: { - permanent: false; - destination: string; - }; - }; + } as const; } - const users = usersWithoutAvatar.map((user) => ({ - ...user, - avatar: `/${user.username}/avatar.png`, - })); - const isNonOrgUser = (user: { profile: UserProfile }) => { return !user.profile?.organization; }; - const isThereAnyNonOrgUser = users.some(isNonOrgUser); + const isThereAnyNonOrgUser = usersInOrgContext.some(isNonOrgUser); - if (!users.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) { + if (!usersInOrgContext.length || (!isValidOrgDomain && !isThereAnyNonOrgUser)) { return { notFound: true, - } as { - notFound: true; - }; + } as const; } - const [user] = users; //to be used when dealing with single user, not dynamic group + const [user] = usersInOrgContext; //to be used when dealing with single user, not dynamic group const profile = { name: user.name || user.username || "", image: getUserAvatarUrl({ - ...user, - profile: user.profile, + avatarUrl: user.avatarUrl, }), theme: user.theme, brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR, @@ -183,11 +166,11 @@ export const getServerSideProps: GetServerSideProps = async (cont const safeBio = markdownToSafeHTML(user.bio) || ""; const markdownStrippedBio = stripMarkdown(user?.bio || ""); - const org = usersWithoutAvatar[0].profile.organization; + const org = usersInOrgContext[0].profile.organization; return { props: { - users: users.map((user) => ({ + users: usersInOrgContext.map((user) => ({ name: user.name, username: user.username, bio: user.bio, @@ -197,6 +180,7 @@ export const getServerSideProps: GetServerSideProps = async (cont away: user.away, })), entity: { + ...(org?.logoUrl ? { logoUrl: org?.logoUrl } : {}), considerUnpublished: !isARedirectFromNonOrgLink && org?.slug === null, orgSlug: currentOrgDomain, name: org?.name ?? null, diff --git a/apps/web/modules/users/views/users-public-view.tsx b/apps/web/modules/users/views/users-public-view.tsx index ab1d30dee0be9..beb6abe00c14d 100644 --- a/apps/web/modules/users/views/users-public-view.tsx +++ b/apps/web/modules/users/views/users-public-view.tsx @@ -3,7 +3,6 @@ import classNames from "classnames"; import type { InferGetServerSidePropsType } from "next"; import Link from "next/link"; -import { useSearchParams } from "next/navigation"; import { Toaster } from "react-hot-toast"; import { @@ -23,7 +22,6 @@ import { type getServerSideProps } from "./users-public-view.getServerSideProps" export function UserPage(props: InferGetServerSidePropsType) { const { users, profile, eventTypes, markdownStrippedBio, entity } = props; - const searchParams = useSearchParams(); const [user] = users; //To be used when we only have a single user, not dynamic group useTheme(profile.theme); @@ -43,8 +41,6 @@ export function UserPage(props: InferGetServerSidePropsType { diff --git a/apps/web/pages/api/cron/monthlyDigestEmail.ts b/apps/web/pages/api/cron/monthlyDigestEmail.ts index ed08276cf1ae8..2ff9314a362aa 100644 --- a/apps/web/pages/api/cron/monthlyDigestEmail.ts +++ b/apps/web/pages/api/cron/monthlyDigestEmail.ts @@ -286,7 +286,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) in: userIds as number[], }, }, - select: { id: true, name: true, email: true, avatar: true, username: true }, + select: { id: true, name: true, email: true, avatarUrl: true, username: true }, }); const userHashMap = new Map(); diff --git a/apps/web/pages/api/user/avatar.ts b/apps/web/pages/api/user/avatar.ts deleted file mode 100644 index ba9ba8209bfb3..0000000000000 --- a/apps/web/pages/api/user/avatar.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { z } from "zod"; - -import { - orgDomainConfig, - whereClauseForOrgWithSlugOrRequestedSlug, -} from "@calcom/features/ee/organizations/lib/orgDomains"; -import { AVATAR_FALLBACK } from "@calcom/lib/constants"; -import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; -import logger from "@calcom/lib/logger"; -import prisma from "@calcom/prisma"; - -const log = logger.getSubLogger({ prefix: ["team/[slug]"] }); -const querySchema = z - .object({ - username: z.string(), - teamname: z.string(), - /** - * Passed when we want to fetch avatar of a particular organization - */ - orgSlug: z.string(), - /** - * Allow fetching avatar of a particular organization - * Avatars being public, we need not worry about others accessing it. - */ - orgId: z.string().transform((s) => Number(s)), - }) - .partial(); - -async function getIdentityData(req: NextApiRequest) { - const { username, teamname, orgId, orgSlug } = querySchema.parse(req.query); - const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req); - - const org = isValidOrgDomain ? currentOrgDomain : null; - - const orgQuery = orgId - ? { - id: orgId, - } - : org - ? whereClauseForOrgWithSlugOrRequestedSlug(org) - : null; - - if (username) { - const users = await prisma.user.findMany({ - where: { - ...(orgQuery - ? { - profiles: { - some: { - username, - organization: orgQuery, - }, - }, - } - : { - username, - // If a user is moved, it isn't actually available outside of the organization. So, for non-org domain check for movedToProfileId - movedToProfileId: null, - }), - }, - select: { avatar: true, email: true }, - }); - - if (users.length > 1) { - throw new Error(`More than one user found for username "${username}"`); - } - const [user] = users; - return { - name: username, - email: user?.email, - avatar: user?.avatar, - org, - }; - } - - if (teamname) { - const team = await prisma.team.findFirst({ - where: { - slug: teamname, - parent: orgQuery, - }, - select: { logo: true }, - }); - - return { - org, - name: teamname, - email: null, - avatar: getPlaceholderAvatar(team?.logo, teamname), - }; - } - - if (orgSlug) { - const orgs = await prisma.team.findMany({ - where: { - ...whereClauseForOrgWithSlugOrRequestedSlug(orgSlug), - }, - select: { - slug: true, - logo: true, - name: true, - }, - }); - - if (orgs.length > 1) { - // This should never happen, but instead of throwing error, we are just logging to be able to observe when it happens. - log.error("More than one organization found for slug", orgSlug); - } - - const org = orgs[0]; - return { - org: org?.slug, - name: org?.name, - email: null, - avatar: getPlaceholderAvatar(org?.logo, org?.name), - }; - } -} - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const identity = await getIdentityData(req); - const img = identity?.avatar; - // If image isn't set or links to this route itself, use default avatar - if (!img) { - if (identity?.org) { - res.setHeader("x-cal-org", identity.org); - } - res.writeHead(302, { - Location: AVATAR_FALLBACK, - }); - - return res.end(); - } - - if (!img.includes("data:image")) { - if (identity.org) { - res.setHeader("x-cal-org", identity.org); - } - res.writeHead(302, { Location: img }); - return res.end(); - } - - const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", ""); - const imageResp = Buffer.from(decoded, "base64"); - if (identity.org) { - res.setHeader("x-cal-org", identity.org); - } - res.writeHead(200, { - "Content-Type": "image/png", - "Content-Length": imageResp.length, - }); - res.end(imageResp); -} diff --git a/apps/web/pages/settings/my-account/profile.tsx b/apps/web/pages/settings/my-account/profile.tsx index d662b32bec156..14cde6aaea6c0 100644 --- a/apps/web/pages/settings/my-account/profile.tsx +++ b/apps/web/pages/settings/my-account/profile.tsx @@ -22,7 +22,6 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import type { AppRouter } from "@calcom/trpc/server/routers/_app"; -import type { Ensure } from "@calcom/types/utils"; import { Alert, Button, @@ -85,7 +84,7 @@ type Email = { export type FormValues = { username: string; - avatar: string; + avatarUrl: string | null; name: string; email: string; bio: string; @@ -98,15 +97,10 @@ const ProfileView = () => { const { update } = useSession(); const { data: user, isPending } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); - const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, { - enabled: !isPending && !user?.avatarUrl, - }); - const updateProfileMutation = trpc.viewer.updateProfile.useMutation({ onSuccess: async (res) => { await update(res); utils.viewer.me.invalidate(); - utils.viewer.avatar.invalidate(); utils.viewer.shouldVerifyEmail.invalidate(); if (res.hasEmailBeenChanged && res.sendEmailVerification) { @@ -250,10 +244,7 @@ const ProfileView = () => { const userEmail = user.email || ""; const defaultValues = { username: user.username || "", - avatar: getUserAvatarUrl({ - ...user, - profile: user.profile, - }), + avatarUrl: user.avatarUrl, name: user.name || "", email: userEmail, bio: user.bio || "", @@ -284,7 +275,7 @@ const ProfileView = () => { key={JSON.stringify(defaultValues)} defaultValues={defaultValues} isPending={updateProfileMutation.isPending} - isFallbackImg={!user.avatarUrl && !avatarData?.avatar} + isFallbackImg={!user.avatarUrl} user={user} userOrganization={user.organization} onSubmit={(values) => { @@ -529,7 +520,7 @@ const ProfileForm = ({ const profileFormSchema = z.object({ username: z.string(), - avatar: z.string(), + avatarUrl: z.string().nullable(), name: z .string() .trim() @@ -622,17 +613,9 @@ const ProfileForm = ({
{ - const showRemoveAvatarButton = value === null ? false : !isFallbackImg; - const organization = - userOrganization && userOrganization.id - ? { - ...(userOrganization as Ensure, "id">), - slug: userOrganization.slug || null, - requestedSlug: userOrganization.metadata?.requestedSlug || null, - } - : null; + name="avatarUrl" + render={({ field: { value, onChange } }) => { + const showRemoveAvatarButton = value !== null; return ( <> @@ -644,9 +627,9 @@ const ProfileForm = ({ id="avatar-upload" buttonMsg={t("upload_avatar")} handleAvatarChange={(newAvatar) => { - formMethods.setValue("avatar", newAvatar, { shouldDirty: true }); + onChange(newAvatar); }} - imageSrc={value} + imageSrc={getUserAvatarUrl({ avatarUrl: value })} triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"} /> @@ -654,7 +637,7 @@ const ProfileForm = ({ diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index 48a15746870af..67f99c7e5a449 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -13,7 +13,7 @@ import { useEffect } from "react"; import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription"; -import { getOrgAvatarUrl, getTeamAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import useTheme from "@calcom/lib/hooks/useTheme"; @@ -67,6 +67,7 @@ function TeamPage({
@@ -161,10 +162,7 @@ function TeamPage({
); - const profileImageSrc = - isValidOrgDomain || team.isOrganization - ? getOrgAvatarUrl({ slug: currentOrgDomain, logoUrl: team.logoUrl }) - : getTeamAvatarUrl({ slug: team.slug, logoUrl: team.logoUrl, organizationId: team.parent?.id }); + const profileImageSrc = getPlaceholderAvatar(team.logoUrl || team.parent?.logoUrl, team.name); return ( <> diff --git a/apps/web/playwright/change-username.e2e.ts b/apps/web/playwright/change-username.e2e.ts index d92b58196dd07..e88335dc8b569 100644 --- a/apps/web/playwright/change-username.e2e.ts +++ b/apps/web/playwright/change-username.e2e.ts @@ -74,10 +74,6 @@ test.describe("Change username on settings", () => { }); expect(updatedUser.username).toBe("demo.username"); - - // Check if user avatar can be accessed and response headers contain 'image/' in the content type - const response = await page.goto("/demo.username/avatar.png"); - expect(response?.headers()?.["content-type"]).toContain("image/"); }); test("User can update to PREMIUM username", async ({ page, users }, testInfo) => { diff --git a/apps/web/playwright/settings/upload-avatar.e2e.ts b/apps/web/playwright/settings/upload-avatar.e2e.ts index e652d94c18a27..adcf31df50103 100644 --- a/apps/web/playwright/settings/upload-avatar.e2e.ts +++ b/apps/web/playwright/settings/upload-avatar.e2e.ts @@ -1,15 +1,18 @@ import { expect } from "@playwright/test"; import path from "path"; +import { CAL_URL } from "@calcom/lib/constants"; import { prisma } from "@calcom/prisma"; import { test } from "../lib/fixtures"; -test.describe("UploadAvatar", async () => { - test("can upload an image", async ({ page, users }) => { - const user = await users.create({}); +test.describe("User Avatar", async () => { + test("it can upload a user profile image", async ({ page, users }) => { + const user = await users.create({ name: "John Doe" }); await user.apiLogin(); + let objectKey: string; + await test.step("Can upload an initial picture", async () => { await page.goto("/settings/my-account/profile"); @@ -26,8 +29,6 @@ test.describe("UploadAvatar", async () => { await page.getByTestId("upload-avatar").click(); - await page.locator("input[name='name']").fill(user.email); - await page.getByText("Update").click(); await page.waitForSelector("text=Settings updated successfully"); @@ -41,18 +42,165 @@ test.describe("UploadAvatar", async () => { }, }); - // Wait for the avatar image with the http src to appear instead of the data uri - const avatar = page.getByTestId("profile-upload-avatar").locator("img[src^=http]"); + objectKey = response.objectKey; - const src = await avatar.getAttribute("src"); + const avatarImage = page.getByTestId("profile-upload-avatar").locator("img"); - await expect(src).toContain(response.objectKey); + await expect(avatarImage).toHaveAttribute("src", new RegExp(`^\/api\/avatar\/${objectKey}\.png$`)); - const urlResponse = await page.request.get(`/api/avatar/${response.objectKey}.png`, { + const urlResponse = await page.request.get((await avatarImage.getAttribute("src")) || "", { maxRedirects: 0, }); await expect(urlResponse?.status()).toBe(200); }); + + await test.step("View avatar on the public page", async () => { + await page.goto(`/${user.username}`); + + await expect(page.locator(`img`)).toHaveAttribute( + "src", + new RegExp(`\/api\/avatar\/${objectKey}\.png$`) + ); + // verify objectKey is passed to the OG image + // yes, OG image URI encodes at multiple places.. don't want to mess with that. + await expect(page.locator('meta[property="og:image"]')).toHaveAttribute( + "content", + new RegExp( + encodeURIComponent(`meetingImage=${encodeURIComponent(`${CAL_URL}/api/avatar/${objectKey}.png`)}`) + ) + ); + }); + }); +}); + +test.describe("Team Logo", async () => { + test("it can upload a team logo image", async ({ page, users }) => { + const user = await users.create(undefined, { hasTeam: true }); + + const { team } = await user.getFirstTeamMembership(); + + await user.apiLogin(); + + await page.goto(`/settings/teams/${team.id}/profile`); + + await test.step("Can upload an initial picture", async () => { + await page.getByTestId("open-upload-avatar-dialog").click(); + + const [fileChooser] = await Promise.all([ + // It is important to call waitForEvent before click to set up waiting. + page.waitForEvent("filechooser"), + // Opens the file chooser. + page.getByTestId("open-upload-image-filechooser").click(), + ]); + + await fileChooser.setFiles(`${path.dirname(__filename)}/../fixtures/cal.png`); + + await page.getByTestId("upload-avatar").click(); + + await page.getByText("Update").click(); + await page.waitForSelector("text=Your team has been updated successfully."); + + const response = await prisma.avatar.findUniqueOrThrow({ + where: { + teamId_userId_isBanner: { + userId: 0, + teamId: team.id, + isBanner: false, + }, + }, + }); + + const avatarImage = page.getByTestId("profile-upload-logo").locator("img"); + + await expect(avatarImage).toHaveAttribute( + "src", + new RegExp(`^\/api\/avatar\/${response.objectKey}\.png$`) + ); + + const urlResponse = await page.request.get((await avatarImage.getAttribute("src")) || "", { + maxRedirects: 0, + }); + + await expect(urlResponse?.status()).toBe(200); + + await expect( + page.getByTestId("tab-teams").locator(`img[src="/api/avatar/${response.objectKey}.png"]`) + ).toBeVisible(); + }); + }); +}); + +test.describe("Organization Logo", async () => { + test("it can upload a organization logo image", async ({ page, users, orgs }) => { + const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true }); + const { team: org } = await owner.getOrgMembership(); + + await owner.apiLogin(); + await page.goto("/settings/organizations/profile"); + + let objectKey: string; + + await test.step("Can upload an initial picture", async () => { + await page.getByTestId("open-upload-avatar-dialog").click(); + + const [fileChooser] = await Promise.all([ + // It is important to call waitForEvent before click to set up waiting. + page.waitForEvent("filechooser"), + // Opens the file chooser. + page.getByTestId("open-upload-image-filechooser").click(), + ]); + + await fileChooser.setFiles(`${path.dirname(__filename)}/../fixtures/cal.png`); + + await page.getByTestId("upload-avatar").click(); + + await page.getByText("Update").click(); + await page.waitForSelector("text=Your organization updated successfully"); + + const response = await prisma.avatar.findUniqueOrThrow({ + where: { + teamId_userId_isBanner: { + userId: 0, + teamId: org.id, + isBanner: false, + }, + }, + }); + + objectKey = response.objectKey; + + const avatarImage = page.getByTestId("profile-upload-logo").locator("img"); + + await expect(avatarImage).toHaveAttribute( + "src", + new RegExp(`^\/api\/avatar\/${response.objectKey}\.png$`) + ); + + const urlResponse = await page.request.get((await avatarImage.getAttribute("src")) || "", { + maxRedirects: 0, + }); + + await expect(urlResponse?.status()).toBe(200); + + // TODO: Implement the org logo updating in the sidebar + // this should be done in the orgBrandingContext + }); + + const requestedSlug = org.metadata?.requestedSlug; + + await test.step("it shows the correct logo on the unpublished public page", async () => { + await page.goto(`/org/${requestedSlug}`); + + expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1); + + await expect(page.locator(`img`)).toHaveAttribute( + "src", + new RegExp(`^\/api\/avatar\/${objectKey}\.png$`) + ); + }); + + // TODO: add test for published team. + // unpublished works regardless of orgDomain but when it is published it does work }); }); diff --git a/apps/web/playwright/unpublished.e2e.ts b/apps/web/playwright/unpublished.e2e.ts index fb486c3859cd7..ae16a56f0e9ba 100644 --- a/apps/web/playwright/unpublished.e2e.ts +++ b/apps/web/playwright/unpublished.e2e.ts @@ -9,7 +9,6 @@ test.describe.configure({ mode: "parallel" }); const title = (name: string) => `${name} is unpublished`; const description = (entity: string) => `This ${entity} link is currently not available. Please contact the ${entity} owner or ask them to publish it.`; -const avatar = (slug: string, entity = "team") => `/${entity}/${slug}/avatar.png`; test.afterAll(async ({ users }) => { await users.deleteAll(); @@ -24,7 +23,7 @@ test.describe("Unpublished", () => { expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1); expect(await page.locator(`h2:has-text("${title(team.name)}")`).count()).toBe(1); expect(await page.locator(`div:text("${description("team")}")`).count()).toBe(1); - await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug)); + await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }); test("Regular team event type", async ({ page, users }) => { @@ -40,7 +39,7 @@ test.describe("Unpublished", () => { expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1); expect(await page.locator(`h2:has-text("${title(team.name)}")`).count()).toBe(1); expect(await page.locator(`div:text("${description("team")}")`).count()).toBe(1); - await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug)); + await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }); test("Organization profile", async ({ users, page }) => { @@ -52,7 +51,7 @@ test.describe("Unpublished", () => { expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1); expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1); expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1); - await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org")); + await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }); test("Organization sub-team", async ({ users, page }) => { @@ -70,7 +69,7 @@ test.describe("Unpublished", () => { expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1); expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1); expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1); - await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org")); + await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }); test("Organization sub-team event-type", async ({ users, page }) => { @@ -90,7 +89,7 @@ test.describe("Unpublished", () => { expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1); expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1); expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1); - await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org")); + await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }); test("Organization user", async ({ users, page }) => { @@ -102,7 +101,7 @@ test.describe("Unpublished", () => { expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1); expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1); expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1); - await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org")); + await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }); test("Organization user event-type", async ({ users, page }) => { @@ -115,6 +114,6 @@ test.describe("Unpublished", () => { expect(await page.locator('[data-testid="empty-screen"]').count()).toBe(1); expect(await page.locator(`h2:has-text("${title(org.name)}")`).count()).toBe(1); expect(await page.locator(`div:text("${description("organization")}")`).count()).toBe(1); - await expect(page.locator(`img`)).toHaveAttribute("src", avatar(requestedSlug, "org")); + await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }); }); diff --git a/packages/app-store/googlecalendar/api/callback.ts b/packages/app-store/googlecalendar/api/callback.ts index 16e8a712caa9f..2231ac3068138 100644 --- a/packages/app-store/googlecalendar/api/callback.ts +++ b/packages/app-store/googlecalendar/api/callback.ts @@ -184,7 +184,7 @@ async function updateProfilePhoto(oAuth2Client: Auth.OAuth2Client, userId: numbe if (userDetails.data?.picture) { // Using updateMany here since if the user already has a profile it would throw an error because no records were found to update the profile picture await prisma.user.updateMany({ - where: { id: userId, avatarUrl: null, avatar: null }, + where: { id: userId, avatarUrl: null }, data: { avatarUrl: userDetails.data.picture, }, diff --git a/packages/embeds/.eslintrc.js b/packages/embeds/.eslintrc.js index 62e45fcb34bbf..948fdc713de94 100644 --- a/packages/embeds/.eslintrc.js +++ b/packages/embeds/.eslintrc.js @@ -10,4 +10,12 @@ module.exports = { }, ], }, + overrides: [ + { + files: ["embed-core/playwright/**/*"], + rules: { + "no-restricted-imports": "off", + }, + }, + ], }; diff --git a/packages/features/auth/lib/getServerSession.ts b/packages/features/auth/lib/getServerSession.ts index a2c2a9daf36b9..734debd0e7bee 100644 --- a/packages/features/auth/lib/getServerSession.ts +++ b/packages/features/auth/lib/getServerSession.ts @@ -58,8 +58,6 @@ export async function getServerSession(options: { where: { email: token.email.toLowerCase(), }, - // TODO: Re-enable once we get confirmation from compliance that this is okay. - // cacheStrategy: { ttl: 60, swr: 1 }, }); if (!userFromDb) { @@ -97,8 +95,7 @@ export async function getServerSession(options: { email_verified: user.emailVerified !== null, role: user.role, image: getUserAvatarUrl({ - ...user, - profile: user.profile, + avatarUrl: user.avatarUrl, }), belongsToActiveTeam: token.belongsToActiveTeam, org: token.org, diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index ada217709d8c5..192eb9ceec3d7 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -475,6 +475,7 @@ export const AUTH_OPTIONS: AuthOptions = { select: { id: true, username: true, + avatarUrl: true, name: true, email: true, role: true, @@ -524,6 +525,7 @@ export const AUTH_OPTIONS: AuthOptions = { id: profileOrg.id, name: profileOrg.name, slug: profileOrg.slug ?? profileOrg.requestedSlug ?? "", + logoUrl: profileOrg.logoUrl, fullDomain: getOrgFullOrigin(profileOrg.slug ?? profileOrg.requestedSlug ?? ""), domainSuffix: subdomainSuffix(), } diff --git a/packages/features/auth/signup/utils/prefillAvatar.ts b/packages/features/auth/signup/utils/prefillAvatar.ts index 89ec7681bb132..082f9c105934b 100644 --- a/packages/features/auth/signup/utils/prefillAvatar.ts +++ b/packages/features/auth/signup/utils/prefillAvatar.ts @@ -47,7 +47,6 @@ export const prefillAvatar = async ({ email }: IPrefillAvatar) => { const data: Prisma.UserUpdateInput = {}; data.avatarUrl = avatarUrl; - data.avatar = avatar; await prisma.user.update({ where: { email: email }, diff --git a/packages/features/ee/organizations/context/provider.ts b/packages/features/ee/organizations/context/provider.ts index 05e9f86499c61..6419bc2603b0c 100644 --- a/packages/features/ee/organizations/context/provider.ts +++ b/packages/features/ee/organizations/context/provider.ts @@ -16,6 +16,8 @@ export type OrganizationBranding = name?: string; /** acme */ slug: string; + /** logo url */ + logoUrl?: string | null; /** https://acme.cal.com */ fullDomain: string; /** cal.com */ diff --git a/packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx b/packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx index f5a5eb4dd6616..5039695f800c9 100644 --- a/packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx +++ b/packages/features/ee/organizations/pages/components/OtherTeamListItem.tsx @@ -43,7 +43,7 @@ export default function OtherTeamListItem(props: Props) {
diff --git a/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx b/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx index 1511b95d6062c..b7317d5610fef 100644 --- a/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx +++ b/packages/features/ee/organizations/pages/settings/other-team-profile-view.tsx @@ -51,7 +51,7 @@ const teamProfileFormSchema = z.object({ message: "Url can only have alphanumeric characters(a-z, 0-9) and hyphen(-) symbol.", }) .min(1, { message: "Url cannot be left empty" }), - logo: z.string(), + logoUrl: z.string().nullable(), bio: z.string(), }); @@ -105,7 +105,7 @@ const OtherTeamProfileView = () => { if (team) { form.setValue("name", team.name || ""); form.setValue("slug", team.slug || ""); - form.setValue("logo", team.logo || ""); + form.setValue("logoUrl", team.logoUrl); form.setValue("bio", team.bio || ""); if (team.slug === null && (team?.metadata as Prisma.JsonObject)?.requestedSlug) { form.setValue("slug", ((team?.metadata as Prisma.JsonObject)?.requestedSlug as string) || ""); @@ -179,7 +179,7 @@ const OtherTeamProfileView = () => { handleSubmit={(values) => { if (team) { const variables = { - logo: values.logo, + logoUrl: values.logoUrl, name: values.name, slug: values.slug, bio: values.bio, @@ -193,18 +193,16 @@ const OtherTeamProfileView = () => {
( + name="logoUrl" + render={({ field: { value, onChange } }) => ( <> - +
{ - form.setValue("logo", newLogo); - }} + handleAvatarChange={onChange} imageSrc={value} />
@@ -218,15 +216,13 @@ const OtherTeamProfileView = () => { ( + render={({ field: { value, onChange } }) => (
{ - form.setValue("name", e?.target.value); - }} + onChange={(e) => onChange(e?.target.value)} />
)} @@ -234,7 +230,7 @@ const OtherTeamProfileView = () => { ( + render={({ field: { value, onChange } }) => (
{ } onChange={(e) => { form.clearErrors("slug"); - form.setValue("slug", slugify(e?.target.value, true)); + onChange(slugify(e?.target.value, true)); }} />
diff --git a/packages/features/ee/organizations/pages/settings/profile.tsx b/packages/features/ee/organizations/pages/settings/profile.tsx index 3e5f2b5b82317..c16e15065ed8e 100644 --- a/packages/features/ee/organizations/pages/settings/profile.tsx +++ b/packages/features/ee/organizations/pages/settings/profile.tsx @@ -39,14 +39,14 @@ import { useOrgBranding } from "../../../organizations/context/provider"; const orgProfileFormSchema = z.object({ name: z.string(), - logo: z.string().nullable(), + logoUrl: z.string().nullable(), banner: z.string().nullable(), bio: z.string(), }); type FormValues = { name: string; - logo: string | null; + logoUrl: string | null; banner: string | null; bio: string; slug: string; @@ -111,7 +111,7 @@ const OrgProfileView = () => { const defaultValues: FormValues = { name: currentOrganisation?.name || "", - logo: currentOrganisation?.logo || "", + logoUrl: currentOrganisation?.logoUrl, banner: currentOrganisation?.bannerUrl || "", bio: currentOrganisation?.bio || "", slug: @@ -177,7 +177,7 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => { }, onSuccess: async (res) => { reset({ - logo: (res.data?.logo || "") as string, + logoUrl: res.data?.logoUrl, name: (res.data?.name || "") as string, bio: (res.data?.bio || "") as string, slug: defaultValues["slug"], @@ -201,7 +201,7 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => { form={form} handleSubmit={(values) => { const variables = { - logo: values.logo, + logoUrl: values.logoUrl, name: values.name, slug: values.slug, bio: values.bio, @@ -214,36 +214,29 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
{ - const showRemoveLogoButton = !!value; - + name="logoUrl" + render={({ field: { value, onChange } }) => { + const showRemoveLogoButton = value !== null; return ( <>
{ - form.setValue("logo", newLogo, { shouldDirty: true }); - }} - imageSrc={value || undefined} + handleAvatarChange={onChange} + imageSrc={getPlaceholderAvatar(value, form.getValues("name"))} triggerButtonColor={showRemoveLogoButton ? "secondary" : "primary"} /> {showRemoveLogoButton && ( - )} @@ -259,7 +252,7 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => { { + render={({ field: { value, onChange } }) => { const showRemoveBannerButton = !!value; return ( @@ -279,18 +272,12 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => { uploadInstruction={t("org_banner_instructions", { height: 500, width: 1500 })} id="banner-upload" buttonMsg={t("upload_banner")} - handleAvatarChange={(newBanner) => { - form.setValue("banner", newBanner, { shouldDirty: true }); - }} + handleAvatarChange={onChange} imageSrc={value || undefined} triggerButtonColor={showRemoveBannerButton ? "secondary" : "primary"} /> {showRemoveBannerButton && ( - )} diff --git a/packages/features/ee/teams/components/TeamListItem.tsx b/packages/features/ee/teams/components/TeamListItem.tsx index 91ceaf7c1b4ab..c96830708eb7e 100644 --- a/packages/features/ee/teams/components/TeamListItem.tsx +++ b/packages/features/ee/teams/components/TeamListItem.tsx @@ -95,7 +95,7 @@ export default function TeamListItem(props: Props) {
diff --git a/packages/features/ee/teams/lib/getUserAdminTeams.ts b/packages/features/ee/teams/lib/getUserAdminTeams.ts index 247a32394afe2..ed4b0c74f6155 100644 --- a/packages/features/ee/teams/lib/getUserAdminTeams.ts +++ b/packages/features/ee/teams/lib/getUserAdminTeams.ts @@ -7,13 +7,13 @@ export type UserAdminTeams = (Prisma.TeamGetPayload<{ select: { id: true; name: true; - logo: true; + logoUrl: true; credentials?: true; parent?: { select: { id: true; name: true; - logo: true; + logoUrl: true; credentials: true; }; }; @@ -45,14 +45,14 @@ const getUserAdminTeams = async ({ select: { id: true, name: true, - logo: true, + logoUrl: true, ...(includeCredentials && { credentials: true }), ...(getParentInfo && { parent: { select: { id: true, name: true, - logo: true, + logoUrl: true, credentials: true, }, }, @@ -72,7 +72,7 @@ const getUserAdminTeams = async ({ select: { id: true, name: true, - avatar: true, + avatarUrl: true, ...(includeCredentials && { credentials: true }), }, }); @@ -81,7 +81,7 @@ const getUserAdminTeams = async ({ const userObject = { id: user.id, name: user.name || "me", - logo: user?.avatar === "" ? null : user?.avatar, + logoUrl: user?.avatarUrl, // bit ugly, no? isUser: true, credentials: includeCredentials ? user.credentials : [], parent: null, diff --git a/packages/features/ee/teams/lib/types.ts b/packages/features/ee/teams/lib/types.ts index e84b5ea5fe18e..b2b561b6db372 100644 --- a/packages/features/ee/teams/lib/types.ts +++ b/packages/features/ee/teams/lib/types.ts @@ -13,6 +13,6 @@ export interface PendingMember { id?: number; username: string | null; role: MembershipRole; - avatar: string | null; + avatarUrl?: string | null; sendInviteEmail?: boolean; } diff --git a/packages/features/ee/teams/pages/team-profile-view.tsx b/packages/features/ee/teams/pages/team-profile-view.tsx index 78f9331c5313e..c99de595fb1fc 100644 --- a/packages/features/ee/teams/pages/team-profile-view.tsx +++ b/packages/features/ee/teams/pages/team-profile-view.tsx @@ -98,7 +98,7 @@ const ProfileView = () => { isPending, error, } = trpc.viewer.teams.get.useQuery( - { teamId, includeTeamLogo: true }, + { teamId }, { enabled: !!teamId, } @@ -259,12 +259,14 @@ const TeamProfileForm = ({ team }: TeamProfileFormProps) => { }, async onSuccess(res) { reset({ - logo: (res?.logo || "") as string, + logo: res?.logoUrl, name: (res?.name || "") as string, bio: (res?.bio || "") as string, slug: res?.slug as string, }); await utils.viewer.teams.get.invalidate(); + // TODO: Not all changes require list invalidation + await utils.viewer.teams.list.invalidate(); showToast(t("your_team_updated_successfully"), "success"); }, }); @@ -320,7 +322,7 @@ const TeamProfileForm = ({ team }: TeamProfileFormProps) => { }}>
{!team.parent && ( -
+
{ return ( <>
{showRemoveLogoButton && ( - )} diff --git a/packages/features/ee/users/components/UserForm.tsx b/packages/features/ee/users/components/UserForm.tsx index 1bc3101308b99..4d18849152c6d 100644 --- a/packages/features/ee/users/components/UserForm.tsx +++ b/packages/features/ee/users/components/UserForm.tsx @@ -2,6 +2,7 @@ import { noop } from "lodash"; import { Controller, useForm } from "react-hook-form"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { localeOptions } from "@calcom/lib/i18n"; import { nameOfDay } from "@calcom/lib/weekday"; @@ -35,7 +36,7 @@ type OptionValues = { identityProvider: Option; }; -type FormValues = Pick & OptionValues; +type FormValues = Pick & OptionValues; export const UserForm = ({ defaultValues, @@ -76,9 +77,10 @@ export const UserForm = ({ { value: "SAML", label: "SAML" }, ]; const defaultLocale = defaultValues?.locale || localeOptions[0].value; + const form = useForm({ defaultValues: { - avatar: defaultValues?.avatar, + avatarUrl: defaultValues?.avatarUrl, name: defaultValues?.name, username: defaultValues?.username, email: defaultValues?.email, @@ -116,19 +118,25 @@ export const UserForm = ({
( + name="avatarUrl" + render={({ field: { value, onChange } }) => ( <> - +
{ - form.setValue("avatar", newAvatar, { shouldDirty: true }); - }} - imageSrc={value || undefined} + handleAvatarChange={onChange} + imageSrc={getUserAvatarUrl({ + avatarUrl: value, + })} />
diff --git a/packages/features/ee/users/server/trpc-router.ts b/packages/features/ee/users/server/trpc-router.ts index e9f1b02e596d1..7d62928ec233a 100644 --- a/packages/features/ee/users/server/trpc-router.ts +++ b/packages/features/ee/users/server/trpc-router.ts @@ -1,8 +1,6 @@ import { z } from "zod"; import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; -import { WEBAPP_URL } from "@calcom/lib/constants"; -import { AVATAR_FALLBACK } from "@calcom/lib/constants"; import { RedirectType } from "@calcom/prisma/enums"; import { _UserModel as User } from "@calcom/prisma/zod"; import type { inferRouterOutputs } from "@calcom/trpc"; @@ -32,30 +30,9 @@ const userBodySchema = User.pick({ identityProvider: true, // away: true, role: true, - avatar: true, + avatarUrl: true, }); -/** - * @deprecated in favour of @calcom/lib/getAvatarUrl - */ -/** This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url */ -export function getAvatarUrlFromUser(user: { - avatar: string | null; - username: string | null; - email: string; -}) { - if (!user.avatar || !user.username) return AVATAR_FALLBACK; - return `${WEBAPP_URL}/${user.username}/avatar.png`; -} - -/** @see https://www.prisma.io/docs/concepts/components/prisma-client/excluding-fields#excluding-the-password-field */ -function exclude(user: UserType, keys: Key[]): Omit { - for (const key of keys) { - delete user[key]; - } - return user; -} - /** Reusable logic that checks for admin permissions and if the requested user exists */ //const authedAdminWithUserMiddleware = middleware(); @@ -83,15 +60,7 @@ export const userAdminRouter = router({ const { prisma } = ctx; // TODO: Add search, pagination, etc. const users = await prisma.user.findMany(); - return users.map((user) => ({ - ...user, - /** - * FIXME: This should be either a prisma extension or middleware - * @see https://www.prisma.io/docs/concepts/components/prisma-client/middleware - * @see https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions/result - **/ - avatar: getAvatarUrlFromUser(user), - })); + return users; }), add: authedAdminProcedure.input(userBodySchema).mutation(async ({ ctx, input }) => { const { prisma } = ctx; diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index 5db4fe9bc178e..d9eb20e196884 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -7,6 +7,7 @@ import { getAppFromSlug } from "@calcom/app-store/utils"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client"; @@ -177,8 +178,7 @@ export const getPublicEvent = async ( name: users[0].name, weekStart: users[0].weekStart, image: getUserAvatarUrl({ - ...users[0], - profile: users[0].profile, + avatarUrl: users[0].avatarUrl, }), brandColor: users[0].brandColor, darkBrandColor: users[0].darkBrandColor, @@ -336,21 +336,10 @@ function getProfileFromEvent(event: Event) { weekStart, image: team ? undefined - : // TODO: There must be a better way to do this, maybe a prisma middleware? - // This should come pre-proccessed from the database IMO instead of replacing everywhere - getUserAvatarUrl({ - username: username || "", - profile: { - id: nonTeamprofile?.id || null, - username: username || null, - organizationId: nonTeamprofile?.organization?.id || null, - organization: nonTeamprofile?.organization - ? { ...nonTeamprofile?.organization, requestedSlug: null } - : null, - }, + : getUserAvatarUrl({ avatarUrl: nonTeamprofile?.avatarUrl, }), - logo: !team ? undefined : team.logoUrl, + logo: !team ? undefined : getPlaceholderAvatar(team.logoUrl, team.name), brandColor: profile.brandColor, darkBrandColor: profile.darkBrandColor, theme: profile.theme, diff --git a/packages/features/filters/components/TeamsFilter.tsx b/packages/features/filters/components/TeamsFilter.tsx index 74700545ccc68..a8154ccba13f3 100644 --- a/packages/features/filters/components/TeamsFilter.tsx +++ b/packages/features/filters/components/TeamsFilter.tsx @@ -114,7 +114,7 @@ export const TeamsFilter = ({ icon={ } diff --git a/packages/features/insights/filters/TeamAndSelfList.tsx b/packages/features/insights/filters/TeamAndSelfList.tsx index f647310097185..698f62c17c63d 100644 --- a/packages/features/insights/filters/TeamAndSelfList.tsx +++ b/packages/features/insights/filters/TeamAndSelfList.tsx @@ -129,8 +129,8 @@ export const TeamAndSelfList = () => { }} icon={ } diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index b02ecfcc8de64..a13daf4175d6e 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -122,7 +122,7 @@ export interface IResultTeamList { id: number; slug: string | null; name: string | null; - logo: string | null; + logoUrl: string | null; userId?: number; isOrg?: boolean; } @@ -1195,7 +1195,7 @@ export const insightsRouter = router({ id: true, slug: true, name: true, - logo: true, + logoUrl: true, }, }); const orgTeam = await ctx.insightsDb.team.findUnique({ @@ -1206,7 +1206,7 @@ export const insightsRouter = router({ id: true, slug: true, name: true, - logo: true, + logoUrl: true, }, }); if (!orgTeam) { @@ -1218,11 +1218,11 @@ export const insightsRouter = router({ id: orgTeam.id, slug: orgTeam.slug, name: orgTeam.name, - logo: orgTeam.logo, + logoUrl: orgTeam.logoUrl, isOrg: true, }, ...teamsFromOrg.map( - (team: Prisma.TeamGetPayload<{ select: { id: true; slug: true; name: true; logo: true } }>) => { + (team: Prisma.TeamGetPayload<{ select: { id: true; slug: true; name: true; logoUrl: true } }>) => { return { ...team, }; @@ -1241,7 +1241,7 @@ export const insightsRouter = router({ select: { id: true, name: true, - logo: true, + logoUrl: true, slug: true, metadata: true, }, diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx index 245a96f24741d..8babd6ceff7e7 100644 --- a/packages/features/settings/layouts/SettingsLayout.tsx +++ b/packages/features/settings/layouts/SettingsLayout.tsx @@ -149,7 +149,7 @@ const useTabs = () => { tab.avatar = getUserAvatarUrl(user); } else if (tab.href === "/settings/organizations") { tab.name = orgBranding?.name || "organization"; - tab.avatar = `${orgBranding?.fullDomain}/org/${orgBranding?.slug}/avatar.png`; + tab.avatar = getPlaceholderAvatar(orgBranding?.logoUrl, orgBranding?.name); } else if ( tab.href === "/settings/security" && user?.identityProvider === IdentityProvider.GOOGLE && @@ -195,6 +195,140 @@ interface SettingsSidebarContainerProps { bannersHeight?: number; } +const TeamListCollapsible = () => { + const { data: teams } = trpc.viewer.teams.list.useQuery(); + const { t } = useLocale(); + const [teamMenuState, setTeamMenuState] = + useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>(); + const searchParams = useCompatSearchParams(); + useEffect(() => { + if (teams) { + const teamStates = teams?.map((team) => ({ + teamId: team.id, + teamMenuOpen: String(team.id) === searchParams?.get("id"), + })); + setTeamMenuState(teamStates); + setTimeout(() => { + const tabMembers = Array.from(document.getElementsByTagName("a")).filter( + (bottom) => bottom.dataset.testid === "vertical-tab-Members" + )[1]; + tabMembers?.scrollIntoView({ behavior: "smooth" }); + }, 100); + } + }, [searchParams?.get("id"), teams]); + + return ( + <> + {teams && + teamMenuState && + teams.map((team, index: number) => { + if (!teamMenuState[index]) { + return null; + } + if (teamMenuState.some((teamState) => teamState.teamId === team.id)) + return ( + + setTeamMenuState([ + ...teamMenuState, + (teamMenuState[index] = { + ...teamMenuState[index], + teamMenuOpen: !teamMenuState[index].teamMenuOpen, + }), + ]) + }> + +
+ setTeamMenuState([ + ...teamMenuState, + (teamMenuState[index] = { + ...teamMenuState[index], + teamMenuOpen: !teamMenuState[index].teamMenuOpen, + }), + ]) + }> +
+ {teamMenuState[index].teamMenuOpen ? ( + + ) : ( + + )} +
+ {!team.parentId && ( + {team.name + )} +

{team.name}

+ {!team.accepted && ( + + Inv. + + )} +
+
+ + {team.accepted && ( + + )} + + {(team.role === MembershipRole.OWNER || + team.role === MembershipRole.ADMIN || + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this exists wtf? + (team.isOrgAdmin && team.isOrgAdmin)) && ( + <> + {/* TODO */} + {/* */} + + {/* Hide if there is a parent ID */} + {!team.parentId ? ( + <> + + + ) : null} + + )} + +
+ ); + })} + + ); +}; + const SettingsSidebarContainer = ({ className = "", navigationIsOpenedOnMobile, @@ -203,15 +337,12 @@ const SettingsSidebarContainer = ({ const searchParams = useCompatSearchParams(); const { t } = useLocale(); const tabsWithPermissions = useTabs(); - const [teamMenuState, setTeamMenuState] = - useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>(); const [otherTeamMenuState, setOtherTeamMenuState] = useState< { teamId: number | undefined; teamMenuOpen: boolean; }[] >(); - const { data: teams } = trpc.viewer.teams.list.useQuery(); const session = useSession(); const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { enabled: !!session.data?.user?.org, @@ -221,22 +352,6 @@ const SettingsSidebarContainer = ({ enabled: !!session.data?.user?.org, }); - useEffect(() => { - if (teams) { - const teamStates = teams?.map((team) => ({ - teamId: team.id, - teamMenuOpen: String(team.id) === searchParams?.get("id"), - })); - setTeamMenuState(teamStates); - setTimeout(() => { - const tabMembers = Array.from(document.getElementsByTagName("a")).filter( - (bottom) => bottom.dataset.testid === "vertical-tab-Members" - )[1]; - tabMembers?.scrollIntoView({ behavior: "smooth" }); - }, 100); - } - }, [searchParams?.get("id"), teams]); - // Same as above but for otherTeams useEffect(() => { if (otherTeams) { @@ -299,7 +414,7 @@ const SettingsSidebarContainer = ({ User Avatar )} -
+
{tab && tab.icon && ( @@ -349,112 +464,7 @@ const SettingsSidebarContainer = ({
- {teams && - teamMenuState && - teams.map((team, index: number) => { - if (!teamMenuState[index]) { - return null; - } - if (teamMenuState.some((teamState) => teamState.teamId === team.id)) - return ( - - setTeamMenuState([ - ...teamMenuState, - (teamMenuState[index] = { - ...teamMenuState[index], - teamMenuOpen: !teamMenuState[index].teamMenuOpen, - }), - ]) - }> - -
- setTeamMenuState([ - ...teamMenuState, - (teamMenuState[index] = { - ...teamMenuState[index], - teamMenuOpen: !teamMenuState[index].teamMenuOpen, - }), - ]) - }> -
- {teamMenuState[index].teamMenuOpen ? ( - - ) : ( - - )} -
- {!team.parentId && ( - {team.name - )} -

{team.name}

- {!team.accepted && ( - - Inv. - - )} -
-
- - {team.accepted && ( - - )} - - {(team.role === MembershipRole.OWNER || - team.role === MembershipRole.ADMIN || - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore this exists wtf? - (team.isOrgAdmin && team.isOrgAdmin)) && ( - <> - {/* TODO */} - {/* */} - - {/* Hide if there is a parent ID */} - {!team.parentId ? ( - <> - - - ) : null} - - )} - -
- ); - })} + {(!currentOrg || (currentOrg && currentOrg?.user?.role !== "MEMBER")) && ( {!otherTeam.parentId && ( {otherTeam.name diff --git a/packages/features/settings/layouts/SettingsLayoutAppDir.tsx b/packages/features/settings/layouts/SettingsLayoutAppDir.tsx index 8096e0f67d6d4..6f9c454bd57ba 100644 --- a/packages/features/settings/layouts/SettingsLayoutAppDir.tsx +++ b/packages/features/settings/layouts/SettingsLayoutAppDir.tsx @@ -376,7 +376,7 @@ const SettingsSidebarContainer = ({
{!team.parentId && ( {team.name @@ -523,7 +523,7 @@ const SettingsSidebarContainer = ({
{!otherTeam.parentId && ( {otherTeam.name diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 911bb2db70e71..1ab3c685a88c2 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -49,6 +49,7 @@ import { TOP_BANNER_HEIGHT, WEBAPP_URL, } from "@calcom/lib/constants"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useFormbricks } from "@calcom/lib/formbricks-client"; import getBrandColours from "@calcom/lib/getBrandColours"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; @@ -922,7 +923,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {

diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx index b98159114c135..4536f0b739d18 100644 --- a/packages/features/users/components/UserTable/UserListTable.tsx +++ b/packages/features/users/components/UserTable/UserListTable.tsx @@ -4,6 +4,7 @@ import { useSession } from "next-auth/react"; import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { MembershipRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc"; @@ -25,6 +26,7 @@ export interface User { email: string; timeZone: string; role: MembershipRole; + avatarUrl: string | null; accepted: boolean; disableImpersonation: boolean; completedOnboarding: boolean; @@ -163,10 +165,16 @@ export function UserListTable() { accessorFn: (data) => data.email, header: `Member (${totalDBRowCount})`, cell: ({ row }) => { - const { username, email } = row.original; + const { username, email, avatarUrl } = row.original; return (

- +
& { - profile: Omit; - avatarUrl: string | null; - }) - | undefined -) => { +export const getUserAvatarUrl = (user: Pick | undefined) => { if (user?.avatarUrl) { const isAbsoluteUrl = z.string().url().safeParse(user.avatarUrl).success; - if (isAbsoluteUrl) { return user.avatarUrl; } else { return CAL_URL + user.avatarUrl; } } - if (!user?.username) return CAL_URL + AVATAR_FALLBACK; - // avatar.png automatically redirects to fallback avatar if user doesn't have one - return `${CAL_URL}/${user.profile?.username}/avatar.png${ - user.profile?.organizationId ? `?orgId=${user.profile.organizationId}` : "" - }`; -}; - -export function getTeamAvatarUrl( - team: Pick & { - organizationId?: number | null; - logoUrl?: string | null; - requestedSlug?: string | null; - } -) { - if (team.logoUrl) { - return team.logoUrl; - } - const slug = team.slug ?? team.requestedSlug; - return `${WEBAPP_URL}/team/${slug}/avatar.png${team.organizationId ? `?orgId=${team.organizationId}` : ""}`; -} - -export const getOrgAvatarUrl = ( - org: Pick & { - logoUrl?: string | null; - requestedSlug?: string | null; - } -) => { - if (org.logoUrl) { - return org.logoUrl; - } - const slug = org.slug ?? org.requestedSlug; - return `${WEBAPP_URL}/org/${slug}/avatar.png`; + return CAL_URL + AVATAR_FALLBACK; }; diff --git a/packages/lib/server/getBrand.ts b/packages/lib/server/getBrand.ts index e70bf1ec5ec02..60b30b9d5fef3 100644 --- a/packages/lib/server/getBrand.ts +++ b/packages/lib/server/getBrand.ts @@ -11,7 +11,7 @@ export const getBrand = async (orgId: number | null) => { id: orgId, }, select: { - logo: true, + logoUrl: true, name: true, slug: true, metadata: true, diff --git a/packages/lib/server/queries/teams/index.ts b/packages/lib/server/queries/teams/index.ts index e2732d7f921a0..6543966e1fe84 100644 --- a/packages/lib/server/queries/teams/index.ts +++ b/packages/lib/server/queries/teams/index.ts @@ -11,7 +11,6 @@ import { unlockedManagedEventTypeProps, } from "@calcom/prisma/zod-utils"; -import { WEBAPP_URL } from "../../../constants"; import { getBookerBaseUrlSync } from "../../../getBookerUrl/client"; import { getTeam, getOrg } from "../../repository/team"; import { UserRepository } from "../../repository/user"; @@ -23,7 +22,6 @@ export async function getTeamWithMembers(args: { slug?: string; userId?: number; orgSlug?: string | null; - includeTeamLogo?: boolean; isTeamView?: boolean; currentOrg?: Pick | null; /** @@ -31,7 +29,7 @@ export async function getTeamWithMembers(args: { */ isOrgView?: boolean; }) { - const { id, slug, currentOrg: _currentOrg, userId, orgSlug, isTeamView, isOrgView, includeTeamLogo } = args; + const { id, slug, currentOrg: _currentOrg, userId, orgSlug, isTeamView, isOrgView } = args; // This should improve performance saving already app data found. const appDataMap = new Map(); @@ -87,7 +85,6 @@ export async function getTeamWithMembers(args: { name: true, slug: true, isOrganization: true, - ...(!!includeTeamLogo ? { logo: true } : {}), logoUrl: true, bio: true, hideBranding: true, @@ -101,6 +98,7 @@ export async function getTeamWithMembers(args: { name: true, isPrivate: true, isOrganization: true, + logoUrl: true, metadata: true, }, }, @@ -183,7 +181,6 @@ export async function getTeamWithMembers(args: { .filter((membership) => membership.team.id !== teamOrOrg.id) .map((membership) => membership.team.slug) : null, - avatar: `${WEBAPP_URL}/${m.user.username}/avatar.png`, bookerUrl: getBookerBaseUrlSync(profile?.organization?.slug || ""), connectedApps: !isTeamView ? credentials?.map((cred) => { diff --git a/packages/lib/server/repository/profile.ts b/packages/lib/server/repository/profile.ts index c5441e6b36ff6..2614bea6c8d5d 100644 --- a/packages/lib/server/repository/profile.ts +++ b/packages/lib/server/repository/profile.ts @@ -40,6 +40,7 @@ const organizationSelect = { slug: true, name: true, metadata: true, + logoUrl: true, calVideoLogo: true, bannerUrl: true, }; diff --git a/packages/lib/server/repository/user.ts b/packages/lib/server/repository/user.ts index 308722cee98b9..d51b78140aba2 100644 --- a/packages/lib/server/repository/user.ts +++ b/packages/lib/server/repository/user.ts @@ -30,7 +30,6 @@ const userSelect = Prisma.validator()({ email: true, emailVerified: true, bio: true, - avatar: true, avatarUrl: true, timeZone: true, startTime: true, diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 088e933df03aa..fea2cfb43ce22 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -193,12 +193,53 @@ export const buildCalendarEvent = ( }; type UserPayload = Prisma.UserGetPayload<{ - include: { + select: { + locked: true; + name: true; + email: true; + timeZone: true; + username: true; + id: true; + allowDynamicBooking: true; credentials: true; destinationCalendar: true; availability: true; selectedCalendars: true; schedules: true; + avatarUrl: true; + away: true; + backupCodes: true; + bio: true; + brandColor: true; + completedOnboarding: true; + createdDate: true; + bufferTime: true; + darkBrandColor: true; + defaultScheduleId: true; + disableImpersonation: true; + emailVerified: true; + endTime: true; + hideBranding: true; + identityProvider: true; + identityProviderId: true; + invitedTo: true; + locale: true; + metadata: true; + role: true; + startTime: true; + theme: true; + appTheme: true; + timeFormat: true; + trialEndsAt: true; + twoFactorEnabled: true; + twoFactorSecret: true; + verified: true; + weekStart: true; + organizationId: true; + allowSEOIndexing: true; + receiveMonthlyDigestEmail: true; + movedToProfileId: true; + isPlatformManaged: true; }; }>; export const buildUser = >( @@ -213,7 +254,6 @@ export const buildUser = >( id: 0, allowDynamicBooking: true, availability: [], - avatar: "", avatarUrl: "", away: false, backupCodes: null, diff --git a/packages/prisma/seed-huge-event-types.ts b/packages/prisma/seed-huge-event-types.ts index 58892548ea896..5d746c5ab041c 100644 --- a/packages/prisma/seed-huge-event-types.ts +++ b/packages/prisma/seed-huge-event-types.ts @@ -5,7 +5,6 @@ */ import { createTeamAndAddUsers, createUserAndEventType } from "./seed-utils"; -const avatar = ""; const getEventTypes = (numberOfEventTypes: number) => { const eventTypes = Array<{ title: string; @@ -46,7 +45,6 @@ async function createTeamsWithEventTypes({ password: `enterprise-member-${i + 1}`, username: `enterprise-member-${i + 1}`, theme: "light", - avatar, }, }) ); @@ -86,7 +84,6 @@ export default async function main() { password: "enterprise", username: `enterprise`, theme: "light", - avatar, }, eventTypes: getEventTypes(100), }); diff --git a/packages/prisma/seed-performance-testing.ts b/packages/prisma/seed-performance-testing.ts index 255aae462c88b..408ce2aecf19b 100644 --- a/packages/prisma/seed-performance-testing.ts +++ b/packages/prisma/seed-performance-testing.ts @@ -13,7 +13,7 @@ import { BookingStatus } from "@calcom/prisma/enums"; import { createUserAndEventType } from "./seed-utils"; -async function createManyDifferentUsersWithDifferentEventTypesAndBookings({ +async function _createManyDifferentUsersWithDifferentEventTypesAndBookings({ tillUser, startFrom = 0, }: { diff --git a/packages/prisma/seed-utils.ts b/packages/prisma/seed-utils.ts index e4ab8377e4bbc..ef06d0822d45f 100644 --- a/packages/prisma/seed-utils.ts +++ b/packages/prisma/seed-utils.ts @@ -24,7 +24,7 @@ export async function createUserAndEventType({ timeZone?: string; role?: UserPermissionRole; theme?: "dark" | "light"; - avatar?: string; + avatarUrl?: string | null; }; eventTypes?: Array< Prisma.EventTypeUncheckedCreateInput & { @@ -38,7 +38,7 @@ export async function createUserAndEventType({ appId: string; } | null)[]; }) { - const { password, ...restOfUser } = user; + const { password: _password, ...restOfUser } = user; const userData = { ...restOfUser, emailVerified: new Date(), diff --git a/packages/prisma/selects/event-types.ts b/packages/prisma/selects/event-types.ts index 35733ede28d92..9551466f7abf7 100644 --- a/packages/prisma/selects/event-types.ts +++ b/packages/prisma/selects/event-types.ts @@ -56,17 +56,17 @@ export const bookEventTypeSelect = Prisma.validator()({ name: true, email: true, bio: true, - avatar: true, + avatarUrl: true, theme: true, }, }, successRedirectUrl: true, team: { select: { - logo: true, + logoUrl: true, parent: { select: { - logo: true, + logoUrl: true, name: true, }, }, @@ -111,7 +111,7 @@ export const availiblityPageEventTypeSelect = Prisma.validator { - if (!UNSTABLE_HANDLER_CACHE.avatar) { - UNSTABLE_HANDLER_CACHE.avatar = (await import("./avatar.handler")).avatarHandler; - } - - // Unreachable code but required for type safety - if (!UNSTABLE_HANDLER_CACHE.avatar) { - throw new Error("Failed to load handler"); - } - - return UNSTABLE_HANDLER_CACHE.avatar({ ctx }); - }), - deleteMe: authedProcedure.input(ZDeleteMeInputSchema).mutation(async ({ ctx, input }) => { if (!UNSTABLE_HANDLER_CACHE.deleteMe) { UNSTABLE_HANDLER_CACHE.deleteMe = (await import("./deleteMe.handler")).deleteMeHandler; diff --git a/packages/trpc/server/routers/loggedInViewer/avatar.handler.ts b/packages/trpc/server/routers/loggedInViewer/avatar.handler.ts deleted file mode 100644 index e0bd82bc9fd90..0000000000000 --- a/packages/trpc/server/routers/loggedInViewer/avatar.handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -import prisma from "@calcom/prisma"; -import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; - -type AvatarOptions = { - ctx: { - user: NonNullable; - }; -}; - -export const avatarHandler = async ({ ctx }: AvatarOptions) => { - const data = await prisma.user.findUnique({ - where: { - id: ctx.user.id, - }, - select: { - avatar: true, - }, - }); - return { - avatar: data?.avatar, - }; -}; diff --git a/packages/trpc/server/routers/loggedInViewer/avatar.schema.ts b/packages/trpc/server/routers/loggedInViewer/avatar.schema.ts deleted file mode 100644 index cb0ff5c3b541f..0000000000000 --- a/packages/trpc/server/routers/loggedInViewer/avatar.schema.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts b/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts index 8f612fad27f98..aa2e5ec6ee83e 100644 --- a/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/integrations.handler.ts @@ -28,7 +28,7 @@ type TeamQuery = Prisma.TeamGetPayload<{ select: typeof import("@calcom/prisma/selects/credential").credentialForCalendarServiceSelect; }; name: true; - logo: true; + logoUrl: true; members: { select: { role: true; @@ -72,7 +72,7 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) = select: credentialForCalendarServiceSelect, }, name: true, - logo: true, + logoUrl: true, members: { where: { userId: user.id, @@ -88,7 +88,7 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) = select: credentialForCalendarServiceSelect, }, name: true, - logo: true, + logoUrl: true, members: { where: { userId: user.id, @@ -150,7 +150,7 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) = return { teamId: team.id, name: team.name, - logo: team.logo, + logoUrl: team.logoUrl, credentialId: c.id, isAdmin: team.members[0].role === MembershipRole.ADMIN || diff --git a/packages/trpc/server/routers/loggedInViewer/teamsAndUserProfilesQuery.handler.ts b/packages/trpc/server/routers/loggedInViewer/teamsAndUserProfilesQuery.handler.ts index cdcb95e331751..d40eae9e58ef7 100644 --- a/packages/trpc/server/routers/loggedInViewer/teamsAndUserProfilesQuery.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/teamsAndUserProfilesQuery.handler.ts @@ -1,5 +1,6 @@ +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { withRoleCanCreateEntity } from "@calcom/lib/entityPermissionUtils"; -import { getTeamAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import type { PrismaClient } from "@calcom/prisma"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; @@ -16,7 +17,6 @@ type TeamsAndUserProfileOptions = { export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOptions) => { const { prisma } = ctx; - const profile = ctx.user.profile; const user = await prisma.user.findUnique({ where: { id: ctx.user.id, @@ -26,7 +26,6 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti id: true, username: true, name: true, - avatar: true, teams: { where: { accepted: true, @@ -37,6 +36,7 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti select: { id: true, isOrganization: true, + logoUrl: true, name: true, slug: true, metadata: true, @@ -72,8 +72,7 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti name: user.name, slug: user.username, image: getUserAvatarUrl({ - ...user, - profile: profile, + avatarUrl: user.avatarUrl, }), readOnly: false, }, @@ -81,11 +80,7 @@ export const teamsAndUserProfilesQuery = async ({ ctx }: TeamsAndUserProfileOpti teamId: membership.team.id, name: membership.team.name, slug: membership.team.slug ? `team/${membership.team.slug}` : null, - image: getTeamAvatarUrl({ - slug: membership.team.slug, - requestedSlug: membership.team.metadata?.requestedSlug ?? null, - organizationId: membership.team.parentId, - }), + image: getPlaceholderAvatar(membership.team.logoUrl, membership.team.name), role: membership.role, readOnly: !withRoleCanCreateEntity(membership.role), })), diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts index 31726fd41347f..392af360f1f48 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts @@ -79,8 +79,6 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) const data: Prisma.UserUpdateInput = { ...rest, - // DO NOT OVERWRITE AVATAR. - avatar: undefined, metadata: userMetadata, secondaryEmails: undefined, }; @@ -198,22 +196,14 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) data.identityProviderId = null; } - // if defined AND a base 64 string, upload and set the avatar URL - if (input.avatar && input.avatar.startsWith("data:image/png;base64,")) { - const avatar = await resizeBase64Image(input.avatar); + // if defined AND a base 64 string, upload and update the avatar URL + if (input.avatarUrl && input.avatarUrl.startsWith("data:image/png;base64,")) { data.avatarUrl = await uploadAvatar({ - avatar, + avatar: await resizeBase64Image(input.avatarUrl), userId: user.id, }); - // as this is still used in the backwards compatible endpoint, we also write it here - // to ensure no data loss. - data.avatar = avatar; - } - // Unset avatar url if avatar is empty string. - if ("" === input.avatar) { - data.avatarUrl = null; - data.avatar = null; } + if (input.completedOnboarding) { const userTeams = await prisma.user.findFirst({ where: { @@ -374,9 +364,6 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) } } - // don't return avatar, we don't need it anymore. - delete input.avatar; - if (secondaryEmails.length) { const recordsToDelete = secondaryEmails .filter((secondaryEmail) => secondaryEmail.isDeleted) diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts index b63b311c296bb..e26df26be63bd 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts @@ -13,7 +13,7 @@ export const ZUpdateProfileInputSchema = z.object({ name: z.string().max(FULL_NAME_LENGTH_MAX_LIMIT).optional(), email: z.string().optional(), bio: z.string().optional(), - avatar: z.string().nullable().optional(), + avatarUrl: z.string().nullable().optional(), timeZone: z.string().optional(), weekStart: z.string().optional(), hideBranding: z.boolean().optional(), diff --git a/packages/trpc/server/routers/viewer/organizations/getOtherTeam.handler.ts b/packages/trpc/server/routers/viewer/organizations/getOtherTeam.handler.ts index 212a7dba49cf3..0cd69c62544ef 100644 --- a/packages/trpc/server/routers/viewer/organizations/getOtherTeam.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/getOtherTeam.handler.ts @@ -30,7 +30,7 @@ export const getOtherTeamHandler = async ({ input }: GetOptions) => { id: true, name: true, slug: true, - logo: true, + logoUrl: true, bio: true, metadata: true, isPrivate: true, diff --git a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts index bedc8234dfbd8..2bdc64c51c33e 100644 --- a/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/listMembers.handler.ts @@ -69,6 +69,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { id: true, username: true, email: true, + avatarUrl: true, timeZone: true, disableImpersonation: true, completedOnboarding: true, @@ -111,6 +112,7 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => { accepted: membership.accepted, disableImpersonation: user.disableImpersonation, completedOnboarding: user.completedOnboarding, + avatarUrl: user.avatarUrl, teams: user.teams .filter((team) => team.team.id !== organizationId) // In this context we dont want to return the org team .map((team) => { diff --git a/packages/trpc/server/routers/viewer/organizations/listOtherTeamMembers.handler.ts b/packages/trpc/server/routers/viewer/organizations/listOtherTeamMembers.handler.ts index 19a1bb5b55e75..9c9c856d5edc5 100644 --- a/packages/trpc/server/routers/viewer/organizations/listOtherTeamMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/listOtherTeamMembers.handler.ts @@ -89,7 +89,6 @@ export const listOtherTeamMembers = async ({ input }: ListOptions) => { username: true, name: true, email: true, - avatar: true, avatarUrl: true, }, }, diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index e6fd32dda946b..c46767b634523 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -6,6 +6,7 @@ import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import { uploadLogo } from "@calcom/lib/server/uploadLogo"; import { closeComUpdateTeam } from "@calcom/lib/sync/SyncServiceManager"; +import type { PrismaClient } from "@calcom/prisma"; import { prisma } from "@calcom/prisma"; import { UserPermissionRole } from "@calcom/prisma/enums"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -22,6 +23,70 @@ type UpdateOptions = { input: TUpdateInputSchema; }; +const updateOrganizationSettings = async ({ + organizationId, + input, + tx, +}: { + organizationId: number; + input: TUpdateInputSchema; + tx: Parameters[0]>[0]; +}) => { + // if lockEventTypeCreation isn't given we don't do anything. + if (typeof input.lockEventTypeCreation === "undefined") { + return; + } + await tx.organizationSettings.update({ + where: { + organizationId, + }, + data: { + lockEventTypeCreationForUsers: !!input.lockEventTypeCreation, + }, + }); + + if (input.lockEventTypeCreation) { + switch (input.lockEventTypeCreationOptions) { + case "HIDE": + await tx.eventType.updateMany({ + where: { + teamId: null, // Not assigned to a team + parentId: null, // Not a managed event type + owner: { + profiles: { + some: { + organizationId, + }, + }, + }, + }, + data: { + hidden: true, + }, + }); + + break; + case "DELETE": + await tx.eventType.deleteMany({ + where: { + teamId: null, // Not assigned to a team + parentId: null, // Not a managed event type + owner: { + profiles: { + some: { + organizationId, + }, + }, + }, + }, + }); + break; + default: + break; + } + } +}; + export const updateHandler = async ({ ctx, input }: UpdateOptions) => { // A user can only have one org so we pass in their currentOrgId here const currentOrgId = ctx.user?.organization?.id || input.orgId; @@ -63,6 +128,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema.unwrap(), prevOrganisation.metadata); const data: Prisma.TeamUpdateArgs["data"] = { + logoUrl: input.logoUrl, name: input.name, calVideoLogo: input.calVideoLogo, bio: input.bio, @@ -88,14 +154,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { data.bannerUrl = null; } - if (input.logo && input.logo.startsWith("data:image/png;base64,")) { - data.logo = input.logo; + if (input.logoUrl && input.logoUrl.startsWith("data:image/png;base64,")) { data.logoUrl = await uploadLogo({ - logo: input.logo, + logo: await resizeBase64Image(input.logoUrl), teamId: currentOrgId, }); - } else if (typeof input.logo !== "undefined" && !input.logo) { - data.logo = data.logoUrl = null; } if (input.slug) { @@ -122,55 +185,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { data, }); - await tx.organizationSettings.update({ - where: { - organizationId: currentOrgId, - }, - data: { - lockEventTypeCreationForUsers: !!input.lockEventTypeCreation, - }, - }); - - if (input.lockEventTypeCreation) { - switch (input.lockEventTypeCreationOptions) { - case "HIDE": - await tx.eventType.updateMany({ - where: { - teamId: null, // Not assigned to a team - parentId: null, // Not a managed event type - owner: { - profiles: { - some: { - organizationId: currentOrgId, - }, - }, - }, - }, - data: { - hidden: true, - }, - }); - - break; - case "DELETE": - await tx.eventType.deleteMany({ - where: { - teamId: null, // Not assigned to a team - parentId: null, // Not a managed event type - owner: { - profiles: { - some: { - organizationId: currentOrgId, - }, - }, - }, - }, - }); - break; - default: - break; - } - } + await updateOrganizationSettings({ tx, input, organizationId: currentOrgId }); return updatedOrganisation; }); @@ -178,7 +193,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { // Sync Services: Close.com if (prevOrganisation) closeComUpdateTeam(prevOrganisation, updatedOrganisation); - return { update: true, userId: ctx.user.id, data }; + return { update: true, userId: ctx.user.id, data: updatedOrganisation }; }; export default updateHandler; diff --git a/packages/trpc/server/routers/viewer/organizations/update.schema.ts b/packages/trpc/server/routers/viewer/organizations/update.schema.ts index 4c17d053549d2..80a3fb61fc946 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.schema.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.schema.ts @@ -1,6 +1,5 @@ import { z } from "zod"; -import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; export const ZUpdateInputSchema = z.object({ @@ -12,11 +11,7 @@ export const ZUpdateInputSchema = z.object({ .or(z.number()) .optional(), bio: z.string().optional(), - logo: z - .string() - .transform(async (val) => await resizeBase64Image(val)) - .optional() - .nullable(), + logoUrl: z.string().optional().nullable(), calVideoLogo: z .string() .optional() diff --git a/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts b/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts index ba03c6992d65e..531bee5af25c0 100644 --- a/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts @@ -111,7 +111,6 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => { name: input.name, timeZone: input.timeZone, username: input.username, - avatar: undefined, }; if (input.avatar && input.avatar.startsWith("data:image/png;base64,")) { @@ -120,11 +119,9 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => { avatar, userId: user.id, }); - data.avatar = avatar; } if (input.avatar === "") { data.avatarUrl = null; - data.avatar = null; } // Update user diff --git a/packages/trpc/server/routers/viewer/teams/create.handler.ts b/packages/trpc/server/routers/viewer/teams/create.handler.ts index 8ac0e0899006b..07879d78031f3 100644 --- a/packages/trpc/server/routers/viewer/teams/create.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/create.handler.ts @@ -1,6 +1,8 @@ import { generateTeamCheckoutSession } from "@calcom/features/ee/teams/lib/payments"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; +import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; +import { uploadLogo } from "@calcom/lib/server/uploadLogo"; import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -47,7 +49,7 @@ const generateCheckoutSession = async ({ export const createHandler = async ({ ctx, input }: CreateOptions) => { const { user } = ctx; - const { slug, name, logo } = input; + const { slug, name } = input; const isOrgChildTeam = !!user.profile?.organizationId; // For orgs we want to create teams under the org @@ -95,7 +97,6 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { data: { slug, name, - logo, members: { create: { userId: ctx.user.id, @@ -106,7 +107,21 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { ...(isOrgChildTeam && { parentId: user.profile?.organizationId }), }, }); - + // Upload logo, create doesn't allow logo removal + if (input.logo && input.logo.startsWith("data:image/png;base64,")) { + const logoUrl = await uploadLogo({ + logo: await resizeBase64Image(input.logo), + teamId: createdTeam.id, + }); + await prisma.team.update({ + where: { + id: createdTeam.id, + }, + data: { + logoUrl, + }, + }); + } // Sync Services: Close.com closeComUpsertTeamUser(createdTeam, ctx.user, MembershipRole.OWNER); diff --git a/packages/trpc/server/routers/viewer/teams/get.handler.ts b/packages/trpc/server/routers/viewer/teams/get.handler.ts index 501a40c35ed9c..8173e9cdc6dc2 100644 --- a/packages/trpc/server/routers/viewer/teams/get.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/get.handler.ts @@ -19,7 +19,6 @@ export const getHandler = async ({ ctx, input }: GetOptions) => { id: input.teamId, currentOrg: ctx.user.profile?.organization ?? null, userId: ctx.user.organization?.isOrgAdmin ? undefined : ctx.user.id, - includeTeamLogo: input.includeTeamLogo, isOrgView: input?.isOrg, }); diff --git a/packages/trpc/server/routers/viewer/teams/get.schema.ts b/packages/trpc/server/routers/viewer/teams/get.schema.ts index 3af0fa2460a6f..fcf650e1c7a9a 100644 --- a/packages/trpc/server/routers/viewer/teams/get.schema.ts +++ b/packages/trpc/server/routers/viewer/teams/get.schema.ts @@ -3,7 +3,6 @@ import { z } from "zod"; export const ZGetInputSchema = z.object({ teamId: z.number(), isOrg: z.boolean().optional(), - includeTeamLogo: z.boolean().optional(), }); export type TGetInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/teams/list.handler.ts b/packages/trpc/server/routers/viewer/teams/list.handler.ts index d3c0ac4ab00ca..bcf9026660e36 100644 --- a/packages/trpc/server/routers/viewer/teams/list.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/list.handler.ts @@ -26,7 +26,6 @@ export const listHandler = async ({ ctx, input }: ListOptions) => { id: true, name: true, slug: true, - logo: true, logoUrl: true, isOrganization: true, metadata: true, @@ -44,14 +43,13 @@ export const listHandler = async ({ ctx, input }: ListOptions) => { if (input?.includeOrgs) return true; return !mmship.team.isOrganization; }) - .map(({ team: { inviteTokens, logo, logoUrl, ..._team }, ...membership }) => ({ + .map(({ team: { inviteTokens, ...team }, ...membership }) => ({ role: membership.role, accepted: membership.accepted, - ..._team, - logo: logoUrl || logo, - metadata: teamMetadataSchema.parse(_team.metadata), + ...team, + metadata: teamMetadataSchema.parse(team.metadata), /** To prevent breaking we only return non-email attached token here, if we have one */ - inviteToken: inviteTokens.find((token) => token.identifier === `invite-link-for-teamId-${_team.id}`), + inviteToken: inviteTokens.find((token) => token.identifier === `invite-link-for-teamId-${team.id}`), })); }; diff --git a/packages/trpc/server/routers/viewer/teams/update.handler.ts b/packages/trpc/server/routers/viewer/teams/update.handler.ts index be49a3158b511..38b54d33f8129 100644 --- a/packages/trpc/server/routers/viewer/teams/update.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/update.handler.ts @@ -59,10 +59,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { }; if (input.logo && input.logo.startsWith("data:image/png;base64,")) { - data.logo = input.logo; data.logoUrl = await uploadLogo({ teamId: input.id, logo: input.logo }); } else if (typeof input.logo !== "undefined" && !input.logo) { - data.logo = data.logoUrl = null; + data.logoUrl = null; } if ( @@ -131,7 +130,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { if (prevTeam) closeComUpdateTeam(prevTeam, updatedTeam); return { - logo: updatedTeam.logo, + logoUrl: updatedTeam.logoUrl, name: updatedTeam.name, bio: updatedTeam.bio, slug: updatedTeam.slug, diff --git a/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts index 6ea1f3e9ba6b6..9b62ac8a9b829 100644 --- a/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/getByViewer.handler.ts @@ -53,7 +53,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => { }, select: { username: true, - avatar: true, + avatarUrl: true, name: true, webhooks: true, teams: { diff --git a/packages/types/next-auth.d.ts b/packages/types/next-auth.d.ts index 4abdb5b180946..d6f313849857f 100644 --- a/packages/types/next-auth.d.ts +++ b/packages/types/next-auth.d.ts @@ -27,10 +27,12 @@ declare module "next-auth" { id: number; name?: string; slug: string; + logoUrl?: string | null; fullDomain: string; domainSuffix: string; }; username?: PrismaUser["username"]; + avatarUrl?: PrismaUser["avatarUrl"]; role?: PrismaUser["role"] | "INACTIVE_ADMIN"; locale?: string | null; profile: UserProfile; @@ -42,6 +44,7 @@ declare module "next-auth/jwt" { id?: string | number; name?: string | null; username?: string | null; + avatarUrl?: string | null; email?: string | null; upId?: string; profileId?: number | null; @@ -55,6 +58,7 @@ declare module "next-auth/jwt" { id: number; name?: string; slug: string; + logoUrl?: string | null; fullDomain: string; domainSuffix: string; }; diff --git a/packages/ui/components/apps/AppCard.tsx b/packages/ui/components/apps/AppCard.tsx index 3356a70ca4444..7ceefeb10f08d 100644 --- a/packages/ui/components/apps/AppCard.tsx +++ b/packages/ui/components/apps/AppCard.tsx @@ -7,7 +7,7 @@ import { doesAppSupportTeamInstall } from "@calcom/app-store/utils"; import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner"; import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams"; import classNames from "@calcom/lib/classNames"; -import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { AppFrontendPayload as App } from "@calcom/types/App"; import type { CredentialFrontendPayload as Credential } from "@calcom/types/Credential"; @@ -273,8 +273,8 @@ const InstallAppButtonChild = ({ key={team.id} CustomStartIcon={ } diff --git a/packages/ui/components/avatar/UserAvatar.tsx b/packages/ui/components/avatar/UserAvatar.tsx index 371791f9e6fc3..f209a097e6452 100644 --- a/packages/ui/components/avatar/UserAvatar.tsx +++ b/packages/ui/components/avatar/UserAvatar.tsx @@ -1,5 +1,6 @@ import { classNames } from "@calcom/lib"; -import { getOrgAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; import type { User } from "@calcom/prisma/client"; import type { UserProfile } from "@calcom/types/UserProfile"; import { Avatar } from "@calcom/ui"; @@ -39,13 +40,12 @@ function OrganizationIndicator({ organization, user, }: Pick & { organization: Organization }) { - const organizationUrl = organization.logoUrl ?? getOrgAvatarUrl(organization); const indicatorSize = size && indicatorBySize[size]; return (
{user.username diff --git a/packages/ui/components/image-uploader/BannerUploader.tsx b/packages/ui/components/image-uploader/BannerUploader.tsx index f744d0a14c6a6..dea72ea30812a 100644 --- a/packages/ui/components/image-uploader/BannerUploader.tsx +++ b/packages/ui/components/image-uploader/BannerUploader.tsx @@ -142,7 +142,7 @@ export default function BannerUploader({ color={triggerButtonColor ?? "secondary"} type="button" disabled={disabled} - data-testid="open-upload-avatar-dialog" + data-testid={`open-upload-${target}-dialog`} className="cursor-pointer py-1 text-sm"> {buttonMsg} diff --git a/packages/ui/components/unpublished-entity/UnpublishedEntity.tsx b/packages/ui/components/unpublished-entity/UnpublishedEntity.tsx index 4ae53c35413e5..63f1e9307ab34 100644 --- a/packages/ui/components/unpublished-entity/UnpublishedEntity.tsx +++ b/packages/ui/components/unpublished-entity/UnpublishedEntity.tsx @@ -1,3 +1,4 @@ +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { EmptyScreen, Avatar } from "@calcom/ui"; @@ -12,6 +13,8 @@ export type UnpublishedEntityProps = { * It conveys two things - Slug for the team and that it is an organization infact */ orgSlug?: string | null; + /* logo url for entity */ + logoUrl?: string | null; /** * Team or Organization name */ @@ -24,13 +27,7 @@ export function UnpublishedEntity(props: UnpublishedEntityProps) { return (
- } + avatar={} headline={t("team_is_unpublished", { team: props.name, })}