import { Controller, FormProvider, useForm } from "react-hook-form"; import { faCaretDown, faChevronLeft, faClock, faPlus, faSave } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { format, formatDistance } from "date-fns"; import ms from "ms"; import { twMerge } from "tailwind-merge"; import { z } from "zod"; import { TtlFormLabel } from "@app/components/features"; import { createNotification } from "@app/components/notifications"; import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, FormControl, FormLabel, Input, Popover, PopoverContent, PopoverTrigger, Tag, Tooltip } from "@app/components/v2"; import { ProjectPermissionMemberActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; import { useCreateProjectUserAdditionalPrivilege, useGetProjectUserPrivilegeDetails, useUpdateProjectUserAdditionalPrivilege } from "@app/hooks/api"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/projectUserAdditionalPrivilege/types"; import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies"; import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState"; import { formRolePermission2API, isConditionalSubjects, PROJECT_PERMISSION_OBJECT, projectRoleFormSchema, rolePermission2Form } from "@app/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils"; import { renderConditionalComponents } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection"; type Props = { privilegeId?: string; projectMembershipId: string; onGoBack: () => void; isDisabled?: boolean; }; export const formSchema = z.object({ slug: z.string().optional(), temporaryAccess: z .discriminatedUnion("isTemporary", [ z.object({ isTemporary: z.literal(true), temporaryRange: z.string().min(1), temporaryAccessStartTime: z.string().datetime(), temporaryAccessEndTime: z.string().datetime().nullable().optional() }), z.object({ isTemporary: z.literal(false) }) ]) .default({ isTemporary: false }), permissions: projectRoleFormSchema.shape.permissions }); type TFormSchema = z.infer; export const MembershipProjectAdditionalPrivilegeModifySection = ({ privilegeId, onGoBack, projectMembershipId, isDisabled }: Props) => { const isCreate = !privilegeId; const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; const { data: privilegeDetails, isPending } = useGetProjectUserPrivilegeDetails( privilegeId || "" ); const { permission } = useProjectPermission(); const isMemberEditDisabled = permission.cannot( ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member ); const form = useForm({ values: privilegeDetails ? { ...privilegeDetails, permissions: rolePermission2Form(privilegeDetails.permissions), temporaryAccess: privilegeDetails.isTemporary ? { isTemporary: true, temporaryRange: privilegeDetails.temporaryRange || "", temporaryAccessEndTime: privilegeDetails.temporaryAccessEndTime || "", temporaryAccessStartTime: privilegeDetails.temporaryAccessStartTime || "" } : { isTemporary: privilegeDetails.isTemporary } } : undefined, resolver: zodResolver(formSchema) }); const { handleSubmit, formState: { isDirty, isSubmitting } } = form; const { mutateAsync: updateUserProjectAdditionalPrivilege } = useUpdateProjectUserAdditionalPrivilege(); const { mutateAsync: createUserProjectAdditionalPrivilege } = useCreateProjectUserAdditionalPrivilege(); const onSubmit = async (el: TFormSchema) => { const accessType = !el.temporaryAccess.isTemporary ? { isTemporary: false as const } : { isTemporary: true as const, temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, temporaryRange: el.temporaryAccess.temporaryRange, temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime }; try { if (isCreate) { await createUserProjectAdditionalPrivilege({ permissions: formRolePermission2API(el.permissions), projectMembershipId, slug: el.slug || undefined, type: accessType }); createNotification({ type: "success", text: "Successfully created privilege" }); } else { if (!projectId || !privilegeDetails?.id) return; await updateUserProjectAdditionalPrivilege({ privilegeId: privilegeDetails.id, permissions: formRolePermission2API(el.permissions), projectMembershipId, slug: el.slug || undefined, type: accessType }); createNotification({ type: "success", text: "Successfully updated privilege" }); } onGoBack(); } catch (err) { console.log(err); createNotification({ type: "error", text: "Failed to update privilege" }); } }; const onNewPolicy = (selectedSubject: ProjectPermissionSub) => { const rootPolicyValue = form.getValues(`permissions.${selectedSubject}`); if (rootPolicyValue && isConditionalSubjects(selectedSubject)) { form.setValue( `permissions.${selectedSubject}`, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error akhilmhdh: this is because of ts collision with both [...rootPolicyValue, {}], { shouldDirty: true, shouldTouch: true } ); } else { form.setValue( `permissions.${selectedSubject}`, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-error akhilmhdh: this is because of ts collision with both [{}], { shouldDirty: true, shouldTouch: true } ); } }; const privilegeTemporaryAccess = form.watch("temporaryAccess"); const isTemporary = privilegeTemporaryAccess?.isTemporary; const isExpired = privilegeTemporaryAccess?.isTemporary && new Date() > new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""); let text = "Permanent"; let toolTipText = "Non-Expiring Access"; if (isTemporary) { if (isExpired) { text = "Access Expired"; toolTipText = "Timed Access Expired"; } else { text = formatDistance( new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""), new Date() ); toolTipText = `Until ${format( new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""), "yyyy-MM-dd hh:mm:ss aaa" )}`; } } return (
{isDirty && ( )}
{Object.keys(PROJECT_PERMISSION_OBJECT) .sort((a, b) => PROJECT_PERMISSION_OBJECT[a as keyof typeof PROJECT_PERMISSION_OBJECT].title .toLowerCase() .localeCompare( PROJECT_PERMISSION_OBJECT[ b as keyof typeof PROJECT_PERMISSION_OBJECT ].title.toLowerCase() ) ) .map((subject) => ( onNewPolicy(subject as ProjectPermissionSub)} > {PROJECT_PERMISSION_OBJECT[subject as ProjectPermissionSub].title} ))}
Overview

Additional privileges take precedence over roles when permissions conflict

( )} />
Configure Timed Access
{isExpired && Expired} ( } isError={Boolean(error?.message)} errorText={error?.message} > )} />
{isTemporary && ( )}
Policies
{(isCreate || !isPending) && } {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( {renderConditionalComponents(subject, isDisabled)} ))}
); };