diff --git a/apps/web/components/ui/avatar/TeamAvatar.tsx b/apps/web/components/ui/avatar/TeamAvatar.tsx new file mode 100644 index 00000000000000..e8ef26aa899b32 --- /dev/null +++ b/apps/web/components/ui/avatar/TeamAvatar.tsx @@ -0,0 +1,22 @@ +import { getTeamAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import type { Team } from "@calcom/prisma/client"; +import { Avatar } from "@calcom/ui"; + +type TeamAvatarProps = Omit, "alt" | "imageSrc"> & { + team: Pick & { + organizationId?: number | null; + requestedSlug: string | null; + }; + /** + * Useful when allowing the user to upload their own avatar and showing the avatar before it's uploaded + */ + previewSrc?: string | null; +}; + +/** + * It is aware of the user's organization to correctly show the avatar from the correct URL + */ +export function TeamAvatar(props: TeamAvatarProps) { + const { team, previewSrc = getTeamAvatarUrl(team), ...rest } = props; + return ; +} diff --git a/apps/web/pages/api/user/avatar.ts b/apps/web/pages/api/user/avatar.ts index 11512759fd8aa7..79f14326011513 100644 --- a/apps/web/pages/api/user/avatar.ts +++ b/apps/web/pages/api/user/avatar.ts @@ -100,6 +100,22 @@ async function getIdentityData(req: NextApiRequest) { avatar: getPlaceholderAvatar(org?.logo, org?.name), }; } + + // If just orgId is specified, we return the org avatar + if (orgId) { + const org = await prisma.team.findUnique({ + where: { + id: orgId, + }, + }); + + return { + org: org?.slug, + name: org?.name, + email: null, + avatar: getPlaceholderAvatar(org?.logo, org?.name), + }; + } } export default async function handler(req: NextApiRequest, res: NextApiResponse) { diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index 151221c8df1140..29367bcb4e3d80 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -30,7 +30,6 @@ import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc, TRPCClientError } from "@calcom/trpc/react"; import { Alert, - Avatar, Badge, Button, ButtonGroup, @@ -72,6 +71,8 @@ import useMeQuery from "@lib/hooks/useMeQuery"; import PageWrapper from "@components/PageWrapper"; import SkeletonLoader from "@components/eventtype/SkeletonLoader"; +import { TeamAvatar } from "@components/ui/avatar/TeamAvatar"; +import { UserAvatar } from "@components/ui/avatar/UserAvatar"; import { UserAvatarGroup } from "@components/ui/avatar/UserAvatarGroup"; type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"]; @@ -83,6 +84,7 @@ interface EventTypeListHeadingProps { membershipCount: number; teamId?: number | null; bookerUrl: string; + organizationId: number | null; } type EventTypeGroup = EventTypeGroups[number]; @@ -693,6 +695,7 @@ export const EventTypeList = ({ const EventTypeListHeading = ({ profile, + organizationId, membershipCount, teamId, bookerUrl, @@ -709,15 +712,37 @@ const EventTypeListHeading = ({ }, }); + // I think profile.slug shouldn't contain team/ prefix for a team because that's a path and not a slug + // But we need to handle it for now instead of changing at the source to avoid side effects at other places. + const userOrTeamSlug = profile.slug?.replace(/^team\//, ""); + return (
- + {!teamId ? ( + + ) : ( + + )}
- diff --git a/packages/lib/getAvatarUrl.ts b/packages/lib/getAvatarUrl.ts index 3ef573c63f8f48..c153bbf2cf1369 100644 --- a/packages/lib/getAvatarUrl.ts +++ b/packages/lib/getAvatarUrl.ts @@ -29,6 +29,11 @@ export function getTeamAvatarUrl( if (team.logoUrl) { return team.logoUrl; } + + if (team.organizationId) { + // For an organization, all it's teams have the same logo as the organization + return `${WEBAPP_URL}/api/user/avatar?orgId=${team.organizationId}`; + } const slug = team.slug ?? team.requestedSlug; return `${WEBAPP_URL}/team/${slug}/avatar.png${team.organizationId ? `?orgId=${team.organizationId}` : ""}`; } diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts index 9ce7b0ae0fef87..78e970a54954b9 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.handler.ts @@ -186,10 +186,12 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => type EventTypeGroup = { teamId?: number | null; parentId?: number | null; + organizationId: number | null; bookerUrl: string; membershipRole?: MembershipRole | null; profile: { slug: (typeof user)["username"]; + requestedSlug?: string | null; name: (typeof user)["name"]; image: string; }; @@ -212,6 +214,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => teamId: null, bookerUrl, membershipRole: null, + organizationId: user.organizationId, profile: { slug: user.username, name: user.name, @@ -272,6 +275,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => } return { teamId: team.id, + organizationId: team.parentId, parentId: team.parentId, bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null), membershipRole: @@ -285,6 +289,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => organizationId: team.parentId, }), name: team.name, + requestedSlug: team.metadata?.requestedSlug ?? null, slug, }, metadata: { diff --git a/packages/trpc/server/routers/viewer/teams/list.handler.ts b/packages/trpc/server/routers/viewer/teams/list.handler.ts index 23a29411ff5df9..4c415e0b137736 100644 --- a/packages/trpc/server/routers/viewer/teams/list.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/list.handler.ts @@ -30,14 +30,25 @@ export const listHandler = async ({ ctx }: ListOptions) => { }); return memberships - .filter((mmship) => { + .map((mmship) => { const metadata = teamMetadataSchema.parse(mmship.team.metadata); - return !metadata?.isOrganization; + mmship.team.metadata = metadata; + return { + ...mmship, + team: { + ...mmship.team, + metadata, + }, + }; + }) + .filter((mmship) => { + return !mmship.team.metadata?.isOrganization; }) .map(({ team: { inviteTokens, ..._team }, ...membership }) => ({ role: membership.role, accepted: membership.accepted, ..._team, + requestedSlug: _team.metadata?.requestedSlug, /** 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}`), }));