From 353c85120f41486e174adc6147196f037f9309e9 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 7 Jul 2023 14:10:36 +0530 Subject: [PATCH] feat: bulk invite for project (#1466) * feat: bulk invite for project * feat: members dropdown updated * fix: error message added ,style: ui improvement * feat: added add members button for scenarios with multiple members * chore: updated watch to fields --- .../project/send-project-invitation-modal.tsx | 321 +++++++++++------- apps/app/services/project.service.ts | 3 +- apps/app/types/projects.d.ts | 4 + 3 files changed, 201 insertions(+), 127 deletions(-) diff --git a/apps/app/components/project/send-project-invitation-modal.tsx b/apps/app/components/project/send-project-invitation-modal.tsx index 204633a85..65643d834 100644 --- a/apps/app/components/project/send-project-invitation-modal.tsx +++ b/apps/app/components/project/send-project-invitation-modal.tsx @@ -1,14 +1,23 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; -import { useForm, Controller } from "react-hook-form"; +import { useForm, Controller, useFieldArray } from "react-hook-form"; import { Dialog, Transition } from "@headlessui/react"; // ui -import { CustomSelect, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; +import { + Avatar, + CustomSearchSelect, + CustomSelect, + PrimaryButton, + SecondaryButton, +} from "components/ui"; +//icons +import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { ChevronDownIcon } from "@heroicons/react/20/solid"; // services import projectService from "services/project.service"; import workspaceService from "services/workspace.service"; @@ -17,9 +26,9 @@ import { useProjectMyMembership } from "contexts/project-member.context"; // hooks import useToast from "hooks/use-toast"; // types -import { ICurrentUserResponse, IProjectMemberInvitation } from "types"; +import { ICurrentUserResponse } from "types"; // fetch-keys -import { PROJECT_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; +import { PROJECT_MEMBERS, WORKSPACE_MEMBERS } from "constants/fetch-keys"; // constants import { ROLE } from "constants/workspace"; @@ -30,17 +39,22 @@ type Props = { user: ICurrentUserResponse | undefined; }; -type ProjectMember = IProjectMemberInvitation & { +type member = { + role: 5 | 10 | 15 | 20; member_id: string; - user_id: string; }; -const defaultValues: Partial = { - email: "", - message: "", - role: 5, - member_id: "", - user_id: "", +type FormValues = { + members: member[]; +}; + +const defaultValues: FormValues = { + members: [ + { + role: 5, + member_id: "", + }, + ], }; const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, members, user }) => { @@ -56,14 +70,16 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member ); const { - register, formState: { errors, isSubmitting }, - handleSubmit, + reset, - setValue, + handleSubmit, control, - } = useForm({ - defaultValues, + } = useForm(); + + const { fields, append, remove } = useFieldArray({ + control, + name: "members", }); const uninvitedPeople = people?.filter((person) => { @@ -71,20 +87,14 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member return !isInvited; }); - const onSubmit = async (formData: ProjectMember) => { + const onSubmit = async (formData: FormValues) => { if (!workspaceSlug || !projectId || isSubmitting) return; + const payload = { ...formData }; await projectService - .inviteProject(workspaceSlug as string, projectId as string, formData, user) - .then((response) => { + .inviteProject(workspaceSlug as string, projectId as string, payload, user) + .then(() => { setIsOpen(false); - mutate( - PROJECT_INVITATIONS, - (prevData) => { - if (!prevData) return prevData; - return [{ ...formData, ...response }, ...(prevData ?? [])]; - }, - false - ); + mutate(PROJECT_MEMBERS(projectId as string)); setToastAlert({ title: "Success", type: "success", @@ -93,6 +103,9 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member }) .catch((error) => { console.log(error); + }) + .finally(() => { + reset(defaultValues); }); }; @@ -104,6 +117,35 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member }, 500); }; + const appendField = () => { + append({ + role: 5, + member_id: "", + }); + }; + + useEffect(() => { + if (fields.length === 0) { + append([ + { + role: 5, + member_id: "", + }, + ]); + } + }, [fields, append]); + + const options = uninvitedPeople?.map((person) => ({ + value: person.member.id, + query: person.member.email, + content: ( +
+ + {person.member.email} +
+ ), + })); + return ( @@ -116,11 +158,11 @@ const SendProjectInvitationModal: React.FC = ({ isOpen, setIsOpen, member leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
-
+
= ({ isOpen, setIsOpen, member leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
-
+
Invite Members -
-

- Invite members to work on your project. -

+
+ +
+
+
Email
+
Role
-
-
- ( - - {value && value !== "" - ? people?.find((p) => p.member.id === value)?.member.email - : "Select email"} -
- } - onChange={(val: string) => { - onChange(val); - const person = uninvitedPeople?.find((p) => p.member.id === val); - setValue("member_id", val); - setValue("email", person?.member.email ?? ""); - }} - input - width="w-full" - > - {uninvitedPeople && uninvitedPeople.length > 0 ? ( - <> - {uninvitedPeople?.map((person) => ( - - {person.member.email} - - ))} - - ) : ( -
- Invite members to workspace before adding them to a project. -
+
+ {fields.map((field, index) => ( +
+
+ ( + + {value && value !== "" ? ( +
+ p.member.id === value)?.member + } + /> + {people?.find((p) => p.member.id === value)?.member.email} +
+ ) : ( +
Select co-worker’s email
+ )} +
-
-
Role
- ( - - {field.value ? ROLE[field.value] : "Select role"} - - } - input - width="w-full" - > - {Object.entries(ROLE).map(([key, label]) => { - if (parseInt(key) > (memberDetails?.role ?? 5)) return null; + /> + {errors.members && errors.members[index]?.member_id && ( + + {errors.members[index]?.member_id?.message} + + )} +
- return ( - - {label} - - ); - })} - - )} - /> -
-
-