mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-18 03:20:35 -05:00
chore: custom avatar removal (#6408)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -45,7 +45,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("LandingSidebar component", () => {
|
||||
const user = { id: "u1", name: "Alice", email: "alice@example.com", imageUrl: "" } as any;
|
||||
const user = { id: "u1", name: "Alice", email: "alice@example.com" } as any;
|
||||
const organization = { id: "o1", name: "orgOne" } as any;
|
||||
const organizations = [
|
||||
{ id: "o2", name: "betaOrg" },
|
||||
|
||||
@@ -82,7 +82,7 @@ export const LandingSidebar = ({
|
||||
id="userDropdownTrigger"
|
||||
className="w-full rounded-br-xl border-t p-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
|
||||
<div tabIndex={0} className={cn("flex cursor-pointer flex-row items-center gap-3")}>
|
||||
<ProfileAvatar userId={user.id} imageUrl={user.imageUrl} />
|
||||
<ProfileAvatar userId={user.id} />
|
||||
<>
|
||||
<div className="grow overflow-hidden">
|
||||
<p
|
||||
|
||||
@@ -113,7 +113,6 @@ const mockUser = {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -111,7 +111,6 @@ const mockUser = {
|
||||
id: "user1",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
imageUrl: "http://example.com/avatar.png",
|
||||
emailVerified: new Date(),
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
|
||||
@@ -342,7 +342,7 @@ export const MainNavigation = ({
|
||||
"flex cursor-pointer flex-row items-center gap-3",
|
||||
isCollapsed ? "justify-center px-2" : "px-4"
|
||||
)}>
|
||||
<ProfileAvatar userId={user.id} imageUrl={user.imageUrl} />
|
||||
<ProfileAvatar userId={user.id} />
|
||||
{!isCollapsed && !isTextVisible && (
|
||||
<>
|
||||
<div
|
||||
|
||||
@@ -37,7 +37,6 @@ describe("EnvironmentPage", () => {
|
||||
id: mockUserId,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
verifyUserPassword,
|
||||
} from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/lib/user";
|
||||
import { EMAIL_VERIFICATION_DISABLED } from "@/lib/constants";
|
||||
import { deleteFile } from "@/lib/storage/service";
|
||||
import { getFileNameWithIdFromUrl } from "@/lib/storage/utils";
|
||||
import { getUser, updateUser } from "@/lib/user/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
@@ -15,8 +13,6 @@ import { applyRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { sendForgotPasswordEmail, sendVerificationNewEmail } from "@/modules/email";
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { AuthenticationError, AuthorizationError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import {
|
||||
TUserPersonalInfoUpdateInput,
|
||||
@@ -97,58 +93,6 @@ export const updateUserAction = authenticatedActionClient.schema(ZUserPersonalIn
|
||||
)
|
||||
);
|
||||
|
||||
const ZUpdateAvatarAction = z.object({
|
||||
avatarUrl: z.string(),
|
||||
});
|
||||
|
||||
export const updateAvatarAction = authenticatedActionClient.schema(ZUpdateAvatarAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"user",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const oldObject = await getUser(ctx.user.id);
|
||||
const result = await updateUser(ctx.user.id, { imageUrl: parsedInput.avatarUrl });
|
||||
ctx.auditLoggingCtx.userId = ctx.user.id;
|
||||
ctx.auditLoggingCtx.oldObject = oldObject;
|
||||
ctx.auditLoggingCtx.newObject = result;
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZRemoveAvatarAction = z.object({
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
export const removeAvatarAction = authenticatedActionClient.schema(ZRemoveAvatarAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"user",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const oldObject = await getUser(ctx.user.id);
|
||||
const imageUrl = ctx.user.imageUrl;
|
||||
if (!imageUrl) {
|
||||
throw new Error("Image not found");
|
||||
}
|
||||
|
||||
const fileName = getFileNameWithIdFromUrl(imageUrl);
|
||||
if (!fileName) {
|
||||
throw new Error("Invalid filename");
|
||||
}
|
||||
|
||||
const deletionResult = await deleteFile(parsedInput.environmentId, "public", fileName);
|
||||
if (!deletionResult.success) {
|
||||
throw new Error("Deletion failed");
|
||||
}
|
||||
const result = await updateUser(ctx.user.id, { imageUrl: null });
|
||||
ctx.auditLoggingCtx.userId = ctx.user.id;
|
||||
ctx.auditLoggingCtx.oldObject = oldObject;
|
||||
ctx.auditLoggingCtx.newObject = result;
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const resetPasswordAction = authenticatedActionClient.action(
|
||||
withAuditLogging(
|
||||
"passwordReset",
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import * as profileActions from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions";
|
||||
import * as fileUploadHooks from "@/app/lib/fileUpload";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Session } from "next-auth";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { EditProfileAvatarForm } from "./EditProfileAvatarForm";
|
||||
|
||||
vi.mock("@/modules/ui/components/avatars", () => ({
|
||||
ProfileAvatar: ({ imageUrl }) => <div data-testid="profile-avatar">{imageUrl || "No Avatar"}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
refresh: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({
|
||||
updateAvatarAction: vi.fn(),
|
||||
removeAvatarAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/lib/fileUpload", () => ({
|
||||
handleFileUpload: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockSession: Session = {
|
||||
user: { id: "user-id" },
|
||||
expires: "session-expires-at",
|
||||
};
|
||||
const environmentId = "test-env-id";
|
||||
|
||||
describe("EditProfileAvatarForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(profileActions.updateAvatarAction).mockResolvedValue({});
|
||||
vi.mocked(profileActions.removeAvatarAction).mockResolvedValue({});
|
||||
vi.mocked(fileUploadHooks.handleFileUpload).mockResolvedValue({
|
||||
url: "new-avatar.jpg",
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("renders correctly without an existing image", () => {
|
||||
render(<EditProfileAvatarForm session={mockSession} environmentId={environmentId} imageUrl={null} />);
|
||||
expect(screen.getByTestId("profile-avatar")).toHaveTextContent("No Avatar");
|
||||
expect(screen.getByText("environments.settings.profile.upload_image")).toBeInTheDocument();
|
||||
expect(screen.queryByText("environments.settings.profile.remove_image")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with an existing image", () => {
|
||||
render(
|
||||
<EditProfileAvatarForm
|
||||
session={mockSession}
|
||||
environmentId={environmentId}
|
||||
imageUrl="existing-avatar.jpg"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId("profile-avatar")).toHaveTextContent("existing-avatar.jpg");
|
||||
expect(screen.getByText("environments.settings.profile.change_image")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.settings.profile.remove_image")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles image removal successfully", async () => {
|
||||
render(
|
||||
<EditProfileAvatarForm
|
||||
session={mockSession}
|
||||
environmentId={environmentId}
|
||||
imageUrl="existing-avatar.jpg"
|
||||
/>
|
||||
);
|
||||
const removeButton = screen.getByText("environments.settings.profile.remove_image");
|
||||
await userEvent.click(removeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(profileActions.removeAvatarAction).toHaveBeenCalledWith({ environmentId });
|
||||
});
|
||||
});
|
||||
|
||||
test("shows error if removeAvatarAction fails", async () => {
|
||||
vi.mocked(profileActions.removeAvatarAction).mockRejectedValue(new Error("API error"));
|
||||
render(
|
||||
<EditProfileAvatarForm
|
||||
session={mockSession}
|
||||
environmentId={environmentId}
|
||||
imageUrl="existing-avatar.jpg"
|
||||
/>
|
||||
);
|
||||
const removeButton = screen.getByText("environments.settings.profile.remove_image");
|
||||
await userEvent.click(removeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(toast.error)).toHaveBeenCalledWith(
|
||||
"environments.settings.profile.avatar_update_failed"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,178 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
removeAvatarAction,
|
||||
updateAvatarAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions";
|
||||
import { handleFileUpload } from "@/app/lib/fileUpload";
|
||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FormError, FormField, FormItem, FormProvider } from "@/modules/ui/components/form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { Session } from "next-auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
|
||||
interface EditProfileAvatarFormProps {
|
||||
session: Session;
|
||||
environmentId: string;
|
||||
imageUrl: string | null;
|
||||
}
|
||||
|
||||
export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: EditProfileAvatarFormProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const { t } = useTranslate();
|
||||
const fileSchema =
|
||||
typeof window !== "undefined"
|
||||
? z
|
||||
.instanceof(FileList)
|
||||
.refine((files) => files.length === 1, t("environments.settings.profile.you_must_select_a_file"))
|
||||
.refine((files) => {
|
||||
const file = files[0];
|
||||
const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
|
||||
return allowedTypes.includes(file.type);
|
||||
}, t("environments.settings.profile.invalid_file_type"))
|
||||
.refine((files) => {
|
||||
const file = files[0];
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
return file.size <= maxSize;
|
||||
}, t("environments.settings.profile.file_size_must_be_less_than_10mb"))
|
||||
: z.any();
|
||||
|
||||
const formSchema = z.object({
|
||||
file: fileSchema,
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
mode: "onChange",
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
const handleUpload = async (file: File, environmentId: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (imageUrl) {
|
||||
// If avatar image already exists, then remove it before update action
|
||||
await removeAvatarAction({ environmentId });
|
||||
}
|
||||
const { url, error } = await handleFileUpload(file, environmentId);
|
||||
|
||||
if (error) {
|
||||
toast.error(error);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAvatarAction({ avatarUrl: url });
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
toast.error(t("environments.settings.profile.avatar_update_failed"));
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await removeAvatarAction({ environmentId });
|
||||
} catch (err) {
|
||||
toast.error(t("environments.settings.profile.avatar_update_failed"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
form.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
const file = data.file[0];
|
||||
if (file) {
|
||||
await handleUpload(file, environmentId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative h-10 w-10 overflow-hidden rounded-full">
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
|
||||
<svg className="h-7 w-7 animate-spin text-slate-200" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProfileAvatar userId={session.user.id} imageUrl={imageUrl} />
|
||||
</div>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-4">
|
||||
<FormField
|
||||
name="file"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<FormItem>
|
||||
<div className="flex">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="mr-2"
|
||||
variant={!!fieldState.error?.message ? "destructive" : "secondary"}
|
||||
onClick={() => {
|
||||
inputRef.current?.click();
|
||||
}}>
|
||||
{imageUrl
|
||||
? t("environments.settings.profile.change_image")
|
||||
: t("environments.settings.profile.upload_image")}
|
||||
<input
|
||||
type="file"
|
||||
id="hiddenFileInput"
|
||||
ref={(e) => {
|
||||
field.ref(e);
|
||||
inputRef.current = e;
|
||||
}}
|
||||
className="hidden"
|
||||
accept="image/jpeg, image/png, image/webp"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.files);
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{imageUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
className="mr-2"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={handleRemove}>
|
||||
{t("environments.settings.profile.remove_image")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormError />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -49,15 +49,12 @@ describe("Loading", () => {
|
||||
);
|
||||
|
||||
const loadingCards = screen.getAllByTestId("loading-card");
|
||||
expect(loadingCards).toHaveLength(3);
|
||||
expect(loadingCards).toHaveLength(2);
|
||||
|
||||
expect(loadingCards[0]).toHaveTextContent("environments.settings.profile.personal_information");
|
||||
expect(loadingCards[0]).toHaveTextContent("environments.settings.profile.update_personal_info");
|
||||
|
||||
expect(loadingCards[1]).toHaveTextContent("common.avatar");
|
||||
expect(loadingCards[1]).toHaveTextContent("environments.settings.profile.organization_identification");
|
||||
|
||||
expect(loadingCards[2]).toHaveTextContent("environments.settings.profile.delete_account");
|
||||
expect(loadingCards[2]).toHaveTextContent("environments.settings.profile.confirm_delete_account");
|
||||
expect(loadingCards[1]).toHaveTextContent("environments.settings.profile.delete_account");
|
||||
expect(loadingCards[1]).toHaveTextContent("environments.settings.profile.confirm_delete_account");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,11 +19,6 @@ const Loading = () => {
|
||||
{ classes: "h-6 w-64" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("common.avatar"),
|
||||
description: t("environments.settings.profile.organization_identification"),
|
||||
skeletonLines: [{ classes: "h-10 w-10" }, { classes: "h-8 w-24" }],
|
||||
},
|
||||
{
|
||||
title: t("environments.settings.profile.delete_account"),
|
||||
description: t("environments.settings.profile.confirm_delete_account"),
|
||||
|
||||
@@ -55,11 +55,6 @@ vi.mock(
|
||||
vi.mock("./components/DeleteAccount", () => ({
|
||||
DeleteAccount: ({ user }) => <div data-testid="delete-account">DeleteAccount: {user.id}</div>,
|
||||
}));
|
||||
vi.mock("./components/EditProfileAvatarForm", () => ({
|
||||
EditProfileAvatarForm: ({ _, environmentId }) => (
|
||||
<div data-testid="edit-profile-avatar-form">EditProfileAvatarForm: {environmentId}</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("./components/EditProfileDetailsForm", () => ({
|
||||
EditProfileDetailsForm: ({ user }) => (
|
||||
<div data-testid="edit-profile-details-form">EditProfileDetailsForm: {user.id}</div>
|
||||
@@ -73,7 +68,6 @@ const mockUser = {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
imageUrl: "http://example.com/avatar.png",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
notificationSettings: { alert: {}, unsubscribedOrganizationIds: [] },
|
||||
@@ -117,7 +111,6 @@ describe("ProfilePage", () => {
|
||||
"AccountSettingsNavbar: env-123 profile"
|
||||
);
|
||||
expect(screen.getByTestId("edit-profile-details-form")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("edit-profile-avatar-form")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("account-security")).toBeInTheDocument(); // Shown because 2FA license is enabled
|
||||
expect(screen.queryByTestId("upgrade-prompt")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("delete-account")).toBeInTheDocument();
|
||||
|
||||
@@ -12,7 +12,6 @@ import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { SettingsCard } from "../../components/SettingsCard";
|
||||
import { DeleteAccount } from "./components/DeleteAccount";
|
||||
import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm";
|
||||
import { EditProfileDetailsForm } from "./components/EditProfileDetailsForm";
|
||||
|
||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
@@ -50,17 +49,6 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
isPasswordResetEnabled={isPasswordResetEnabled}
|
||||
/>
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title={t("common.avatar")}
|
||||
description={t("environments.settings.profile.organization_identification")}>
|
||||
{user && (
|
||||
<EditProfileAvatarForm
|
||||
session={session}
|
||||
environmentId={environmentId}
|
||||
imageUrl={user.imageUrl}
|
||||
/>
|
||||
)}
|
||||
</SettingsCard>
|
||||
{user.identityProvider === "email" && (
|
||||
<SettingsCard
|
||||
title={t("common.security")}
|
||||
|
||||
@@ -126,7 +126,6 @@ const mockUser = {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
notificationSettings: { alert: {} },
|
||||
|
||||
@@ -128,7 +128,6 @@ const mockUser = {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -145,7 +145,6 @@ const mockUser = {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -291,7 +291,6 @@ const mockUser: TUser = {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "https://example.com/avatar.jpg",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -250,7 +250,6 @@ const mockUser: TUser = {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "https://example.com/avatar.jpg",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -158,7 +158,6 @@ const mockUser = {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
@@ -174,7 +173,6 @@ const mockSession = {
|
||||
id: mockUserId,
|
||||
name: mockUser.name,
|
||||
email: mockUser.email,
|
||||
image: mockUser.imageUrl,
|
||||
role: mockUser.role,
|
||||
plan: "free",
|
||||
status: "active",
|
||||
|
||||
@@ -118,7 +118,6 @@ describe("Page", () => {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: null,
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
@@ -161,7 +160,6 @@ describe("Page", () => {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: null,
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
@@ -250,7 +248,6 @@ describe("Page", () => {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: null,
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
@@ -339,7 +336,6 @@ describe("Page", () => {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: null,
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -122,7 +122,6 @@ export const mockUser: TUser = {
|
||||
name: "mock User",
|
||||
email: "test@unit.com",
|
||||
emailVerified: currentDate,
|
||||
imageUrl: "https://www.google.com",
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
twoFactorEnabled: false,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IdentityProvider, Objective, Prisma, Role } from "@prisma/client";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUserLocale, TUserUpdateInput } from "@formbricks/types/user";
|
||||
import { deleteUser, getUser, getUserByEmail, getUsersWithOrganization, updateUser } from "./service";
|
||||
@@ -20,10 +20,6 @@ vi.mock("@formbricks/database", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/fileValidation", () => ({
|
||||
isValidImageFile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/organization/service", () => ({
|
||||
getOrganizationsWhereUserIsSingleOwner: vi.fn(),
|
||||
deleteOrganization: vi.fn(),
|
||||
@@ -39,7 +35,6 @@ describe("User Service", () => {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: new Date(),
|
||||
imageUrl: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: Role.project_manager,
|
||||
@@ -200,13 +195,6 @@ describe("User Service", () => {
|
||||
|
||||
await expect(updateUser("nonexistent", { name: "New Name" })).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("should throw InvalidInputError when invalid image URL is provided", async () => {
|
||||
const { isValidImageFile } = await import("@/lib/fileValidation");
|
||||
vi.mocked(isValidImageFile).mockReturnValue(false);
|
||||
|
||||
await expect(updateUser("user1", { imageUrl: "invalid-image-url" })).rejects.toThrow(InvalidInputError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteUser", () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "server-only";
|
||||
import { isValidImageFile } from "@/lib/fileValidation";
|
||||
import { deleteOrganization, getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||
import { deleteBrevoCustomerByEmail } from "@/modules/auth/lib/brevo";
|
||||
import { Prisma } from "@prisma/client";
|
||||
@@ -8,7 +7,7 @@ import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TUser, TUserLocale, TUserUpdateInput, ZUserUpdateInput } from "@formbricks/types/user";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
|
||||
@@ -17,7 +16,6 @@ const responseSelection = {
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
imageUrl: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
@@ -79,7 +77,6 @@ export const getUserByEmail = reactCache(async (email: string): Promise<TUser |
|
||||
// function to update a user's user
|
||||
export const updateUser = async (personId: string, data: TUserUpdateInput): Promise<TUser> => {
|
||||
validateInputs([personId, ZId], [data, ZUserUpdateInput.partial()]);
|
||||
if (data.imageUrl && !isValidImageFile(data.imageUrl)) throw new InvalidInputError("Invalid image file");
|
||||
|
||||
try {
|
||||
const updatedUser = await prisma.user.update({
|
||||
|
||||
@@ -112,7 +112,6 @@ describe("withAuditLogging", () => {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: null,
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email" as const,
|
||||
createdAt: new Date(),
|
||||
@@ -151,7 +150,6 @@ describe("withAuditLogging", () => {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: null,
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email" as const,
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -148,7 +148,6 @@ describe("authOptions", () => {
|
||||
email: mockUser.email,
|
||||
password: mockHashedPassword,
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "http://example.com/avatar.png",
|
||||
twoFactorEnabled: false,
|
||||
};
|
||||
|
||||
@@ -161,7 +160,6 @@ describe("authOptions", () => {
|
||||
id: fakeUser.id,
|
||||
email: fakeUser.email,
|
||||
emailVerified: fakeUser.emailVerified,
|
||||
imageUrl: fakeUser.imageUrl,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -206,7 +206,6 @@ export const authOptions: NextAuthOptions = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
emailVerified: user.emailVerified,
|
||||
imageUrl: user.imageUrl,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,6 @@ export const mockUser: TUser = {
|
||||
name: "mock User",
|
||||
email: "john.doe@example.com",
|
||||
emailVerified: new Date("2024-01-01T00:00:00.000Z"),
|
||||
imageUrl: "https://www.google.com",
|
||||
createdAt: new Date("2024-01-01T00:00:00.000Z"),
|
||||
updatedAt: new Date("2024-01-01T00:00:00.000Z"),
|
||||
twoFactorEnabled: false,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isValidImageFile } from "@/lib/fileValidation";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
@@ -11,10 +10,6 @@ import { TUserCreateInput, TUserUpdateInput, ZUserEmail, ZUserUpdateInput } from
|
||||
export const updateUser = async (id: string, data: TUserUpdateInput) => {
|
||||
validateInputs([id, ZId], [data, ZUserUpdateInput.partial()]);
|
||||
|
||||
if (data.imageUrl && !isValidImageFile(data.imageUrl)) {
|
||||
throw new InvalidInputError("Invalid image file");
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
|
||||
@@ -94,7 +94,6 @@ const fullUser = {
|
||||
updatedAt: new Date(),
|
||||
email: "test@example.com",
|
||||
emailVerified: null,
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
organizationId: "org1",
|
||||
|
||||
@@ -28,7 +28,6 @@ describe("ResponseTimeline", () => {
|
||||
name: "Test User",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
imageUrl: null,
|
||||
objective: null,
|
||||
role: "founder",
|
||||
email: "test@example.com",
|
||||
|
||||
@@ -51,7 +51,6 @@ export const getSSOProviders = () => [
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -76,7 +75,6 @@ export const getSSOProviders = () => [
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
name: [profile.firstName, profile.lastName].filter(Boolean).join(" "),
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
options: {
|
||||
|
||||
@@ -13,7 +13,6 @@ export const mockUser: TUser = {
|
||||
unsubscribedOrganizationIds: [],
|
||||
},
|
||||
emailVerified: new Date(),
|
||||
imageUrl: "https://example.com/image.png",
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "google",
|
||||
locale: "en-US",
|
||||
|
||||
@@ -56,7 +56,6 @@ const mockUser = {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email",
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -131,7 +131,6 @@ describe("CreateOrganizationPage", () => {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
emailVerified: null,
|
||||
imageUrl: null,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "email" as const,
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { isValidImageFile } from "@/lib/fileValidation";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { updateUser } from "./user";
|
||||
|
||||
@@ -24,7 +23,6 @@ describe("updateUser", () => {
|
||||
id: "user-123",
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
imageUrl: "https://example.com/image.png",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "project_manager",
|
||||
@@ -41,7 +39,6 @@ describe("updateUser", () => {
|
||||
});
|
||||
|
||||
test("successfully updates a user", async () => {
|
||||
vi.mocked(isValidImageFile).mockReturnValue(true);
|
||||
vi.mocked(prisma.user.update).mockResolvedValue(mockUser as any);
|
||||
|
||||
const updateData = { name: "Updated Name" };
|
||||
@@ -55,7 +52,6 @@ describe("updateUser", () => {
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
imageUrl: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
@@ -72,17 +68,7 @@ describe("updateUser", () => {
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
test("throws InvalidInputError when image file is invalid", async () => {
|
||||
vi.mocked(isValidImageFile).mockReturnValue(false);
|
||||
|
||||
const updateData = { imageUrl: "invalid-image.xyz" };
|
||||
await expect(updateUser("user-123", updateData)).rejects.toThrow(InvalidInputError);
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("throws ResourceNotFoundError when user does not exist", async () => {
|
||||
vi.mocked(isValidImageFile).mockReturnValue(true);
|
||||
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
||||
code: PrismaErrorType.RecordDoesNotExist,
|
||||
clientVersion: "1.0.0",
|
||||
@@ -96,8 +82,6 @@ describe("updateUser", () => {
|
||||
});
|
||||
|
||||
test("re-throws other errors", async () => {
|
||||
vi.mocked(isValidImageFile).mockReturnValue(true);
|
||||
|
||||
const otherError = new Error("Some other error");
|
||||
vi.mocked(prisma.user.update).mockRejectedValue(otherError);
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { isValidImageFile } from "@/lib/fileValidation";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TUser, TUserUpdateInput } from "@formbricks/types/user";
|
||||
|
||||
// function to update a user's user
|
||||
export const updateUser = async (personId: string, data: TUserUpdateInput): Promise<TUser> => {
|
||||
if (data.imageUrl && !isValidImageFile(data.imageUrl)) throw new InvalidInputError("Invalid image file");
|
||||
|
||||
try {
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: {
|
||||
@@ -20,7 +17,6 @@ export const updateUser = async (personId: string, data: TUserUpdateInput): Prom
|
||||
name: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
imageUrl: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
role: true,
|
||||
|
||||
@@ -12,13 +12,6 @@ vi.mock("boring-avatars", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock next/image
|
||||
vi.mock("next/image", () => ({
|
||||
default: ({ src, width, height, className, alt }: any) => (
|
||||
<img src={src} width={width} height={height} className={className} alt={alt} data-testid="next-image" />
|
||||
),
|
||||
}));
|
||||
|
||||
describe("Avatar Components", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@@ -44,7 +37,7 @@ describe("Avatar Components", () => {
|
||||
});
|
||||
|
||||
describe("ProfileAvatar", () => {
|
||||
test("renders Boring Avatar when imageUrl is not provided", () => {
|
||||
test("renders Boring Avatar", () => {
|
||||
render(<ProfileAvatar userId="user-123" />);
|
||||
|
||||
const avatar = screen.getByTestId("boring-avatar-bauhaus");
|
||||
@@ -52,32 +45,5 @@ describe("Avatar Components", () => {
|
||||
expect(avatar).toHaveAttribute("data-size", "40");
|
||||
expect(avatar).toHaveAttribute("data-name", "user-123");
|
||||
});
|
||||
|
||||
test("renders Boring Avatar when imageUrl is null", () => {
|
||||
render(<ProfileAvatar userId="user-123" imageUrl={null} />);
|
||||
|
||||
const avatar = screen.getByTestId("boring-avatar-bauhaus");
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders Image component when imageUrl is provided", () => {
|
||||
render(<ProfileAvatar userId="user-123" imageUrl="https://example.com/avatar.jpg" />);
|
||||
|
||||
const image = screen.getByTestId("next-image");
|
||||
expect(image).toBeInTheDocument();
|
||||
expect(image).toHaveAttribute("src", "https://example.com/avatar.jpg");
|
||||
expect(image).toHaveAttribute("width", "40");
|
||||
expect(image).toHaveAttribute("height", "40");
|
||||
expect(image).toHaveAttribute("alt", "Avatar placeholder");
|
||||
expect(image).toHaveClass("h-10", "w-10", "rounded-full", "object-cover");
|
||||
});
|
||||
|
||||
test("renders Image component with different imageUrl", () => {
|
||||
render(<ProfileAvatar userId="user-123" imageUrl="https://example.com/different-avatar.png" />);
|
||||
|
||||
const image = screen.getByTestId("next-image");
|
||||
expect(image).toBeInTheDocument();
|
||||
expect(image).toHaveAttribute("src", "https://example.com/different-avatar.png");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Avatar from "boring-avatars";
|
||||
import Image from "next/image";
|
||||
|
||||
const colors = ["#00C4B8", "#ccfbf1", "#334155"];
|
||||
|
||||
@@ -13,20 +12,8 @@ export const PersonAvatar: React.FC<PersonAvatarProps> = ({ personId }) => {
|
||||
|
||||
interface ProfileAvatar {
|
||||
userId: string;
|
||||
imageUrl?: string | null;
|
||||
}
|
||||
|
||||
export const ProfileAvatar: React.FC<ProfileAvatar> = ({ userId, imageUrl }) => {
|
||||
if (imageUrl) {
|
||||
return (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
width="40"
|
||||
height="40"
|
||||
className="h-10 w-10 rounded-full object-cover"
|
||||
alt="Avatar placeholder"
|
||||
/>
|
||||
);
|
||||
}
|
||||
export const ProfileAvatar: React.FC<ProfileAvatar> = ({ userId }) => {
|
||||
return <Avatar size={40} name={userId} variant="bauhaus" colors={colors} />;
|
||||
};
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"@vercel/functions": "2.2.8",
|
||||
"@vercel/og": "0.8.5",
|
||||
"bcryptjs": "3.0.2",
|
||||
"boring-avatars": "1.11.2",
|
||||
"boring-avatars": "2.0.1",
|
||||
"cache-manager": "6.4.3",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `imageUrl` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "imageUrl";
|
||||
@@ -824,7 +824,6 @@ model User {
|
||||
email String @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
|
||||
imageUrl String?
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
backupCodes String?
|
||||
|
||||
@@ -57,7 +57,6 @@ export const ZUser = z.object({
|
||||
Omit<
|
||||
User,
|
||||
| "emailVerified"
|
||||
| "imageUrl"
|
||||
| "twoFactorSecret"
|
||||
| "twoFactorEnabled"
|
||||
| "backupCodes"
|
||||
|
||||
@@ -48,7 +48,6 @@ export const ZUser = z.object({
|
||||
name: ZUserName,
|
||||
email: ZUserEmail,
|
||||
emailVerified: z.date().nullable(),
|
||||
imageUrl: z.string().url().nullable(),
|
||||
twoFactorEnabled: z.boolean(),
|
||||
identityProvider: ZUserIdentityProvider,
|
||||
createdAt: z.date(),
|
||||
@@ -70,7 +69,6 @@ export const ZUserUpdateInput = z.object({
|
||||
password: ZUserPassword.optional(),
|
||||
role: ZRole.optional(),
|
||||
objective: ZUserObjective.nullish(),
|
||||
imageUrl: z.string().nullish(),
|
||||
notificationSettings: ZUserNotificationSettings.optional(),
|
||||
locale: ZUserLocale.optional(),
|
||||
lastLoginAt: z.date().nullish(),
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -292,8 +292,8 @@ importers:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
boring-avatars:
|
||||
specifier: 1.11.2
|
||||
version: 1.11.2
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
cache-manager:
|
||||
specifier: 6.4.3
|
||||
version: 6.4.3
|
||||
@@ -5069,8 +5069,11 @@ packages:
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
boring-avatars@1.11.2:
|
||||
resolution: {integrity: sha512-3+wkwPeObwS4R37FGXMYViqc4iTrIRj5yzfX9Qy4mnpZ26sX41dGMhsAgmKks1r/uufY1pl4vpgzMWHYfJRb2A==}
|
||||
boring-avatars@2.0.1:
|
||||
resolution: {integrity: sha512-TeBnZrp7WxHcQPuLhGQamklgNqaL7eUAUh3E11kFj9rTn0Hari2ZKVTchqNrp62UOHN/XOe5bZGcbzVGwHjHwg==}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
|
||||
bowser@2.11.0:
|
||||
resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==}
|
||||
@@ -15116,7 +15119,10 @@ snapshots:
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
boring-avatars@1.11.2: {}
|
||||
boring-avatars@2.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
bowser@2.11.0: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user