style: member role visibility (#2919)

* style: member role visibility

* fix: build errors
This commit is contained in:
Lakhan Baheti 2023-11-28 17:11:04 +05:30 committed by sriram veeraghanta
parent 18587395c9
commit 72b592b9ec

View File

@ -2,7 +2,17 @@ import React, { useEffect, useRef, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
import { Control, Controller, FieldArrayWithId, UseFieldArrayRemove, useFieldArray, useForm } from "react-hook-form"; import {
Control,
Controller,
FieldArrayWithId,
UseFieldArrayRemove,
UseFormGetValues,
UseFormSetValue,
UseFormWatch,
useFieldArray,
useForm,
} from "react-hook-form";
import { Check, ChevronDown, Plus, XCircle } from "lucide-react"; import { Check, ChevronDown, Plus, XCircle } from "lucide-react";
// services // services
import { WorkspaceService } from "services/workspace.service"; import { WorkspaceService } from "services/workspace.service";
@ -34,6 +44,7 @@ type Props = {
type EmailRole = { type EmailRole = {
email: string; email: string;
role: TUserWorkspaceRole; role: TUserWorkspaceRole;
role_active: boolean;
}; };
type FormValues = { type FormValues = {
@ -44,6 +55,9 @@ type InviteMemberFormProps = {
index: number; index: number;
remove: UseFieldArrayRemove; remove: UseFieldArrayRemove;
control: Control<FormValues, any>; control: Control<FormValues, any>;
setValue: UseFormSetValue<FormValues>;
getValues: UseFormGetValues<FormValues>;
watch: UseFormWatch<FormValues>;
field: FieldArrayWithId<FormValues, "emails", "id">; field: FieldArrayWithId<FormValues, "emails", "id">;
fields: FieldArrayWithId<FormValues, "emails", "id">[]; fields: FieldArrayWithId<FormValues, "emails", "id">[];
errors: any; errors: any;
@ -53,9 +67,33 @@ type InviteMemberFormProps = {
// services // services
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
const placeholderEmails = [
"charlie.taylor@frstflit.com",
"octave.chanute@frstflit.com",
"george.spratt@frstflit.com",
"frank.coffyn@frstflit.com",
"amos.root@frstflit.com",
"edward.deeds@frstflit.com",
"charles.m.manly@frstflit.com",
"glenn.curtiss@frstflit.com",
"thomas.selfridge@frstflit.com",
"albert.zahm@frstflit.com",
];
const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => { const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
const { control, index, fields, remove, errors, isInvitationDisabled, setIsInvitationDisabled } = props; const {
control,
index,
fields,
remove,
errors,
isInvitationDisabled,
setIsInvitationDisabled,
setValue,
getValues,
watch,
} = props;
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
@ -64,131 +102,159 @@ const InviteMemberForm: React.FC<InviteMemberFormProps> = (props) => {
useDynamicDropdownPosition(isDropdownOpen, () => setIsDropdownOpen(false), buttonRef, dropdownRef); useDynamicDropdownPosition(isDropdownOpen, () => setIsDropdownOpen(false), buttonRef, dropdownRef);
const email = watch(`emails.${index}.email`);
const emailOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value === "") {
const validEmail = fields.map((_, i) => emailRegex.test(getValues(`emails.${i}.email`))).includes(true);
if (validEmail) {
setIsInvitationDisabled(false);
} else {
setIsInvitationDisabled(true);
}
if (getValues(`emails.${index}.role_active`)) {
setValue(`emails.${index}.role_active`, false);
}
} else {
if (!getValues(`emails.${index}.role_active`)) {
setValue(`emails.${index}.role_active`, true);
}
if (isInvitationDisabled && emailRegex.test(event.target.value)) {
setIsInvitationDisabled(false);
} else if (!isInvitationDisabled && !emailRegex.test(event.target.value)) {
setIsInvitationDisabled(true);
}
}
};
return ( return (
<div className="group relative grid grid-cols-11 gap-4"> <div>
<div className="col-span-7 bg-onboarding-background-200 rounded-md"> <div className="group relative grid grid-cols-11 gap-4">
<Controller <div className="col-span-7 bg-onboarding-background-200 rounded-md">
control={control} <Controller
name={`emails.${index}.email`} control={control}
rules={{ name={`emails.${index}.email`}
pattern: { rules={{
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, pattern: {
message: "Invalid Email ID", value: emailRegex,
}, message: "Invalid Email ID",
}} },
render={({ field: { value, onChange, ref } }) => ( }}
<Input render={({ field: { value, onChange, ref } }) => (
id={`emails.${index}.email`} <Input
name={`emails.${index}.email`} id={`emails.${index}.email`}
type="text" name={`emails.${index}.email`}
value={value} type="text"
onChange={(event) => { value={value}
if (event.target.value === "") { onChange={(event) => {
const validEmail = !fields emailOnChange(event);
.filter((ele) => { onChange(event);
ele.id !== `emails.${index}.email`; }}
}) ref={ref}
.map((ele) => ele.email) hasError={Boolean(errors.emails?.[index]?.email)}
.includes(""); placeholder={placeholderEmails[index % placeholderEmails.length]}
if (validEmail) { className="text-xs sm:text-sm w-full h-12 placeholder:text-onboarding-text-400 border-onboarding-border-100"
setIsInvitationDisabled(false); />
} else { )}
setIsInvitationDisabled(true); />
} </div>
} else if ( <div className="col-span-3 bg-onboarding-background-200 rounded-md border items-center flex border-onboarding-border-100">
isInvitationDisabled && <Controller
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(event.target.value) control={control}
) { name={`emails.${index}.role`}
setIsInvitationDisabled(false); rules={{ required: true }}
} else if ( render={({ field: { value, onChange } }) => (
!isInvitationDisabled && <Listbox
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(event.target.value) as="div"
) { value={value}
setIsInvitationDisabled(true); onChange={(val) => {
} onChange(val);
onChange(event); setIsDropdownOpen(false);
}} setValue(`emails.${index}.role_active`, true);
ref={ref} }}
hasError={Boolean(errors.emails?.[index]?.email)} className="flex-shrink-0 text-left w-full"
placeholder="Enter their email..."
className="text-xs sm:text-sm w-full h-12 placeholder:text-onboarding-text-400 border-onboarding-border-100"
/>
)}
/>
</div>
<div className="col-span-3 bg-onboarding-background-200 rounded-md border items-center flex border-onboarding-border-100">
<Controller
control={control}
name={`emails.${index}.role`}
rules={{ required: true }}
render={({ field: { value, onChange } }) => (
<Listbox
as="div"
value={value}
onChange={(val) => {
onChange(val);
setIsDropdownOpen(false);
}}
className="flex-shrink-0 text-left w-full"
>
<Listbox.Button
type="button"
ref={buttonRef}
onClick={() => setIsDropdownOpen((prev) => !prev)}
className="flex items-center px-2.5 h-11 py-2 text-xs justify-between gap-1 w-full rounded-md duration-300"
> >
<span className="text-xs text-onboarding-text-400 sm:text-sm">{ROLE[value]}</span> <Listbox.Button
type="button"
<ChevronDown className="h-4 w-4 stroke-onboarding-text-400" /> ref={buttonRef}
</Listbox.Button> onClick={() => setIsDropdownOpen((prev) => !prev)}
className="flex items-center px-2.5 h-11 py-2 text-xs justify-between gap-1 w-full rounded-md duration-300"
<Transition
show={isDropdownOpen}
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options
ref={dropdownRef}
className="fixed w-36 z-10 border border-onboarding-border-100 mt-1 overflow-y-auto rounded-md bg-onboarding-background-200 text-xs shadow-lg focus:outline-none max-h-48"
> >
<div className="space-y-1 p-2"> <span
{Object.entries(ROLE).map(([key, value]) => ( className={`text-xs ${
<Listbox.Option !getValues(`emails.${index}.role_active`)
key={key} ? "text-onboarding-text-400"
value={parseInt(key)} : "text-onboarding-text-100"
className={({ active, selected }) => } sm:text-sm`}
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${active || selected ? "bg-onboarding-background-400/40" : "" >
} ${selected ? "text-onboarding-text-100" : "text-custom-text-200"}` {ROLE[value]}
} </span>
>
{({ selected }) => ( <ChevronDown
<div className="flex items-center justify-between gap-2"> className={`h-4 w-4 ${
<div className="flex items-center gap-2">{value}</div> !getValues(`emails.${index}.role_active`)
{selected && <Check className="h-4 w-4 flex-shrink-0" />} ? "stroke-onboarding-text-400"
</div> : "stroke-onboarding-text-100"
)} }`}
</Listbox.Option> />
))} </Listbox.Button>
</div>
</Listbox.Options> <Transition
</Transition> show={isDropdownOpen}
</Listbox> as={React.Fragment}
)} enter="transition ease-out duration-100"
/> enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options
ref={dropdownRef}
className="fixed w-36 z-10 border border-onboarding-border-100 mt-1 overflow-y-auto rounded-md bg-onboarding-background-200 text-xs shadow-lg focus:outline-none max-h-48"
>
<div className="space-y-1 p-2">
{Object.entries(ROLE).map(([key, value]) => (
<Listbox.Option
key={key}
value={parseInt(key)}
className={({ active, selected }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || selected ? "bg-onboarding-background-400/40" : ""
} ${selected ? "text-onboarding-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">{value}</div>
{selected && <Check className="h-4 w-4 flex-shrink-0" />}
</div>
)}
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Transition>
</Listbox>
)}
/>
</div>
{fields.length > 1 && (
<button
type="button"
className="hidden group-hover:grid self-center place-items-center rounded ml-3"
onClick={() => remove(index)}
>
<XCircle className="h-3.5 w-3.5 text-custom-text-400" />
</button>
)}
</div> </div>
{fields.length > 1 && ( {email && !emailRegex.test(email) && (
<button <div className="">
type="button" <span className="text-sm">🤥</span>{" "}
className="hidden group-hover:grid self-center place-items-center rounded ml-3" <span className="text-xs text-red-500 mt-1">That doesn{"'"}t look like an email address.</span>
onClick={() => remove(index)} </div>
>
<XCircle className="h-3.5 w-3.5 text-custom-text-400" />
</button>
)} )}
</div> </div>
); );
@ -204,6 +270,9 @@ export const InviteMembers: React.FC<Props> = (props) => {
const { const {
control, control,
watch,
getValues,
setValue,
handleSubmit, handleSubmit,
formState: { isSubmitting, errors, isValid }, formState: { isSubmitting, errors, isValid },
} = useForm<FormValues>(); } = useForm<FormValues>();
@ -229,7 +298,12 @@ export const InviteMembers: React.FC<Props> = (props) => {
payload = { emails: payload.emails.filter((email) => email.email !== "") }; payload = { emails: payload.emails.filter((email) => email.email !== "") };
await workspaceService await workspaceService
.inviteWorkspace(workspace.slug, payload) .inviteWorkspace(workspace.slug, {
emails: payload.emails.map((email) => ({
email: email.email,
role: email.role,
})),
})
.then(async () => { .then(async () => {
setToastAlert({ setToastAlert({
type: "success", type: "success",
@ -249,16 +323,16 @@ export const InviteMembers: React.FC<Props> = (props) => {
}; };
const appendField = () => { const appendField = () => {
append({ email: "", role: 15 }); append({ email: "", role: 15, role_active: false });
}; };
useEffect(() => { useEffect(() => {
if (fields.length === 0) { if (fields.length === 0) {
append( append(
[ [
{ email: "", role: 15 }, { email: "", role: 15, role_active: false },
{ email: "", role: 15 }, { email: "", role: 15, role_active: false },
{ email: "", role: 15 }, { email: "", role: 15, role_active: false },
], ],
{ {
focusIndex: 0, focusIndex: 0,
@ -325,6 +399,9 @@ export const InviteMembers: React.FC<Props> = (props) => {
<div className="space-y-3 sm:space-y-4 mb-3"> <div className="space-y-3 sm:space-y-4 mb-3">
{fields.map((field, index) => ( {fields.map((field, index) => (
<InviteMemberForm <InviteMemberForm
watch={watch}
getValues={getValues}
setValue={setValue}
isInvitationDisabled={isInvitationDisabled} isInvitationDisabled={isInvitationDisabled}
setIsInvitationDisabled={(value: boolean) => setIsInvitationDisabled(value)} setIsInvitationDisabled={(value: boolean) => setIsInvitationDisabled(value)}
control={control} control={control}