diff --git a/apps/console/src/coredata/TrustCenterAccess.ts b/apps/console/src/coredata/TrustCenterAccess.ts new file mode 100644 index 000000000..24e16e699 --- /dev/null +++ b/apps/console/src/coredata/TrustCenterAccess.ts @@ -0,0 +1,14 @@ +import type { TrustCenterDocumentAccess } from "./TrustCenterDocumentAccess"; + +export interface TrustCenterAccess { + id: string; + email: string; + name: string; + active: boolean; + hasAcceptedNonDisclosureAgreement: boolean; + createdAt: string; + lastTokenExpiresAt: string | null; + pendingRequestCount: number; + activeCount: number; + documentAccesses?: TrustCenterDocumentAccess[]; +}; diff --git a/apps/console/src/coredata/TrustCenterDocumentAccess.ts b/apps/console/src/coredata/TrustCenterDocumentAccess.ts new file mode 100644 index 000000000..d12c25be5 --- /dev/null +++ b/apps/console/src/coredata/TrustCenterDocumentAccess.ts @@ -0,0 +1,25 @@ +export type TrustCenterDocumentAccess = { + active: boolean; + status: string; + requested: boolean; + document?: { + id: string; + title: string; + documentType: string; + } | null; + report?: { + id: string; + filename: string; + audit?: { + id: string; + framework: { + name: string; + }; + } | null; + } | null; + trustCenterFile?: { + id: string; + name: string; + category: string; + } | null; +}; diff --git a/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab.tsx b/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab.tsx deleted file mode 100644 index 0f53c777d..000000000 --- a/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab.tsx +++ /dev/null @@ -1,706 +0,0 @@ -import { - Badge, - Button, - Checkbox, - Dialog, - DialogContent, - DialogFooter, - Field, - Spinner, - Table, - Tbody, - Td, - Th, - Thead, - Tr, - useDialogRef, - IconTrashCan, - IconPencil, - IconCheckmark1, - IconCrossLargeX, - IconChevronDown, - IconPlusLarge, -} from "@probo/ui"; -import { useTranslate } from "@probo/i18n"; -import { formatDate } from "@probo/helpers"; -import { useOutletContext } from "react-router"; -import { useState, useCallback, useEffect, useRef, use, useMemo } from "react"; -import { useQueryLoader, usePreloadedQuery, type PreloadedQuery } from 'react-relay'; -import z from "zod"; -import { - useTrustCenterAccesses, - createTrustCenterAccessMutation, - updateTrustCenterAccessMutation, - deleteTrustCenterAccessMutation, - loadTrustCenterAccessDocumentAccessesQuery -} from "/hooks/graph/TrustCenterAccessGraph"; -import { useFormWithSchema } from "/hooks/useFormWithSchema"; -import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; -import { PermissionsContext } from "/providers/PermissionsContext"; -import type { TrustCenterAccessGraphLoadDocumentAccessesQuery } from "/hooks/graph/__generated__/TrustCenterAccessGraphLoadDocumentAccessesQuery.graphql"; - -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; - }; - }>; - }; - }; -}; - -type DocumentAccessInfo = { - active: boolean; - status: string; - requested: boolean; - document?: { - id: string; - title: string; - documentType: string; - } | null; - report?: { - id: string; - filename: string; - audit?: { - id: string; - framework: { - name: string; - }; - } | null; - } | null; - trustCenterFile?: { - id: string; - name: string; - category: string; - } | null; -}; - -function DocumentAccessesLoader({ - queryReference, - onDataLoaded -}: { - queryReference: PreloadedQuery; - onDataLoaded: (documentAccesses: DocumentAccessInfo[]) => void; -}) { - const data = usePreloadedQuery(loadTrustCenterAccessDocumentAccessesQuery, queryReference); - - useEffect(() => { - if (data && typeof data === 'object' && 'node' in data) { - const node = data.node; - if (node?.availableDocumentAccesses?.edges) { - const documentAccesses: DocumentAccessInfo[] = node.availableDocumentAccesses.edges.map((edge) => edge.node); - onDataLoaded(documentAccesses); - } - } - }, [data, onDataLoaded]); - - return null; -} - -export default function TrustCenterAccessTab() { - const { __ } = useTranslate(); - const { organization } = useOutletContext(); - const { isAuthorized } = use(PermissionsContext); - 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")), - }); - - const editSchema = z.object({ - name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")), - active: z.boolean(), - }); - - const [createInvitation, isCreating] = useMutationWithToasts(createTrustCenterAccessMutation, { - successMessage: __("Access created successfully"), - errorMessage: __("Failed to create access"), - }); - const [updateInvitation, isUpdating] = useMutationWithToasts(updateTrustCenterAccessMutation, { - successMessage: __("Access updated successfully"), - errorMessage: __("Failed to update access"), - }); - const [deleteInvitation, isDeleting] = useMutationWithToasts(deleteTrustCenterAccessMutation, { - successMessage: __("Access deleted successfully"), - errorMessage: __("Failed to delete access"), - }); - - const dialogRef = useDialogRef(); - const editDialogRef = useDialogRef(); - const [editingAccess, setEditingAccess] = useState(null); - const [editingDocumentAccesses, setEditingDocumentAccesses] = useState([]); - const [selectedDocumentAccesses, setSelectedDocumentAccesses] = useState>(new Set()); - const [pendingEditEmail, setPendingEditEmail] = useState(null); - const [documentAccessesQueryReference, loadDocumentAccessesQuery] = useQueryLoader(loadTrustCenterAccessDocumentAccessesQuery); - const loadedAccessIdRef = useRef(null); - const [isLoadingDocumentAccesses, setIsLoadingDocumentAccesses] = useState(false); - - useEffect(() => { - if (editingAccess?.id && loadedAccessIdRef.current !== editingAccess.id) { - loadedAccessIdRef.current = editingAccess.id; - setIsLoadingDocumentAccesses(true); - loadDocumentAccessesQuery({ accessId: editingAccess.id }, { fetchPolicy: 'network-only' }); - } - }, [editingAccess?.id, loadDocumentAccessesQuery]); - - const handleDocumentAccessesLoaded = useCallback((documentAccesses: DocumentAccessInfo[]) => { - setEditingDocumentAccesses(documentAccesses); - - const activeIds = new Set( - documentAccesses - .filter((docAccess) => docAccess.active) - .map((docAccess) => docAccess.document?.id || docAccess.report?.id || docAccess.trustCenterFile?.id) - .filter((id: unknown): id is string => typeof id === 'string') - ); - setSelectedDocumentAccesses(activeIds); - setIsLoadingDocumentAccesses(false); - }, []); - - const formattedDocumentAccesses: NonNullable>[] = editingDocumentAccesses - ?.map((docAccess) => getDocumentAccessInfo(docAccess, __)) - ?.filter((info) => info !== null) ?? []; - - const inviteForm = useFormWithSchema(inviteSchema, { - defaultValues: { name: "", email: "" }, - }); - - const editForm = useFormWithSchema(editSchema, { - defaultValues: { name: "", active: false }, - }); - - function getDocumentAccessInfo( - docAccess: DocumentAccessInfo, - __: (key: string) => string - ) { - if (docAccess.document) { - return { - variant: "info" as const, - name: docAccess.document?.title, - type: __("Document"), - category: docAccess.document?.documentType, - id: docAccess.document?.id, - requested: docAccess.requested, - active: docAccess.active, - status: docAccess.status, - }; - } - if (docAccess.report) { - return { - variant: "success" as const, - name: docAccess.report?.filename, - type: __("Report"), - category: docAccess.report?.audit?.framework?.name, - id: docAccess.report?.id, - requested: docAccess.requested, - active: docAccess.active, - status: docAccess.status, - }; - } - if (docAccess.trustCenterFile) { - return { - variant: "highlight" as const, - name: docAccess.trustCenterFile?.name, - type: __("File"), - category: docAccess.trustCenterFile?.category, - id: docAccess.trustCenterFile?.id, - requested: docAccess.requested, - active: docAccess.active, - status: docAccess.status, - }; - } - - throw new Error("Unknown trust center access document type"); - } - - type AccessType = { - id: string; - email: string; - name: string; - active: boolean; - hasAcceptedNonDisclosureAgreement: boolean; - createdAt: string; - lastTokenExpiresAt: string | null; - pendingRequestCount: number; - activeCount: number; - documentAccesses?: DocumentAccessInfo[]; - }; - - const { data: trustCenterData, loadMore, hasNext, isLoadingNext } = useTrustCenterAccesses(organization.trustCenter?.id || ""); - - const accesses: AccessType[] = useMemo( - () => trustCenterData?.accesses?.edges.map((edge) => edge.node) ?? [], [trustCenterData?.accesses?.edges] - ); - - const handleInvite = inviteForm.handleSubmit(async (data) => { - if (!organization.trustCenter?.id) { - return; - } - - const connectionId = trustCenterData?.accesses?.__id; - const email = data.email.trim(); - - await createInvitation({ - variables: { - input: { - trustCenterId: organization.trustCenter.id, - email: email, - name: data.name.trim(), - active: false, - }, - connections: connectionId ? [connectionId] : [], - }, - onSuccess: () => { - setPendingEditEmail(email); - }, - }); - }); - - const handleDelete = useCallback(async (id: string) => { - const connectionId = trustCenterData?.accesses?.__id; - - await deleteInvitation({ - variables: { - input: { id }, - connections: connectionId ? [connectionId] : [], - }, - }); - }, [deleteInvitation, trustCenterData]); - - - - const handleEditAccess = useCallback((access: AccessType) => { - loadedAccessIdRef.current = null; - setEditingAccess(access); - setEditingDocumentAccesses([]); - setSelectedDocumentAccesses(new Set()); - setIsLoadingDocumentAccesses(false); - editForm.reset({ name: access.name, active: access.active }); - editDialogRef.current?.open(); - }, [editDialogRef, editForm]); - - useEffect(() => { - if (pendingEditEmail && accesses.length > 0) { - const newAccess = accesses.find(access => access.email === pendingEditEmail); - if (newAccess) { - setPendingEditEmail(null); - loadedAccessIdRef.current = null; - setEditingAccess(newAccess); - editForm.reset({ name: newAccess.name, active: true }); - setEditingDocumentAccesses([]); - setSelectedDocumentAccesses(new Set()); - editDialogRef.current?.open(); - setTimeout(() => { - dialogRef.current?.close(); - }, 50); - setTimeout(() => { - inviteForm.reset(); - }, 300); - } - } - }, [accesses, pendingEditEmail, editForm, dialogRef, editDialogRef, inviteForm]); - - const handleToggleDocumentAccess = useCallback((documentId: string, active: boolean) => { - setSelectedDocumentAccesses(prev => { - const newSet = new Set(prev); - if (active) { - newSet.add(documentId); - } else { - newSet.delete(documentId); - } - return newSet; - }); - }, []); - - const handleUpdateName = editForm.handleSubmit(async (data) => { - if (!editingAccess) return; - - const { documentIds, reportIds, trustCenterFileIds } = editingDocumentAccesses.reduce( - (acc, docAccess) => { - const id = docAccess.document?.id || docAccess.report?.id || docAccess.trustCenterFile?.id; - if (id && selectedDocumentAccesses.has(id)) { - if (docAccess.document?.id) { - acc.documentIds.push(docAccess.document.id); - } else if (docAccess.report?.id) { - acc.reportIds.push(docAccess.report.id); - } else if (docAccess.trustCenterFile?.id) { - acc.trustCenterFileIds.push(docAccess.trustCenterFile.id); - } - } - return acc; - }, - { documentIds: [] as string[], reportIds: [] as string[], trustCenterFileIds: [] as string[] } - ); - - await updateInvitation({ - variables: { - input: { - id: editingAccess.id, - name: data.name.trim(), - active: data.active, - documentIds, - reportIds, - trustCenterFileIds, - }, - }, - onSuccess: () => { - editDialogRef.current?.close(); - setEditingAccess(null); - editForm.reset(); - setEditingDocumentAccesses([]); - setSelectedDocumentAccesses(new Set()); - }, - }); - }); - - return ( -
-
-
-

{__("External Access")}

-

- {__("Manage who can access your trust center with time-limited tokens")} -

-
- {organization.trustCenter?.id && ( - isAuthorized("TrustCenter", "createTrustCenterAccess") && ( - - ) - )} -
- - {!organization.trustCenter?.id ? ( - - - - - - -
- -
- ) : accesses.length === 0 ? ( - - - - - - -
- {__("No external access granted yet")} -
- ) : ( - <> - - - - - - - - - - - - - - - - {accesses.map((access) => { - const isExpired = access.lastTokenExpiresAt ? new Date(access.lastTokenExpiresAt) < new Date() : false; - - return ( - handleEditAccess(access)} - className="cursor-pointer hover:bg-bg-secondary transition-colors" - > - - - - - - - - - - - ); - })} - -
{__("Name")}{__("Email")}{__("Date")}{__("Expires")}{__("Active")}{__("Access")}{__("Requests")}{__("NDA")}
{access.name}{access.email} - {formatDate(access.createdAt)} - - {access.lastTokenExpiresAt ? formatDate(access.lastTokenExpiresAt) : "-"} - -
- {access.active ? ( - - ) : ( - - )} -
-
- {access.activeCount} - - {access.pendingRequestCount > 0 ? access.pendingRequestCount : ""} - -
- {access.hasAcceptedNonDisclosureAgreement && ( - - )} -
-
-
e.stopPropagation()} - > - {isAuthorized("TrustCenterAccess", "updateTrustCenterAccess") && ( -
-
- {hasNext && ( - - )} - - )} - - -
- -
-

- {__("Send a 30-day access token to an external person to view your trust center")} -

- - - -
- -
-
-
- - - - -
-
- - - {documentAccessesQueryReference && ( - - )} -
- -
-

- {__("Update access settings and document permissions")} -

- - - -
-
- -

- {__("Enable or disable access for this user")} -

-
- editForm.setValue("active", checked)} - /> -
-
- -
-
-

- {__("Document Access Permissions")} -

- {!isLoadingDocumentAccesses && formattedDocumentAccesses.length > 0 && ( - - )} -
- - {isLoadingDocumentAccesses ? ( -
- -
- ) : formattedDocumentAccesses.length > 0 ? ( -
- - - - - - - - - - - - {formattedDocumentAccesses.map((info) => { - const { variant, name, type, category, id, status } = info; - - return ( - - - - - - - - ); - })} - -
{__("Name")}{__("Type")}{__("Category")} - {__("Access")} -
-
- {name} -
-
- - {type} - - -
- {category || "-"} -
-
- - {status} - - -
- -
-
-
- ) : ( -
- {__("No documents available")} -
- )} -
-
- - - - -
-
-
- ); -} diff --git a/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessEditDialog.tsx b/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessEditDialog.tsx new file mode 100644 index 000000000..6b9e88b95 --- /dev/null +++ b/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessEditDialog.tsx @@ -0,0 +1,315 @@ +import { + Badge, + Button, + Checkbox, + Dialog, + DialogContent, + DialogFooter, + Field, + Spinner, + Table, + Tbody, + Td, + Th, + Thead, + Tr, +} from "@probo/ui"; +import { usePreloadedQuery, type PreloadedQuery, useQueryLoader } from "react-relay"; +import type { TrustCenterAccessGraphLoadDocumentAccessesQuery } from "/hooks/graph/__generated__/TrustCenterAccessGraphLoadDocumentAccessesQuery.graphql"; +import type { TrustCenterDocumentAccess } from "/coredata/TrustCenterDocumentAccess"; +import { loadTrustCenterAccessDocumentAccessesQuery, updateTrustCenterAccessMutation } from "/hooks/graph/TrustCenterAccessGraph"; +import { useTranslate } from "@probo/i18n"; +import z from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import type { TrustCenterAccess } from "/coredata/TrustCenterAccess"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { Suspense, useEffect } from "react"; + +function getDocumentAccessInfo( + docAccess: TrustCenterDocumentAccess, + __: (key: string) => string +) { + if (docAccess.document) { + return { + variant: "info" as const, + name: docAccess.document?.title, + type: __("Document"), + category: docAccess.document?.documentType, + id: docAccess.document?.id, + requested: docAccess.requested, + active: docAccess.active, + status: docAccess.status, + }; + } + if (docAccess.report) { + return { + variant: "success" as const, + name: docAccess.report?.filename, + type: __("Report"), + category: docAccess.report?.audit?.framework?.name, + id: docAccess.report?.id, + requested: docAccess.requested, + active: docAccess.active, + status: docAccess.status, + }; + } + if (docAccess.trustCenterFile) { + return { + variant: "highlight" as const, + name: docAccess.trustCenterFile?.name, + type: __("File"), + category: docAccess.trustCenterFile?.category, + id: docAccess.trustCenterFile?.id, + requested: docAccess.requested, + active: docAccess.active, + status: docAccess.status, + }; + } + + throw new Error("Unknown trust center access document type"); +} + +interface TrustCenterAccessEditDialogProps { + access: TrustCenterAccess; + onClose: () => void; +} + +export function TrustCenterAccessEditDialog(props: TrustCenterAccessEditDialogProps) { + const { access, onClose } = props; + + const { __ } = useTranslate(); + + const [queryRef, loadDocumentAccessesQuery] = + useQueryLoader(loadTrustCenterAccessDocumentAccessesQuery); + + useEffect(() => { + loadDocumentAccessesQuery({ + accessId: access.id + }); + }, [access.id, loadDocumentAccessesQuery]) + + return ( + + {queryRef && + + + + } + + ); +} + +interface TrustCenterAccessEditFormProps { + access: TrustCenterAccess; + onSubmit: () => void; + queryRef: PreloadedQuery; +} + +export function TrustCenterAccessEditForm(props: TrustCenterAccessEditFormProps) { + const { access, onSubmit, queryRef } = props; + + const { __ } = useTranslate(); + const data = usePreloadedQuery( + loadTrustCenterAccessDocumentAccessesQuery, + queryRef, + ) + const documentAccesses: TrustCenterDocumentAccess[] = data.node.availableDocumentAccesses?.edges.map(edge => edge.node) ?? []; + + const editSchema = z.object({ + name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")), + active: z.boolean(), + }); + const editForm = useFormWithSchema(editSchema, { + defaultValues: { name: access.name, active: access.active }, + }); + + const [updateTrustCenterAccess, isUpdating] = useMutationWithToasts(updateTrustCenterAccessMutation, { + successMessage: __("Access updated successfully"), + errorMessage: __("Failed to update access"), + }); + + const handleSubmit = editForm.handleSubmit(async (data) => { + const { documentIds, reportIds, trustCenterFileIds } = documentAccesses.reduce( + (acc, docAccess) => { + // TODO status update + if (docAccess.document?.id) { + acc.documentIds.push(docAccess.document.id); + } else if (docAccess.report?.id) { + acc.reportIds.push(docAccess.report.id); + } else if (docAccess.trustCenterFile?.id) { + acc.trustCenterFileIds.push(docAccess.trustCenterFile.id); + } + return acc; + }, + { documentIds: [] as string[], reportIds: [] as string[], trustCenterFileIds: [] as string[] } + ); + + await updateTrustCenterAccess({ + variables: { + input: { + id: access.id, + name: data.name.trim(), + active: data.active, + documentIds, + reportIds, + trustCenterFileIds, + }, + }, + onSuccess: () => { + onSubmit(); + }, + }); + }); + + return ( +
+ +
+

+ {__("Update access settings and document permissions")} +

+ + + +
+
+ +

+ {__("Enable or disable access for this user")} +

+
+ editForm.setValue("active", checked)} + /> +
+
+ + +
+ + + + +
+ ); +} + +function TrustCenterDocumentAccessList(props: { + documentAccesses: TrustCenterDocumentAccess[]; +}) { + const { documentAccesses } = props; + + const { __ } = useTranslate(); + const formattedDocumentAccesses: NonNullable>[] = documentAccesses + ?.map((docAccess) => getDocumentAccessInfo(docAccess, __)) ?? []; + + const showGrantCTA = formattedDocumentAccesses.some(da => da.status !== "GRANTED"); + const showRejectCTA = formattedDocumentAccesses.some(da => da.status !== "REJECTED" && da.status !== "REVOKED"); + + return ( +
+
+

+ {__("Document Access Permissions")} +

+ {showGrantCTA && + + } + {showRejectCTA && + + } +
+ + {formattedDocumentAccesses.length > 0 ? ( +
+ + + + + + + + + + + + {formattedDocumentAccesses.map((info) => { + const { variant, name, type, category, id, status } = info; + + return ( + + + + + + + + ); + })} + +
{__("Name")}{__("Type")}{__("Category")} + {__("Access")} +
+
+ {name} +
+
+ + {type} + + +
+ {category || "-"} +
+
+ + {status} + + +
+ {/* TODO DROPDOWN */} +
+
+
+ ) : ( +
+ {__("No documents available")} +
+ )} +
+ ) +} diff --git a/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessItem.tsx b/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessItem.tsx new file mode 100644 index 000000000..31ef47a43 --- /dev/null +++ b/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessItem.tsx @@ -0,0 +1,105 @@ +import { Button, IconCheckmark1, IconCrossLargeX, IconPencil, IconTrashCan, Td, Tr } from "@probo/ui"; +import type { TrustCenterAccess } from "/coredata/TrustCenterAccess"; +import { formatDate } from "@probo/helpers"; +import { use, useCallback, useState } from "react"; +import { PermissionsContext } from "/providers/PermissionsContext"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { deleteTrustCenterAccessMutation } from "/hooks/graph/TrustCenterAccessGraph"; +import { useTranslate } from "@probo/i18n"; +import { TrustCenterAccessEditDialog } from "./TrustCenterAccessEditDialog"; + +interface TrustCenterAccessItemProps { + openDialog: boolean, + access: TrustCenterAccess + connectionId?: string; +} + +export function TrustCenterAccessItem(props: TrustCenterAccessItemProps) { + // TODO openDialog after access creation + const { access, connectionId } = props; + + const { __ } = useTranslate(); + const { isAuthorized } = use(PermissionsContext); + const [dialogOpen, setDialogOpen] = useState(false) + + 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 isExpired = access.lastTokenExpiresAt ? new Date(access.lastTokenExpiresAt) < new Date() : false; + + return ( + <> + setDialogOpen(true)} + className="cursor-pointer hover:bg-bg-secondary transition-colors" + > + {access.name} + {access.email} + + {formatDate(access.createdAt)} + + + {access.lastTokenExpiresAt ? formatDate(access.lastTokenExpiresAt) : "-"} + + +
+ {access.active ? ( + + ) : ( + + )} +
+ + + {access.activeCount} + + + {access.pendingRequestCount > 0 ? access.pendingRequestCount : ""} + + +
+ {access.hasAcceptedNonDisclosureAgreement && ( + + )} +
+ + +
e.stopPropagation()} + > + {isAuthorized("TrustCenterAccess", "updateTrustCenterAccess") && ( +
+ + + + {dialogOpen && setDialogOpen(false)} />} + + ); +} diff --git a/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessTab.tsx b/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessTab.tsx new file mode 100644 index 000000000..3e166b293 --- /dev/null +++ b/apps/console/src/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessTab.tsx @@ -0,0 +1,266 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Field, + Spinner, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + useDialogRef, + IconChevronDown, + IconPlusLarge, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { useOutletContext } from "react-router"; +import { useState, useEffect, use, useMemo } from "react"; +import z from "zod"; +import { + useTrustCenterAccesses, + createTrustCenterAccessMutation, +} from "/hooks/graph/TrustCenterAccessGraph"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { PermissionsContext } from "/providers/PermissionsContext"; +import type { TrustCenterAccess } from "/coredata/TrustCenterAccess"; +import { TrustCenterAccessItem } from "./TrustCenterAccessItem"; + +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; + }; + }>; + }; + }; +}; + +export default function TrustCenterAccessTab() { + const { __ } = useTranslate(); + const { organization } = useOutletContext(); + const { isAuthorized } = use(PermissionsContext); + 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")), + }); + + const [createInvitation, isCreating] = useMutationWithToasts(createTrustCenterAccessMutation, { + successMessage: __("Access created successfully"), + errorMessage: __("Failed to create access"), + }); + + const dialogRef = useDialogRef(); + const [editingAccess, setEditingAccess] = useState(null); + const [pendingEditEmail, setPendingEditEmail] = useState(null); + + const inviteForm = useFormWithSchema(inviteSchema, { + defaultValues: { name: "", email: "" }, + }); + + 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 handleInvite = inviteForm.handleSubmit(async (data) => { + if (!organization.trustCenter?.id) { + return; + } + + const connectionId = trustCenterData?.accesses?.__id; + const email = data.email.trim(); + + await createInvitation({ + variables: { + input: { + trustCenterId: organization.trustCenter.id, + email: email, + name: data.name.trim(), + active: false, + }, + connections: connectionId ? [connectionId] : [], + }, + onSuccess: () => { + setPendingEditEmail(email); + }, + }); + }); + + useEffect(() => { + if (pendingEditEmail && accesses.length > 0) { + const newAccess = accesses.find(access => access.email === pendingEditEmail); + if (newAccess) { + setPendingEditEmail(null); + setEditingAccess(newAccess); + setTimeout(() => { + dialogRef.current?.close(); + }, 50); + setTimeout(() => { + inviteForm.reset(); + }, 300); + } + } + }, [accesses, pendingEditEmail, dialogRef, inviteForm]); + + return ( +
+
+
+

{__("External Access")}

+

+ {__("Manage who can access your trust center with time-limited tokens")} +

+
+ {organization.trustCenter?.id && ( + isAuthorized("TrustCenter", "createTrustCenterAccess") && ( + + ) + )} +
+ + {!organization.trustCenter?.id ? ( + + + + + + +
+ +
+ ) : accesses.length === 0 ? ( + + + + + + +
+ {__("No external access granted yet")} +
+ ) : ( + <> + + + + + + + + + + + + + + + + {accesses.map((access) => ( + + ))} + +
{__("Name")}{__("Email")}{__("Date")}{__("Expires")}{__("Active")}{__("Access")}{__("Requests")}{__("NDA")}
+ {hasNext && ( + + )} + + )} + + +
+ +
+

+ {__("Send a 30-day access token to an external person to view your trust center")} +

+ + + +
+ +
+
+
+ + + + +
+
+
+ ); +} diff --git a/apps/console/src/routes/trustCenterRoutes.ts b/apps/console/src/routes/trustCenterRoutes.ts index f86a542b2..ba1a3feb1 100644 --- a/apps/console/src/routes/trustCenterRoutes.ts +++ b/apps/console/src/routes/trustCenterRoutes.ts @@ -64,7 +64,7 @@ export const trustCenterRoutes = [ path: "access", Fallback: LinkCardSkeleton, Component: lazy( - () => import("/pages/organizations/trustCenter/TrustCenterAccessTab") + () => import("/pages/organizations/trustCenter/TrustCenterAccessTab/TrustCenterAccessTab") ), }, ],