chore: front-end-avatars (#12716)

* Update UserAvatar and remove org avatar

* Update Imports

* Fix imports to use calcom/ui

* type: fix imports

* fix: use testId on profile

* test: use image src instead of innerHTML

* fix: Allow alt on useravatar

* test: add testId to org profile

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
sean-brydon 2024-01-05 20:36:44 +10:00 committed by GitHub
parent 0dddc2224a
commit 698d8ae4bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 118 additions and 84 deletions

View File

@ -3,7 +3,6 @@ import type { FormEvent } from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
@ -11,6 +10,7 @@ import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react";
import type { Ensure } from "@calcom/types/utils";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
type FormData = {
@ -108,9 +108,7 @@ const UserProfile = () => {
return (
<form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end">
{user && (
<OrganizationMemberAvatar size="lg" user={user} previewSrc={imageSrc} organization={organization} />
)}
{user && <UserAvatar size="lg" user={user} previewSrc={imageSrc} organization={organization} />}
<input
ref={avatarRef}
type="hidden"

View File

@ -5,8 +5,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { UserAvatar } from "@components/ui/avatar/UserAvatar";
import { UserAvatar } from "@calcom/ui";
type TeamType = Omit<NonNullable<TeamWithMembers>, "inviteToken">;
type MembersType = TeamType["members"];

View File

@ -1,19 +0,0 @@
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { User } from "@calcom/prisma/client";
import { Avatar } from "@calcom/ui";
type UserAvatarProps = Omit<React.ComponentProps<typeof Avatar>, "alt" | "imageSrc"> & {
user: Pick<User, "organizationId" | "name" | "username">;
/**
* 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 UserAvatar(props: UserAvatarProps) {
const { user, previewSrc = getUserAvatarUrl(user), ...rest } = props;
return <Avatar {...rest} alt={user.name || "Nameless User"} imageSrc={previewSrc} />;
}

View File

@ -11,7 +11,6 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar";
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
@ -28,6 +27,7 @@ import { RedirectType, type EventType, type User } from "@calcom/prisma/client";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { HeadSeo, UnpublishedEntity } from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { EmbedProps } from "@lib/withEmbedSsr";
@ -101,7 +101,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
"max-w-3xl px-4 py-24"
)}>
<div className="mb-8 text-center">
<OrganizationMemberAvatar
<UserAvatar
size="xl"
user={{
organizationId: profile.organization?.id,

View File

@ -6,7 +6,6 @@ import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import OrganizationMemberAvatar from "@calcom/features/ee/organizations/components/OrganizationMemberAvatar";
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
@ -41,6 +40,7 @@ import {
SkeletonText,
TextField,
} from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { AlertTriangle, Trash2 } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
@ -448,7 +448,8 @@ const ProfileForm = ({
: null;
return (
<>
<OrganizationMemberAvatar
<UserAvatar
data-testid="profile-upload-avatar"
previewSrc={value}
size="lg"
user={user}

View File

@ -43,8 +43,11 @@ test.describe("UploadAvatar", async () => {
// todo: remove this; ideally the organization-avatar is updated the moment
// 'Settings updated succesfully' is saved.
await page.waitForLoadState("networkidle");
const avatar = page.getByTestId("profile-upload-avatar").locator("img");
await expect(await page.getByTestId("organization-avatar").innerHTML()).toContain(response.objectKey);
const src = await avatar.getAttribute("src");
await expect(src).toContain(response.objectKey);
const urlResponse = await page.request.get(`/api/avatar/${response.objectKey}.png`, {
maxRedirects: 0,

View File

@ -1,47 +0,0 @@
import classNames from "@calcom/lib/classNames";
import { getOrgAvatarUrl } from "@calcom/lib/getAvatarUrl";
// import { Avatar } from "@calcom/ui";
import { UserAvatar } from "@calcom/web/components/ui/avatar/UserAvatar";
type OrganizationMemberAvatarProps = React.ComponentProps<typeof UserAvatar> & {
organization: {
id: number;
slug: string | null;
requestedSlug: string | null;
} | null;
};
/**
* Shows the user's avatar along with a small organization's avatar
*/
const OrganizationMemberAvatar = ({
size,
user,
organization,
previewSrc,
...rest
}: OrganizationMemberAvatarProps) => {
return (
<UserAvatar
data-testid="organization-avatar"
size={size}
user={user}
previewSrc={previewSrc}
indicator={
organization ? (
<div
className={classNames("absolute bottom-0 right-0 z-10", size === "lg" ? "h-6 w-6" : "h-10 w-10")}>
<img
src={getOrgAvatarUrl(organization)}
alt={user.username || ""}
className="flex h-full items-center justify-center rounded-full"
/>
</div>
) : null
}
{...rest}
/>
);
};
export default OrganizationMemberAvatar;

View File

@ -12,11 +12,10 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip,
UserAvatar,
} from "@calcom/ui";
import { ExternalLink, MoreHorizontal } from "@calcom/ui/components/icon";
import { UserAvatar } from "@components/ui/avatar/UserAvatar";
interface Props {
member: RouterOutputs["viewer"]["organizations"]["listOtherTeamMembers"]["rows"][number];
}

View File

@ -205,6 +205,7 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
return (
<>
<Avatar
data-testid="profile-upload-avatar"
alt={defaultValues.name || ""}
imageSrc={getPlaceholderAvatar(value, defaultValues.name as string)}
size="lg"

View File

@ -13,9 +13,16 @@ import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry";
import { MembershipRole } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Badge, Button, showToast, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
import {
Badge,
Button,
showToast,
SkeletonButton,
SkeletonContainer,
SkeletonText,
UserAvatar,
} from "@calcom/ui";
import { ArrowRight, Plus, Trash2 } from "@calcom/ui/components/icon";
import { UserAvatar } from "@calcom/web/components/ui/avatar/UserAvatar";
type TeamMember = RouterOutputs["viewer"]["teams"]["get"]["members"][number];

View File

@ -26,8 +26,8 @@ import {
showToast,
Tooltip,
} from "@calcom/ui";
import { UserAvatar } from "@calcom/ui";
import { ExternalLink, MoreHorizontal, Edit2, Lock, UserX } from "@calcom/ui/components/icon";
import { UserAvatar } from "@calcom/web/components/ui/avatar/UserAvatar";
import MemberChangeRoleModal from "./MemberChangeRoleModal";
import TeamAvailabilityModal from "./TeamAvailabilityModal";

View File

@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import { Button, ButtonGroup, DataTable } from "@calcom/ui";
import { UserAvatar } from "@calcom/web/components/ui/avatar/UserAvatar";
import { UserAvatar } from "@calcom/ui";
import { UpgradeTip } from "../../tips/UpgradeTip";
import { TBContext, createTimezoneBuddyStore } from "../store";

View File

@ -0,0 +1,37 @@
/* eslint-disable playwright/missing-playwright-await */
import { render } from "@testing-library/react";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import { UserAvatar } from "./UserAvatar";
const mockUser = {
name: "John Doe",
username: "pro",
organizationId: null,
};
describe("tests for UserAvatar component", () => {
test("Should render the UsersAvatar Correctly", () => {
const { getByTestId } = render(<UserAvatar user={mockUser} data-testid="user-avatar-test" />);
const avatar = getByTestId("user-avatar-test");
expect(avatar).toBeInTheDocument();
});
test("It should render the organization logo if a organization is passed in", () => {
const { getByTestId } = render(
<UserAvatar
user={mockUser}
organization={{ id: -1, requestedSlug: "steve", slug: "steve", logoUrl: AVATAR_FALLBACK }}
data-testid="user-avatar-test"
/>
);
const avatar = getByTestId("user-avatar-test");
const organizationLogo = getByTestId("organization-logo");
expect(avatar).toBeInTheDocument();
expect(organizationLogo).toBeInTheDocument();
});
});

View File

@ -0,0 +1,54 @@
import { classNames } from "@calcom/lib";
import { getOrgAvatarUrl, getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import type { User } from "@calcom/prisma/client";
import { Avatar } from "@calcom/ui";
type Organization = {
id: number;
slug: string | null;
requestedSlug: string | null;
logoUrl?: string;
};
type UserAvatarProps = Omit<React.ComponentProps<typeof Avatar>, "alt" | "imageSrc"> & {
user: Pick<User, "organizationId" | "name" | "username">;
/**
* Useful when allowing the user to upload their own avatar and showing the avatar before it's uploaded
*/
previewSrc?: string | null;
organization?: Organization | null;
alt?: string | null;
};
function OrganizationIndicator({
size,
organization,
user,
}: Pick<UserAvatarProps, "size" | "user"> & { organization: Organization }) {
const organizationUrl = organization.logoUrl ?? getOrgAvatarUrl(organization);
return (
<div className={classNames("absolute bottom-0 right-0 z-10", size === "lg" ? "h-6 w-6" : "h-10 w-10")}>
<img
data-testId="organization-logo"
src={organizationUrl}
alt={user.username || ""}
className="flex h-full items-center justify-center rounded-full"
/>
</div>
);
}
/**
* It is aware of the user's organization to correctly show the avatar from the correct URL
*/
export function UserAvatar(props: UserAvatarProps) {
const { user, previewSrc = getUserAvatarUrl(user), ...rest } = props;
const indicator = props.organization ? (
<OrganizationIndicator size={props.size} organization={props.organization} user={props.user} />
) : (
props.indicator
);
return <Avatar {...rest} alt={user.name || "Nameless User"} imageSrc={previewSrc} indicator={indicator} />;
}

View File

@ -1,4 +1,5 @@
export { Avatar } from "./Avatar";
export { UserAvatar } from "./UserAvatar";
export type { AvatarProps } from "./Avatar";
export { AvatarGroup } from "./AvatarGroup";
export type { AvatarGroupProps } from "./AvatarGroup";

View File

@ -1,4 +1,4 @@
export { Avatar, AvatarGroup } from "./components/avatar";
export { Avatar, AvatarGroup, UserAvatar } from "./components/avatar";
export type { AvatarProps, AvatarGroupProps } from "./components/avatar";
export { ArrowButton } from "./components/arrow-button";
export type { ArrowButtonProps } from "./components/arrow-button";