Refactor compliance page vendors

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-28 19:17:27 +04:00
parent c74631ae54
commit 42557e1640
15 changed files with 555 additions and 436 deletions

View File

@@ -0,0 +1,93 @@
/**
* @generated SignedSource<<b8031a4cc01354ec19a8dbce53ae3162>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type CompliancePageVendorListFragment$data = {
readonly vendors: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageVendorListItem_vendorFragment">;
};
}>;
};
readonly " $fragmentType": "CompliancePageVendorListFragment";
};
export type CompliancePageVendorListFragment$key = {
readonly " $data"?: CompliancePageVendorListFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageVendorListFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "CompliancePageVendorListFragment",
"selections": [
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "CompliancePageVendorListItem_vendorFragment"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "vendors(first:100)"
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "dc9e2540ae0f8b56dc322ef6cbbfe805";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<c0dfdaac2c2c1c873c8104a45b6077ac>>
* @generated SignedSource<<4374d6a33690e83fc1bb684baf5b05c4>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -35,21 +35,21 @@ export type UpdateVendorInput = {
trustPageUrl?: string | null | undefined;
websiteUrl?: string | null | undefined;
};
export type TrustCenterVendorGraphUpdateMutation$variables = {
export type CompliancePageVendorListItemMutation$variables = {
input: UpdateVendorInput;
};
export type TrustCenterVendorGraphUpdateMutation$data = {
export type CompliancePageVendorListItemMutation$data = {
readonly updateVendor: {
readonly vendor: {
readonly id: string;
readonly showOnTrustCenter: boolean;
readonly " $fragmentSpreads": FragmentRefs<"TrustCenterVendorsCardFragment">;
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageVendorListItem_vendorFragment">;
};
};
};
export type TrustCenterVendorGraphUpdateMutation = {
response: TrustCenterVendorGraphUpdateMutation$data;
variables: TrustCenterVendorGraphUpdateMutation$variables;
export type CompliancePageVendorListItemMutation = {
response: CompliancePageVendorListItemMutation$data;
variables: CompliancePageVendorListItemMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -86,7 +86,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TrustCenterVendorGraphUpdateMutation",
"name": "CompliancePageVendorListItemMutation",
"selections": [
{
"alias": null,
@@ -109,7 +109,7 @@ return {
{
"args": null,
"kind": "FragmentSpread",
"name": "TrustCenterVendorsCardFragment"
"name": "CompliancePageVendorListItem_vendorFragment"
}
],
"storageKey": null
@@ -125,7 +125,7 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TrustCenterVendorGraphUpdateMutation",
"name": "CompliancePageVendorListItemMutation",
"selections": [
{
"alias": null,
@@ -145,13 +145,6 @@ return {
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -163,14 +156,7 @@ return {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"name": "name",
"storageKey": null
},
{
@@ -195,16 +181,16 @@ return {
]
},
"params": {
"cacheID": "b2d05d661624daec04a081aebf2dd4e2",
"cacheID": "20e9056d9d5d98f50940d4a0c4ff67df",
"id": null,
"metadata": {},
"name": "TrustCenterVendorGraphUpdateMutation",
"name": "CompliancePageVendorListItemMutation",
"operationKind": "mutation",
"text": "mutation TrustCenterVendorGraphUpdateMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n id\n showOnTrustCenter\n ...TrustCenterVendorsCardFragment\n }\n }\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n canUpdate: permission(action: \"core:vendor:update\")\n}\n"
"text": "mutation CompliancePageVendorListItemMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n id\n showOnTrustCenter\n ...CompliancePageVendorListItem_vendorFragment\n }\n }\n}\n\nfragment CompliancePageVendorListItem_vendorFragment on Vendor {\n id\n category\n name\n showOnTrustCenter\n canUpdate: permission(action: \"core:vendor:update\")\n}\n"
}
};
})();
(node as any).hash = "2b4395821fcfbd30bb06448db5429289";
(node as any).hash = "27c5e5e0d77dfbbd57480d6c49d0d87a";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<0138987bbb69846be5f1792b314854e4>>
* @generated SignedSource<<5acd0b499b718393c592fa3d05cf3157>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,26 +11,24 @@
import { ReaderFragment } from 'relay-runtime';
export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL";
import { FragmentRefs } from "relay-runtime";
export type TrustCenterVendorsCardFragment$data = {
export type CompliancePageVendorListItem_vendorFragment$data = {
readonly canUpdate: boolean;
readonly category: VendorCategory;
readonly createdAt: string;
readonly description: string | null | undefined;
readonly id: string;
readonly name: string;
readonly showOnTrustCenter: boolean;
readonly " $fragmentType": "TrustCenterVendorsCardFragment";
readonly " $fragmentType": "CompliancePageVendorListItem_vendorFragment";
};
export type TrustCenterVendorsCardFragment$key = {
readonly " $data"?: TrustCenterVendorsCardFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"TrustCenterVendorsCardFragment">;
export type CompliancePageVendorListItem_vendorFragment$key = {
readonly " $data"?: CompliancePageVendorListItem_vendorFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageVendorListItem_vendorFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "TrustCenterVendorsCardFragment",
"name": "CompliancePageVendorListItem_vendorFragment",
"selections": [
{
"alias": null,
@@ -39,13 +37,6 @@ const node: ReaderFragment = {
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -57,7 +48,7 @@ const node: ReaderFragment = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"name": "name",
"storageKey": null
},
{
@@ -67,13 +58,6 @@ const node: ReaderFragment = {
"name": "showOnTrustCenter",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
@@ -92,6 +76,6 @@ const node: ReaderFragment = {
"abstractKey": null
};
(node as any).hash = "bac8046182ce2402a5a20ce27a25f7e6";
(node as any).hash = "147628fced6667323cce1bf7e81fabfb";
export default node;

View File

@@ -0,0 +1,196 @@
/**
* @generated SignedSource<<e0eb06c2450a1c6748294affef817ea1>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type CompliancePageVendorsPageQuery$variables = {
organizationId: string;
};
export type CompliancePageVendorsPageQuery$data = {
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"CompliancePageVendorListFragment">;
};
};
export type CompliancePageVendorsPageQuery = {
response: CompliancePageVendorsPageQuery$data;
variables: CompliancePageVendorsPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "CompliancePageVendorsPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "CompliancePageVendorListFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "CompliancePageVendorsPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "showOnTrustCenter",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:vendor:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:vendor:update\")"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "vendors(first:100)"
}
],
"type": "Organization",
"abstractKey": null
},
(v2/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "83bd7ff13958372665e18d59f9636d55",
"id": null,
"metadata": {},
"name": "CompliancePageVendorsPageQuery",
"operationKind": "query",
"text": "query CompliancePageVendorsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...CompliancePageVendorListFragment\n id\n }\n}\n\nfragment CompliancePageVendorListFragment on Organization {\n vendors(first: 100) {\n edges {\n node {\n id\n ...CompliancePageVendorListItem_vendorFragment\n }\n }\n }\n}\n\nfragment CompliancePageVendorListItem_vendorFragment on Vendor {\n id\n category\n name\n showOnTrustCenter\n canUpdate: permission(action: \"core:vendor:update\")\n}\n"
}
};
})();
(node as any).hash = "991a68cadfe4984bf303a75dcda66a53";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<2d62f164c109bbc5fa46add3bd2dbf23>>
* @generated SignedSource<<cbab0933c2f9f3fad67a6ac7ffc49a67>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,6 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type TrustCenterGraphQuery$variables = {
organizationId: string;
};
@@ -36,14 +35,6 @@ export type TrustCenterGraphQuery$data = {
readonly ndaFileUrl: string | null | undefined;
readonly updatedAt: string;
} | null | undefined;
readonly vendors?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"TrustCenterVendorsCardFragment">;
};
}>;
};
};
};
export type TrustCenterGraphQuery = {
@@ -113,13 +104,6 @@ v5 = {
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"concreteType": "TrustCenter",
@@ -149,7 +133,13 @@ v7 = {
"name": "ndaFileUrl",
"storageKey": null
},
(v6/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -237,14 +227,7 @@ v7 = {
}
],
"storageKey": null
},
v8 = [
{
"kind": "Literal",
"name": "first",
"value": 100
}
];
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
@@ -267,46 +250,7 @@ return {
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": (v8/*: any*/),
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "TrustCenterVendorsCardFragment"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "vendors(first:100)"
}
(v6/*: any*/)
],
"type": "Organization",
"abstractKey": null
@@ -346,77 +290,7 @@ return {
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": (v8/*: any*/),
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "showOnTrustCenter",
"storageKey": null
},
(v6/*: any*/),
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:vendor:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:vendor:update\")"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "vendors(first:100)"
}
(v6/*: any*/)
],
"type": "Organization",
"abstractKey": null
@@ -427,16 +301,16 @@ return {
]
},
"params": {
"cacheID": "050ad842df04d5130394e7b0a052bf69",
"cacheID": "be28b37377c063e9b63304ee7353e845",
"id": null,
"metadata": {},
"name": "TrustCenterGraphQuery",
"operationKind": "query",
"text": "query TrustCenterGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n canCreateTrustCenterFile: permission(action: \"core:trust-center-file:create\")\n customDomain {\n id\n domain\n }\n trustCenter {\n id\n active\n ndaFileName\n ndaFileUrl\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:trust-center:update\")\n canGetNDA: permission(action: \"core:trust-center:get-nda\")\n canUploadNDA: permission(action: \"core:trust-center:upload-nda\")\n canDeleteNDA: permission(action: \"core:trust-center:delete-nda\")\n canCreateReference: permission(action: \"core:trust-center-reference:create\")\n canCreateAccess: permission(action: \"core:trust-center-access:create\")\n }\n vendors(first: 100) {\n edges {\n node {\n id\n ...TrustCenterVendorsCardFragment\n }\n }\n }\n }\n id\n }\n}\n\nfragment TrustCenterVendorsCardFragment on Vendor {\n id\n name\n category\n description\n showOnTrustCenter\n createdAt\n canUpdate: permission(action: \"core:vendor:update\")\n}\n"
"text": "query TrustCenterGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n canCreateTrustCenterFile: permission(action: \"core:trust-center-file:create\")\n customDomain {\n id\n domain\n }\n trustCenter {\n id\n active\n ndaFileName\n ndaFileUrl\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:trust-center:update\")\n canGetNDA: permission(action: \"core:trust-center:get-nda\")\n canUploadNDA: permission(action: \"core:trust-center:upload-nda\")\n canDeleteNDA: permission(action: \"core:trust-center:delete-nda\")\n canCreateReference: permission(action: \"core:trust-center-reference:create\")\n canCreateAccess: permission(action: \"core:trust-center-access:create\")\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "4804fdce46976e5597cb1fa263686b8e";
(node as any).hash = "9e6d6626e164fd2e3ea553d3f51eb34c";
export default node;

View File

@@ -1,151 +0,0 @@
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
Badge,
Button,
IconCheckmark1,
IconChevronDown,
IconCrossLargeX,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@probo/ui";
import { useMemo, useState } from "react";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { TrustCenterVendorsCardFragment$key } from "#/__generated__/core/TrustCenterVendorsCardFragment.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";
const trustCenterVendorFragment = graphql`
fragment TrustCenterVendorsCardFragment on Vendor {
id
name
category
description
showOnTrustCenter
createdAt
canUpdate: permission(action: "core:vendor:update")
}
`;
type Mutation<Params> = (p: {
variables: {
input: {
id: string;
showOnTrustCenter: boolean;
} & Params;
};
}) => Promise<void>;
type Props<Params> = {
vendors: TrustCenterVendorsCardFragment$key[];
params: Params;
disabled?: boolean;
onToggleVisibility: Mutation<Params>;
};
export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
const { __ } = useTranslate();
const [limit, setLimit] = useState<number | null>(100);
const vendors = useMemo(() => {
return limit ? props.vendors.slice(0, limit) : props.vendors;
}, [props.vendors, limit]);
const showMoreButton = limit !== null && props.vendors.length > limit;
const onToggleVisibility = async (vendorId: string, showOnTrustCenter: boolean) => {
await props.onToggleVisibility({
variables: {
input: {
id: vendorId,
showOnTrustCenter,
...props.params,
},
},
});
};
return (
<div className="space-y-[10px]">
<Table>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("Category")}</Th>
<Th>{__("Visibility")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{vendors.length === 0 && (
<Tr>
<Td colSpan={4} className="text-center text-txt-secondary">
{__("No vendors available")}
</Td>
</Tr>
)}
{vendors.map((vendor, index) => (
<VendorRow
key={index}
vendor={vendor}
onToggleVisibility={onToggleVisibility}
disabled={props.disabled}
/>
))}
</Tbody>
</Table>
{showMoreButton && (
<Button
variant="tertiary"
onClick={() => setLimit(null)}
className="mt-3 mx-auto"
icon={IconChevronDown}
>
{sprintf(__("Show %s more"), props.vendors.length - limit)}
</Button>
)}
</div>
);
}
function VendorRow(props: {
vendor: TrustCenterVendorsCardFragment$key;
onToggleVisibility: (vendorId: string, showOnTrustCenter: boolean) => Promise<void>;
disabled?: boolean;
}) {
const vendor = useFragment(trustCenterVendorFragment, props.vendor);
const organizationId = useOrganizationId();
const { __ } = useTranslate();
return (
<Tr to={`/organizations/${organizationId}/vendors/${vendor.id}/overview`}>
<Td>
<div className="flex gap-4 items-center">{vendor.name}</div>
</Td>
<Td>
<Badge variant="neutral">{vendor.category}</Badge>
</Td>
<Td>
<Badge variant={vendor.showOnTrustCenter ? "success" : "danger"}>
{vendor.showOnTrustCenter ? __("Visible") : __("None")}
</Badge>
</Td>
<Td noLink width={100} className="text-end">
{vendor.canUpdate && (
<Button
variant="secondary"
onClick={() =>
void props.onToggleVisibility(vendor.id, !vendor.showOnTrustCenter)}
icon={vendor.showOnTrustCenter ? IconCrossLargeX : IconCheckmark1}
disabled={props.disabled}
>
{vendor.showOnTrustCenter ? __("Hide") : __("Show")}
</Button>
)}
</Td>
</Tr>
);
}

View File

@@ -32,14 +32,6 @@ export const trustCenterQuery = graphql`
)
canCreateAccess: permission(action: "core:trust-center-access:create")
}
vendors(first: 100) {
edges {
node {
id
...TrustCenterVendorsCardFragment
}
}
}
}
}
}

View File

@@ -1,29 +0,0 @@
import { useTranslate } from "@probo/i18n";
import { graphql } from "relay-runtime";
import type { TrustCenterVendorGraphUpdateMutation } from "#/__generated__/core/TrustCenterVendorGraphUpdateMutation.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
export const trustCenterVendorUpdateMutation = graphql`
mutation TrustCenterVendorGraphUpdateMutation($input: UpdateVendorInput!) {
updateVendor(input: $input) {
vendor {
id
showOnTrustCenter
...TrustCenterVendorsCardFragment
}
}
}
`;
export function useTrustCenterVendorUpdate() {
const { __ } = useTranslate();
return useMutationWithToasts<TrustCenterVendorGraphUpdateMutation>(
trustCenterVendorUpdateMutation,
{
successMessage: __("Vendor visibility updated successfully."),
errorMessage: __("Failed to update vendor visibility"),
},
);
}

View File

@@ -34,6 +34,11 @@ export const compliancePageRoutes = [
Fallback: LinkCardSkeleton,
Component: lazy(() => import("#/pages/organizations/compliance-page/files/CompliancePageFilesPageLoader")),
},
{
path: "vendors",
Fallback: LinkCardSkeleton,
Component: lazy(() => import("#/pages/organizations/compliance-page/vendors/CompliancePageVendorsPageLoader")),
},
],
},
];

View File

@@ -0,0 +1,42 @@
import { useTranslate } from "@probo/i18n";
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
import type { CompliancePageVendorsPageQuery } from "#/__generated__/core/CompliancePageVendorsPageQuery.graphql";
import { CompliancePageVendorList } from "./_components/CompliancePageVendorList";
export const compliancePageVendorsPageQuery = graphql`
query CompliancePageVendorsPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
...CompliancePageVendorListFragment
}
}
`;
export function CompliancePageVendorsPage(props: {
queryRef: PreloadedQuery<CompliancePageVendorsPageQuery>;
}) {
const { queryRef } = props;
const { __ } = useTranslate();
const { organization } = usePreloadedQuery<CompliancePageVendorsPageQuery>(
compliancePageVendorsPageQuery,
queryRef,
);
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-base font-medium">{__("Vendors")}</h3>
<p className="text-sm text-txt-tertiary">
{__("Manage vendor assessments and third-party risk information")}
</p>
</div>
</div>
<CompliancePageVendorList fragmentRef={organization} />
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import type { CompliancePageVendorsPageQuery } from "#/__generated__/core/CompliancePageVendorsPageQuery.graphql";
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CoreRelayProvider } from "#/providers/CoreRelayProvider";
import { CompliancePageVendorsPage, compliancePageVendorsPageQuery } from "./CompliancePageVendorsPage";
function CompliancePageVendorsPageQueryLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<CompliancePageVendorsPageQuery>(compliancePageVendorsPageQuery);
useEffect(() => {
if (!queryRef) {
loadQuery({ organizationId });
}
});
if (!queryRef) return <LinkCardSkeleton />;
return <CompliancePageVendorsPage queryRef={queryRef} />;
}
export default function CompliancePageVendorsPageLoader() {
return (
<CoreRelayProvider>
<CompliancePageVendorsPageQueryLoader />
</CoreRelayProvider>
);
}

View File

@@ -0,0 +1,59 @@
import { useTranslate } from "@probo/i18n";
import { Table, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { CompliancePageVendorListFragment$key } from "#/__generated__/core/CompliancePageVendorListFragment.graphql";
import { CompliancePageVendorListItem } from "./CompliancePageVendorListItem";
const fragment = graphql`
fragment CompliancePageVendorListFragment on Organization {
vendors(first: 100) {
edges {
node {
id
...CompliancePageVendorListItem_vendorFragment
}
}
}
}
`;
export function CompliancePageVendorList(props: { fragmentRef: CompliancePageVendorListFragment$key }) {
const { fragmentRef } = props;
const { __ } = useTranslate();
const { vendors } = useFragment<CompliancePageVendorListFragment$key>(fragment, fragmentRef);
return (
<div className="space-y-[10px]">
<Table>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("Category")}</Th>
<Th>{__("Visibility")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{vendors.edges.length === 0 && (
<Tr>
<Td colSpan={4} className="text-center text-txt-secondary">
{__("No vendors available")}
</Td>
</Tr>
)}
{vendors.edges.map(({ node: vendor }) => (
<CompliancePageVendorListItem
key={vendor.id}
vendorFragmentRef={vendor}
/>
))}
</Tbody>
</Table>
</div>
);
}

View File

@@ -0,0 +1,90 @@
import { useTranslate } from "@probo/i18n";
import { Badge, Button, IconCheckmark1, IconCrossLargeX, Td, Tr } from "@probo/ui";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { CompliancePageVendorListItem_vendorFragment$key } from "#/__generated__/core/CompliancePageVendorListItem_vendorFragment.graphql";
import type { CompliancePageVendorListItemMutation } from "#/__generated__/core/CompliancePageVendorListItemMutation.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
const vendorFragment = graphql`
fragment CompliancePageVendorListItem_vendorFragment on Vendor {
id
category
name
showOnTrustCenter
canUpdate: permission(action: "core:vendor:update")
}
`;
const updateVendorVisibilityMutation = graphql`
mutation CompliancePageVendorListItemMutation($input: UpdateVendorInput!) {
updateVendor(input: $input) {
vendor {
id
showOnTrustCenter
...CompliancePageVendorListItem_vendorFragment
}
}
}
`;
export function CompliancePageVendorListItem(props: {
vendorFragmentRef: CompliancePageVendorListItem_vendorFragment$key;
}) {
const { vendorFragmentRef } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const vendor = useFragment<CompliancePageVendorListItem_vendorFragment$key>(
vendorFragment,
vendorFragmentRef,
);
const [updateVendorVisibility, isUpadtingVendorVisibility] = useMutationWithToasts<
CompliancePageVendorListItemMutation
>(
updateVendorVisibilityMutation,
{
successMessage: __("Vendor visibility updated successfully."),
errorMessage: __("Failed to update vendor visibility"),
},
);
return (
<Tr to={`/organizations/${organizationId}/vendors/${vendor.id}/overview`}>
<Td>
<div className="flex gap-4 items-center">{vendor.name}</div>
</Td>
<Td>
<Badge variant="neutral">{vendor.category}</Badge>
</Td>
<Td>
<Badge variant={vendor.showOnTrustCenter ? "success" : "danger"}>
{vendor.showOnTrustCenter ? __("Visible") : __("None")}
</Badge>
</Td>
<Td noLink width={100} className="text-end">
{vendor.canUpdate && (
<Button
variant="secondary"
onClick={() =>
void updateVendorVisibility({
variables: {
input: {
id: vendor.id,
showOnTrustCenter: !vendor.showOnTrustCenter,
},
},
})}
icon={vendor.showOnTrustCenter ? IconCrossLargeX : IconCheckmark1}
disabled={isUpadtingVendorVisibility}
>
{vendor.showOnTrustCenter ? __("Hide") : __("Show")}
</Button>
)}
</Td>
</Tr>
);
};

View File

@@ -1,46 +0,0 @@
import { useTranslate } from "@probo/i18n";
import { Spinner } from "@probo/ui";
import { useOutletContext } from "react-router";
import type { TrustCenterVendorsCardFragment$key } from "#/__generated__/core/TrustCenterVendorsCardFragment.graphql";
import { TrustCenterVendorsCard } from "#/components/trustCenter/TrustCenterVendorsCard";
import { useTrustCenterVendorUpdate } from "#/hooks/graph/TrustCenterVendorGraph";
type ContextType = {
organization: {
vendors?: {
edges: Array<{
node: TrustCenterVendorsCardFragment$key;
}>;
};
};
};
export default function TrustCenterVendorsTab() {
const { __ } = useTranslate();
const { organization } = useOutletContext<ContextType>();
const [updateVendorVisibility, isUpdatingVendors]
= useTrustCenterVendorUpdate();
const vendors = organization.vendors?.edges?.map(edge => edge.node) || [];
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-base font-medium">{__("Vendors")}</h3>
<p className="text-sm text-txt-tertiary">
{__("Manage vendor assessments and third-party risk information")}
</p>
</div>
{isUpdatingVendors && <Spinner />}
</div>
<TrustCenterVendorsCard
vendors={vendors}
params={{}}
disabled={isUpdatingVendors}
onToggleVisibility={updateVendorVisibility}
/>
</div>
);
}

View File

@@ -29,14 +29,6 @@ export const trustCenterRoutes = [
lazy(() => import("#/pages/organizations/trustCenter/TrustCenterPage")),
),
children: [
{
path: "vendors",
Fallback: LinkCardSkeleton,
Component: lazy(
() =>
import("#/pages/organizations/trustCenter/TrustCenterVendorsTab"),
),
},
{
path: "access",
Fallback: LinkCardSkeleton,