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",
|
||||
"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.",
|
||||
"bio":"Bio",
|
||||
"delete_account_modal_title": "Delete Account",
|
||||
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
|
||||
"delete_my_account": "Delete my account",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { Dispatch } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import shallow from "zustand/shallow";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc/react";
|
||||
|
@ -15,6 +15,7 @@ import {
|
|||
Label,
|
||||
showToast,
|
||||
Avatar,
|
||||
ImageUploader,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { Action } from "../UserListTable";
|
||||
|
@ -23,8 +24,9 @@ import { useEditMode } from "./store";
|
|||
const editSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
avatar: z.string(),
|
||||
bio: z.string(),
|
||||
role: z.enum(["ADMIN", "MEMBER"]),
|
||||
role: z.enum(["ADMIN", "MEMBER", "OWNER"]),
|
||||
timeZone: z.string(),
|
||||
// schedules: z.array(z.string()),
|
||||
// teams: z.array(z.string()),
|
||||
|
@ -51,6 +53,7 @@ export function EditForm({
|
|||
defaultValues: {
|
||||
name: selectedUser?.name ?? "",
|
||||
email: selectedUser?.email ?? "",
|
||||
avatar: avatarUrl,
|
||||
bio: selectedUser?.bio ?? "",
|
||||
role: selectedUser?.role ?? "",
|
||||
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
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
avatar: values.avatar,
|
||||
bio: values.bio,
|
||||
timeZone: values.timeZone,
|
||||
});
|
||||
}}>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Avatar size="lg" alt={`${selectedUser?.name} avatar`} imageSrc={avatarUrl} />
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<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">
|
||||
<span className="text-emphasis text-lg font-semibold">{selectedUser?.name ?? "Nameless User"}</span>
|
||||
<p className="subtle text-sm font-normal">
|
||||
|
|
|
@ -97,7 +97,7 @@ export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dis
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-grow">
|
||||
<div className="mb-4 flex-grow">
|
||||
<EditForm
|
||||
selectedUser={loadedUser}
|
||||
avatarUrl={avatarURL}
|
||||
|
|
|
@ -169,7 +169,7 @@ export function UserListTable() {
|
|||
<div className="text-emphasis text-sm font-medium leading-none">
|
||||
{username || "No username"}
|
||||
</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>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
// export type OrganisationWithMembers = Awaited<ReturnType<typeof getOrganizationMembers>>;
|
||||
|
||||
|
@ -9,7 +10,7 @@ export async function isOrganisationAdmin(userId: number, orgId: number) {
|
|||
where: {
|
||||
userId,
|
||||
teamId: orgId,
|
||||
OR: [{ role: "ADMIN" }, { role: "OWNER" }],
|
||||
OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }],
|
||||
},
|
||||
})) || false
|
||||
);
|
||||
|
@ -19,7 +20,7 @@ export async function isOrganisationOwner(userId: number, orgId: number) {
|
|||
where: {
|
||||
userId,
|
||||
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 { MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
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" });
|
||||
|
||||
// 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?
|
||||
const requestedMember = await prisma.membership.findFirst({
|
||||
where: {
|
||||
|
@ -33,6 +40,11 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
|||
if (!requestedMember)
|
||||
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
|
||||
await prisma.$transaction([
|
||||
prisma.user.update({
|
||||
|
@ -44,6 +56,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
|||
email: input.email,
|
||||
name: input.name,
|
||||
timeZone: input.timeZone,
|
||||
avatar,
|
||||
},
|
||||
}),
|
||||
prisma.membership.update({
|
||||
|
|
|
@ -5,7 +5,8 @@ export const ZUpdateUserInputSchema = z.object({
|
|||
bio: z.string().optional(),
|
||||
name: 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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -150,7 +150,7 @@ const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Con
|
|||
ref={ref}
|
||||
{...props}
|
||||
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 && (
|
||||
<div className="mt-auto flex justify-end">
|
||||
<div className="flex gap-2">{bottomActions}</div>
|
||||
|
|
Loading…
Reference in New Issue