Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Use org logo for organization's teams #12961

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/web/components/ui/avatar/TeamAvatar.tsx
Original file line number Diff line number Diff line change
@@ -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} />;
}
16 changes: 16 additions & 0 deletions apps/web/pages/api/user/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
39 changes: 31 additions & 8 deletions apps/web/pages/event-types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Alert,
Avatar,
Badge,
Button,
ButtonGroup,
Expand Down Expand Up @@ -72,6 +71,8 @@

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"];
Expand All @@ -83,6 +84,7 @@
membershipCount: number;
teamId?: number | null;
bookerUrl: string;
organizationId: number | null;
}

type EventTypeGroup = EventTypeGroups[number];
Expand Down Expand Up @@ -693,6 +695,7 @@

const EventTypeListHeading = ({
profile,
organizationId,
membershipCount,
teamId,
bookerUrl,
Expand All @@ -711,13 +714,32 @@

return (
<div className="mb-4 flex items-center space-x-2">
<Avatar
alt={profile?.name || ""}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={`${bookerUrl}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`}
size="md"
className="mt-1 inline-flex justify-center"
/>
{!teamId ? (
<UserAvatar
href="/settings/my-account/profile"
user={{
name: profile.name,
username: profile.slug,
organizationId,
}}
size="md"
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>
<Link
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
Expand Down Expand Up @@ -836,7 +858,7 @@
}) => {
const isMobile = useMediaQuery("(max-width: 768px)");
const searchParams = useCompatSearchParams();
const orgBranding = useOrgBranding();

Check warning on line 861 in apps/web/pages/event-types/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/pages/event-types/index.tsx#L861

[@typescript-eslint/no-unused-vars] 'orgBranding' is assigned a value but never used. Allowed unused vars must match /^_/u.

if (!data || status === "loading") {
return <SkeletonLoader />;
Expand All @@ -861,6 +883,7 @@
data-testid={`slug-${group.profile.slug}`}
key={group.profile.slug}>
<EventTypeListHeading
organizationId={group.organizationId}
profile={group.profile}
membershipCount={group.metadata.membershipCount}
teamId={group.teamId}
Expand Down Expand Up @@ -907,7 +930,7 @@
const searchParams = useCompatSearchParams();
const { open } = useIntercom();
const { data: user } = useMeQuery();
const [showProfileBanner, setShowProfileBanner] = useState(false);

Check warning on line 933 in apps/web/pages/event-types/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/pages/event-types/index.tsx#L933

[@typescript-eslint/no-unused-vars] 'showProfileBanner' is assigned a value but never used. Allowed unused elements of array destructuring patterns must match /^_/u.
const orgBranding = useOrgBranding();
const routerQuery = useRouterQuery();
const filters = getTeamsFiltersFromQuery(routerQuery);
Expand All @@ -919,7 +942,7 @@
staleTime: 1 * 60 * 60 * 1000,
});

function closeBanner() {

Check warning on line 945 in apps/web/pages/event-types/index.tsx

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

apps/web/pages/event-types/index.tsx#L945

[@typescript-eslint/no-unused-vars] 'closeBanner' is defined but never used. Allowed unused vars must match /^_/u.
setShowProfileBanner(false);
document.cookie = `calcom-profile-banner=1;max-age=${60 * 60 * 24 * 90}`; // 3 months
showToast(t("we_wont_show_again"), "success");
Expand Down
14 changes: 9 additions & 5 deletions packages/features/ee/teams/components/TeamListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import { useState } from "react";
import InviteLinkSettingsModal from "@calcom/ee/teams/components/InviteLinkSettingsModal";
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { getTeamUrlSync } from "@calcom/lib/getBookerUrl/client";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import {
Avatar,
Badge,
Button,
ButtonGroup,
Expand Down Expand Up @@ -42,6 +40,8 @@ import {
X,
} from "@calcom/ui/components/icon";

import { TeamAvatar } from "@components/ui/avatar/TeamAvatar";

import { useOrgBranding } from "../../organizations/context/provider";
import { TeamRole } from "./TeamPill";

Expand Down Expand Up @@ -97,10 +97,14 @@ export default function TeamListItem(props: Props) {

const teamInfo = (
<div className="item-center flex px-5 py-5">
<Avatar
<TeamAvatar
size="md"
imageSrc={getPlaceholderAvatar(team?.logo, team?.name as string)}
alt="Team Logo"
team={{
name: team.name,
slug: team.slug,
organizationId: team.parentId,
requestedSlug: team.requestedSlug || null,
}}
className="inline-flex justify-center"
/>
<div className="ms-3 inline-block truncate">
Expand Down
5 changes: 5 additions & 0 deletions packages/lib/getAvatarUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}` : ""}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -285,6 +289,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
organizationId: team.parentId,
}),
name: team.name,
requestedSlug: team.metadata?.requestedSlug ?? null,
slug,
},
metadata: {
Expand Down
15 changes: 13 additions & 2 deletions packages/trpc/server/routers/viewer/teams/list.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`),
}));
Expand Down
Loading