feat: Add ability to edit avatar (#11399)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>pull/11494/head
parent
0d653afd0c
commit
e0c87cf664
|
@ -1276,6 +1276,7 @@
|
||||||
"personal_cal_url": "My personal {{appName}} URL",
|
"personal_cal_url": "My personal {{appName}} URL",
|
||||||
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
|
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
|
||||||
"user_has_no_bio": "This user has not added a bio yet.",
|
"user_has_no_bio": "This user has not added a bio yet.",
|
||||||
|
"bio":"Bio",
|
||||||
"delete_account_modal_title": "Delete Account",
|
"delete_account_modal_title": "Delete Account",
|
||||||
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
|
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
|
||||||
"delete_my_account": "Delete my account",
|
"delete_my_account": "Delete my account",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import type { Dispatch } from "react";
|
import type { Dispatch } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import shallow from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc, type RouterOutputs } from "@calcom/trpc/react";
|
import { trpc, type RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
@ -15,6 +15,7 @@ import {
|
||||||
Label,
|
Label,
|
||||||
showToast,
|
showToast,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
ImageUploader,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import type { Action } from "../UserListTable";
|
import type { Action } from "../UserListTable";
|
||||||
|
@ -23,8 +24,9 @@ import { useEditMode } from "./store";
|
||||||
const editSchema = z.object({
|
const editSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
avatar: z.string(),
|
||||||
bio: z.string(),
|
bio: z.string(),
|
||||||
role: z.enum(["ADMIN", "MEMBER"]),
|
role: z.enum(["ADMIN", "MEMBER", "OWNER"]),
|
||||||
timeZone: z.string(),
|
timeZone: z.string(),
|
||||||
// schedules: z.array(z.string()),
|
// schedules: z.array(z.string()),
|
||||||
// teams: z.array(z.string()),
|
// teams: z.array(z.string()),
|
||||||
|
@ -51,6 +53,7 @@ export function EditForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: selectedUser?.name ?? "",
|
name: selectedUser?.name ?? "",
|
||||||
email: selectedUser?.email ?? "",
|
email: selectedUser?.email ?? "",
|
||||||
|
avatar: avatarUrl,
|
||||||
bio: selectedUser?.bio ?? "",
|
bio: selectedUser?.bio ?? "",
|
||||||
role: selectedUser?.role ?? "",
|
role: selectedUser?.role ?? "",
|
||||||
timeZone: selectedUser?.timeZone ?? "",
|
timeZone: selectedUser?.timeZone ?? "",
|
||||||
|
@ -88,12 +91,32 @@ export function EditForm({
|
||||||
role: values.role as "ADMIN" | "MEMBER", // Cast needed as we dont provide an option for owner
|
role: values.role as "ADMIN" | "MEMBER", // Cast needed as we dont provide an option for owner
|
||||||
name: values.name,
|
name: values.name,
|
||||||
email: values.email,
|
email: values.email,
|
||||||
|
avatar: values.avatar,
|
||||||
bio: values.bio,
|
bio: values.bio,
|
||||||
timeZone: values.timeZone,
|
timeZone: values.timeZone,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
<div className="mt-4 flex items-center gap-2">
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
<Avatar size="lg" alt={`${selectedUser?.name} avatar`} imageSrc={avatarUrl} />
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="avatar"
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Avatar alt={`${selectedUser?.name} avatar`} imageSrc={value} size="lg" />
|
||||||
|
<div className="ml-4">
|
||||||
|
<ImageUploader
|
||||||
|
target="avatar"
|
||||||
|
id="avatar-upload"
|
||||||
|
buttonMsg={t("change_avatar")}
|
||||||
|
handleAvatarChange={(newAvatar) => {
|
||||||
|
form.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
imageSrc={value || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div className="space-between flex flex-col leading-none">
|
<div className="space-between flex flex-col leading-none">
|
||||||
<span className="text-emphasis text-lg font-semibold">{selectedUser?.name ?? "Nameless User"}</span>
|
<span className="text-emphasis text-lg font-semibold">{selectedUser?.name ?? "Nameless User"}</span>
|
||||||
<p className="subtle text-sm font-normal">
|
<p className="subtle text-sm font-normal">
|
||||||
|
|
|
@ -97,7 +97,7 @@ export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dis
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-grow">
|
<div className="mb-4 flex-grow">
|
||||||
<EditForm
|
<EditForm
|
||||||
selectedUser={loadedUser}
|
selectedUser={loadedUser}
|
||||||
avatarUrl={avatarURL}
|
avatarUrl={avatarURL}
|
||||||
|
|
|
@ -169,7 +169,7 @@ export function UserListTable() {
|
||||||
<div className="text-emphasis text-sm font-medium leading-none">
|
<div className="text-emphasis text-sm font-medium leading-none">
|
||||||
{username || "No username"}
|
{username || "No username"}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-subtle text-sm leading-none">{email}</div>
|
<div className="text-subtle mt-1 text-sm leading-none">{email}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
// export type OrganisationWithMembers = Awaited<ReturnType<typeof getOrganizationMembers>>;
|
// export type OrganisationWithMembers = Awaited<ReturnType<typeof getOrganizationMembers>>;
|
||||||
|
|
||||||
|
@ -9,7 +10,7 @@ export async function isOrganisationAdmin(userId: number, orgId: number) {
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
teamId: orgId,
|
teamId: orgId,
|
||||||
OR: [{ role: "ADMIN" }, { role: "OWNER" }],
|
OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }],
|
||||||
},
|
},
|
||||||
})) || false
|
})) || false
|
||||||
);
|
);
|
||||||
|
@ -19,7 +20,7 @@ export async function isOrganisationOwner(userId: number, orgId: number) {
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
teamId: orgId,
|
teamId: orgId,
|
||||||
role: "OWNER",
|
role: MembershipRole.OWNER,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
import { isOrganisationAdmin, isOrganisationOwner } from "@calcom/lib/server/queries/organisations";
|
||||||
|
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
import { MembershipRole } from "@calcom/prisma/enums";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
@ -21,6 +23,11 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
||||||
|
|
||||||
if (!(await isOrganisationAdmin(userId, organizationId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
if (!(await isOrganisationAdmin(userId, organizationId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|
||||||
|
// only OWNER can update the role to OWNER
|
||||||
|
if (input.role === MembershipRole.OWNER && !(await isOrganisationOwner(userId, organizationId))) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
|
||||||
// Is requested user a member of the organization?
|
// Is requested user a member of the organization?
|
||||||
const requestedMember = await prisma.membership.findFirst({
|
const requestedMember = await prisma.membership.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
@ -33,6 +40,11 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
||||||
if (!requestedMember)
|
if (!requestedMember)
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "User does not belong to your organization" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "User does not belong to your organization" });
|
||||||
|
|
||||||
|
let avatar = input.avatar;
|
||||||
|
if (input.avatar) {
|
||||||
|
avatar = await resizeBase64Image(input.avatar);
|
||||||
|
}
|
||||||
|
|
||||||
// Update user
|
// Update user
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
prisma.user.update({
|
prisma.user.update({
|
||||||
|
@ -44,6 +56,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
||||||
email: input.email,
|
email: input.email,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
timeZone: input.timeZone,
|
timeZone: input.timeZone,
|
||||||
|
avatar,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.membership.update({
|
prisma.membership.update({
|
||||||
|
|
|
@ -5,7 +5,8 @@ export const ZUpdateUserInputSchema = z.object({
|
||||||
bio: z.string().optional(),
|
bio: z.string().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
email: z.string().optional(),
|
email: z.string().optional(),
|
||||||
role: z.enum(["ADMIN", "MEMBER"]),
|
avatar: z.string().optional(),
|
||||||
|
role: z.enum(["ADMIN", "MEMBER", "OWNER"]),
|
||||||
timeZone: z.string(),
|
timeZone: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,7 @@ const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Con
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(sheetVariants({ position, size }), className)}>
|
className={classNames(sheetVariants({ position, size }), className)}>
|
||||||
<div className="h-full overflow-y-scroll">{children}</div>
|
<div className="no-scrollbar h-full overflow-y-auto">{children}</div>
|
||||||
{bottomActions && (
|
{bottomActions && (
|
||||||
<div className="mt-auto flex justify-end">
|
<div className="mt-auto flex justify-end">
|
||||||
<div className="flex gap-2">{bottomActions}</div>
|
<div className="flex gap-2">{bottomActions}</div>
|
||||||
|
|
Loading…
Reference in New Issue