mirror of
https://github.com/Infisical/infisical.git
synced 2026-05-02 03:02:03 -04:00
feat: ui changes for secret approval group
This commit is contained in:
@@ -9,12 +9,12 @@ export const useCreateSecretApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
|
||||
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
|
||||
mutationFn: async ({ environment, workspaceId, approvals, approverUserIds, secretPath, name }) => {
|
||||
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
||||
environment,
|
||||
workspaceId,
|
||||
approvals,
|
||||
approvers,
|
||||
approverUserIds,
|
||||
secretPath,
|
||||
name
|
||||
});
|
||||
@@ -30,10 +30,10 @@ export const useUpdateSecretApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
||||
mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
|
||||
mutationFn: async ({ id, approverUserIds, approvals, secretPath, name }) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||
approvals,
|
||||
approvers,
|
||||
approverUserIds,
|
||||
secretPath,
|
||||
name
|
||||
});
|
||||
|
||||
@@ -7,8 +7,8 @@ export type TSecretApprovalPolicy = {
|
||||
envId: string;
|
||||
environment: WorkspaceEnv;
|
||||
secretPath?: string;
|
||||
approvers: string[];
|
||||
approvals: number;
|
||||
userApprovers: { userId: string }[];
|
||||
};
|
||||
|
||||
export type TGetSecretApprovalPoliciesDTO = {
|
||||
@@ -26,14 +26,14 @@ export type TCreateSecretPolicyDTO = {
|
||||
name?: string;
|
||||
environment: string;
|
||||
secretPath?: string | null;
|
||||
approvers?: string[];
|
||||
approverUserIds?: string[];
|
||||
approvals?: number;
|
||||
};
|
||||
|
||||
export type TUpdateSecretPolicyDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
approvers?: string[];
|
||||
approverUserIds?: string[];
|
||||
secretPath?: string | null;
|
||||
approvals?: number;
|
||||
// for invalidating list
|
||||
|
||||
@@ -47,10 +47,14 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
|
||||
isReplicated?: boolean;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
committerId: string;
|
||||
committerUserId: string;
|
||||
reviewers: {
|
||||
member: string;
|
||||
userId: string;
|
||||
status: ApprovalStatus;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
}[];
|
||||
workspace: string;
|
||||
environment: string;
|
||||
@@ -58,8 +62,30 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
|
||||
secretPath: string;
|
||||
hasMerged: boolean;
|
||||
status: "open" | "close";
|
||||
policy: TSecretApprovalPolicy;
|
||||
statusChangeBy: string;
|
||||
policy: Omit<TSecretApprovalPolicy, "approvers"> & {
|
||||
approvers: {
|
||||
userId: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
}[];
|
||||
};
|
||||
statusChangedByUserId: string;
|
||||
statusChangedByUser?: {
|
||||
userId: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
};
|
||||
committerUser: {
|
||||
userId: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
};
|
||||
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
|
||||
commits: ({
|
||||
// if there is no secret means it was creation
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -145,13 +147,20 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<SecretPolicyForm
|
||||
workspaceId={workspaceId}
|
||||
<Modal
|
||||
isOpen={popUp.secretPolicyForm.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||
members={members}
|
||||
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
|
||||
/>
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||
>
|
||||
<ModalContent title={popUp.secretPolicyForm.data ? "Edit policy" : "Create policy"}>
|
||||
<SecretPolicyForm
|
||||
workspaceId={workspaceId}
|
||||
isOpen={popUp.secretPolicyForm.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||
members={members}
|
||||
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
deleteKey="remove"
|
||||
|
||||
@@ -51,7 +51,7 @@ export const SecretApprovalPolicyRow = ({
|
||||
{
|
||||
workspaceId,
|
||||
id: policy.id,
|
||||
approvers: selectedApprovers
|
||||
approverUserIds: selectedApprovers
|
||||
},
|
||||
{
|
||||
onSettled: () => {
|
||||
@@ -60,7 +60,7 @@ export const SecretApprovalPolicyRow = ({
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setSelectedApprovers(policy.approvers);
|
||||
setSelectedApprovers(policy.userApprovers.map(({ userId }) => userId));
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -73,7 +73,9 @@ export const SecretApprovalPolicyRow = ({
|
||||
>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={policy.approvers?.length ? `${policy.approvers.length} selected` : "None"}
|
||||
value={
|
||||
policy?.userApprovers.length ? `${policy.userApprovers.length} selected` : "None"
|
||||
}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -84,17 +86,17 @@ export const SecretApprovalPolicyRow = ({
|
||||
<DropdownMenuLabel>
|
||||
Select members that are allowed to approve changes
|
||||
</DropdownMenuLabel>
|
||||
{members?.map(({ id, user }) => {
|
||||
const isChecked = selectedApprovers.includes(id);
|
||||
{members?.map(({ user }) => {
|
||||
const isChecked = selectedApprovers.includes(user.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setSelectedApprovers((state) =>
|
||||
isChecked ? state.filter((el) => el !== id) : [...state, id]
|
||||
isChecked ? state.filter((el) => el !== user.id) : [...state, user.id]
|
||||
);
|
||||
}}
|
||||
key={`create-policy-members-${id}`}
|
||||
key={`create-policy-members-${user.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -15,8 +14,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
@@ -40,9 +37,9 @@ const formSchema = z
|
||||
name: z.string().optional(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
approvals: z.number().min(1),
|
||||
approvers: z.string().array().min(1)
|
||||
approverUserIds: z.string().array().min(1)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
});
|
||||
@@ -50,7 +47,6 @@ const formSchema = z
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
export const SecretPolicyForm = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
members = [],
|
||||
workspaceId,
|
||||
@@ -59,20 +55,22 @@ export const SecretPolicyForm = ({
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
|
||||
values: editValues
|
||||
? {
|
||||
...editValues,
|
||||
approverUserIds: editValues.userApprovers.map(({ userId }) => userId),
|
||||
environment: editValues.environment.slug
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const selectedEnvironment = watch("environment");
|
||||
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({});
|
||||
}, [isOpen]);
|
||||
|
||||
const isEditMode = Boolean(editValues);
|
||||
|
||||
@@ -131,8 +129,6 @@ export const SecretPolicyForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalContent title={isEditMode ? "Edit policy" : "Create policy"}>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -155,6 +151,7 @@ export const SecretPolicyForm = ({
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
isDisabled={isEditMode}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
@@ -186,7 +183,7 @@ export const SecretPolicyForm = ({
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="approvers"
|
||||
name="approverUserIds"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Approvers Required"
|
||||
@@ -208,17 +205,19 @@ export const SecretPolicyForm = ({
|
||||
<DropdownMenuLabel>
|
||||
Select members that are allowed to approve changes
|
||||
</DropdownMenuLabel>
|
||||
{members.map(({ id, user }) => {
|
||||
const isChecked = value?.includes(id);
|
||||
{members.map(({ user }) => {
|
||||
const isChecked = value?.includes(user.id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked ? value?.filter((el) => el !== id) : [...(value || []), id]
|
||||
isChecked
|
||||
? value?.filter((el) => el !== user.id)
|
||||
: [...(value || []), user.id]
|
||||
);
|
||||
}}
|
||||
key={`create-policy-members-${id}`}
|
||||
key={`create-policy-members-${user.id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
@@ -258,7 +257,6 @@ export const SecretPolicyForm = ({
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
useGetSecretApprovalRequests,
|
||||
useGetWorkspaceUsers
|
||||
} from "@app/hooks/api";
|
||||
import { ApprovalStatus, TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types";
|
||||
import { ApprovalStatus, TSecretApprovalRequest } from "@app/hooks/api/types";
|
||||
|
||||
import {
|
||||
generateCommitText,
|
||||
@@ -63,14 +63,9 @@ export const SecretApprovalRequest = () => {
|
||||
});
|
||||
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
|
||||
useGetSecretApprovalRequestCount({ workspaceId });
|
||||
const { user: presentUser } = useUser();
|
||||
const { user: userSession } = useUser();
|
||||
const { permission } = useProjectPermission();
|
||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
|
||||
(prev, curr) => ({ ...prev, [curr.id]: curr }),
|
||||
{}
|
||||
);
|
||||
const myMembershipId = members?.find(({ user }) => user.id === presentUser?.id)?.id;
|
||||
const isSecretApprovalScreen = Boolean(selectedApproval);
|
||||
|
||||
const handleGoBackSecretRequestDetail = () => {
|
||||
@@ -93,10 +88,8 @@ export const SecretApprovalRequest = () => {
|
||||
>
|
||||
<SecretApprovalRequestChanges
|
||||
workspaceId={workspaceId}
|
||||
members={membersGroupById}
|
||||
approvalRequestId={selectedApproval?.id || ""}
|
||||
onGoBack={handleGoBackSecretRequestDetail}
|
||||
committer={membersGroupById?.[selectedApproval?.committerId || ""]}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
@@ -182,10 +175,12 @@ export const SecretApprovalRequest = () => {
|
||||
{members?.map(({ user, id }) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setCommitterFilter((state) => (state === id ? undefined : id))
|
||||
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
|
||||
}
|
||||
key={`request-filter-member-${id}`}
|
||||
icon={committerFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
icon={
|
||||
committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />
|
||||
}
|
||||
iconPos="right"
|
||||
>
|
||||
{user.username}
|
||||
@@ -208,19 +203,16 @@ export const SecretApprovalRequest = () => {
|
||||
const {
|
||||
id: reqId,
|
||||
commits,
|
||||
committerId,
|
||||
createdAt,
|
||||
policy,
|
||||
reviewers,
|
||||
status,
|
||||
committerUser,
|
||||
isReplicated: isReplication
|
||||
} = secretApproval;
|
||||
const isApprover = policy?.approvers?.indexOf(myMembershipId || "") !== -1;
|
||||
const isReviewed =
|
||||
reviewers.findIndex(
|
||||
({ member, status: reviewStatus }) =>
|
||||
member === myMembershipId && reviewStatus === ApprovalStatus.APPROVED
|
||||
) !== -1;
|
||||
const isReviewed = reviewers.some(
|
||||
({ status: reviewStatus, userId }) =>
|
||||
userId === userSession.id && reviewStatus === ApprovalStatus.APPROVED
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={reqId}
|
||||
@@ -239,11 +231,9 @@ export const SecretApprovalRequest = () => {
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
|
||||
{membersGroupById?.[committerId]?.user?.firstName}{" "}
|
||||
{membersGroupById?.[committerId]?.user?.lastName} (
|
||||
{membersGroupById?.[committerId]?.user?.email})
|
||||
{isReplication && " via replication"}
|
||||
{isApprover && !isReviewed && status === "open" && " - Review required"}
|
||||
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
|
||||
{committerUser?.email}){isReplication && " via replication"}
|
||||
{!isReviewed && status === "open" && " - Review required"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ type Props = {
|
||||
status: "close" | "open";
|
||||
approvals: number;
|
||||
canApprove?: boolean;
|
||||
statusChangeByEmail: string;
|
||||
statusChangeByEmail?: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
useGetUserWsKey,
|
||||
useUpdateSecretApprovalReviewStatus
|
||||
} from "@app/hooks/api";
|
||||
import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types";
|
||||
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
|
||||
import { formatReservedPaths } from "@app/lib/fn/string";
|
||||
|
||||
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
|
||||
@@ -73,18 +73,14 @@ type Props = {
|
||||
workspaceId: string;
|
||||
approvalRequestId: string;
|
||||
onGoBack: () => void;
|
||||
committer?: TWorkspaceUser;
|
||||
members?: Record<string, TWorkspaceUser>;
|
||||
};
|
||||
|
||||
export const SecretApprovalRequestChanges = ({
|
||||
approvalRequestId,
|
||||
onGoBack,
|
||||
committer,
|
||||
workspaceId,
|
||||
members = {}
|
||||
workspaceId
|
||||
}: Props) => {
|
||||
const { user } = useUser();
|
||||
const { user: userSession } = useUser();
|
||||
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
|
||||
const {
|
||||
data: secretApprovalRequestDetails,
|
||||
@@ -105,22 +101,20 @@ export const SecretApprovalRequestChanges = ({
|
||||
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
|
||||
|
||||
// membership of present user
|
||||
const myMembership = Object.values(members).find(
|
||||
({ user: membershipUser }) => membershipUser.email === user.email
|
||||
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some(
|
||||
({ userId }) => userId === userSession.id
|
||||
);
|
||||
const myMembershipId = myMembership?.id || "";
|
||||
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.includes(myMembershipId);
|
||||
const reviewedMembers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
|
||||
Record<string, ApprovalStatus>
|
||||
>(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr.member]: curr.status
|
||||
[curr.userId]: curr.status
|
||||
}),
|
||||
{}
|
||||
);
|
||||
const hasApproved = reviewedMembers?.[myMembershipId] === ApprovalStatus.APPROVED;
|
||||
const hasRejected = reviewedMembers?.[myMembershipId] === ApprovalStatus.REJECTED;
|
||||
const hasApproved = reviewedUsers?.[userSession.id] === ApprovalStatus.APPROVED;
|
||||
const hasRejected = reviewedUsers?.[userSession.id] === ApprovalStatus.REJECTED;
|
||||
|
||||
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
|
||||
try {
|
||||
@@ -159,7 +153,7 @@ export const SecretApprovalRequestChanges = ({
|
||||
const isMergable =
|
||||
secretApprovalRequestDetails?.policy?.approvals <=
|
||||
secretApprovalRequestDetails?.policy?.approvers?.filter(
|
||||
(approverId) => reviewedMembers?.[approverId] === ApprovalStatus.APPROVED
|
||||
({ userId }) => reviewedUsers?.[userId] === ApprovalStatus.APPROVED
|
||||
).length;
|
||||
const hasMerged = secretApprovalRequestDetails?.hasMerged;
|
||||
|
||||
@@ -191,8 +185,9 @@ export const SecretApprovalRequestChanges = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-bunker-300">
|
||||
{committer?.user?.firstName}
|
||||
{committer?.user?.lastName} ({committer?.user?.email}) wants to change{" "}
|
||||
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
|
||||
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
|
||||
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
|
||||
{secretApprovalRequestDetails.commits.length} secret values in
|
||||
<span className="mx-1 rounded bg-primary-600/60 px-1 text-primary-300">
|
||||
{secretApprovalRequestDetails.environment}
|
||||
@@ -256,9 +251,7 @@ export const SecretApprovalRequestChanges = ({
|
||||
approvals={secretApprovalRequestDetails.policy.approvals || 0}
|
||||
status={secretApprovalRequestDetails.status}
|
||||
isMergable={isMergable}
|
||||
statusChangeByEmail={
|
||||
members[secretApprovalRequestDetails?.statusChangeBy || ""]?.user?.email || ""
|
||||
}
|
||||
statusChangeByEmail={secretApprovalRequestDetails.statusChangedByUser?.email}
|
||||
workspaceId={workspaceId}
|
||||
/>
|
||||
</div>
|
||||
@@ -266,17 +259,19 @@ export const SecretApprovalRequestChanges = ({
|
||||
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
|
||||
<div className="text-sm text-bunker-300">Reviewers</div>
|
||||
<div className="mt-2 flex flex-col space-y-2 text-sm">
|
||||
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApproverId) => {
|
||||
const userDetails = members?.[requiredApproverId]?.user;
|
||||
const status = reviewedMembers?.[requiredApproverId];
|
||||
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
|
||||
const status = reviewedUsers?.[requiredApprover.userId];
|
||||
return (
|
||||
<div
|
||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||
key={`required-approver-${requiredApproverId}`}
|
||||
key={`required-approver-${requiredApprover.userId}`}
|
||||
>
|
||||
<div className="flex-grow text-sm">
|
||||
<Tooltip content={`${userDetails.firstName} ${userDetails.lastName}`}>
|
||||
<span>{userDetails?.email} </span>
|
||||
<Tooltip
|
||||
content={`${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""
|
||||
}`}
|
||||
>
|
||||
<span>{requiredApprover?.email} </span>
|
||||
</Tooltip>
|
||||
<span className="text-red">*</span>
|
||||
</div>
|
||||
@@ -290,19 +285,21 @@ export const SecretApprovalRequestChanges = ({
|
||||
})}
|
||||
{secretApprovalRequestDetails?.reviewers
|
||||
.filter(
|
||||
({ member }) => !secretApprovalRequestDetails?.policy?.approvers?.includes(member)
|
||||
(reviewer) =>
|
||||
!secretApprovalRequestDetails?.policy?.approvers?.some(
|
||||
({ userId }) => userId === reviewer.userId
|
||||
)
|
||||
)
|
||||
.map((reviewer) => {
|
||||
const userDetails = members?.[reviewer.member]?.user;
|
||||
const status = reviewedMembers?.[reviewer.status];
|
||||
const status = reviewedUsers?.[reviewer.userId];
|
||||
return (
|
||||
<div
|
||||
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
|
||||
key={`required-approver-${reviewer.member}`}
|
||||
key={`required-approver-${reviewer.userId}`}
|
||||
>
|
||||
<div className="flex-grow text-sm">
|
||||
<Tooltip content={`${userDetails.firstName} ${userDetails.lastName}`}>
|
||||
<span>{userDetails?.email} </span>
|
||||
<Tooltip content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}>
|
||||
<span>{reviewer?.email} </span>
|
||||
</Tooltip>
|
||||
<span className="text-red">*</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user