Fix trust center access tab permissions handling
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
TrustCenterAccess,
|
||||
TrustCenterDocumentAccessStatus,
|
||||
} from "@probo/coredata";
|
||||
import type { TrustCenterDocumentAccessStatus } from "@probo/coredata";
|
||||
import {
|
||||
getTrustCenterDocumentAccessInfo,
|
||||
type TrustCenterDocumentAccessInfo,
|
||||
@@ -31,9 +28,11 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { Suspense, useCallback, useEffect, useState } from "react";
|
||||
import { TrustCenterDocumentAccessList } from "./TrustCenterDocumentAccessList";
|
||||
import type { TrustCenterAccessGraph_accesses$data } from "/__generated__/core/TrustCenterAccessGraph_accesses.graphql";
|
||||
import type { NodeOf } from "/types";
|
||||
|
||||
interface TrustCenterAccessEditDialogProps {
|
||||
access: TrustCenterAccess;
|
||||
access: NodeOf<TrustCenterAccessGraph_accesses$data["accesses"]>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -76,7 +75,7 @@ export function TrustCenterAccessEditDialog(
|
||||
}
|
||||
|
||||
interface TrustCenterAccessEditFormProps {
|
||||
access: TrustCenterAccess;
|
||||
access: NodeOf<TrustCenterAccessGraph_accesses$data["accesses"]>;
|
||||
onSubmit: () => void;
|
||||
queryRef: PreloadedQuery<TrustCenterAccessGraphLoadDocumentAccessesQuery>;
|
||||
}
|
||||
|
||||
@@ -1,56 +1,71 @@
|
||||
import type { TrustCenterAccess } from "@probo/coredata";
|
||||
import { Button, IconCheckmark1, IconCrossLargeX, IconPencil, IconTrashCan, Td, Tr } from "@probo/ui";
|
||||
import {
|
||||
Button,
|
||||
IconCheckmark1,
|
||||
IconCrossLargeX,
|
||||
IconPencil,
|
||||
IconTrashCan,
|
||||
Td,
|
||||
Tr,
|
||||
} from "@probo/ui";
|
||||
import { formatDate } from "@probo/helpers";
|
||||
import { use, useCallback, useState } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { deleteTrustCenterAccessMutation } from "/hooks/graph/TrustCenterAccessGraph";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { TrustCenterAccessEditDialog } from "./TrustCenterAccessEditDialog";
|
||||
import type { TrustCenterAccessGraph_accesses$data } from "/__generated__/core/TrustCenterAccessGraph_accesses.graphql";
|
||||
import type { NodeOf } from "/types";
|
||||
|
||||
interface TrustCenterAccessItemProps {
|
||||
access: TrustCenterAccess
|
||||
access: NodeOf<TrustCenterAccessGraph_accesses$data["accesses"]>;
|
||||
connectionId?: string;
|
||||
dialogOpen: boolean,
|
||||
dialogOpen: boolean;
|
||||
}
|
||||
|
||||
export function TrustCenterAccessItem(props: TrustCenterAccessItemProps) {
|
||||
const { access, connectionId, dialogOpen: initialDialogOpen } = props;
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(initialDialogOpen)
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(initialDialogOpen);
|
||||
|
||||
const [deleteInvitation, isDeleting] = useMutationWithToasts(deleteTrustCenterAccessMutation, {
|
||||
successMessage: __("Access deleted successfully"),
|
||||
errorMessage: __("Failed to delete access"),
|
||||
});
|
||||
const [deleteInvitation, isDeleting] = useMutationWithToasts(
|
||||
deleteTrustCenterAccessMutation,
|
||||
{
|
||||
successMessage: __("Access deleted successfully"),
|
||||
errorMessage: __("Failed to delete access"),
|
||||
},
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(async (id: string) => {
|
||||
await deleteInvitation({
|
||||
variables: {
|
||||
input: { id },
|
||||
connections: connectionId ? [connectionId] : [],
|
||||
},
|
||||
});
|
||||
}, [deleteInvitation, connectionId]);
|
||||
const handleDelete = useCallback(
|
||||
async (id: string) => {
|
||||
await deleteInvitation({
|
||||
variables: {
|
||||
input: { id },
|
||||
connections: connectionId ? [connectionId] : [],
|
||||
},
|
||||
});
|
||||
},
|
||||
[deleteInvitation, connectionId],
|
||||
);
|
||||
|
||||
const isExpired = access.lastTokenExpiresAt ? new Date(access.lastTokenExpiresAt) < new Date() : false;
|
||||
const isExpired = access.lastTokenExpiresAt
|
||||
? new Date(access.lastTokenExpiresAt) < new Date()
|
||||
: false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
key={access.id}
|
||||
onClick={() => setDialogOpen(true)}
|
||||
onClick={() => access.canUpdate && setDialogOpen(true)}
|
||||
className="cursor-pointer hover:bg-bg-secondary transition-colors"
|
||||
>
|
||||
<Td className="font-medium">{access.name}</Td>
|
||||
<Td>{access.email}</Td>
|
||||
<Td>
|
||||
{formatDate(access.createdAt)}
|
||||
</Td>
|
||||
<Td>{formatDate(access.createdAt)}</Td>
|
||||
<Td className={isExpired ? "text-txt-danger" : ""}>
|
||||
{access.lastTokenExpiresAt ? formatDate(access.lastTokenExpiresAt) : "-"}
|
||||
{access.lastTokenExpiresAt
|
||||
? formatDate(access.lastTokenExpiresAt)
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-center">
|
||||
@@ -61,9 +76,7 @@ export function TrustCenterAccessItem(props: TrustCenterAccessItemProps) {
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
<Td className="text-center">
|
||||
{access.activeCount}
|
||||
</Td>
|
||||
<Td className="text-center">{access.activeCount}</Td>
|
||||
<Td className="text-center">
|
||||
{access.pendingRequestCount > 0 ? access.pendingRequestCount : ""}
|
||||
</Td>
|
||||
@@ -79,14 +92,14 @@ export function TrustCenterAccessItem(props: TrustCenterAccessItemProps) {
|
||||
className="flex gap-2 justify-end"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isAuthorized("TrustCenterAccess", "updateTrustCenterAccess") && (
|
||||
{access.canUpdate && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
icon={IconPencil}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("TrustCenterAccess", "deleteTrustCenterAccess") && (
|
||||
{access.canDelete && (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => handleDelete(access.id)}
|
||||
@@ -98,7 +111,12 @@ export function TrustCenterAccessItem(props: TrustCenterAccessItemProps) {
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
{dialogOpen && <TrustCenterAccessEditDialog access={access} onClose={() => setDialogOpen(false)} />}
|
||||
{access.canUpdate && dialogOpen && (
|
||||
<TrustCenterAccessEditDialog
|
||||
access={access}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useOutletContext } from "react-router";
|
||||
import { useState, useEffect, use, useMemo } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import z from "zod";
|
||||
import {
|
||||
useTrustCenterAccesses,
|
||||
@@ -25,77 +25,53 @@ import {
|
||||
} from "/hooks/graph/TrustCenterAccessGraph";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
import { TrustCenterAccessItem } from "./TrustCenterAccessItem";
|
||||
import type { TrustCenterAccess } from "@probo/coredata";
|
||||
|
||||
type ContextType = {
|
||||
organization: {
|
||||
id: string;
|
||||
trustCenter?: {
|
||||
id: string;
|
||||
};
|
||||
documents?: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
title: string;
|
||||
documentType: string;
|
||||
trustCenterVisibility: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
audits?: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
filename: string;
|
||||
trustCenterVisibility: string;
|
||||
framework: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
trustCenterFiles?: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
trustCenterVisibility: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
import type { TrustCenterGraphQuery$data } from "/__generated__/core/TrustCenterGraphQuery.graphql";
|
||||
import type { NodeOf } from "/types";
|
||||
import type { TrustCenterAccessGraph_accesses$data } from "/__generated__/core/TrustCenterAccessGraph_accesses.graphql";
|
||||
|
||||
export default function TrustCenterAccessTab() {
|
||||
const { __ } = useTranslate();
|
||||
const { organization } = useOutletContext<ContextType>();
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const { organization } = useOutletContext<TrustCenterGraphQuery$data>();
|
||||
const inviteSchema = z.object({
|
||||
name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")),
|
||||
email: z.string().min(1, __("Email is required")).email(__("Please enter a valid email address")),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, __("Name is required"))
|
||||
.min(2, __("Name must be at least 2 characters long")),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, __("Email is required"))
|
||||
.email(__("Please enter a valid email address")),
|
||||
});
|
||||
|
||||
const [createInvitation, isCreating] = useMutationWithToasts(createTrustCenterAccessMutation, {
|
||||
successMessage: __("Access created successfully"),
|
||||
errorMessage: __("Failed to create access"),
|
||||
});
|
||||
const [createInvitation, isCreating] = useMutationWithToasts(
|
||||
createTrustCenterAccessMutation,
|
||||
{
|
||||
successMessage: __("Access created successfully"),
|
||||
errorMessage: __("Failed to create access"),
|
||||
},
|
||||
);
|
||||
|
||||
const dialogRef = useDialogRef();
|
||||
const [editingAccess, setEditingAccess] = useState<TrustCenterAccess | null>(null);
|
||||
const [editingAccess, setEditingAccess] = useState<NodeOf<
|
||||
TrustCenterAccessGraph_accesses$data["accesses"]
|
||||
> | null>(null);
|
||||
const [pendingEditEmail, setPendingEditEmail] = useState<string | null>(null);
|
||||
|
||||
const inviteForm = useFormWithSchema(inviteSchema, {
|
||||
defaultValues: { name: "", email: "" },
|
||||
});
|
||||
|
||||
const { data: trustCenterData, loadMore, hasNext, isLoadingNext } = useTrustCenterAccesses(organization.trustCenter?.id || "");
|
||||
const {
|
||||
data: trustCenterData,
|
||||
loadMore,
|
||||
hasNext,
|
||||
isLoadingNext,
|
||||
} = useTrustCenterAccesses(organization.trustCenter?.id || "");
|
||||
|
||||
const accesses: TrustCenterAccess[] = useMemo(
|
||||
() => trustCenterData?.accesses?.edges.map((edge) => edge.node) ?? [], [trustCenterData?.accesses?.edges]
|
||||
const accesses = useMemo(
|
||||
() => trustCenterData?.accesses?.edges.map((edge) => edge.node) ?? [],
|
||||
[trustCenterData?.accesses?.edges],
|
||||
);
|
||||
|
||||
const handleInvite = inviteForm.handleSubmit(async (data) => {
|
||||
@@ -124,7 +100,9 @@ export default function TrustCenterAccessTab() {
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingEditEmail && accesses.length > 0) {
|
||||
const newAccess = accesses.find(access => access.email === pendingEditEmail);
|
||||
const newAccess = accesses.find(
|
||||
(access) => access.email === pendingEditEmail,
|
||||
);
|
||||
if (newAccess) {
|
||||
setPendingEditEmail(null);
|
||||
setEditingAccess(newAccess);
|
||||
@@ -144,19 +122,23 @@ export default function TrustCenterAccessTab() {
|
||||
<div>
|
||||
<h3 className="text-base font-medium">{__("External Access")}</h3>
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Manage who can access your trust center with time-limited tokens")}
|
||||
{__(
|
||||
"Manage who can access your trust center with time-limited tokens",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{organization.trustCenter?.id && (
|
||||
isAuthorized("TrustCenter", "createTrustCenterAccess") && (
|
||||
<Button icon={IconPlusLarge} onClick={() => {
|
||||
inviteForm.reset();
|
||||
dialogRef.current?.open();
|
||||
}}>
|
||||
{organization.trustCenter?.id &&
|
||||
organization.trustCenter.canCreateAccess && (
|
||||
<Button
|
||||
icon={IconPlusLarge}
|
||||
onClick={() => {
|
||||
inviteForm.reset();
|
||||
dialogRef.current?.open();
|
||||
}}
|
||||
>
|
||||
{__("Add Access")}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!organization.trustCenter?.id ? (
|
||||
@@ -221,15 +203,14 @@ export default function TrustCenterAccessTab() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
ref={dialogRef}
|
||||
title={__("Invite External Access")}
|
||||
>
|
||||
<Dialog ref={dialogRef} title={__("Invite External Access")}>
|
||||
<form onSubmit={handleInvite}>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div>
|
||||
<p className="text-txt-secondary text-sm mb-4">
|
||||
{__("Send a 30-day access token to an external person to view your trust center")}
|
||||
{__(
|
||||
"Send a 30-day access token to an external person to view your trust center",
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Field
|
||||
|
||||
Reference in New Issue
Block a user