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
Denzil Samuel 2023-09-23 04:42:35 +05:30 committed by GitHub
parent 0d653afd0c
commit e0c87cf664
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 51 additions and 12 deletions

View File

@ -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",

View File

@ -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">

View File

@ -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}

View File

@ -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>
);

View File

@ -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,
},
}));
}

View File

@ -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({

View File

@ -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(),
});

View File

@ -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>