chore: Remove all avatar/logo references (#14532)
* chore: Remove all avatar/logo references * Updated user profile to only use avatarUrl * fix: minor style issue * chore: Remove redundant includeTeamLogo * fix: Use right avatar default in event type list * fix: placeholder avatar team profile, target * chore: Add logoUrl/avatarUrl to JWT * fix: Bunch of org avatar issues, fix members list * Fix logoUrl on org pages * More type fixes * Hopefully final type fixes * Another round of type fixes * fix: UserForm ts * fix: Handle as return types, not input types * fix: Remove profile and add avatarUrl * fix: notFound as const * fix: Seeder avatarUrl params * revert: Migration changes for easier recovery * fix: Add explicit type to builder as avatar is now set * Add logoUrl to unpublished entity * Avatar test out of scope here * fix: Use the new avatarUrl after save * chore: Removed getOrgAvatarUrl/getTeamAvatarUrl * fix: Update sidebar image immediately after change * Unable to safe unnamed user, so default * fix: Unpublished page, add organization test * Add more tests
This commit is contained in:
1 parent
5695ba7c13
commit
d25c043046
73 files changed
+644
-770
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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={
|
||||
<Avatar
|
||||
alt={team.logo || ""}
|
||||
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
|
||||
alt={team.logoUrl || ""}
|
||||
imageSrc={getPlaceholderAvatar(team.logoUrl, team.name)} // if no image, use default avatar
|
||||
size="sm"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
logoUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -98,6 +98,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
isPrivate: true,
|
||||
isOrganization: true,
|
||||
metadata: true,
|
||||
logoUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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<UserPageProps> = 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<UserPageProps> = 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<UserPageProps> = 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<UserPageProps> = async (cont
|
||||
away: user.away,
|
||||
})),
|
||||
entity: {
|
||||
...(org?.logoUrl ? { logoUrl: org?.logoUrl } : {}),
|
||||
considerUnpublished: !isARedirectFromNonOrgLink && org?.slug === null,
|
||||
orgSlug: currentOrgDomain,
|
||||
name: org?.name ?? null,
|
||||
|
||||
@@ -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<typeof getServerSideProps>) {
|
||||
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<typeof getServerSide
|
||||
...query
|
||||
} = useRouterQuery();
|
||||
|
||||
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||
const fromUserNameRedirected = searchParams?.get("username") || "";
|
||||
/*
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 = ({
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => {
|
||||
const showRemoveAvatarButton = value === null ? false : !isFallbackImg;
|
||||
const organization =
|
||||
userOrganization && userOrganization.id
|
||||
? {
|
||||
...(userOrganization as Ensure<NonNullable<typeof user.organization>, "id">),
|
||||
slug: userOrganization.slug || null,
|
||||
requestedSlug: userOrganization.metadata?.requestedSlug || null,
|
||||
}
|
||||
: null;
|
||||
name="avatarUrl"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const showRemoveAvatarButton = value !== null;
|
||||
return (
|
||||
<>
|
||||
<UserAvatar data-testid="profile-upload-avatar" previewSrc={value} size="lg" user={user} />
|
||||
@@ -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 = ({
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
formMethods.setValue("avatar", "", { shouldDirty: true });
|
||||
onChange(null);
|
||||
}}>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
|
||||
@@ -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({
|
||||
<div className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<UnpublishedEntity
|
||||
{...{ [slugPropertyName]: team.parent ? parentSlug : teamSlug }}
|
||||
logoUrl={team.parent?.logoUrl || team.logoUrl}
|
||||
name={team.parent ? team.parent.name : team.name}
|
||||
/>
|
||||
</div>
|
||||
@@ -161,10 +162,7 @@ function TeamPage({
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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", /.*/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user