mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 17:26:07 +00:00
Merge pull request #2814 from Infisical/add-group-to-project-filterable-selects
Improvement: User/Group/Identity Modals Dropdown to Filterable Select Refactor + User Groups and Secret Tags Table Pagination
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole, TOrgRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
enum OrgMembershipRole {
|
enum OrgMembershipRole {
|
||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
@@ -23,3 +23,8 @@ export const formatProjectRoleName = (name: string) => {
|
|||||||
|
|
||||||
export const isCustomProjectRole = (slug: string) =>
|
export const isCustomProjectRole = (slug: string) =>
|
||||||
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
||||||
|
|
||||||
|
export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) =>
|
||||||
|
isCustomOrgRole(roleIdOrSlug)
|
||||||
|
? roles.find((r) => r.id === roleIdOrSlug)
|
||||||
|
: roles.find((r) => r.slug === roleIdOrSlug);
|
||||||
|
|||||||
+21
-23
@@ -6,14 +6,14 @@ import { z } from "zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent
|
||||||
Select,
|
|
||||||
SelectItem
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
|
import { findOrgMembershipRole } from "@app/helpers/roles";
|
||||||
import { useCreateGroup, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
|
import { useCreateGroup, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ const GroupFormSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(5, "Slug must be at least 5 characters long")
|
.min(5, "Slug must be at least 5 characters long")
|
||||||
.max(36, "Slug must be 36 characters or fewer"),
|
.max(36, "Slug must be 36 characters or fewer"),
|
||||||
role: z.string()
|
role: z.object({ name: z.string(), slug: z.string() })
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TGroupFormData = z.infer<typeof GroupFormSchema>;
|
export type TGroupFormData = z.infer<typeof GroupFormSchema>;
|
||||||
@@ -62,13 +62,13 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr
|
|||||||
reset({
|
reset({
|
||||||
name: group.name,
|
name: group.name,
|
||||||
slug: group.slug,
|
slug: group.slug,
|
||||||
role: group?.customRole?.slug ?? group.role
|
role: group?.customRole ?? findOrgMembershipRole(roles, group.role)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
name: "",
|
name: "",
|
||||||
slug: "",
|
slug: "",
|
||||||
role: roles[0].slug
|
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [popUp?.group?.data, roles]);
|
}, [popUp?.group?.data, roles]);
|
||||||
@@ -88,14 +88,14 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr
|
|||||||
id: group.groupId,
|
id: group.groupId,
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
role: role || undefined
|
role: role.slug || undefined
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await createMutateAsync({
|
await createMutateAsync({
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
organizationId: currentOrg.id,
|
organizationId: currentOrg.id,
|
||||||
role: role || undefined
|
role: role.slug || undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handlePopUpToggle("group", false);
|
handlePopUpToggle("group", false);
|
||||||
@@ -121,7 +121,10 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr
|
|||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title={`${popUp?.group?.data ? "Update" : "Create"} Group`}>
|
<ModalContent
|
||||||
|
bodyClassName="overflow-visible"
|
||||||
|
title={`${popUp?.group?.data ? "Update" : "Create"} Group`}
|
||||||
|
>
|
||||||
<form onSubmit={handleSubmit(onGroupModalSubmit)}>
|
<form onSubmit={handleSubmit(onGroupModalSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -144,26 +147,21 @@ export const OrgGroupModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Pr
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="role"
|
name="role"
|
||||||
defaultValue=""
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
<FormControl
|
||||||
label={`${popUp?.group?.data ? "Update" : ""} Role`}
|
label={`${popUp?.group?.data ? "Update" : ""} Role`}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
>
|
>
|
||||||
<Select
|
<FilterableSelect
|
||||||
defaultValue={field.value}
|
options={roles}
|
||||||
{...field}
|
placeholder="Select role..."
|
||||||
onValueChange={(e) => onChange(e)}
|
onChange={onChange}
|
||||||
className="w-full"
|
value={value}
|
||||||
>
|
getOptionValue={(option) => option.slug}
|
||||||
{(roles || []).map(({ name, slug }) => (
|
getOptionLabel={(option) => option.name}
|
||||||
<SelectItem value={slug} key={`org-group-role-${slug}`}>
|
/>
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+23
-28
@@ -9,27 +9,24 @@ import { z } from "zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent
|
||||||
Select,
|
|
||||||
SelectItem
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
|
import { findOrgMembershipRole } from "@app/helpers/roles";
|
||||||
import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||||
import {
|
import { useAddIdentityUniversalAuth } from "@app/hooks/api/identities";
|
||||||
// IdentityAuthMethod,
|
|
||||||
useAddIdentityUniversalAuth
|
|
||||||
} from "@app/hooks/api/identities";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string().min(1, "Required"),
|
||||||
role: z.string(),
|
role: z.object({ slug: z.string(), name: z.string() }),
|
||||||
metadata: z
|
metadata: z
|
||||||
.object({
|
.object({
|
||||||
key: z.string().trim().min(1),
|
key: z.string().trim().min(1),
|
||||||
@@ -101,13 +98,13 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
if (identity) {
|
if (identity) {
|
||||||
reset({
|
reset({
|
||||||
name: identity.name,
|
name: identity.name,
|
||||||
role: identity?.customRole?.slug ?? identity.role,
|
role: identity.customRole ?? findOrgMembershipRole(roles, identity.role),
|
||||||
metadata: identity.metadata
|
metadata: identity.metadata
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
name: "",
|
name: "",
|
||||||
role: roles[0].slug
|
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [popUp?.identity?.data, roles]);
|
}, [popUp?.identity?.data, roles]);
|
||||||
@@ -126,7 +123,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId: identity.identityId,
|
identityId: identity.identityId,
|
||||||
name,
|
name,
|
||||||
role: role || undefined,
|
role: role.slug || undefined,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
metadata
|
metadata
|
||||||
});
|
});
|
||||||
@@ -137,7 +134,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
|
|
||||||
const { id: createdId } = await createMutateAsync({
|
const { id: createdId } = await createMutateAsync({
|
||||||
name,
|
name,
|
||||||
role: role || undefined,
|
role: role.slug || undefined,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
metadata
|
metadata
|
||||||
});
|
});
|
||||||
@@ -184,7 +181,10 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}>
|
<ModalContent
|
||||||
|
bodyClassName="overflow-visible"
|
||||||
|
title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}
|
||||||
|
>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -199,26 +199,21 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="role"
|
name="role"
|
||||||
defaultValue=""
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
<FormControl
|
||||||
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
|
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
>
|
>
|
||||||
<Select
|
<FilterableSelect
|
||||||
defaultValue={field.value}
|
placeholder="Select role..."
|
||||||
{...field}
|
options={roles}
|
||||||
onValueChange={(e) => onChange(e)}
|
onChange={onChange}
|
||||||
className="w-full"
|
value={value}
|
||||||
>
|
getOptionValue={(option) => option.slug}
|
||||||
{(roles || []).map(({ name, slug }) => (
|
getOptionLabel={(option) => option.name}
|
||||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
/>
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+18
-22
@@ -15,7 +15,7 @@ import {
|
|||||||
TextArea
|
TextArea
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { isCustomOrgRole } from "@app/helpers/roles";
|
import { findOrgMembershipRole } from "@app/helpers/roles";
|
||||||
import {
|
import {
|
||||||
useAddUsersToOrg,
|
useAddUsersToOrg,
|
||||||
useFetchServerStatus,
|
useFetchServerStatus,
|
||||||
@@ -45,7 +45,7 @@ const addMemberFormSchema = z.object({
|
|||||||
)
|
)
|
||||||
.default([]),
|
.default([]),
|
||||||
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
|
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
|
||||||
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG)
|
organizationRole: z.object({ name: z.string(), slug: z.string() })
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||||
@@ -87,16 +87,17 @@ export const AddOrgMemberModal = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (organizationRoles) {
|
if (organizationRoles) {
|
||||||
reset({
|
reset({
|
||||||
organizationRoleSlug: isCustomOrgRole(currentOrg?.defaultMembershipRole!)
|
organizationRole: findOrgMembershipRole(
|
||||||
? organizationRoles?.find((role) => role.id === currentOrg?.defaultMembershipRole)?.slug!
|
organizationRoles,
|
||||||
: currentOrg?.defaultMembershipRole
|
currentOrg?.defaultMembershipRole!
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [organizationRoles]);
|
}, [organizationRoles]);
|
||||||
|
|
||||||
const onAddMembers = async ({
|
const onAddMembers = async ({
|
||||||
emails,
|
emails,
|
||||||
organizationRoleSlug,
|
organizationRole,
|
||||||
projects: selectedProjects,
|
projects: selectedProjects,
|
||||||
projectRoleSlug
|
projectRoleSlug
|
||||||
}: TAddMemberForm) => {
|
}: TAddMemberForm) => {
|
||||||
@@ -138,7 +139,7 @@ export const AddOrgMemberModal = ({
|
|||||||
const { data } = await addUsersMutateAsync({
|
const { data } = await addUsersMutateAsync({
|
||||||
organizationId: currentOrg?.id,
|
organizationId: currentOrg?.id,
|
||||||
inviteeEmails: emails.split(",").map((email) => email.trim()),
|
inviteeEmails: emails.split(",").map((email) => email.trim()),
|
||||||
organizationRoleSlug,
|
organizationRoleSlug: organizationRole.slug,
|
||||||
projects: selectedProjects.map(({ id }) => ({ id, projectRoleSlug: [projectRoleSlug] }))
|
projects: selectedProjects.map(({ id }) => ({ id, projectRoleSlug: [projectRoleSlug] }))
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -207,27 +208,22 @@ export const AddOrgMemberModal = ({
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="organizationRoleSlug"
|
name="organizationRole"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
tooltipText="Select which organization role you want to assign to the user."
|
tooltipText="Select which organization role you want to assign to the user."
|
||||||
label="Assign organization role"
|
label="Assign organization role"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<div>
|
<FilterableSelect
|
||||||
<Select
|
placeholder="Select role..."
|
||||||
className="w-full"
|
options={organizationRoles}
|
||||||
{...field}
|
getOptionValue={(option) => option.slug}
|
||||||
onValueChange={(val) => field.onChange(val)}
|
getOptionLabel={(option) => option.name}
|
||||||
>
|
value={value}
|
||||||
{organizationRoles?.map((role) => (
|
onChange={onChange}
|
||||||
<SelectItem key={role.id} value={role.slug}>
|
/>
|
||||||
{role.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ export const UserPage = withPermission(
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handlePopUpOpen("orgMembership", {
|
handlePopUpOpen("orgMembership", {
|
||||||
membershipId: membership.id,
|
membershipId: membership.id,
|
||||||
role: membership.role
|
role: membership.role,
|
||||||
|
roleId: membership.roleId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={!isAllowed}
|
disabled={!isAllowed}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
|||||||
handlePopUpOpen("orgMembership", {
|
handlePopUpOpen("orgMembership", {
|
||||||
membershipId: membership.id,
|
membershipId: membership.id,
|
||||||
role: membership.role,
|
role: membership.role,
|
||||||
|
roleId: membership.roleId,
|
||||||
metadata: membership.metadata
|
metadata: membership.metadata
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { SingleValue } from "react-select";
|
||||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -8,21 +9,21 @@ import { z } from "zod";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent
|
||||||
Select,
|
|
||||||
SelectItem
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
|
import { findOrgMembershipRole, isCustomOrgRole } from "@app/helpers/roles";
|
||||||
import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api";
|
import { useGetOrgRoles, useUpdateOrgMembership } from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
role: z.string(),
|
role: z.object({ name: z.string(), slug: z.string() }),
|
||||||
metadata: z
|
metadata: z
|
||||||
.object({
|
.object({
|
||||||
key: z.string().trim().min(1),
|
key: z.string().trim().min(1),
|
||||||
@@ -45,7 +46,7 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
|
|
||||||
const { data: roles } = useGetOrgRoles(orgId);
|
const { data: roles = [] } = useGetOrgRoles(orgId);
|
||||||
|
|
||||||
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
|
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
const popUpData = popUp?.orgMembership?.data as {
|
const popUpData = popUp?.orgMembership?.data as {
|
||||||
membershipId: string;
|
membershipId: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
roleId?: string;
|
||||||
metadata: { key: string; value: string }[];
|
metadata: { key: string; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,12 +76,12 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
|
|
||||||
if (popUpData) {
|
if (popUpData) {
|
||||||
reset({
|
reset({
|
||||||
role: popUpData.role,
|
role: findOrgMembershipRole(roles, popUpData.roleId ?? popUpData.role),
|
||||||
metadata: popUpData.metadata
|
metadata: popUpData.metadata
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
role: roles[0].slug
|
role: findOrgMembershipRole(roles, currentOrg!.defaultMembershipRole!)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [popUp?.orgMembership?.data, roles]);
|
}, [popUp?.orgMembership?.data, roles]);
|
||||||
@@ -91,7 +93,7 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
await updateOrgMembership({
|
await updateOrgMembership({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
membershipId: popUpData.membershipId,
|
membershipId: popUpData.membershipId,
|
||||||
role,
|
role: role.slug,
|
||||||
metadata
|
metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,23 +125,26 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title="Update Membership">
|
<ModalContent bodyClassName="overflow-visible" title="Update Membership">
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="role"
|
name="role"
|
||||||
defaultValue=""
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Update Organization Role"
|
label="Update Organization Role"
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
>
|
>
|
||||||
<Select
|
<FilterableSelect
|
||||||
defaultValue={field.value}
|
placeholder="Select role..."
|
||||||
{...field}
|
options={roles}
|
||||||
onValueChange={(e) => {
|
onChange={(newValue) => {
|
||||||
const isCustomRole = !["admin", "member", "no-access"].includes(e);
|
const role = newValue as SingleValue<(typeof roles)[number]>;
|
||||||
|
|
||||||
|
if (!role) return;
|
||||||
|
|
||||||
|
const isCustomRole = isCustomOrgRole(role.slug);
|
||||||
|
|
||||||
if (isCustomRole && subscription && !subscription?.rbac) {
|
if (isCustomRole && subscription && !subscription?.rbac) {
|
||||||
handlePopUpOpen("upgradePlan", {
|
handlePopUpOpen("upgradePlan", {
|
||||||
@@ -149,16 +154,12 @@ export const UserOrgMembershipModal = ({ popUp, handlePopUpOpen, handlePopUpTogg
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(e);
|
onChange(role);
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
value={value}
|
||||||
>
|
getOptionValue={(option) => option.slug}
|
||||||
{(roles || []).map(({ name, slug }) => (
|
getOptionLabel={(option) => option.name}
|
||||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
/>
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+121
-25
@@ -1,6 +1,27 @@
|
|||||||
import { faFolder } from "@fortawesome/free-solid-svg-icons";
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faSearch,
|
||||||
|
faUser
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { EmptyState, Table, TableContainer, TBody, Th, THead, Tr } from "@app/components/v2";
|
import {
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { OrgUser } from "@app/hooks/api/types";
|
import { OrgUser } from "@app/hooks/api/types";
|
||||||
import { useListUserGroupMemberships } from "@app/hooks/api/users/queries";
|
import { useListUserGroupMemberships } from "@app/hooks/api/users/queries";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -12,31 +33,106 @@ type Props = {
|
|||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["removeUserFromGroup"]>, data?: {}) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum UserGroupsOrderBy {
|
||||||
|
Name = "name"
|
||||||
|
}
|
||||||
|
|
||||||
export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => {
|
||||||
const { data: groups, isLoading } = useListUserGroupMemberships(orgMembership.user.username);
|
const { data: groupMemberships = [], isLoading } = useListUserGroupMemberships(
|
||||||
|
orgMembership.user.username
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
setPage,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
offset,
|
||||||
|
orderDirection,
|
||||||
|
toggleOrderDirection
|
||||||
|
} = usePagination(UserGroupsOrderBy.Name, { initPerPage: 10 });
|
||||||
|
|
||||||
|
const filteredGroupMemberships = useMemo(
|
||||||
|
() =>
|
||||||
|
groupMemberships
|
||||||
|
.filter((group) => group.name.toLowerCase().includes(search.trim().toLowerCase()))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const [membershipOne, membershipTwo] =
|
||||||
|
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||||
|
|
||||||
|
return membershipOne.name.toLowerCase().localeCompare(membershipTwo.name.toLowerCase());
|
||||||
|
}),
|
||||||
|
[groupMemberships, orderDirection, search]
|
||||||
|
);
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount: filteredGroupMemberships.length,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<div>
|
||||||
<Table>
|
<Input
|
||||||
<THead>
|
value={search}
|
||||||
<Tr>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<Th>Name</Th>
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
<Th className="w-5" />
|
placeholder="Search groups..."
|
||||||
</Tr>
|
/>
|
||||||
</THead>
|
<TableContainer className="mt-4">
|
||||||
<TBody>
|
<Table>
|
||||||
{groups?.map((group) => (
|
<THead>
|
||||||
<UserGroupsRow
|
<Tr>
|
||||||
key={`user-group-${group.id}`}
|
<Th className="w-full">
|
||||||
group={group}
|
<div className="flex items-center">
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
Name
|
||||||
/>
|
<IconButton
|
||||||
))}
|
variant="plain"
|
||||||
</TBody>
|
className="ml-2"
|
||||||
</Table>
|
ariaLabel="sort"
|
||||||
{!isLoading && !groups?.length && (
|
onClick={toggleOrderDirection}
|
||||||
<EmptyState title="This user has not been assigned to any groups" icon={faFolder} />
|
>
|
||||||
)}
|
<FontAwesomeIcon
|
||||||
</TableContainer>
|
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{filteredGroupMemberships.slice(offset, perPage * page).map((group) => (
|
||||||
|
<UserGroupsRow
|
||||||
|
key={`user-group-${group.id}`}
|
||||||
|
group={group}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{Boolean(filteredGroupMemberships.length) && (
|
||||||
|
<Pagination
|
||||||
|
count={filteredGroupMemberships.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={setPerPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && !filteredGroupMemberships?.length && (
|
||||||
|
<EmptyState
|
||||||
|
title={
|
||||||
|
groupMemberships.length
|
||||||
|
? "No groups match search..."
|
||||||
|
: "This user has not been assigned to any groups"
|
||||||
|
}
|
||||||
|
icon={groupMemberships.length ? faSearch : faUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+83
-92
@@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddGroupToWorkspace,
|
useAddGroupToWorkspace,
|
||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
id: z.string(),
|
group: z.object({ id: z.string(), name: z.string() }),
|
||||||
role: z.string()
|
role: z.object({ slug: z.string(), name: z.string() })
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
@@ -27,7 +27,9 @@ type Props = {
|
|||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
// TODO: update backend to support adding multiple roles at once
|
||||||
|
|
||||||
|
const Content = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
@@ -59,12 +61,12 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = async ({ id, role }: FormData) => {
|
const onFormSubmit = async ({ group, role }: FormData) => {
|
||||||
try {
|
try {
|
||||||
await addGroupToWorkspaceMutateAsync({
|
await addGroupToWorkspaceMutateAsync({
|
||||||
projectId: currentWorkspace?.id || "",
|
projectId: currentWorkspace?.id || "",
|
||||||
groupId: id,
|
groupId: group.id,
|
||||||
role: role || undefined
|
role: role.slug || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
@@ -82,95 +84,84 @@ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return filteredGroupMembershipOrgs.length ? (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="group"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
getOptionValue={(option) => option.id}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
options={filteredGroupMembershipOrgs}
|
||||||
|
placeholder="Select group..."
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="role"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Role"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
options={roles}
|
||||||
|
placeholder="Select role..."
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-6 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{popUp?.group?.data ? "Update" : "Add"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("group", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="text-sm">
|
||||||
|
All groups in your organization have already been added to this project.
|
||||||
|
</div>
|
||||||
|
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
||||||
|
<Button variant="outline_bg">Create a new group</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.group?.isOpen}
|
isOpen={popUp?.group?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => handlePopUpToggle("group", isOpen)}
|
||||||
handlePopUpToggle("group", isOpen);
|
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ModalContent title="Add Group to Project">
|
<ModalContent bodyClassName="overflow-visible" title="Add Group to Project">
|
||||||
{filteredGroupMembershipOrgs.length ? (
|
<Content popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="id"
|
|
||||||
defaultValue={filteredGroupMembershipOrgs?.[0]?.id}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full border border-mineshaft-600"
|
|
||||||
placeholder="Select group..."
|
|
||||||
>
|
|
||||||
{filteredGroupMembershipOrgs.map(({ name, id }) => (
|
|
||||||
<SelectItem value={id} key={`org-group-${id}`}>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="role"
|
|
||||||
defaultValue=""
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Role"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Select role..."
|
|
||||||
>
|
|
||||||
{(roles || []).map(({ name, slug }) => (
|
|
||||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="mt-6 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{popUp?.group?.data ? "Update" : "Create"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpToggle("group", false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<div className="text-sm">
|
|
||||||
All groups in your organization have already been added to this project.
|
|
||||||
</div>
|
|
||||||
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
|
||||||
<Button variant="outline_bg">Create a new group</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
+16
-84
@@ -2,24 +2,11 @@ import { useMemo } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { faCheckCircle, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
FilterableSelect,
|
|
||||||
FormControl,
|
|
||||||
Modal,
|
|
||||||
ModalContent
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddUsersToOrg,
|
useAddUsersToOrg,
|
||||||
@@ -33,7 +20,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
|
|
||||||
const addMemberFormSchema = z.object({
|
const addMemberFormSchema = z.object({
|
||||||
orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1),
|
orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1),
|
||||||
projectRoleSlugs: z.array(z.string().trim().min(1)).min(1)
|
projectRoleSlugs: z.array(z.object({ slug: z.string().trim(), name: z.string().trim() })).min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||||
@@ -64,7 +51,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
formState: { isSubmitting, errors }
|
formState: { isSubmitting, errors }
|
||||||
} = useForm<TAddMemberForm>({
|
} = useForm<TAddMemberForm>({
|
||||||
resolver: zodResolver(addMemberFormSchema),
|
resolver: zodResolver(addMemberFormSchema),
|
||||||
defaultValues: { orgMemberships: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
|
defaultValues: { orgMemberships: [], projectRoleSlugs: [] }
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: addMembersToProject } = useAddUsersToOrg();
|
const { mutateAsync: addMembersToProject } = useAddUsersToOrg();
|
||||||
@@ -94,7 +81,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
{
|
{
|
||||||
slug: currentWorkspace.slug,
|
slug: currentWorkspace.slug,
|
||||||
id: currentWorkspace.id,
|
id: currentWorkspace.id,
|
||||||
projectRoleSlug: projectRoleSlugs
|
projectRoleSlug: projectRoleSlugs.map((role) => role.slug)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -172,78 +159,23 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="projectRoleSlugs"
|
name="projectRoleSlugs"
|
||||||
render={({ field }) => (
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
className="w-full"
|
className="w-full"
|
||||||
label="Select roles"
|
label="Select roles"
|
||||||
tooltipText="Select the roles that you wish to assign to the users"
|
tooltipText="Select the roles that you wish to assign to the users"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
>
|
>
|
||||||
<DropdownMenu>
|
<FilterableSelect
|
||||||
<DropdownMenuTrigger asChild>
|
options={roles}
|
||||||
{roles && roles.length > 0 ? (
|
placeholder="Select roles..."
|
||||||
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
value={value}
|
||||||
{/* eslint-disable-next-line no-nested-ternary */}
|
onChange={onChange}
|
||||||
{selectedRoleSlugs.length === 1
|
isMulti
|
||||||
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
|
getOptionValue={(option) => option.slug}
|
||||||
: selectedRoleSlugs.length === 0
|
getOptionLabel={(option) => option.name}
|
||||||
? "Select at least one role"
|
/>
|
||||||
: `${selectedRoleSlugs.length} roles selected`}
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faChevronDown}
|
|
||||||
className={twMerge("ml-2 text-xs")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
|
||||||
No roles found
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
align="start"
|
|
||||||
className="thin-scrollbar z-[100] max-h-80"
|
|
||||||
>
|
|
||||||
{roles && roles.length > 0 ? (
|
|
||||||
roles.map((role) => {
|
|
||||||
const isSelected = selectedRoleSlugs.includes(role.slug);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(event) => roles.length > 1 && event.preventDefault()}
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedRoleSlugs.includes(String(role.slug))) {
|
|
||||||
field.onChange(
|
|
||||||
selectedRoleSlugs.filter(
|
|
||||||
(roleSlug: string) => roleSlug !== String(role.slug)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
field.onChange([...selectedRoleSlugs, role.slug]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
key={`role-slug-${role.slug}`}
|
|
||||||
icon={
|
|
||||||
isSelected ? (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faCheckCircle}
|
|
||||||
className="pr-0.5 text-primary"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="pl-[1.01rem]" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
iconPos="left"
|
|
||||||
className="w-[28.4rem] text-sm"
|
|
||||||
>
|
|
||||||
{role.name}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+1
-2
@@ -19,7 +19,6 @@ import { SecretTagsTable } from "./SecretTagsTable";
|
|||||||
type DeleteModalData = { name: string; id: string };
|
type DeleteModalData = { name: string; id: string };
|
||||||
|
|
||||||
export const SecretTagsSection = (): JSX.Element => {
|
export const SecretTagsSection = (): JSX.Element => {
|
||||||
|
|
||||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||||
"CreateSecretTag",
|
"CreateSecretTag",
|
||||||
"deleteTagConfirmation"
|
"deleteTagConfirmation"
|
||||||
@@ -65,7 +64,7 @@ export const SecretTagsSection = (): JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Create tag
|
Create Tag
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
|||||||
+126
-49
@@ -1,10 +1,20 @@
|
|||||||
import { faTags, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
import { useMemo } from "react";
|
||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faSearch,
|
||||||
|
faTag,
|
||||||
|
faTrashCan
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -15,7 +25,9 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
import { useGetWsTags } from "@app/hooks/api";
|
import { useGetWsTags } from "@app/hooks/api";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -31,59 +43,124 @@ type Props = {
|
|||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum TagsOrderBy {
|
||||||
|
Slug = "slug"
|
||||||
|
}
|
||||||
|
|
||||||
export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
export const SecretTagsTable = ({ handlePopUpOpen }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { data, isLoading } = useGetWsTags(currentWorkspace?.id ?? "");
|
const { data: tags = [], isLoading } = useGetWsTags(currentWorkspace?.id ?? "");
|
||||||
|
|
||||||
|
const {
|
||||||
|
search,
|
||||||
|
setSearch,
|
||||||
|
setPage,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
setPerPage,
|
||||||
|
offset,
|
||||||
|
orderDirection,
|
||||||
|
toggleOrderDirection
|
||||||
|
} = usePagination(TagsOrderBy.Slug, { initPerPage: 10 });
|
||||||
|
|
||||||
|
const filteredTags = useMemo(
|
||||||
|
() =>
|
||||||
|
tags
|
||||||
|
.filter((tag) => tag.slug.toLowerCase().includes(search.trim().toLowerCase()))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const [tagOne, tagTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
|
||||||
|
|
||||||
|
return tagOne.slug.toLowerCase().localeCompare(tagTwo.slug.toLowerCase());
|
||||||
|
}),
|
||||||
|
[tags, orderDirection, search]
|
||||||
|
);
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount: filteredTags.length,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer className="mt-4">
|
<div>
|
||||||
<Table>
|
<Input
|
||||||
<THead>
|
value={search}
|
||||||
<Tr>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<Th>Slug</Th>
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
<Th aria-label="button" />
|
placeholder="Search tags..."
|
||||||
</Tr>
|
/>
|
||||||
</THead>
|
<TableContainer className="mt-4">
|
||||||
<TBody>
|
<Table>
|
||||||
{isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
|
<THead>
|
||||||
{!isLoading &&
|
|
||||||
data &&
|
|
||||||
data.map(({ id, slug }) => (
|
|
||||||
<Tr key={id}>
|
|
||||||
<Td>{slug}</Td>
|
|
||||||
<Td className="flex items-center justify-end">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Delete}
|
|
||||||
a={ProjectPermissionSub.Tags}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
handlePopUpOpen("deleteTagConfirmation", {
|
|
||||||
name: slug,
|
|
||||||
id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
colorSchema="danger"
|
|
||||||
ariaLabel="update"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrashCan} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
{!isLoading && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={3}>
|
<Th className="w-full">
|
||||||
<EmptyState title="No secret tags found" icon={faTags} />
|
<div className="flex items-center">
|
||||||
</Td>
|
Slug
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className="ml-2"
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={toggleOrderDirection}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th aria-label="button" />
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
</THead>
|
||||||
</TBody>
|
<TBody>
|
||||||
</Table>
|
{isLoading && <TableSkeleton columns={3} innerKey="secret-tags" />}
|
||||||
</TableContainer>
|
{!isLoading &&
|
||||||
|
filteredTags.slice(offset, perPage * page).map(({ id, slug }) => (
|
||||||
|
<Tr key={id}>
|
||||||
|
<Td>{slug}</Td>
|
||||||
|
<Td className="flex items-center justify-end">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={ProjectPermissionSub.Tags}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("deleteTagConfirmation", {
|
||||||
|
name: slug,
|
||||||
|
id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
size="xs"
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="update"
|
||||||
|
variant="plain"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrashCan} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{Boolean(filteredTags.length) && (
|
||||||
|
<Pagination
|
||||||
|
count={filteredTags.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={setPage}
|
||||||
|
onChangePerPage={setPerPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isLoading && !filteredTags?.length && (
|
||||||
|
<EmptyState
|
||||||
|
title={tags.length ? "No tags match search..." : "No tags found for project"}
|
||||||
|
icon={tags.length ? faSearch : faTag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user