From 1c75fc84f0dadc5e90497461ca0b7b17276ba13b Mon Sep 17 00:00:00 2001 From: = Date: Fri, 13 Sep 2024 00:29:47 +0530 Subject: [PATCH 1/3] feat: added a temporary combobox for identity addition to project --- .../src/components/v2/ComboBox/ComboBox.tsx | 83 +++++++++++++++++ frontend/src/components/v2/ComboBox/index.tsx | 1 + frontend/src/components/v2/Modal/Modal.tsx | 18 +++- .../IdentityTab/components/IdentityModal.tsx | 90 ++++++++++--------- 4 files changed, 150 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/v2/ComboBox/ComboBox.tsx create mode 100644 frontend/src/components/v2/ComboBox/index.tsx diff --git a/frontend/src/components/v2/ComboBox/ComboBox.tsx b/frontend/src/components/v2/ComboBox/ComboBox.tsx new file mode 100644 index 000000000..692d19e63 --- /dev/null +++ b/frontend/src/components/v2/ComboBox/ComboBox.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import { faCaretDown, faCheck } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Combobox, Transition } from "@headlessui/react"; +import { ByComparator } from "@headlessui/react/dist/types"; +import { twMerge } from "tailwind-merge"; + +type MyComboBoxProps = { + value?: T; + className?: string; + items: { + value: T; + key: string; + label: string; + }[]; + by: ByComparator; + defaultValue?: T; + displayValue: (value: T) => string; + onSelectChange: (value: T) => void; + onFilter: (value: { value: T }, filterQuery: string) => boolean; +}; + +// TODO(akhilmhdh): This is a very temporary one due to limitation of present situation +// don't mind the api for now will be switched to aria later +export const ComboBox = ({ + onSelectChange, + onFilter, + displayValue, + by, + items, + ...props +}: MyComboBoxProps) => { + const [query, setQuery] = useState(""); + + const filteredResult = + query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query)); + + return ( + +
+ setQuery(event.target.value)} + displayValue={displayValue} + className=" inline-flex w-full items-center justify-between rounded-md bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200" + /> + + + setQuery("")} + > + + {filteredResult.map(({ value, key, label }) => ( + + `relative cursor-pointer select-none py-2 pl-10 pr-4 transition-all hover:bg-mineshaft-500 ${ + active ? "text-primary" : "text-white" + }` + } + > + {({ selected }) => ( + <> + {label} + {selected ? ( +
+ +
+ ) : null} + + )} +
+ ))} +
+
+
+
+ ); +}; diff --git a/frontend/src/components/v2/ComboBox/index.tsx b/frontend/src/components/v2/ComboBox/index.tsx new file mode 100644 index 000000000..912296adf --- /dev/null +++ b/frontend/src/components/v2/ComboBox/index.tsx @@ -0,0 +1 @@ +export { ComboBox } from "./ComboBox"; diff --git a/frontend/src/components/v2/Modal/Modal.tsx b/frontend/src/components/v2/Modal/Modal.tsx index 90abfecbd..a741d6d02 100644 --- a/frontend/src/components/v2/Modal/Modal.tsx +++ b/frontend/src/components/v2/Modal/Modal.tsx @@ -11,13 +11,24 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & { title?: ReactNode; subTitle?: ReactNode; footerContent?: ReactNode; + bodyClassName?: string; onClose?: () => void; overlayClassName?: string; }; export const ModalContent = forwardRef( ( - { children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props }, + { + children, + title, + subTitle, + className, + overlayClassName, + footerContent, + bodyClassName, + onClose, + ...props + }, forwardedRef ) => ( @@ -35,7 +46,10 @@ export const ModalContent = forwardRef( style={{ maxHeight: "90%" }} > {title && {title}} - + {children} {footerContent && {footerContent}} diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx index d8c554524..0ca6f70f1 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx @@ -5,15 +5,8 @@ import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { createNotification } from "@app/components/notifications"; -import { - Button, - FormControl, - Modal, - ModalClose, - ModalContent, - Select, - SelectItem -} from "@app/components/v2"; +import { Button, FormControl, Modal, ModalClose, ModalContent } from "@app/components/v2"; +import { ComboBox } from "@app/components/v2/ComboBox"; import { useOrganization, useWorkspace } from "@app/context"; import { useAddIdentityToWorkspace, @@ -25,8 +18,14 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = yup .object({ - identityId: yup.string().required("Identity id is required"), - role: yup.string() + identity: yup.object({ + id: yup.string().required("Identity id is required"), + name: yup.string().required("Identity name is required") + }), + role: yup.object({ + slug: yup.string().required("role slug is required"), + name: yup.string().required("role name is required") + }) }) .required(); @@ -79,12 +78,12 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { resolver: yupResolver(schema) }); - const onFormSubmit = async ({ identityId, role }: FormData) => { + const onFormSubmit = async ({ identity, role }: FormData) => { try { await addIdentityToWorkspaceMutateAsync({ workspaceId, - identityId, - role: role || undefined + identityId: identity.id, + role: role.slug || undefined }); createNotification({ @@ -114,34 +113,41 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { reset(); }} > - + {filteredIdentityMembershipOrgs.length ? (
( - + by="id" + value={{ id: field.value.id, name: field.value.name }} + defaultValue={{ id: field.value.id, name: field.value.name }} + onSelectChange={(value) => onChange({ id: value.id, name: value.name })} + displayValue={(el) => el.name} + onFilter={({ value }, filterQuery) => + value.name.toLowerCase().includes(filterQuery.toLowerCase()) + } + items={filteredIdentityMembershipOrgs.map(({ identity }) => ({ + key: identity.id, + value: { id: identity.id, name: identity.name }, + label: identity.name + }))} + /> )} /> ( { isError={Boolean(error)} className="mt-4" > - + by="slug" + value={{ slug: field.value.slug, name: field.value.name }} + defaultValue={{ slug: field.value.slug, name: field.value.name }} + onSelectChange={(value) => onChange({ slug: value.slug, name: value.name })} + displayValue={(el) => el.name} + onFilter={({ value }, filterQuery) => + value.name.toLowerCase().includes(filterQuery.toLowerCase()) + } + items={(roles || []).map(({ slug, name }) => ({ + key: slug, + value: { slug, name }, + label: name + }))} + /> )} /> From d6601687003c921d47da6617f9a47ac2daf92e57 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 13 Sep 2024 00:32:50 +0530 Subject: [PATCH 2/3] fix: org invite check only when needed --- backend/src/services/org/org-service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 5b4efb992..c0a640112 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -458,7 +458,6 @@ export const orgServiceFactory = ({ const appCfg = getConfig(); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); - ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); const org = await orgDAL.findOrgById(orgId); @@ -583,6 +582,8 @@ export const orgServiceFactory = ({ // if there exist no org membership we set is as given by the request if (!inviteeMembership) { + // as its used by project invite also + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); let roleId; const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug; if (isCustomOrgRole) { From 02d54da74a31c32ca355a807b23307793f7fb68b Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 12 Sep 2024 15:22:05 -0700 Subject: [PATCH 3/3] resolve change requests --- .../src/components/v2/ComboBox/ComboBox.tsx | 6 ++-- .../IdentityTab/components/IdentityModal.tsx | 35 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/v2/ComboBox/ComboBox.tsx b/frontend/src/components/v2/ComboBox/ComboBox.tsx index 692d19e63..c590110c3 100644 --- a/frontend/src/components/v2/ComboBox/ComboBox.tsx +++ b/frontend/src/components/v2/ComboBox/ComboBox.tsx @@ -5,7 +5,7 @@ import { Combobox, Transition } from "@headlessui/react"; import { ByComparator } from "@headlessui/react/dist/types"; import { twMerge } from "tailwind-merge"; -type MyComboBoxProps = { +type ComboBoxProps = { value?: T; className?: string; items: { @@ -29,11 +29,11 @@ export const ComboBox = ({ by, items, ...props -}: MyComboBoxProps) => { +}: ComboBoxProps) => { const [query, setQuery] = useState(""); const filteredResult = - query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query)); + query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query)).slice(0, 20); return ( diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx index 0ca6f70f1..6ddd2d097 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import Link from "next/link"; import { yupResolver } from "@hookform/resolvers/yup"; @@ -55,7 +55,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { }); const identityMemberships = identityMembershipsData?.identityMemberships; - const { data: roles } = useGetProjectRoles(projectSlug); + const { + data: roles, + isLoading: isRolesLoading, + isFetched: isRolesFetched + } = useGetProjectRoles(projectSlug); const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace(); @@ -73,11 +77,18 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { control, handleSubmit, reset, + setValue, formState: { isSubmitting } } = useForm({ resolver: yupResolver(schema) }); + useEffect(() => { + if (!isRolesFetched || !roles) return; + + setValue("role", { name: roles[0]?.name, slug: roles[0]?.slug }); + }, [isRolesFetched, roles]); + const onFormSubmit = async ({ identity, role }: FormData) => { try { await addIdentityToWorkspaceMutateAsync({ @@ -91,7 +102,17 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { type: "success" }); - reset(); + const nextAvailableMembership = filteredIdentityMembershipOrgs.filter( + (membership) => membership.identity.id !== identity.id + )[0]; + + // prevents combobox from displaying previously added identity + reset({ + identity: { + name: nextAvailableMembership?.identity.name, + id: nextAvailableMembership?.identity.id + } + }); handlePopUpToggle("identity", false); } catch (err) { console.error(err); @@ -113,14 +134,14 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { reset(); }} > - + {filteredIdentityMembershipOrgs.length ? ( ( @@ -197,7 +218,9 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { All identities in your organization have already been added to this project. - + )}