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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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