fix: Use org logo for organization's teams

This commit is contained in:
Hariom 2023-12-29 13:26:54 +05:30
parent c4792c55fe
commit ea3cd09be0
7 changed files with 101 additions and 15 deletions

View File

@ -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<React.ComponentProps<typeof Avatar>, "alt" | "imageSrc"> & {
team: Pick<Team, "slug" | "name"> & {
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 <Avatar {...rest} alt={team.name || "Nameless Team"} imageSrc={previewSrc} />;
}

View File

@ -100,6 +100,22 @@ async function getIdentityData(req: NextApiRequest) {
avatar: getPlaceholderAvatar(org?.logo, org?.name), 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) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@ -30,7 +30,6 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc, TRPCClientError } from "@calcom/trpc/react"; import { trpc, TRPCClientError } from "@calcom/trpc/react";
import { import {
Alert, Alert,
Avatar,
Badge, Badge,
Button, Button,
ButtonGroup, ButtonGroup,
@ -72,6 +71,8 @@ import useMeQuery from "@lib/hooks/useMeQuery";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
import SkeletonLoader from "@components/eventtype/SkeletonLoader"; 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"; import { UserAvatarGroup } from "@components/ui/avatar/UserAvatarGroup";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"]; type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
@ -83,6 +84,7 @@ interface EventTypeListHeadingProps {
membershipCount: number; membershipCount: number;
teamId?: number | null; teamId?: number | null;
bookerUrl: string; bookerUrl: string;
organizationId: number | null;
} }
type EventTypeGroup = EventTypeGroups[number]; type EventTypeGroup = EventTypeGroups[number];
@ -693,6 +695,7 @@ export const EventTypeList = ({
const EventTypeListHeading = ({ const EventTypeListHeading = ({
profile, profile,
organizationId,
membershipCount, membershipCount,
teamId, teamId,
bookerUrl, bookerUrl,
@ -711,13 +714,32 @@ const EventTypeListHeading = ({
return ( return (
<div className="mb-4 flex items-center space-x-2"> <div className="mb-4 flex items-center space-x-2">
<Avatar {!teamId ? (
alt={profile?.name || ""} <UserAvatar
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"} href="/settings/my-account/profile"
imageSrc={`${bookerUrl}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`} user={{
name: profile.name,
username: profile.slug,
organizationId,
}}
size="md" size="md"
className="mt-1 inline-flex justify-center" className="mt-1 inline-flex justify-center"
/> />
) : (
<TeamAvatar
href={`/settings/teams/${teamId}/profile`}
team={{
name: profile.name || "",
// 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.
slug: profile.slug?.replace(/^team\//, "") || null,
organizationId,
requestedSlug: profile.requestedSlug || null,
}}
size="md"
className="mt-1 inline-flex justify-center"
/>
)}
<div> <div>
<Link <Link
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"} href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
@ -861,6 +883,7 @@ const Main = ({
data-testid={`slug-${group.profile.slug}`} data-testid={`slug-${group.profile.slug}`}
key={group.profile.slug}> key={group.profile.slug}>
<EventTypeListHeading <EventTypeListHeading
organizationId={group.organizationId}
profile={group.profile} profile={group.profile}
membershipCount={group.metadata.membershipCount} membershipCount={group.metadata.membershipCount}
teamId={group.teamId} teamId={group.teamId}

View File

@ -5,7 +5,6 @@ import { useState } from "react";
import InviteLinkSettingsModal from "@calcom/ee/teams/components/InviteLinkSettingsModal"; import InviteLinkSettingsModal from "@calcom/ee/teams/components/InviteLinkSettingsModal";
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal"; import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client"; import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -13,7 +12,6 @@ import { MembershipRole } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { import {
Avatar,
Badge, Badge,
Button, Button,
ButtonGroup, ButtonGroup,
@ -42,6 +40,8 @@ import {
X, X,
} from "@calcom/ui/components/icon"; } from "@calcom/ui/components/icon";
import { TeamAvatar } from "@components/ui/avatar/TeamAvatar";
import { useOrgBranding } from "../../organizations/context/provider"; import { useOrgBranding } from "../../organizations/context/provider";
import { TeamRole } from "./TeamPill"; import { TeamRole } from "./TeamPill";
@ -97,10 +97,14 @@ export default function TeamListItem(props: Props) {
const teamInfo = ( const teamInfo = (
<div className="item-center flex px-5 py-5"> <div className="item-center flex px-5 py-5">
<Avatar <TeamAvatar
size="md" size="md"
imageSrc={getPlaceholderAvatar(team?.logo, team?.name as string)} team={{
alt="Team Logo" name: team.name,
slug: team.slug,
organizationId: team.parentId,
requestedSlug: team.requestedSlug || null,
}}
className="inline-flex justify-center" className="inline-flex justify-center"
/> />
<div className="ms-3 inline-block truncate"> <div className="ms-3 inline-block truncate">

View File

@ -29,6 +29,11 @@ export function getTeamAvatarUrl(
if (team.logoUrl) { if (team.logoUrl) {
return 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; const slug = team.slug ?? team.requestedSlug;
return `${WEBAPP_URL}/team/${slug}/avatar.png${team.organizationId ? `?orgId=${team.organizationId}` : ""}`; return `${WEBAPP_URL}/team/${slug}/avatar.png${team.organizationId ? `?orgId=${team.organizationId}` : ""}`;
} }

View File

@ -186,10 +186,12 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
type EventTypeGroup = { type EventTypeGroup = {
teamId?: number | null; teamId?: number | null;
parentId?: number | null; parentId?: number | null;
organizationId: number | null;
bookerUrl: string; bookerUrl: string;
membershipRole?: MembershipRole | null; membershipRole?: MembershipRole | null;
profile: { profile: {
slug: (typeof user)["username"]; slug: (typeof user)["username"];
requestedSlug?: string | null;
name: (typeof user)["name"]; name: (typeof user)["name"];
image: string; image: string;
}; };
@ -212,6 +214,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
teamId: null, teamId: null,
bookerUrl, bookerUrl,
membershipRole: null, membershipRole: null,
organizationId: user.organizationId,
profile: { profile: {
slug: user.username, slug: user.username,
name: user.name, name: user.name,
@ -272,6 +275,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
} }
return { return {
teamId: team.id, teamId: team.id,
organizationId: team.parentId,
parentId: team.parentId, parentId: team.parentId,
bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null), bookerUrl: getBookerBaseUrlSync(team.parent?.slug ?? null),
membershipRole: membershipRole:
@ -285,6 +289,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
organizationId: team.parentId, organizationId: team.parentId,
}), }),
name: team.name, name: team.name,
requestedSlug: team.metadata?.requestedSlug ?? null,
slug, slug,
}, },
metadata: { metadata: {

View File

@ -30,14 +30,25 @@ export const listHandler = async ({ ctx }: ListOptions) => {
}); });
return memberships return memberships
.filter((mmship) => { .map((mmship) => {
const metadata = teamMetadataSchema.parse(mmship.team.metadata); 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 }) => ({ .map(({ team: { inviteTokens, ..._team }, ...membership }) => ({
role: membership.role, role: membership.role,
accepted: membership.accepted, accepted: membership.accepted,
..._team, ..._team,
requestedSlug: _team.metadata?.requestedSlug,
/** To prevent breaking we only return non-email attached token here, if we have one */ /** 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}`),
})); }));