Add trust center access requests
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -8,10 +8,12 @@ import {
|
||||
Th,
|
||||
Button,
|
||||
IconArrowDown,
|
||||
IconLock,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { FrameworkLogo } from "/components/FrameworkLogo";
|
||||
import { TrustCenterAccessRequestDialog } from "./TrustCenterAccessRequestDialog";
|
||||
|
||||
type Audit = {
|
||||
id: string;
|
||||
@@ -27,16 +29,21 @@ type Audit = {
|
||||
filename: string;
|
||||
downloadUrl: string | null;
|
||||
} | null;
|
||||
reportUrl: string | null;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
audits: Audit[];
|
||||
organizationName: string;
|
||||
isAuthenticated: boolean;
|
||||
trustCenterId: string;
|
||||
};
|
||||
|
||||
export function PublicTrustCenterAudits({ audits, organizationName, isAuthenticated }: Props) {
|
||||
export function PublicTrustCenterAudits({
|
||||
audits,
|
||||
organizationName,
|
||||
isAuthenticated,
|
||||
trustCenterId
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
if (audits.length === 0) {
|
||||
@@ -74,8 +81,8 @@ export function PublicTrustCenterAudits({ audits, organizationName, isAuthentica
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{audits.map((audit) => {
|
||||
const hasReport = audit.report || audit.reportUrl;
|
||||
const downloadUrl = audit.report?.downloadUrl || audit.reportUrl;
|
||||
const hasReport = audit.report !== null;
|
||||
const downloadUrl = audit.report?.downloadUrl;
|
||||
const reportName = audit.report?.filename || __("Compliance Report");
|
||||
|
||||
return (
|
||||
@@ -98,9 +105,18 @@ export function PublicTrustCenterAudits({ audits, organizationName, isAuthentica
|
||||
{__("No report")}
|
||||
</span>
|
||||
) : !isAuthenticated ? (
|
||||
<span className="text-txt-tertiary text-sm">
|
||||
{__("Not available")}
|
||||
</span>
|
||||
<TrustCenterAccessRequestDialog
|
||||
trigger={
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={IconLock}
|
||||
>
|
||||
{__("Request Access")}
|
||||
</Button>
|
||||
}
|
||||
trustCenterId={trustCenterId}
|
||||
organizationName={organizationName}
|
||||
/>
|
||||
) : downloadUrl ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
DocumentTypeBadge,
|
||||
Button,
|
||||
IconArrowDown,
|
||||
IconLock,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { buildEndpoint } from "/providers/RelayProviders";
|
||||
// Manual mutation for trust API (not processed by relay compiler)
|
||||
import { TrustCenterAccessRequestDialog } from "./TrustCenterAccessRequestDialog";
|
||||
|
||||
const exportDocumentPDFMutation = {
|
||||
params: {
|
||||
name: "PublicTrustCenterDocumentsExportPDFMutation",
|
||||
@@ -39,6 +41,8 @@ type Document = {
|
||||
type Props = {
|
||||
documents: Document[];
|
||||
isAuthenticated: boolean;
|
||||
trustCenterId: string;
|
||||
organizationName: string;
|
||||
};
|
||||
|
||||
type ExportDocumentPDFResponse = {
|
||||
@@ -50,7 +54,12 @@ type ExportDocumentPDFResponse = {
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
|
||||
export function PublicTrustCenterDocuments({ documents, isAuthenticated }: Props) {
|
||||
export function PublicTrustCenterDocuments({
|
||||
documents,
|
||||
isAuthenticated,
|
||||
trustCenterId,
|
||||
organizationName
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -141,9 +150,18 @@ export function PublicTrustCenterDocuments({ documents, isAuthenticated }: Props
|
||||
</Td>
|
||||
<Td>
|
||||
{!isAuthenticated ? (
|
||||
<span className="text-txt-tertiary text-sm">
|
||||
{__("Not available")}
|
||||
</span>
|
||||
<TrustCenterAccessRequestDialog
|
||||
trigger={
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={IconLock}
|
||||
>
|
||||
{__("Request Access")}
|
||||
</Button>
|
||||
}
|
||||
trustCenterId={trustCenterId}
|
||||
organizationName={organizationName}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
@@ -33,10 +33,10 @@ export function PublicTrustCenterVendors({ vendors, organizationName }: Props) {
|
||||
<Card padded>
|
||||
<div className="text-center py-8">
|
||||
<h2 className="text-xl font-semibold text-txt-primary mb-2">
|
||||
{__("Vendors")}
|
||||
{__("Subcontractors")}
|
||||
</h2>
|
||||
<p className="text-txt-secondary">
|
||||
{__("No vendor information is currently available.")}
|
||||
{__("No subcontractor information is currently available.")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogFooter,
|
||||
DialogContent,
|
||||
Button,
|
||||
Field,
|
||||
useToast,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { z } from "zod";
|
||||
import { buildEndpoint } from "/providers/RelayProviders";
|
||||
|
||||
// Manual mutation for trust API (not processed by relay compiler)
|
||||
const createTrustCenterAccessMutation = {
|
||||
params: {
|
||||
name: "CreateTrustCenterAccessMutation",
|
||||
operationKind: "mutation",
|
||||
text: `
|
||||
mutation CreateTrustCenterAccessMutation(
|
||||
$input: CreateTrustCenterAccessInput!
|
||||
) {
|
||||
createTrustCenterAccess(input: $input) {
|
||||
trustCenterAccess {
|
||||
id
|
||||
email
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
type CreateTrustCenterAccessResponse = {
|
||||
data?: {
|
||||
createTrustCenterAccess?: {
|
||||
trustCenterAccess: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
trigger: React.ReactNode;
|
||||
trustCenterId: string;
|
||||
organizationName: string;
|
||||
};
|
||||
|
||||
export function TrustCenterAccessRequestDialog({
|
||||
trigger,
|
||||
trustCenterId,
|
||||
organizationName
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
const schema = 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 { register, handleSubmit, formState, reset } = useFormWithSchema(schema, {
|
||||
defaultValues: { name: "", email: "" },
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(buildEndpoint("/api/trust/v1/graphql"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
operationName: createTrustCenterAccessMutation.params.name,
|
||||
query: createTrustCenterAccessMutation.params.text,
|
||||
variables: {
|
||||
input: {
|
||||
trustCenterId,
|
||||
email: data.email,
|
||||
name: data.name
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const result: CreateTrustCenterAccessResponse = await response.json();
|
||||
|
||||
if (result.errors) {
|
||||
throw new Error(result.errors[0].message);
|
||||
}
|
||||
|
||||
if (result.data?.createTrustCenterAccess) {
|
||||
toast({
|
||||
title: __("Request Submitted"),
|
||||
description: __("Your access request has been submitted. You will receive an email if your request is approved."),
|
||||
variant: "success",
|
||||
});
|
||||
|
||||
reset();
|
||||
dialogRef.current?.close();
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: __("An error occurred while submitting your request.");
|
||||
|
||||
toast({
|
||||
title: __("Request Failed"),
|
||||
description: errorMessage,
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ref={dialogRef}
|
||||
trigger={trigger}
|
||||
title={__("Request Access")}
|
||||
>
|
||||
<form onSubmit={onSubmit}>
|
||||
<DialogContent padded className="space-y-4">
|
||||
<div className="text-sm text-txt-secondary">
|
||||
{sprintf(__("Request access to %s's Trust Center. Your request will be reviewed and you will receive an email notification with access instructions if approved."), organizationName)}
|
||||
</div>
|
||||
|
||||
<Field
|
||||
label={__("Your Name")}
|
||||
required
|
||||
error={formState.errors.name?.message}
|
||||
{...register("name")}
|
||||
placeholder={__("Enter your full name")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={__("Email Address")}
|
||||
required
|
||||
type="email"
|
||||
error={formState.errors.email?.message}
|
||||
{...register("email")}
|
||||
placeholder={__("Enter your email address")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? __("Submitting...") : __("Submit Request")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -35,7 +35,6 @@ export const publicTrustCenterQuery = {
|
||||
filename
|
||||
downloadUrl
|
||||
}
|
||||
reportUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export const trustCenterAccessesQuery = graphql`
|
||||
id
|
||||
email
|
||||
name
|
||||
active
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
@@ -46,6 +47,7 @@ export const createTrustCenterAccessMutation = graphql`
|
||||
id
|
||||
email
|
||||
name
|
||||
active
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
@@ -53,6 +55,23 @@ export const createTrustCenterAccessMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const updateTrustCenterAccessMutation = graphql`
|
||||
mutation TrustCenterAccessGraphUpdateMutation(
|
||||
$input: UpdateTrustCenterAccessInput!
|
||||
) {
|
||||
updateTrustCenterAccess(input: $input) {
|
||||
trustCenterAccess {
|
||||
id
|
||||
email
|
||||
name
|
||||
active
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const deleteTrustCenterAccessMutation = graphql`
|
||||
mutation TrustCenterAccessGraphDeleteMutation(
|
||||
$input: DeleteTrustCenterAccessInput!
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<08687dbafb5515e9d2073b292a05ea0c>>
|
||||
* @generated SignedSource<<ae95aed7bc22c95f8cc2ab591039c810>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type CreateTrustCenterAccessInput = {
|
||||
active: boolean;
|
||||
email: string;
|
||||
name: string;
|
||||
trustCenterId: string;
|
||||
@@ -23,6 +24,7 @@ export type TrustCenterAccessGraphCreateMutation$data = {
|
||||
readonly trustCenterAccessEdge: {
|
||||
readonly cursor: any;
|
||||
readonly node: {
|
||||
readonly active: boolean;
|
||||
readonly createdAt: any;
|
||||
readonly email: string;
|
||||
readonly id: string;
|
||||
@@ -98,6 +100,13 @@ v3 = {
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "active",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -177,16 +186,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "86c21ac139bec74f2a4abad9caef68b1",
|
||||
"cacheID": "eafddcd0263963235d3249c22eb50593",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "TrustCenterAccessGraphCreateMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation TrustCenterAccessGraphCreateMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccessEdge {\n cursor\n node {\n id\n email\n name\n createdAt\n }\n }\n }\n}\n"
|
||||
"text": "mutation TrustCenterAccessGraphCreateMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccessEdge {\n cursor\n node {\n id\n email\n name\n active\n createdAt\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "59e0ab46fd6b68747566d666f5315292";
|
||||
(node as any).hash = "99676fee0b2de06a92cdad66c577eee7";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<509cd7c9db3cbf15931c386b13a13987>>
|
||||
* @generated SignedSource<<ed4ab6c79b848843012fc091a7eb325c>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type DeleteTrustCenterAccessInput = {
|
||||
accessId: string;
|
||||
id: string;
|
||||
};
|
||||
export type TrustCenterAccessGraphDeleteMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<cbee2b0f0f7bc70f542094f32bd1b91b>>
|
||||
* @generated SignedSource<<2333a6a7d5415f1a08a5612dcaceee8f>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -19,6 +19,7 @@ export type TrustCenterAccessGraphQuery$data = {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly cursor: any;
|
||||
readonly node: {
|
||||
readonly active: boolean;
|
||||
readonly createdAt: any;
|
||||
readonly email: string;
|
||||
readonly id: string;
|
||||
@@ -155,6 +156,13 @@ v5 = [
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "active",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -282,7 +290,7 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "47681869f07fbe7df45ae3f663827c10",
|
||||
"cacheID": "5b83cd4ae2434ce2e00de7230d264432",
|
||||
"id": null,
|
||||
"metadata": {
|
||||
"connection": [
|
||||
@@ -299,11 +307,11 @@ return {
|
||||
},
|
||||
"name": "TrustCenterAccessGraphQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query TrustCenterAccessGraphQuery(\n $trustCenterId: ID!\n) {\n node(id: $trustCenterId) {\n __typename\n ... on TrustCenter {\n id\n accesses(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n email\n name\n createdAt\n __typename\n }\n }\n }\n }\n id\n }\n}\n"
|
||||
"text": "query TrustCenterAccessGraphQuery(\n $trustCenterId: ID!\n) {\n node(id: $trustCenterId) {\n __typename\n ... on TrustCenter {\n id\n accesses(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n email\n name\n active\n createdAt\n __typename\n }\n }\n }\n }\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "06d7307ac23675de91ba2f9cb9d604bc";
|
||||
(node as any).hash = "af598fd2af198e63ed84fd618857a985";
|
||||
|
||||
export default node;
|
||||
|
||||
147
apps/console/src/hooks/graph/__generated__/TrustCenterAccessGraphUpdateMutation.graphql.ts
generated
Normal file
147
apps/console/src/hooks/graph/__generated__/TrustCenterAccessGraphUpdateMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @generated SignedSource<<6de2ecb63a58c88c3008368061943b49>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type UpdateTrustCenterAccessInput = {
|
||||
active?: boolean | null | undefined;
|
||||
id: string;
|
||||
name?: string | null | undefined;
|
||||
};
|
||||
export type TrustCenterAccessGraphUpdateMutation$variables = {
|
||||
input: UpdateTrustCenterAccessInput;
|
||||
};
|
||||
export type TrustCenterAccessGraphUpdateMutation$data = {
|
||||
readonly updateTrustCenterAccess: {
|
||||
readonly trustCenterAccess: {
|
||||
readonly active: boolean;
|
||||
readonly createdAt: any;
|
||||
readonly email: string;
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly updatedAt: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type TrustCenterAccessGraphUpdateMutation = {
|
||||
response: TrustCenterAccessGraphUpdateMutation$data;
|
||||
variables: TrustCenterAccessGraphUpdateMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "UpdateTrustCenterAccessPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "updateTrustCenterAccess",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "TrustCenterAccess",
|
||||
"kind": "LinkedField",
|
||||
"name": "trustCenterAccess",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "active",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "TrustCenterAccessGraphUpdateMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "TrustCenterAccessGraphUpdateMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "683ee01fb5173b49b0a7f3c2d99cf002",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "TrustCenterAccessGraphUpdateMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation TrustCenterAccessGraphUpdateMutation(\n $input: UpdateTrustCenterAccessInput!\n) {\n updateTrustCenterAccess(input: $input) {\n trustCenterAccess {\n id\n email\n name\n active\n createdAt\n updatedAt\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "cccd083b0047f6bc7b504b7494205e9d";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Outlet } from "react-router";
|
||||
import { Logo, Button, IconArrowBoxLeft } from "@probo/ui";
|
||||
import { Logo, Button, IconArrowBoxLeft, useToast } from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { buildEndpoint } from "/providers/RelayProviders";
|
||||
import type { ReactNode } from "react";
|
||||
@@ -8,10 +8,12 @@ type Props = {
|
||||
organizationName: string;
|
||||
organizationLogo?: string | null;
|
||||
children?: ReactNode;
|
||||
isAuthenticated?: boolean;
|
||||
};
|
||||
|
||||
export function PublicTrustCenterLayout({ organizationName, organizationLogo, children }: Props) {
|
||||
export function PublicTrustCenterLayout({ organizationName, organizationLogo, children, isAuthenticated }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
@@ -22,10 +24,13 @@ export function PublicTrustCenterLayout({ organizationName, organizationLogo, ch
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('Logout failed');
|
||||
} finally {
|
||||
window.location.href = "/";
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: __("Logout failed"),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,13 +72,15 @@ export function PublicTrustCenterLayout({ organizationName, organizationLogo, ch
|
||||
<span>Powered by Probo</span>
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={IconArrowBoxLeft}
|
||||
onClick={handleLogout}
|
||||
title={__("Logout")}
|
||||
className="text-sm"
|
||||
/>
|
||||
{isAuthenticated && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
icon={IconArrowBoxLeft}
|
||||
onClick={handleLogout}
|
||||
title={__("Logout")}
|
||||
className="text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,6 @@ interface Audit {
|
||||
filename: string;
|
||||
downloadUrl: string | null;
|
||||
} | null;
|
||||
reportUrl: string | null;
|
||||
}
|
||||
|
||||
interface Vendor {
|
||||
@@ -192,6 +191,7 @@ function PublicTrustCenterContent() {
|
||||
<PublicTrustCenterLayout
|
||||
organizationName={organization.name}
|
||||
organizationLogo={organization.logoUrl}
|
||||
isAuthenticated={isAuthenticated}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<div className="text-center">
|
||||
@@ -208,10 +208,13 @@ function PublicTrustCenterContent() {
|
||||
audits={audits}
|
||||
organizationName={organization.name}
|
||||
isAuthenticated={isAuthenticated}
|
||||
trustCenterId={trustCenterBySlug.id}
|
||||
/>
|
||||
<PublicTrustCenterDocuments
|
||||
documents={documents}
|
||||
isAuthenticated={isAuthenticated}
|
||||
trustCenterId={trustCenterBySlug.id}
|
||||
organizationName={organization.name}
|
||||
/>
|
||||
<PublicTrustCenterVendors
|
||||
vendors={vendors}
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
import { Button, Card, Dialog, DialogContent, DialogFooter, Field, Input, Spinner, Table, Tbody, Td, Th, Thead, Tr, useDialogRef, useToast, IconTrashCan } from "@probo/ui";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Field,
|
||||
Spinner,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useDialogRef,
|
||||
IconTrashCan,
|
||||
IconPencil,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useOutletContext } from "react-router";
|
||||
import { useState, useCallback } from "react";
|
||||
import z from "zod";
|
||||
import {
|
||||
useTrustCenterAccesses,
|
||||
createTrustCenterAccessMutation,
|
||||
updateTrustCenterAccessMutation,
|
||||
deleteTrustCenterAccessMutation
|
||||
} from "/hooks/graph/TrustCenterAccessGraph";
|
||||
import { useMutation } from "react-relay";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
|
||||
type ContextType = {
|
||||
organization: {
|
||||
@@ -20,20 +41,50 @@ type ContextType = {
|
||||
|
||||
export default function TrustCenterAccessTab() {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const { organization } = useOutletContext<ContextType>();
|
||||
|
||||
const [createInvitation, isCreating] = useMutation(createTrustCenterAccessMutation);
|
||||
const [deleteInvitation, isDeleting] = useMutation(deleteTrustCenterAccessMutation);
|
||||
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")),
|
||||
});
|
||||
|
||||
const [createInvitation, isCreating] = useMutationWithToasts(createTrustCenterAccessMutation, {
|
||||
successMessage: __("Access invitation sent successfully"),
|
||||
errorMessage: __("Failed to send invitation. Please try again."),
|
||||
});
|
||||
const [updateInvitation, isUpdating] = useMutationWithToasts(updateTrustCenterAccessMutation, {
|
||||
successMessage: __("Access updated successfully"),
|
||||
errorMessage: __("Failed to update access. Please try again."),
|
||||
});
|
||||
const [deleteInvitation, isDeleting] = useMutationWithToasts(deleteTrustCenterAccessMutation, {
|
||||
successMessage: __("Access deleted successfully"),
|
||||
errorMessage: __("Failed to delete access. Please try again."),
|
||||
});
|
||||
|
||||
const dialogRef = useDialogRef();
|
||||
const [email, setEmail] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const editDialogRef = useDialogRef();
|
||||
const [editingAccess, setEditingAccess] = useState<{
|
||||
id: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
|
||||
const inviteForm = useFormWithSchema(inviteSchema, {
|
||||
defaultValues: { name: "", email: "" },
|
||||
});
|
||||
|
||||
const editForm = useFormWithSchema(editSchema, {
|
||||
defaultValues: { name: "" },
|
||||
});
|
||||
|
||||
type AccessType = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
active: boolean;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
@@ -43,105 +94,78 @@ export default function TrustCenterAccessTab() {
|
||||
id: edge.node.id,
|
||||
email: edge.node.email,
|
||||
name: edge.node.name,
|
||||
active: edge.node.active,
|
||||
createdAt: new Date(edge.node.createdAt)
|
||||
})) ?? [];
|
||||
|
||||
const handleInvite = useCallback(async () => {
|
||||
const handleInvite = inviteForm.handleSubmit(async (data) => {
|
||||
if (!organization.trustCenter?.id) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: __("Trust center not found"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email.trim() || !name.trim()) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: __("Email and name are required"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionId = trustCenterData?.node?.accesses?.__id;
|
||||
|
||||
try {
|
||||
createInvitation({
|
||||
variables: {
|
||||
input: {
|
||||
trustCenterId: organization.trustCenter.id,
|
||||
email: email.trim(),
|
||||
name: name.trim(),
|
||||
},
|
||||
connections: connectionId ? [connectionId] : [],
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors && errors.length > 0) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: errors[0]?.message || __("Failed to send invitation"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogRef.current) {
|
||||
dialogRef.current.close();
|
||||
}
|
||||
setEmail("");
|
||||
setName("");
|
||||
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Access invitation sent successfully"),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message || __("Failed to send invitation. Please try again."),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: __("An unexpected error occurred. Please try again."),
|
||||
variant: "error",
|
||||
});
|
||||
}
|
||||
}, [organization.trustCenter?.id, email, name, trustCenterData, createInvitation, toast, __, dialogRef]);
|
||||
|
||||
const handleDelete = useCallback(async (accessId: string) => {
|
||||
const connectionId = trustCenterData?.node?.accesses?.__id;
|
||||
|
||||
deleteInvitation({
|
||||
await createInvitation({
|
||||
variables: {
|
||||
input: {
|
||||
accessId,
|
||||
trustCenterId: organization.trustCenter.id,
|
||||
email: data.email.trim(),
|
||||
name: data.name.trim(),
|
||||
active: true,
|
||||
},
|
||||
connections: connectionId ? [connectionId] : [],
|
||||
},
|
||||
onCompleted: () => {
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Access deleted successfully"),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message,
|
||||
variant: "error",
|
||||
});
|
||||
onSuccess: () => {
|
||||
dialogRef.current?.close();
|
||||
inviteForm.reset();
|
||||
},
|
||||
});
|
||||
}, [deleteInvitation, toast, __, trustCenterData]);
|
||||
});
|
||||
|
||||
const handleDelete = useCallback(async (id: string) => {
|
||||
const connectionId = trustCenterData?.node?.accesses?.__id;
|
||||
|
||||
await deleteInvitation({
|
||||
variables: {
|
||||
input: { id },
|
||||
connections: connectionId ? [connectionId] : [],
|
||||
},
|
||||
});
|
||||
}, [deleteInvitation, trustCenterData]);
|
||||
|
||||
const handleToggleActive = useCallback(async (id: string, active: boolean) => {
|
||||
await updateInvitation({
|
||||
variables: {
|
||||
input: { id, active },
|
||||
},
|
||||
successMessage: active ? __("Access activated") : __("Access deactivated"),
|
||||
});
|
||||
}, [updateInvitation, __]);
|
||||
|
||||
const handleEditAccess = useCallback((access: AccessType) => {
|
||||
setEditingAccess({ id: access.id, name: access.name });
|
||||
editForm.reset({ name: access.name });
|
||||
editDialogRef.current?.open();
|
||||
}, [editDialogRef, editForm]);
|
||||
|
||||
const handleUpdateName = editForm.handleSubmit(async (data) => {
|
||||
if (!editingAccess) return;
|
||||
|
||||
await updateInvitation({
|
||||
variables: {
|
||||
input: {
|
||||
id: editingAccess.id,
|
||||
name: data.name.trim(),
|
||||
},
|
||||
},
|
||||
successMessage: __("Name updated successfully"),
|
||||
onSuccess: () => {
|
||||
editDialogRef.current?.close();
|
||||
setEditingAccess(null);
|
||||
editForm.reset();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -153,7 +177,10 @@ export default function TrustCenterAccessTab() {
|
||||
</p>
|
||||
</div>
|
||||
{organization.trustCenter?.id && (
|
||||
<Button onClick={() => dialogRef.current?.open()}>
|
||||
<Button onClick={() => {
|
||||
inviteForm.reset();
|
||||
dialogRef.current?.open();
|
||||
}}>
|
||||
{__("Invite")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -175,6 +202,7 @@ export default function TrustCenterAccessTab() {
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("Email")}</Th>
|
||||
<Th>{__("Date")}</Th>
|
||||
<Th>{__("Active")}</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -186,8 +214,20 @@ export default function TrustCenterAccessTab() {
|
||||
<Td>
|
||||
{access.createdAt.toLocaleDateString()}
|
||||
</Td>
|
||||
<Td noLink width={120} className="text-end">
|
||||
<Td>
|
||||
<Checkbox
|
||||
checked={access.active}
|
||||
onChange={(active) => handleToggleActive(access.id, active)}
|
||||
/>
|
||||
</Td>
|
||||
<Td noLink width={160} className="text-end">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleEditAccess(access)}
|
||||
disabled={isUpdating}
|
||||
icon={IconPencil}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => handleDelete(access.id)}
|
||||
@@ -207,35 +247,65 @@ export default function TrustCenterAccessTab() {
|
||||
ref={dialogRef}
|
||||
title={__("Invite External Access")}
|
||||
>
|
||||
<DialogContent padded className="space-y-4">
|
||||
<p className="text-txt-secondary text-sm">
|
||||
{__("Send a 7-day access token to an external person to view your trust center")}
|
||||
</p>
|
||||
<form onSubmit={handleInvite}>
|
||||
<DialogContent padded className="space-y-4">
|
||||
<p className="text-txt-secondary text-sm">
|
||||
{__("Send a 7-day access token to an external person to view your trust center")}
|
||||
</p>
|
||||
|
||||
<Field label={__("Full Name")} required>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
<Field
|
||||
label={__("Full Name")}
|
||||
required
|
||||
error={inviteForm.formState.errors.name?.message}
|
||||
{...inviteForm.register("name")}
|
||||
placeholder={__("John Doe")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={__("Email Address")} required>
|
||||
<Input
|
||||
<Field
|
||||
label={__("Email Address")}
|
||||
required
|
||||
error={inviteForm.formState.errors.email?.message}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
{...inviteForm.register("email")}
|
||||
placeholder={__("john@example.com")}
|
||||
/>
|
||||
</Field>
|
||||
</DialogContent>
|
||||
</DialogContent>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleInvite} disabled={isCreating}>
|
||||
{isCreating && <Spinner />}
|
||||
{__("Send Invitation")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isCreating}>
|
||||
{isCreating && <Spinner />}
|
||||
{__("Send Invitation")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
ref={editDialogRef}
|
||||
title={__("Edit Access Name")}
|
||||
>
|
||||
<form onSubmit={handleUpdateName}>
|
||||
<DialogContent padded className="space-y-4">
|
||||
<p className="text-txt-secondary text-sm">
|
||||
{__("Update the display name for this access invitation")}
|
||||
</p>
|
||||
|
||||
<Field
|
||||
label={__("Full Name")}
|
||||
required
|
||||
error={editForm.formState.errors.name?.message}
|
||||
{...editForm.register("name")}
|
||||
placeholder={__("John Doe")}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isUpdating}>
|
||||
{isUpdating && <Spinner />}
|
||||
{__("Update Name")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user