Rename vendors to third parties

Renames the user-facing 'vendor' concept to 'third party' across the
entire codebase. The shared common_third_parties reference table is
unchanged.

Migration. Renames the vendor_category enum, the vendors and
vendor_<entity> tables (contacts, services, compliance_reports,
business_associate_agreements, data_privacy_agreements,
risk_assessments) and their vendor_id columns, the asset_vendors /
data_vendors / processing_activity_vendors junction tables,
generated_documents.vendors_document_id, the webhook_event_type
'vendor:<verb>' values, and the snapshots_type 'VENDORS' value.

Backend. Renames coredata models and SQL queries, probo services,
GraphQL / MCP API surface, console / trust / webhook resolvers and
types, the CLI (prb vendor* -> prb third-party*; pkg/cmd/vendormgmt
-> pkg/cmd/thirdpartymgmt), the document generator, vetting agent
prompts, and the common-third-parties-import command.

Frontend, packages, n8n, e2e. Renames apps/console pages, components,
hooks, routes, dialogs, and tabs; the shared @probo/vendors package
(now @probo/third-parties); the @probo/ui Vendors atoms (now
ThirdParties, VendorLogo -> ThirdPartyLogo); the n8n community node
actions/vendor folder (now actions/thirdParty); and the e2e Go test
suite (console and MCP). Filesystem and URL paths use kebab-case
(third-parties), GraphQL fields and TypeScript identifiers use
camelCase (thirdParty / thirdParties), Go types use PascalCase
(ThirdParty), and human-facing text uses 'third party' with a space.

Co-authored-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-05-13 16:15:33 +02:00
parent 9eed0d71c8
commit eecbe4c46c
281 changed files with 8491 additions and 8425 deletions

View File

@@ -21,7 +21,7 @@
"@probo/relay": "^1.0.0", "@probo/relay": "^1.0.0",
"@probo/routes": "^1.0.0", "@probo/routes": "^1.0.0",
"@probo/ui": "1.0.0", "@probo/ui": "1.0.0",
"@probo/vendors": "0.0.1", "@probo/third-parties": "0.0.1",
"@tanstack/react-query": "^5.76.1", "@tanstack/react-query": "^5.76.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"react": "^19.1.0", "react": "^19.1.0",

View File

@@ -48,7 +48,7 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
import { EditableTable } from "../table/EditableTable"; import { EditableTable } from "../table/EditableTable";
import { PeopleCell } from "../table/PeopleCell"; import { PeopleCell } from "../table/PeopleCell";
import { VendorsCell } from "../table/VendorsCell"; import { ThirdPartiesCell } from "../table/ThirdPartiesCell";
type Props = { type Props = {
connectionId: string; connectionId: string;
@@ -65,7 +65,7 @@ const schema = z.object({
amount: z.coerce.number().min(1, "Amount is required"), amount: z.coerce.number().min(1, "Amount is required"),
assetType: z.enum(["PHYSICAL", "VIRTUAL"]), assetType: z.enum(["PHYSICAL", "VIRTUAL"]),
ownerId: z.string().trim().min(1, "Owner is required"), ownerId: z.string().trim().min(1, "Owner is required"),
vendorIds: z.array(z.string()).optional(), thirdPartyIds: z.array(z.string()).optional(),
dataTypesStored: z.string().trim().min(1, "Data types stored is required"), dataTypesStored: z.string().trim().min(1, "Data types stored is required"),
organizationId: z.string().trim().min(1, "Organization is required"), organizationId: z.string().trim().min(1, "Organization is required"),
}); });
@@ -75,7 +75,7 @@ const defaultValue = {
amount: 0, amount: 0,
assetType: "VIRTUAL", assetType: "VIRTUAL",
ownerId: "", ownerId: "",
vendorIds: [], thirdPartyIds: [],
dataTypesStored: "", dataTypesStored: "",
organizationId: "", organizationId: "",
} satisfies z.infer<typeof schema>; } satisfies z.infer<typeof schema>;
@@ -99,7 +99,7 @@ export function AssetsTable(props: Props) {
__("Data Types stored"), __("Data Types stored"),
__("Amount"), __("Amount"),
__("Owner"), __("Owner"),
__("Vendors"), __("Third parties"),
]} ]}
schema={schema} schema={schema}
updateMutation={updateAssetMutation} updateMutation={updateAssetMutation}
@@ -154,10 +154,10 @@ export function AssetsTable(props: Props) {
defaultValue={item?.owner} defaultValue={item?.owner}
organizationId={organizationId} organizationId={organizationId}
/> />
<VendorsCell <ThirdPartiesCell
name="vendorIds" name="thirdPartyIds"
organizationId={organizationId} organizationId={organizationId}
defaultValue={item?.vendors?.edges?.map(edge => edge.node) ?? []} defaultValue={item?.thirdParties?.edges?.map(edge => edge.node) ?? []}
/> />
</> </>
)} )}

View File

@@ -50,7 +50,7 @@ export function ReadOnlyAssetsTable(props: Props) {
<Th>{__("Type")}</Th> <Th>{__("Type")}</Th>
<Th>{__("Amount")}</Th> <Th>{__("Amount")}</Th>
<Th>{__("Owner")}</Th> <Th>{__("Owner")}</Th>
<Th>{__("Vendors")}</Th> <Th>{__("Third parties")}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -65,7 +65,7 @@ export function ReadOnlyAssetsTable(props: Props) {
function AssetRow({ entry }: { entry: AssetEntry }) { function AssetRow({ entry }: { entry: AssetEntry }) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const vendors = entry.vendors?.edges.map(edge => edge.node) ?? []; const thirdParties = entry.thirdParties?.edges.map(edge => edge.node) ?? [];
return ( return (
<Tr to={`/organizations/${organizationId}/assets/${entry.id}`}> <Tr to={`/organizations/${organizationId}/assets/${entry.id}`}>
@@ -78,27 +78,27 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
<Td>{entry.amount}</Td> <Td>{entry.amount}</Td>
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td> <Td>{entry.owner?.fullName ?? __("Unassigned")}</Td>
<Td> <Td>
{vendors.length > 0 {thirdParties.length > 0
? ( ? (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{vendors.slice(0, 3).map(vendor => ( {thirdParties.slice(0, 3).map(thirdParty => (
<Badge <Badge
key={vendor.id} key={thirdParty.id}
variant="neutral" variant="neutral"
className="flex items-center gap-1" className="flex items-center gap-1"
> >
<Avatar <Avatar
name={vendor.name} name={thirdParty.name}
src={faviconUrl(vendor.websiteUrl)} src={faviconUrl(thirdParty.websiteUrl)}
size="s" size="s"
/> />
<span className="text-xs">{vendor.name}</span> <span className="text-xs">{thirdParty.name}</span>
</Badge> </Badge>
))} ))}
{vendors.length > 3 && ( {thirdParties.length > 3 && (
<Badge variant="neutral" className="text-xs"> <Badge variant="neutral" className="text-xs">
+ +
{vendors.length - 3} {thirdParties.length - 3}
</Badge> </Badge>
)} )}
</div> </div>

View File

@@ -18,9 +18,9 @@ import { Avatar, Badge, Button, Field, IconCrossLargeX, Option, Select } from "@
import { type ComponentProps, Suspense, useState } from "react"; import { type ComponentProps, Suspense, useState } from "react";
import { type Control, Controller, type FieldValues, type Path } from "react-hook-form"; import { type Control, Controller, type FieldValues, type Path } from "react-hook-form";
import { useVendors } from "#/hooks/graph/VendorGraph"; import { useThirdParties } from "#/hooks/graph/ThirdPartyGraph";
type Vendor = { type ThirdParty = {
id: string; id: string;
name: string; name: string;
websiteUrl: string | null | undefined; websiteUrl: string | null | undefined;
@@ -32,13 +32,13 @@ type Props<T extends FieldValues = FieldValues> = {
name: string; name: string;
label?: string; label?: string;
error?: string; error?: string;
selectedVendors?: Vendor[]; selectedThirdParties?: ThirdParty[];
} & ComponentProps<typeof Field>; } & ComponentProps<typeof Field>;
export function VendorsMultiSelectField<T extends FieldValues = FieldValues>({ export function ThirdPartiesMultiSelectField<T extends FieldValues = FieldValues>({
organizationId, organizationId,
control, control,
selectedVendors = [], selectedThirdParties = [],
...props ...props
}: Props<T>) { }: Props<T>) {
return ( return (
@@ -46,31 +46,31 @@ export function VendorsMultiSelectField<T extends FieldValues = FieldValues>({
<Suspense <Suspense
fallback={<Select variant="editor" disabled placeholder="Loading..." />} fallback={<Select variant="editor" disabled placeholder="Loading..." />}
> >
<VendorsMultiSelectWithQuery <ThirdPartiesMultiSelectWithQuery
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name={props.name} name={props.name}
disabled={props.disabled} disabled={props.disabled}
selectedVendors={selectedVendors} selectedThirdParties={selectedThirdParties}
/> />
</Suspense> </Suspense>
</Field> </Field>
); );
} }
function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>( function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedVendors">, props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedThirdParties">,
) { ) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { name, organizationId, control, selectedVendors = [] } = props; const { name, organizationId, control, selectedThirdParties = [] } = props;
const vendors = useVendors(organizationId); const thirdParties = useThirdParties(organizationId);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const allVendors = [...vendors]; const allThirdParties = [...thirdParties];
if (props.disabled) { if (props.disabled) {
selectedVendors.forEach((selectedVendor) => { selectedThirdParties.forEach((selectedThirdParty) => {
if (!allVendors.find(v => v.id === selectedVendor.id)) { if (!allThirdParties.find(v => v.id === selectedThirdParty.id)) {
allVendors.push(selectedVendor); allThirdParties.push(selectedThirdParty);
} }
}); });
} }
@@ -81,49 +81,49 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
control={control} control={control}
name={name as Path<T>} name={name as Path<T>}
render={({ field }) => { render={({ field }) => {
const selectedVendorIds = (Array.isArray(field.value) ? field.value : []) as string[]; const selectedThirdPartyIds = (Array.isArray(field.value) ? field.value : []) as string[];
const selectedVendors = allVendors.filter(v => selectedVendorIds.includes(v.id)); const selectedThirdParties = allThirdParties.filter(v => selectedThirdPartyIds.includes(v.id));
const availableVendors = allVendors.filter(v => !selectedVendorIds.includes(v.id)); const availableThirdParties = allThirdParties.filter(v => !selectedThirdPartyIds.includes(v.id));
const handleAddVendor = (vendorId: string) => { const handleAddThirdParty = (thirdPartyId: string) => {
const newValue = [...selectedVendorIds, vendorId]; const newValue = [...selectedThirdPartyIds, thirdPartyId];
field.onChange(newValue); field.onChange(newValue);
setIsOpen(false); setIsOpen(false);
}; };
const handleRemoveVendor = (vendorId: string) => { const handleRemoveThirdParty = (thirdPartyId: string) => {
const newValue = selectedVendorIds.filter((id: string) => id !== vendorId); const newValue = selectedThirdPartyIds.filter((id: string) => id !== thirdPartyId);
field.onChange(newValue); field.onChange(newValue);
}; };
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{availableVendors.length > 0 && !props.disabled && ( {availableThirdParties.length > 0 && !props.disabled && (
<Select <Select
disabled={props.disabled} disabled={props.disabled}
id={name} id={name}
variant="editor" variant="editor"
placeholder={__("Add vendors...")} placeholder={__("Add third parties...")}
onValueChange={handleAddVendor} onValueChange={handleAddThirdParty}
key={`${selectedVendorIds.length}-${vendors.length}`} key={`${selectedThirdPartyIds.length}-${thirdParties.length}`}
className="w-full" className="w-full"
value="" value=""
open={isOpen} open={isOpen}
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
> >
{availableVendors.map(vendor => ( {availableThirdParties.map(thirdParty => (
<Option key={vendor.id} value={vendor.id} className="flex gap-2"> <Option key={thirdParty.id} value={thirdParty.id} className="flex gap-2">
<Avatar <Avatar
name={vendor.name} name={thirdParty.name}
src={faviconUrl(vendor.websiteUrl)} src={faviconUrl(thirdParty.websiteUrl)}
size="s" size="s"
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<span>{vendor.name}</span> <span>{thirdParty.name}</span>
{vendor.websiteUrl && ( {thirdParty.websiteUrl && (
<span className="text-xs text-txt-secondary"> <span className="text-xs text-txt-secondary">
{vendor.websiteUrl} {thirdParty.websiteUrl}
</span> </span>
)} )}
</div> </div>
@@ -132,21 +132,21 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
</Select> </Select>
)} )}
{selectedVendors.length > 0 && ( {selectedThirdParties.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{selectedVendors.map(vendor => ( {selectedThirdParties.map(thirdParty => (
<Badge key={vendor.id} variant="neutral" className="flex items-center gap-2"> <Badge key={thirdParty.id} variant="neutral" className="flex items-center gap-2">
<Avatar <Avatar
name={vendor.name} name={thirdParty.name}
src={faviconUrl(vendor.websiteUrl)} src={faviconUrl(thirdParty.websiteUrl)}
size="s" size="s"
/> />
<span>{vendor.name}</span> <span>{thirdParty.name}</span>
{!props.disabled && ( {!props.disabled && (
<Button <Button
variant="tertiary" variant="tertiary"
icon={IconCrossLargeX} icon={IconCrossLargeX}
onClick={() => handleRemoveVendor(vendor.id)} onClick={() => handleRemoveThirdParty(thirdParty.id)}
className="h-4 w-4 p-0 hover:bg-transparent" className="h-4 w-4 p-0 hover:bg-transparent"
/> />
)} )}
@@ -155,9 +155,9 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
</div> </div>
)} )}
{selectedVendors.length === 0 && availableVendors.length === 0 && ( {selectedThirdParties.length === 0 && availableThirdParties.length === 0 && (
<div className="text-sm text-txt-secondary py-2"> <div className="text-sm text-txt-secondary py-2">
{__("No vendors available")} {__("No third parties available")}
</div> </div>
)} )}
</div> </div>

View File

@@ -15,11 +15,11 @@
import { faviconUrl } from "@probo/helpers"; import { faviconUrl } from "@probo/helpers";
import { Avatar, Badge, IconCrossLargeX } from "@probo/ui"; import { Avatar, Badge, IconCrossLargeX } from "@probo/ui";
import type { VendorGraphSelectQuery } from "#/__generated__/core/VendorGraphSelectQuery.graphql"; import type { ThirdPartyGraphSelectQuery } from "#/__generated__/core/ThirdPartyGraphSelectQuery.graphql";
import { GraphQLCell } from "#/components/table/GraphQLCell"; import { GraphQLCell } from "#/components/table/GraphQLCell";
import { vendorsSelectQuery } from "#/hooks/graph/VendorGraph"; import { thirdPartiesSelectQuery } from "#/hooks/graph/ThirdPartyGraph";
type Vendor = { type ThirdParty = {
id: string; id: string;
name: string; name: string;
websiteUrl: string | null | undefined; websiteUrl: string | null | undefined;
@@ -27,47 +27,47 @@ type Vendor = {
type Props = { type Props = {
name: string; name: string;
defaultValue?: Vendor[]; defaultValue?: ThirdParty[];
organizationId: string; organizationId: string;
}; };
const empty = [] as Vendor[]; const empty = [] as ThirdParty[];
export function VendorsCell(props: Props) { export function ThirdPartiesCell(props: Props) {
return ( return (
<GraphQLCell<VendorGraphSelectQuery, Vendor> <GraphQLCell<ThirdPartyGraphSelectQuery, ThirdParty>
multiple multiple
name={props.name} name={props.name}
query={vendorsSelectQuery} query={thirdPartiesSelectQuery}
variables={{ variables={{
organizationId: props.organizationId, organizationId: props.organizationId,
}} }}
items={data => items={data =>
data.organization?.vendors?.edges?.map(edge => edge.node) ?? []} data.organization?.thirdParties?.edges?.map(edge => edge.node) ?? []}
itemRenderer={({ item, onRemove }) => ( itemRenderer={({ item, onRemove }) => (
<VendorBadge vendor={item} onRemove={onRemove} /> <ThirdPartyBadge thirdParty={item} onRemove={onRemove} />
)} )}
defaultValue={props.defaultValue ?? empty} defaultValue={props.defaultValue ?? empty}
/> />
); );
} }
function VendorBadge({ function ThirdPartyBadge({
vendor, thirdParty,
onRemove, onRemove,
}: { }: {
vendor: Vendor; thirdParty: ThirdParty;
onRemove?: (v: Vendor) => void; onRemove?: (v: ThirdParty) => void;
}) { }) {
return ( return (
<Badge variant="neutral" className="flex items-center gap-1"> <Badge variant="neutral" className="flex items-center gap-1">
<Avatar name={vendor.name} src={faviconUrl(vendor.websiteUrl)} size="s" /> <Avatar name={thirdParty.name} src={faviconUrl(thirdParty.websiteUrl)} size="s" />
<span className="max-w-[100px] text-ellipsis overflow-hidden min-w-0 block"> <span className="max-w-[100px] text-ellipsis overflow-hidden min-w-0 block">
{vendor.name} {thirdParty.name}
</span> </span>
{onRemove && ( {onRemove && (
<button <button
onClick={() => onRemove(vendor)} onClick={() => onRemove(thirdParty)}
className="size-4 hover:text-txt-primary cursor-pointer" className="size-4 hover:text-txt-primary cursor-pointer"
type="button" type="button"
> >

View File

@@ -18,7 +18,7 @@ import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { z } from "zod"; import { z } from "zod";
import type { useVendorFormFragment$key } from "#/__generated__/core/useVendorFormFragment.graphql"; import type { useThirdPartyFormFragment$key } from "#/__generated__/core/useThirdPartyFormFragment.graphql";
import { useFormWithSchema } from "../useFormWithSchema"; import { useFormWithSchema } from "../useFormWithSchema";
import { useMutationWithToasts } from "../useMutationWithToasts"; import { useMutationWithToasts } from "../useMutationWithToasts";
@@ -43,8 +43,8 @@ const schema = z.object({
securityOwnerId: z.string().nullish(), securityOwnerId: z.string().nullish(),
}); });
const vendorFormFragment = graphql` const thirdPartyFormFragment = graphql`
fragment useVendorFormFragment on Vendor { fragment useThirdPartyFormFragment on ThirdParty {
id id
name name
description description
@@ -70,46 +70,46 @@ const vendorFormFragment = graphql`
} }
`; `;
const vendorUpdateQuery = graphql` const thirdPartyUpdateQuery = graphql`
mutation useVendorFormMutation($input: UpdateVendorInput!) { mutation useThirdPartyFormMutation($input: UpdateThirdPartyInput!) {
updateVendor(input: $input) { updateThirdParty(input: $input) {
vendor { thirdParty {
...useVendorFormFragment ...useThirdPartyFormFragment
} }
} }
} }
`; `;
export function useVendorForm(vendorKey: useVendorFormFragment$key) { export function useThirdPartyForm(thirdPartyKey: useThirdPartyFormFragment$key) {
const vendor = useFragment(vendorFormFragment, vendorKey); const thirdParty = useFragment(thirdPartyFormFragment, thirdPartyKey);
const { __ } = useTranslate(); const { __ } = useTranslate();
const [mutate] = useMutationWithToasts(vendorUpdateQuery, { const [mutate] = useMutationWithToasts(thirdPartyUpdateQuery, {
successMessage: __("Vendor updated successfully."), successMessage: __("Third party updated successfully."),
errorMessage: __("Failed to update vendor"), errorMessage: __("Failed to update third party"),
}); });
const defaultValues = useMemo( const defaultValues = useMemo(
() => ({ () => ({
name: vendor.name, name: thirdParty.name,
description: vendor.description || null, description: thirdParty.description || null,
category: vendor.category || null, category: thirdParty.category || null,
statusPageUrl: vendor.statusPageUrl || null, statusPageUrl: thirdParty.statusPageUrl || null,
termsOfServiceUrl: vendor.termsOfServiceUrl || null, termsOfServiceUrl: thirdParty.termsOfServiceUrl || null,
privacyPolicyUrl: vendor.privacyPolicyUrl || null, privacyPolicyUrl: thirdParty.privacyPolicyUrl || null,
serviceLevelAgreementUrl: vendor.serviceLevelAgreementUrl || null, serviceLevelAgreementUrl: thirdParty.serviceLevelAgreementUrl || null,
dataProcessingAgreementUrl: vendor.dataProcessingAgreementUrl || null, dataProcessingAgreementUrl: thirdParty.dataProcessingAgreementUrl || null,
websiteUrl: vendor.websiteUrl || null, websiteUrl: thirdParty.websiteUrl || null,
legalName: vendor.legalName || null, legalName: thirdParty.legalName || null,
headquarterAddress: vendor.headquarterAddress || null, headquarterAddress: thirdParty.headquarterAddress || null,
certifications: [...(vendor.certifications ?? [])], certifications: [...(thirdParty.certifications ?? [])],
countries: [...(vendor.countries ?? [])], countries: [...(thirdParty.countries ?? [])],
securityPageUrl: vendor.securityPageUrl || null, securityPageUrl: thirdParty.securityPageUrl || null,
trustPageUrl: vendor.trustPageUrl || null, trustPageUrl: thirdParty.trustPageUrl || null,
businessOwnerId: vendor.businessOwner?.id, businessOwnerId: thirdParty.businessOwner?.id,
securityOwnerId: vendor.securityOwner?.id, securityOwnerId: thirdParty.securityOwner?.id,
}), }),
[vendor], [thirdParty],
); );
const form = useFormWithSchema(schema, { const form = useFormWithSchema(schema, {
@@ -120,7 +120,7 @@ export function useVendorForm(vendorKey: useVendorFormFragment$key) {
return mutate({ return mutate({
variables: { variables: {
input: { input: {
id: vendor.id, id: thirdParty.id,
...data, ...data,
description: data.description || null, description: data.description || null,
statusPageUrl: data.statusPageUrl || null, statusPageUrl: data.statusPageUrl || null,

View File

@@ -58,7 +58,7 @@ export const assetNodeQuery = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -94,7 +94,7 @@ export const createAssetMutation = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -125,7 +125,7 @@ export const updateAssetMutation = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -196,7 +196,7 @@ export const useCreateAsset = (connectionId: string) => {
assetType: AssetType; assetType: AssetType;
ownerId: string; ownerId: string;
organizationId: string; organizationId: string;
vendorIds?: string[]; thirdPartyIds?: string[];
dataTypesStored: string; dataTypesStored: string;
}) => { }) => {
if (!input.name?.trim()) { if (!input.name?.trim()) {
@@ -223,7 +223,7 @@ export const useCreateAsset = (connectionId: string) => {
dataTypesStored: input.dataTypesStored || "", dataTypesStored: input.dataTypesStored || "",
ownerId: input.ownerId, ownerId: input.ownerId,
organizationId: input.organizationId, organizationId: input.organizationId,
vendorIds: input.vendorIds || [], thirdPartyIds: input.thirdPartyIds || [],
}, },
connections: [connectionId], connections: [connectionId],
}, },
@@ -244,7 +244,7 @@ export const useUpdateAsset = () => {
assetType?: AssetType; assetType?: AssetType;
dataTypesStored?: string; dataTypesStored?: string;
ownerId?: string; ownerId?: string;
vendorIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update asset: asset ID is required")); return alert(__("Failed to update asset: asset ID is required"));

View File

@@ -49,7 +49,7 @@ export const datumNodeQuery = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -86,7 +86,7 @@ export const createDatumMutation = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -115,7 +115,7 @@ export const updateDatumMutation = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -186,7 +186,7 @@ export const useCreateDatum = (connectionId: string) => {
dataClassification: string; dataClassification: string;
ownerId: string; ownerId: string;
organizationId: string; organizationId: string;
vendorIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.name?.trim()) { if (!input.name?.trim()) {
return alert(__("Failed to create data: name is required")); return alert(__("Failed to create data: name is required"));
@@ -217,7 +217,7 @@ export const useUpdateDatum = () => {
name?: string; name?: string;
dataClassification?: string; dataClassification?: string;
ownerId?: string; ownerId?: string;
vendorIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update data: missing id")); return alert(__("Failed to update data: missing id"));

View File

@@ -97,7 +97,7 @@ export const processingActivityNodeQuery = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -189,7 +189,7 @@ export const createProcessingActivityMutation = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -236,7 +236,7 @@ export const updateProcessingActivityMutation = graphql`
id id
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -322,7 +322,7 @@ export const useCreateProcessingActivity = (connectionId?: string) => {
nextReviewDate?: string; nextReviewDate?: string;
role: string; role: string;
dataProtectionOfficerId?: string; dataProtectionOfficerId?: string;
vendorIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.organizationId) { if (!input.organizationId) {
return alert( return alert(
@@ -359,7 +359,7 @@ export const useCreateProcessingActivity = (connectionId?: string) => {
nextReviewDate: input.nextReviewDate, nextReviewDate: input.nextReviewDate,
role: input.role, role: input.role,
dataProtectionOfficerId: input.dataProtectionOfficerId, dataProtectionOfficerId: input.dataProtectionOfficerId,
vendorIds: input.vendorIds, thirdPartyIds: input.thirdPartyIds,
}, },
connections: connectionId ? [connectionId] : [], connections: connectionId ? [connectionId] : [],
}, },
@@ -393,7 +393,7 @@ export const useUpdateProcessingActivity = () => {
nextReviewDate?: string | null; nextReviewDate?: string | null;
role?: string; role?: string;
dataProtectionOfficerId?: string | null; dataProtectionOfficerId?: string | null;
vendorIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update processing activity: ID is required")); return alert(__("Failed to update processing activity: ID is required"));

View File

@@ -0,0 +1,246 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { promisifyMutation, sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui";
import { useMemo } from "react";
import { useLazyLoadQuery, useMutation } from "react-relay";
import { graphql } from "relay-runtime";
import type { ThirdPartyGraphCreateMutation } from "#/__generated__/core/ThirdPartyGraphCreateMutation.graphql";
import type { ThirdPartyGraphDeleteMutation } from "#/__generated__/core/ThirdPartyGraphDeleteMutation.graphql";
import type { ThirdPartyGraphSelectQuery } from "#/__generated__/core/ThirdPartyGraphSelectQuery.graphql";
import { useMutationWithToasts } from "../useMutationWithToasts";
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
const createThirdPartyMutation = graphql`
mutation ThirdPartyGraphCreateMutation(
$input: CreateThirdPartyInput!
$connections: [ID!]!
) {
createThirdParty(input: $input) {
thirdPartyEdge @prependEdge(connections: $connections) {
node {
id
name
description
websiteUrl
createdAt
updatedAt
canUpdate: permission(action: "core:thirdParty:update")
canDelete: permission(action: "core:thirdParty:delete")
}
}
}
}
`;
export function useCreateThirdPartyMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<ThirdPartyGraphCreateMutation>(
createThirdPartyMutation,
{
successMessage: __("Third party created successfully."),
errorMessage: __("Failed to create third party"),
},
);
}
const deleteThirdPartyMutation = graphql`
mutation ThirdPartyGraphDeleteMutation(
$input: DeleteThirdPartyInput!
$connections: [ID!]!
) {
deleteThirdParty(input: $input) {
deletedThirdPartyId @deleteEdge(connections: $connections)
}
}
`;
export const useDeleteThirdParty = (
thirdParty: { id?: string; name?: string },
connectionId: string,
) => {
const [mutate] = useMutation<ThirdPartyGraphDeleteMutation>(deleteThirdPartyMutation);
const confirm = useConfirm();
const { __ } = useTranslate();
return () => {
if (!thirdParty.id || !thirdParty.name) {
return alert(__("Failed to delete third party: missing id or name"));
}
confirm(
() =>
promisifyMutation(mutate)({
variables: {
input: {
thirdPartyId: thirdParty.id!,
},
connections: [connectionId],
},
}),
{
message: sprintf(
__(
"This will permanently delete thirdParty \"%s\". This action cannot be undone.",
),
thirdParty.name,
),
},
);
};
};
export const thirdPartyConnectionKey = "ThirdPartiesPage_thirdParties";
export const thirdPartiesQuery = graphql`
query ThirdPartyGraphListQuery($organizationId: ID!) {
node(id: $organizationId) {
... on Organization {
id
canCreateThirdParty: permission(action: "core:thirdParty:create")
canPublishThirdParty: permission(action: "core:thirdParty:publish")
thirdPartiesDocument {
id
currentPublishedMajor
currentPublishedMinor
defaultApprovers {
id
}
}
...ThirdPartyGraphPaginatedFragment
}
}
}
`;
export const paginatedThirdPartiesFragment = graphql`
fragment ThirdPartyGraphPaginatedFragment on Organization
@refetchable(queryName: "ThirdPartiesListQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 50 }
order: { type: "ThirdPartyOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
) {
thirdParties(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
) @connection(key: "ThirdPartiesListQuery_thirdParties") {
__id
edges {
node {
id
name
websiteUrl
updatedAt
riskAssessments(
first: 1
orderBy: { direction: DESC, field: CREATED_AT }
) {
edges {
node {
id
createdAt
expiresAt
dataSensitivity
businessImpact
}
}
}
canUpdate: permission(action: "core:thirdParty:update")
canDelete: permission(action: "core:thirdParty:delete")
}
}
}
}
`;
export const thirdPartyNodeQuery = graphql`
query ThirdPartyGraphNodeQuery($thirdPartyId: ID!) {
node(id: $thirdPartyId) {
id
... on ThirdParty {
name
websiteUrl
canAssess: permission(action: "core:thirdParty:assess")
canUpdate: permission(action: "core:thirdParty:update")
canDelete: permission(action: "core:thirdParty:delete")
canUploadComplianceReport: permission(
action: "core:thirdParty-compliance-report:upload"
)
canCreateRiskAssessment: permission(
action: "core:thirdParty-risk-assessment:create"
)
canCreateContact: permission(action: "core:thirdParty-contact:create")
canCreateService: permission(action: "core:thirdParty-service:create")
canUploadBAA: permission(
action: "core:thirdParty-business-associate-agreement:upload"
)
canUploadDPA: permission(
action: "core:thirdParty-data-privacy-agreement:upload"
)
...useThirdPartyFormFragment
...ThirdPartyComplianceTabFragment
...ThirdPartyContactsTabFragment
...ThirdPartyServicesTabFragment
...ThirdPartyRiskAssessmentTabFragment
...ThirdPartyOverviewTabBusinessAssociateAgreementFragment
...ThirdPartyOverviewTabDataPrivacyAgreementFragment
}
}
viewer {
id
}
}
`;
export const thirdPartiesSelectQuery = graphql`
query ThirdPartyGraphSelectQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
thirdParties(first: 100, orderBy: { direction: ASC, field: NAME }) {
edges {
node {
id
name
websiteUrl
}
}
}
}
}
}
`;
export function useThirdParties(organizationId: string) {
const data = useLazyLoadQuery<ThirdPartyGraphSelectQuery>(
thirdPartiesSelectQuery,
{
organizationId: organizationId,
},
{ fetchPolicy: "network-only" },
);
return useMemo(() => {
return data.organization?.thirdParties?.edges.map(edge => edge.node) ?? [];
}, [data]);
}

View File

@@ -1,246 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { promisifyMutation, sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui";
import { useMemo } from "react";
import { useLazyLoadQuery, useMutation } from "react-relay";
import { graphql } from "relay-runtime";
import type { VendorGraphCreateMutation } from "#/__generated__/core/VendorGraphCreateMutation.graphql";
import type { VendorGraphDeleteMutation } from "#/__generated__/core/VendorGraphDeleteMutation.graphql";
import type { VendorGraphSelectQuery } from "#/__generated__/core/VendorGraphSelectQuery.graphql";
import { useMutationWithToasts } from "../useMutationWithToasts";
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
const createVendorMutation = graphql`
mutation VendorGraphCreateMutation(
$input: CreateVendorInput!
$connections: [ID!]!
) {
createVendor(input: $input) {
vendorEdge @prependEdge(connections: $connections) {
node {
id
name
description
websiteUrl
createdAt
updatedAt
canUpdate: permission(action: "core:vendor:update")
canDelete: permission(action: "core:vendor:delete")
}
}
}
}
`;
export function useCreateVendorMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<VendorGraphCreateMutation>(
createVendorMutation,
{
successMessage: __("Vendor created successfully."),
errorMessage: __("Failed to create vendor"),
},
);
}
const deleteVendorMutation = graphql`
mutation VendorGraphDeleteMutation(
$input: DeleteVendorInput!
$connections: [ID!]!
) {
deleteVendor(input: $input) {
deletedVendorId @deleteEdge(connections: $connections)
}
}
`;
export const useDeleteVendor = (
vendor: { id?: string; name?: string },
connectionId: string,
) => {
const [mutate] = useMutation<VendorGraphDeleteMutation>(deleteVendorMutation);
const confirm = useConfirm();
const { __ } = useTranslate();
return () => {
if (!vendor.id || !vendor.name) {
return alert(__("Failed to delete vendor: missing id or name"));
}
confirm(
() =>
promisifyMutation(mutate)({
variables: {
input: {
vendorId: vendor.id!,
},
connections: [connectionId],
},
}),
{
message: sprintf(
__(
"This will permanently delete vendor \"%s\". This action cannot be undone.",
),
vendor.name,
),
},
);
};
};
export const vendorConnectionKey = "VendorsPage_vendors";
export const vendorsQuery = graphql`
query VendorGraphListQuery($organizationId: ID!) {
node(id: $organizationId) {
... on Organization {
id
canCreateVendor: permission(action: "core:vendor:create")
canPublishVendor: permission(action: "core:vendor:publish")
vendorsDocument {
id
currentPublishedMajor
currentPublishedMinor
defaultApprovers {
id
}
}
...VendorGraphPaginatedFragment
}
}
}
`;
export const paginatedVendorsFragment = graphql`
fragment VendorGraphPaginatedFragment on Organization
@refetchable(queryName: "VendorsListQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 50 }
order: { type: "VendorOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
) {
vendors(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
) @connection(key: "VendorsListQuery_vendors") {
__id
edges {
node {
id
name
websiteUrl
updatedAt
riskAssessments(
first: 1
orderBy: { direction: DESC, field: CREATED_AT }
) {
edges {
node {
id
createdAt
expiresAt
dataSensitivity
businessImpact
}
}
}
canUpdate: permission(action: "core:vendor:update")
canDelete: permission(action: "core:vendor:delete")
}
}
}
}
`;
export const vendorNodeQuery = graphql`
query VendorGraphNodeQuery($vendorId: ID!) {
node(id: $vendorId) {
id
... on Vendor {
name
websiteUrl
canAssess: permission(action: "core:vendor:assess")
canUpdate: permission(action: "core:vendor:update")
canDelete: permission(action: "core:vendor:delete")
canUploadComplianceReport: permission(
action: "core:vendor-compliance-report:upload"
)
canCreateRiskAssessment: permission(
action: "core:vendor-risk-assessment:create"
)
canCreateContact: permission(action: "core:vendor-contact:create")
canCreateService: permission(action: "core:vendor-service:create")
canUploadBAA: permission(
action: "core:vendor-business-associate-agreement:upload"
)
canUploadDPA: permission(
action: "core:vendor-data-privacy-agreement:upload"
)
...useVendorFormFragment
...VendorComplianceTabFragment
...VendorContactsTabFragment
...VendorServicesTabFragment
...VendorRiskAssessmentTabFragment
...VendorOverviewTabBusinessAssociateAgreementFragment
...VendorOverviewTabDataPrivacyAgreementFragment
}
}
viewer {
id
}
}
`;
export const vendorsSelectQuery = graphql`
query VendorGraphSelectQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
vendors(first: 100, orderBy: { direction: ASC, field: NAME }) {
edges {
node {
id
name
websiteUrl
}
}
}
}
}
}
`;
export function useVendors(organizationId: string) {
const data = useLazyLoadQuery<VendorGraphSelectQuery>(
vendorsSelectQuery,
{
organizationId: organizationId,
},
{ fetchPolicy: "network-only" },
);
return useMemo(() => {
return data.organization?.vendors?.edges.map(edge => edge.node) ?? [];
}, [data]);
}

View File

@@ -50,7 +50,7 @@ const fragment = graphql`
canListRisks: permission(action: "core:risk:list") canListRisks: permission(action: "core:risk:list")
canListFrameworks: permission(action: "core:framework:list") canListFrameworks: permission(action: "core:framework:list")
canListMembers: permission(action: "iam:membership:list") canListMembers: permission(action: "iam:membership:list")
canListVendors: permission(action: "core:vendor:list") canListThirdParties: permission(action: "core:thirdParty:list")
canListDocuments: permission(action: "core:document:list") canListDocuments: permission(action: "core:document:list")
canListAssets: permission(action: "core:asset:list") canListAssets: permission(action: "core:asset:list")
canListData: permission(action: "core:datum:list") canListData: permission(action: "core:datum:list")
@@ -127,11 +127,11 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
to={`${prefix}/people`} to={`${prefix}/people`}
/> />
)} )}
{organization.canListVendors && ( {organization.canListThirdParties && (
<SidebarItem <SidebarItem
label={__("Vendors")} label={__("Third parties")}
icon={IconStore} icon={IconStore}
to={`${prefix}/vendors`} to={`${prefix}/third-parties`}
/> />
)} )}
{organization.canListDocuments && ( {organization.canListDocuments && (

View File

@@ -28,9 +28,9 @@ import {
Input, Input,
Option, Option,
Select, Select,
ThirdPartyLogo,
useDialogRef, useDialogRef,
useToast, useToast,
VendorLogo,
} from "@probo/ui"; } from "@probo/ui";
import { type ReactNode, useMemo, useState } from "react"; import { type ReactNode, useMemo, useState } from "react";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
@@ -454,7 +454,7 @@ export function AddAccessSourceDialog({
return ( return (
<Card key={info.provider} padded className="flex items-center gap-3"> <Card key={info.provider} padded className="flex items-center gap-3">
<VendorLogo vendor={info.provider} tint className="size-6 shrink-0" /> <ThirdPartyLogo thirdParty={info.provider} tint className="size-6 shrink-0" />
<div className="mr-auto"> <div className="mr-auto">
<h3 className="font-medium">{info.displayName}</h3> <h3 className="font-medium">{info.displayName}</h3>
</div> </div>

View File

@@ -34,7 +34,7 @@ import { z } from "zod";
import type { AssetGraphNodeQuery } from "#/__generated__/core/AssetGraphNodeQuery.graphql"; import type { AssetGraphNodeQuery } from "#/__generated__/core/AssetGraphNodeQuery.graphql";
import { ControlledField } from "#/components/form/ControlledField"; import { ControlledField } from "#/components/form/ControlledField";
import { PeopleSelectField } from "#/components/form/PeopleSelectField"; import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { VendorsMultiSelectField } from "#/components/form/VendorsMultiSelectField"; import { ThirdPartiesMultiSelectField } from "#/components/form/ThirdPartiesMultiSelectField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
@@ -50,7 +50,7 @@ const updateAssetSchema = z.object({
assetType: z.enum(["PHYSICAL", "VIRTUAL"]), assetType: z.enum(["PHYSICAL", "VIRTUAL"]),
dataTypesStored: z.string().min(1, "Data types stored is required"), dataTypesStored: z.string().min(1, "Data types stored is required"),
ownerId: z.string().min(1, "Owner is required"), ownerId: z.string().min(1, "Owner is required"),
vendorIds: z.array(z.string()).optional(), thirdPartyIds: z.array(z.string()).optional(),
}); });
type Props = { type Props = {
@@ -72,8 +72,8 @@ export default function AssetDetailsPage(props: Props) {
); );
const deleteAsset = useDeleteAsset(assetEntry, connectionId); const deleteAsset = useDeleteAsset(assetEntry, connectionId);
const vendors = assetEntry.vendors?.edges.map(edge => edge.node) ?? []; const thirdParties = assetEntry.thirdParties?.edges.map(edge => edge.node) ?? [];
const vendorIds = vendors.map(vendor => vendor.id); const thirdPartyIds = thirdParties.map(thirdParty => thirdParty.id);
const { control, formState, handleSubmit, register, reset } const { control, formState, handleSubmit, register, reset }
= useFormWithSchema(updateAssetSchema, { = useFormWithSchema(updateAssetSchema, {
@@ -83,7 +83,7 @@ export default function AssetDetailsPage(props: Props) {
assetType: assetEntry.assetType || "VIRTUAL", assetType: assetEntry.assetType || "VIRTUAL",
dataTypesStored: assetEntry.dataTypesStored || "", dataTypesStored: assetEntry.dataTypesStored || "",
ownerId: assetEntry.owner?.id || "", ownerId: assetEntry.owner?.id || "",
vendorIds: vendorIds, thirdPartyIds: thirdPartyIds,
}, },
}); });
@@ -176,12 +176,12 @@ export default function AssetDetailsPage(props: Props) {
disabled={!assetEntry.canUpdate} disabled={!assetEntry.canUpdate}
/> />
<VendorsMultiSelectField <ThirdPartiesMultiSelectField
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="vendorIds" name="thirdPartyIds"
selectedVendors={vendors} selectedThirdParties={thirdParties}
label={__("Vendors")} label={__("Third parties")}
disabled={!assetEntry.canUpdate} disabled={!assetEntry.canUpdate}
/> />

View File

@@ -78,7 +78,7 @@ const paginatedAssetsFragment = graphql`
fullName fullName
} }
# eslint-disable-next-line relay/unused-fields # eslint-disable-next-line relay/unused-fields
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
# eslint-disable-next-line relay/unused-fields # eslint-disable-next-line relay/unused-fields

View File

@@ -27,7 +27,7 @@ import { z } from "zod";
import { ControlledField } from "#/components/form/ControlledField"; import { ControlledField } from "#/components/form/ControlledField";
import { PeopleSelectField } from "#/components/form/PeopleSelectField"; import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { VendorsMultiSelectField } from "#/components/form/VendorsMultiSelectField"; import { ThirdPartiesMultiSelectField } from "#/components/form/ThirdPartiesMultiSelectField";
import { useCreateAsset } from "#/hooks/graph/AssetGraph"; import { useCreateAsset } from "#/hooks/graph/AssetGraph";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
@@ -36,7 +36,7 @@ const schema = z.object({
amount: z.number().min(1, "Amount is required"), amount: z.number().min(1, "Amount is required"),
assetType: z.enum(["PHYSICAL", "VIRTUAL"]), assetType: z.enum(["PHYSICAL", "VIRTUAL"]),
ownerId: z.string().min(1, "Owner is required"), ownerId: z.string().min(1, "Owner is required"),
vendorIds: z.array(z.string()).optional(), thirdPartyIds: z.array(z.string()).optional(),
dataTypesStored: z.string().min(1, "Data types stored is required"), dataTypesStored: z.string().min(1, "Data types stored is required"),
}); });
@@ -59,7 +59,7 @@ export function CreateAssetDialog({
amount: 0, amount: 0,
assetType: "VIRTUAL", assetType: "VIRTUAL",
ownerId: "", ownerId: "",
vendorIds: [], thirdPartyIds: [],
}, },
}); });
const ref = useDialogRef(); const ref = useDialogRef();
@@ -112,11 +112,11 @@ export function CreateAssetDialog({
name="ownerId" name="ownerId"
label={__("Owner")} label={__("Owner")}
/> />
<VendorsMultiSelectField <ThirdPartiesMultiSelectField
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="vendorIds" name="thirdPartyIds"
label={__("Vendors")} label={__("Third parties")}
/> />
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>

View File

@@ -113,7 +113,7 @@ export function CompliancePageLayout(props: { queryRef: PreloadedQuery<Complianc
<IconFolder2 className="size-4" /> <IconFolder2 className="size-4" />
{__("Files")} {__("Files")}
</TabLink> </TabLink>
<TabLink to={`/organizations/${organizationId}/compliance-page/vendors`}> <TabLink to={`/organizations/${organizationId}/compliance-page/third-parties`}>
<IconStore className="size-4" /> <IconStore className="size-4" />
{__("Subprocessors")} {__("Subprocessors")}
</TabLink> </TabLink>

View File

@@ -62,9 +62,9 @@ export const compliancePageRoutes = [
Component: lazy(() => import("#/pages/organizations/compliance-page/files/CompliancePageFilesPageLoader")), Component: lazy(() => import("#/pages/organizations/compliance-page/files/CompliancePageFilesPageLoader")),
}, },
{ {
path: "vendors", path: "third-parties",
Fallback: LinkCardSkeleton, Fallback: LinkCardSkeleton,
Component: lazy(() => import("#/pages/organizations/compliance-page/vendors/CompliancePageVendorsPageLoader")), Component: lazy(() => import("#/pages/organizations/compliance-page/third-parties/CompliancePageThirdPartiesPageLoader")),
}, },
{ {
path: "access", path: "access",

View File

@@ -15,27 +15,27 @@
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
import type { CompliancePageVendorsPageQuery } from "#/__generated__/core/CompliancePageVendorsPageQuery.graphql"; import type { CompliancePageThirdPartiesPageQuery } from "#/__generated__/core/CompliancePageThirdPartiesPageQuery.graphql";
import { CompliancePageVendorList } from "./_components/CompliancePageVendorList"; import { CompliancePageThirdPartyList } from "./_components/CompliancePageThirdPartyList";
export const compliancePageVendorsPageQuery = graphql` export const compliancePageThirdPartiesPageQuery = graphql`
query CompliancePageVendorsPageQuery($organizationId: ID!) { query CompliancePageThirdPartiesPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) { organization: node(id: $organizationId) {
...CompliancePageVendorListFragment ...CompliancePageThirdPartyListFragment
} }
} }
`; `;
export function CompliancePageVendorsPage(props: { export function CompliancePageThirdPartiesPage(props: {
queryRef: PreloadedQuery<CompliancePageVendorsPageQuery>; queryRef: PreloadedQuery<CompliancePageThirdPartiesPageQuery>;
}) { }) {
const { queryRef } = props; const { queryRef } = props;
const { __ } = useTranslate(); const { __ } = useTranslate();
const { organization } = usePreloadedQuery<CompliancePageVendorsPageQuery>( const { organization } = usePreloadedQuery<CompliancePageThirdPartiesPageQuery>(
compliancePageVendorsPageQuery, compliancePageThirdPartiesPageQuery,
queryRef, queryRef,
); );
@@ -50,7 +50,7 @@ export function CompliancePageVendorsPage(props: {
</div> </div>
</div> </div>
<CompliancePageVendorList fragmentRef={organization} /> <CompliancePageThirdPartyList fragmentRef={organization} />
</div> </div>
); );
} }

View File

@@ -15,16 +15,18 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useQueryLoader } from "react-relay"; import { useQueryLoader } from "react-relay";
import type { CompliancePageVendorsPageQuery } from "#/__generated__/core/CompliancePageVendorsPageQuery.graphql"; import type { CompliancePageThirdPartiesPageQuery } from "#/__generated__/core/CompliancePageThirdPartiesPageQuery.graphql";
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton"; import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { CoreRelayProvider } from "#/providers/CoreRelayProvider"; import { CoreRelayProvider } from "#/providers/CoreRelayProvider";
import { CompliancePageVendorsPage, compliancePageVendorsPageQuery } from "./CompliancePageVendorsPage"; import { CompliancePageThirdPartiesPage, compliancePageThirdPartiesPageQuery } from "./CompliancePageThirdPartiesPage";
function CompliancePageVendorsPageQueryLoader() { function CompliancePageThirdPartiesPageQueryLoader() {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<CompliancePageVendorsPageQuery>(compliancePageVendorsPageQuery); const [queryRef, loadQuery] = useQueryLoader<CompliancePageThirdPartiesPageQuery>(
compliancePageThirdPartiesPageQuery,
);
useEffect(() => { useEffect(() => {
if (!queryRef) { if (!queryRef) {
@@ -34,13 +36,13 @@ function CompliancePageVendorsPageQueryLoader() {
if (!queryRef) return <LinkCardSkeleton />; if (!queryRef) return <LinkCardSkeleton />;
return <CompliancePageVendorsPage queryRef={queryRef} />; return <CompliancePageThirdPartiesPage queryRef={queryRef} />;
} }
export default function CompliancePageVendorsPageLoader() { export default function CompliancePageThirdPartiesPageLoader() {
return ( return (
<CoreRelayProvider> <CoreRelayProvider>
<CompliancePageVendorsPageQueryLoader /> <CompliancePageThirdPartiesPageQueryLoader />
</CoreRelayProvider> </CoreRelayProvider>
); );
} }

View File

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

View File

@@ -0,0 +1,104 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
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 { CompliancePageThirdPartyListItem_thirdPartyFragment$key } from "#/__generated__/core/CompliancePageThirdPartyListItem_thirdPartyFragment.graphql";
import type { CompliancePageThirdPartyListItemMutation } from "#/__generated__/core/CompliancePageThirdPartyListItemMutation.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
const thirdPartyFragment = graphql`
fragment CompliancePageThirdPartyListItem_thirdPartyFragment on ThirdParty {
id
category
name
showOnTrustCenter
canUpdate: permission(action: "core:thirdParty:update")
}
`;
const updateThirdPartyVisibilityMutation = graphql`
mutation CompliancePageThirdPartyListItemMutation($input: UpdateThirdPartyInput!) {
updateThirdParty(input: $input) {
thirdParty {
id
showOnTrustCenter
...CompliancePageThirdPartyListItem_thirdPartyFragment
}
}
}
`;
export function CompliancePageThirdPartyListItem(props: {
thirdPartyFragmentRef: CompliancePageThirdPartyListItem_thirdPartyFragment$key;
}) {
const { thirdPartyFragmentRef } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const thirdParty = useFragment<CompliancePageThirdPartyListItem_thirdPartyFragment$key>(
thirdPartyFragment,
thirdPartyFragmentRef,
);
const [updateThirdPartyVisibility, isUpadtingThirdPartyVisibility] = useMutationWithToasts<
CompliancePageThirdPartyListItemMutation
>(
updateThirdPartyVisibilityMutation,
{
successMessage: __("Subprocessor visibility updated successfully."),
errorMessage: __("Failed to update subprocessor visibility"),
},
);
return (
<Tr to={`/organizations/${organizationId}/third-parties/${thirdParty.id}/overview`}>
<Td>
<div className="flex gap-4 items-center">{thirdParty.name}</div>
</Td>
<Td>
<Badge variant="neutral">{thirdParty.category}</Badge>
</Td>
<Td>
<Badge variant={thirdParty.showOnTrustCenter ? "success" : "danger"}>
{thirdParty.showOnTrustCenter ? __("Visible") : __("None")}
</Badge>
</Td>
<Td noLink width={100} className="text-end">
{thirdParty.canUpdate && (
<Button
variant="secondary"
onClick={() =>
void updateThirdPartyVisibility({
variables: {
input: {
id: thirdParty.id,
showOnTrustCenter: !thirdParty.showOnTrustCenter,
},
},
})}
icon={thirdParty.showOnTrustCenter ? IconCrossLargeX : IconCheckmark1}
disabled={isUpadtingThirdPartyVisibility}
>
{thirdParty.showOnTrustCenter ? __("Hide") : __("Show")}
</Button>
)}
</Td>
</Tr>
);
};

View File

@@ -1,104 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
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: __("Subprocessor visibility updated successfully."),
errorMessage: __("Failed to update subprocessor 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

@@ -81,7 +81,7 @@ const paginatedDataFragment = graphql`
owner { owner {
fullName fullName
} }
vendors(first: 50) { thirdParties(first: 50) {
edges { edges {
node { node {
id id
@@ -192,7 +192,7 @@ export default function DataPage(props: Props) {
<Th>{__("Name")}</Th> <Th>{__("Name")}</Th>
<Th>{__("Classification")}</Th> <Th>{__("Classification")}</Th>
<Th>{__("Owner")}</Th> <Th>{__("Owner")}</Th>
<Th>{__("Vendors")}</Th> <Th>{__("Third parties")}</Th>
{hasAnyAction && <Th></Th>} {hasAnyAction && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
@@ -223,7 +223,7 @@ function DataRow({
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const deleteDatum = useDeleteDatum(entry, connectionId); const deleteDatum = useDeleteDatum(entry, connectionId);
const vendors = entry.vendors?.edges.map(edge => edge.node) ?? []; const thirdParties = entry.thirdParties?.edges.map(edge => edge.node) ?? [];
const detailUrl = `/organizations/${organizationId}/data/${entry.id}`; const detailUrl = `/organizations/${organizationId}/data/${entry.id}`;
return ( return (
@@ -234,27 +234,27 @@ function DataRow({
</Td> </Td>
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td> <Td>{entry.owner?.fullName ?? __("Unassigned")}</Td>
<Td> <Td>
{vendors.length > 0 {thirdParties.length > 0
? ( ? (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{vendors.slice(0, 3).map(vendor => ( {thirdParties.slice(0, 3).map(thirdParty => (
<Badge <Badge
key={vendor.id} key={thirdParty.id}
variant="neutral" variant="neutral"
className="flex items-center gap-1" className="flex items-center gap-1"
> >
<Avatar <Avatar
name={vendor.name} name={thirdParty.name}
src={faviconUrl(vendor.websiteUrl)} src={faviconUrl(thirdParty.websiteUrl)}
size="s" size="s"
/> />
<span className="text-xs">{vendor.name}</span> <span className="text-xs">{thirdParty.name}</span>
</Badge> </Badge>
))} ))}
{vendors.length > 3 && ( {thirdParties.length > 3 && (
<Badge variant="neutral" className="text-xs"> <Badge variant="neutral" className="text-xs">
+ +
{vendors.length - 3} {thirdParties.length - 3}
</Badge> </Badge>
)} )}
</div> </div>

View File

@@ -33,7 +33,7 @@ import { z } from "zod";
import type { DatumGraphNodeQuery } from "#/__generated__/core/DatumGraphNodeQuery.graphql"; import type { DatumGraphNodeQuery } from "#/__generated__/core/DatumGraphNodeQuery.graphql";
import { ControlledField } from "#/components/form/ControlledField"; import { ControlledField } from "#/components/form/ControlledField";
import { PeopleSelectField } from "#/components/form/PeopleSelectField"; import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { VendorsMultiSelectField } from "#/components/form/VendorsMultiSelectField"; import { ThirdPartiesMultiSelectField } from "#/components/form/ThirdPartiesMultiSelectField";
import { import {
datumNodeQuery, datumNodeQuery,
useDeleteDatum, useDeleteDatum,
@@ -46,7 +46,7 @@ const updateDatumSchema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
dataClassification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]), dataClassification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]),
ownerId: z.string().min(1, "Owner is required"), ownerId: z.string().min(1, "Owner is required"),
vendorIds: z.array(z.string()).optional(), thirdPartyIds: z.array(z.string()).optional(),
}); });
type Props = { type Props = {
@@ -69,8 +69,8 @@ export default function DatumDetailsPage(props: Props) {
ConnectionHandler.getConnectionID(organizationId, "DataPage_data"), ConnectionHandler.getConnectionID(organizationId, "DataPage_data"),
); );
const vendors = datumEntry?.vendors?.edges.map(edge => edge.node) ?? []; const thirdParties = datumEntry?.thirdParties?.edges.map(edge => edge.node) ?? [];
const vendorIds = vendors.map(vendor => vendor.id); const thirdPartyIds = thirdParties.map(thirdParty => thirdParty.id);
const { control, formState, handleSubmit, register, reset } const { control, formState, handleSubmit, register, reset }
= useFormWithSchema(updateDatumSchema, { = useFormWithSchema(updateDatumSchema, {
@@ -78,7 +78,7 @@ export default function DatumDetailsPage(props: Props) {
name: datumEntry?.name || "", name: datumEntry?.name || "",
dataClassification: datumEntry?.dataClassification || "PUBLIC", dataClassification: datumEntry?.dataClassification || "PUBLIC",
ownerId: datumEntry?.owner?.id || "", ownerId: datumEntry?.owner?.id || "",
vendorIds: vendorIds, thirdPartyIds: thirdPartyIds,
}, },
}); });
@@ -161,13 +161,13 @@ export default function DatumDetailsPage(props: Props) {
disabled={!datumEntry.canUpdate} disabled={!datumEntry.canUpdate}
/> />
<VendorsMultiSelectField <ThirdPartiesMultiSelectField
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="vendorIds" name="thirdPartyIds"
label={__("Vendors")} label={__("Third parties")}
disabled={!datumEntry.canUpdate} disabled={!datumEntry.canUpdate}
selectedVendors={vendors} selectedThirdParties={thirdParties}
/> />
<div className="flex justify-end"> <div className="flex justify-end">

View File

@@ -27,7 +27,7 @@ import { z } from "zod";
import { ControlledField } from "#/components/form/ControlledField"; import { ControlledField } from "#/components/form/ControlledField";
import { PeopleSelectField } from "#/components/form/PeopleSelectField"; import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { VendorsMultiSelectField } from "#/components/form/VendorsMultiSelectField"; import { ThirdPartiesMultiSelectField } from "#/components/form/ThirdPartiesMultiSelectField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useCreateDatum } from "../../../../hooks/graph/DatumGraph"; import { useCreateDatum } from "../../../../hooks/graph/DatumGraph";
@@ -36,7 +36,7 @@ const schema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
dataClassification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]), dataClassification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]),
ownerId: z.string().min(1, "Owner is required"), ownerId: z.string().min(1, "Owner is required"),
vendorIds: z.array(z.string()).optional(), thirdPartyIds: z.array(z.string()).optional(),
}); });
type Props = { type Props = {
@@ -59,7 +59,7 @@ export function CreateDatumDialog({
name: "", name: "",
dataClassification: "PUBLIC", dataClassification: "PUBLIC",
ownerId: "", ownerId: "",
vendorIds: [], thirdPartyIds: [],
}, },
}); });
const ref = useDialogRef(); const ref = useDialogRef();
@@ -105,11 +105,11 @@ export function CreateDatumDialog({
name="ownerId" name="ownerId"
label={__("Owner")} label={__("Owner")}
/> />
<VendorsMultiSelectField <ThirdPartiesMultiSelectField
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="vendorIds" name="thirdPartyIds"
label={__("Vendors")} label={__("Third parties")}
/> />
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>

View File

@@ -46,7 +46,7 @@ import { z } from "zod";
import type { ProcessingActivityGraphNodeQuery } from "#/__generated__/core/ProcessingActivityGraphNodeQuery.graphql"; import type { ProcessingActivityGraphNodeQuery } from "#/__generated__/core/ProcessingActivityGraphNodeQuery.graphql";
import { PeopleSelectField } from "#/components/form/PeopleSelectField"; import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { VendorsMultiSelectField } from "#/components/form/VendorsMultiSelectField"; import { ThirdPartiesMultiSelectField } from "#/components/form/ThirdPartiesMultiSelectField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
@@ -102,7 +102,7 @@ const updateProcessingActivitySchema = z.object({
nextReviewDate: z.string().optional(), nextReviewDate: z.string().optional(),
role: z.enum(["CONTROLLER", "PROCESSOR"] as const), role: z.enum(["CONTROLLER", "PROCESSOR"] as const),
dataProtectionOfficerId: z.string().optional(), dataProtectionOfficerId: z.string().optional(),
vendorIds: z.array(z.string()).optional(), thirdPartyIds: z.array(z.string()).optional(),
}); });
type Props = { type Props = {
@@ -199,8 +199,8 @@ export default function ProcessingActivityDetailsPage(props: Props) {
connectionId, connectionId,
); );
const vendors = activity?.vendors?.edges.map(edge => edge.node) ?? []; const thirdParties = activity?.thirdParties?.edges.map(edge => edge.node) ?? [];
const vendorIds = vendors.map(vendor => vendor.id); const thirdPartyIds = thirdParties.map(thirdParty => thirdParty.id);
const { register, handleSubmit, formState, control } = useFormWithSchema( const { register, handleSubmit, formState, control } = useFormWithSchema(
updateProcessingActivitySchema, updateProcessingActivitySchema,
@@ -229,7 +229,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
nextReviewDate: toDateInput(activity.nextReviewDate), nextReviewDate: toDateInput(activity.nextReviewDate),
role: activity.role || ("CONTROLLER" as const), role: activity.role || ("CONTROLLER" as const),
dataProtectionOfficerId: activity.dataProtectionOfficer?.id || "", dataProtectionOfficerId: activity.dataProtectionOfficer?.id || "",
vendorIds: vendorIds, thirdPartyIds: thirdPartyIds,
}, },
}, },
); );
@@ -287,7 +287,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
nextReviewDate: formatDatetime(formData.nextReviewDate) ?? null, nextReviewDate: formatDatetime(formData.nextReviewDate) ?? null,
role: formData.role, role: formData.role,
dataProtectionOfficerId: formData.dataProtectionOfficerId || null, dataProtectionOfficerId: formData.dataProtectionOfficerId || null,
vendorIds: formData.vendorIds, thirdPartyIds: formData.thirdPartyIds,
}); });
toast({ toast({
@@ -777,12 +777,12 @@ export default function ProcessingActivityDetailsPage(props: Props) {
</div> </div>
</div> </div>
<VendorsMultiSelectField <ThirdPartiesMultiSelectField
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="vendorIds" name="thirdPartyIds"
selectedVendors={vendors} selectedThirdParties={thirdParties}
label={__("Vendors")} label={__("Third parties")}
disabled={!activity.canUpdate} disabled={!activity.canUpdate}
/> />

View File

@@ -34,7 +34,7 @@ import { Controller } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { PeopleSelectField } from "#/components/form/PeopleSelectField"; import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { VendorsMultiSelectField } from "#/components/form/VendorsMultiSelectField"; import { ThirdPartiesMultiSelectField } from "#/components/form/ThirdPartiesMultiSelectField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { import {
@@ -67,7 +67,7 @@ const schema = z.object({
nextReviewDate: z.string().optional(), nextReviewDate: z.string().optional(),
role: z.enum(["CONTROLLER", "PROCESSOR"] as const), role: z.enum(["CONTROLLER", "PROCESSOR"] as const),
dataProtectionOfficerId: z.string().optional(), dataProtectionOfficerId: z.string().optional(),
vendorIds: z.array(z.string()).optional(), thirdPartyIds: z.array(z.string()).optional(),
}); });
type FormData = z.infer<typeof schema>; type FormData = z.infer<typeof schema>;
@@ -110,7 +110,7 @@ export function CreateProcessingActivityDialog({
nextReviewDate: "", nextReviewDate: "",
role: "PROCESSOR" as const, role: "PROCESSOR" as const,
dataProtectionOfficerId: "", dataProtectionOfficerId: "",
vendorIds: [], thirdPartyIds: [],
}, },
}); });
@@ -137,7 +137,7 @@ export function CreateProcessingActivityDialog({
nextReviewDate: formatDatetime(formData.nextReviewDate), nextReviewDate: formatDatetime(formData.nextReviewDate),
role: formData.role, role: formData.role,
dataProtectionOfficerId: formData.dataProtectionOfficerId || undefined, dataProtectionOfficerId: formData.dataProtectionOfficerId || undefined,
vendorIds: formData.vendorIds, thirdPartyIds: formData.thirdPartyIds,
}); });
toast({ toast({
@@ -430,12 +430,12 @@ export function CreateProcessingActivityDialog({
</div> </div>
</div> </div>
<VendorsMultiSelectField <ThirdPartiesMultiSelectField
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="vendorIds" name="thirdPartyIds"
selectedVendors={[]} selectedThirdParties={[]}
label={__("Vendors")} label={__("Third parties")}
/> />
</DialogContent> </DialogContent>

View File

@@ -155,9 +155,9 @@ const deleteWebhookSubscriptionMutation = graphql`
`; `;
const EVENT_TYPES = [ const EVENT_TYPES = [
{ value: "VENDOR_CREATED", label: "vendor:created" }, { value: "THIRD_PARTY_CREATED", label: "third-party:created" },
{ value: "VENDOR_UPDATED", label: "vendor:updated" }, { value: "THIRD_PARTY_UPDATED", label: "third-party:updated" },
{ value: "VENDOR_DELETED", label: "vendor:deleted" }, { value: "THIRD_PARTY_DELETED", label: "third-party:deleted" },
{ value: "USER_CREATED", label: "user:created" }, { value: "USER_CREATED", label: "user:created" },
{ value: "USER_UPDATED", label: "user:updated" }, { value: "USER_UPDATED", label: "user:updated" },
{ value: "USER_DELETED", label: "user:deleted" }, { value: "USER_DELETED", label: "user:deleted" },

View File

@@ -39,75 +39,75 @@ import {
} from "react-relay"; } from "react-relay";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import type { VendorGraphListQuery } from "#/__generated__/core/VendorGraphListQuery.graphql"; import type { ThirdPartyGraphListQuery } from "#/__generated__/core/ThirdPartyGraphListQuery.graphql";
import type { import type {
VendorGraphPaginatedFragment$data, ThirdPartyGraphPaginatedFragment$data,
VendorGraphPaginatedFragment$key, ThirdPartyGraphPaginatedFragment$key,
} from "#/__generated__/core/VendorGraphPaginatedFragment.graphql"; } from "#/__generated__/core/ThirdPartyGraphPaginatedFragment.graphql";
import { SortableTable, SortableTh } from "#/components/SortableTable"; import { SortableTable, SortableTh } from "#/components/SortableTable";
import { import {
paginatedVendorsFragment, paginatedThirdPartiesFragment,
useDeleteVendor, thirdPartiesQuery,
vendorsQuery, useDeleteThirdParty,
} from "#/hooks/graph/VendorGraph"; } from "#/hooks/graph/ThirdPartyGraph";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import type { NodeOf } from "#/types"; import type { NodeOf } from "#/types";
import { CreateVendorDialog } from "./dialogs/CreateVendorDialog"; import { CreateThirdPartyDialog } from "./dialogs/CreateThirdPartyDialog";
import { PublishVendorListDialog } from "./dialogs/PublishVendorListDialog"; import { PublishThirdPartyListDialog } from "./dialogs/PublishThirdPartyListDialog";
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>; type ThirdParty = NodeOf<ThirdPartyGraphPaginatedFragment$data["thirdParties"]>;
type Props = { type Props = {
queryRef: PreloadedQuery<VendorGraphListQuery>; queryRef: PreloadedQuery<ThirdPartyGraphListQuery>;
}; };
export default function VendorsPage(props: Props) { export default function ThirdPartiesPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const navigate = useNavigate(); const navigate = useNavigate();
const data = usePreloadedQuery(vendorsQuery, props.queryRef); const data = usePreloadedQuery(thirdPartiesQuery, props.queryRef);
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const pagination = usePaginationFragment( const pagination = usePaginationFragment(
paginatedVendorsFragment, paginatedThirdPartiesFragment,
data.node as VendorGraphPaginatedFragment$key, data.node as ThirdPartyGraphPaginatedFragment$key,
); );
const vendors = pagination.data.vendors?.edges.map(edge => edge.node); const thirdParties = pagination.data.thirdParties?.edges.map(edge => edge.node);
const connectionId = pagination.data.vendors.__id; const connectionId = pagination.data.thirdParties.__id;
usePageTitle(__("Vendors")); usePageTitle(__("Third parties"));
const hasAnyAction const hasAnyAction
= vendors.some(({ canUpdate, canDelete }) => canUpdate || canDelete); = thirdParties.some(({ canUpdate, canDelete }) => canUpdate || canDelete);
const vendorsDocument = data.node?.vendorsDocument; const thirdPartiesDocument = data.node?.thirdPartiesDocument;
const defaultApproverIds const defaultApproverIds
= vendorsDocument?.defaultApprovers?.map(a => a.id) ?? []; = thirdPartiesDocument?.defaultApprovers?.map(a => a.id) ?? [];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
title={__("Vendors")} title={__("Third parties")}
description={__( description={__(
"Vendors are third-party services that your company uses. Add them to keep track of their risk and compliance status.", "Third parties are external services and providers that your company uses. Add them to keep track of their risk and compliance status.",
)} )}
> >
<div className="flex gap-2"> <div className="flex gap-2">
{vendorsDocument && ( {thirdPartiesDocument && (
<Button <Button
variant="secondary" variant="secondary"
icon={IconPageTextLine} icon={IconPageTextLine}
onClick={() => void navigate( onClick={() => void navigate(
`/organizations/${organizationId}/documents/${vendorsDocument.id}`, `/organizations/${organizationId}/documents/${thirdPartiesDocument.id}`,
)} )}
> >
{__("Document")} {__("Document")}
</Button> </Button>
)} )}
{data.node.canPublishVendor && ( {data.node.canPublishThirdParty && (
<PublishVendorListDialog <PublishThirdPartyListDialog
organizationId={organizationId} organizationId={organizationId}
defaultApproverIds={defaultApproverIds} defaultApproverIds={defaultApproverIds}
onPublished={documentId => void navigate( onPublished={documentId => void navigate(
@@ -117,22 +117,22 @@ export default function VendorsPage(props: Props) {
<Button variant="secondary" icon={IconUpload}> <Button variant="secondary" icon={IconUpload}>
{__("Publish")} {__("Publish")}
</Button> </Button>
</PublishVendorListDialog> </PublishThirdPartyListDialog>
)} )}
{data.node.canCreateVendor && ( {data.node.canCreateThirdParty && (
<CreateVendorDialog <CreateThirdPartyDialog
connection={connectionId} connection={connectionId}
organizationId={organizationId} organizationId={organizationId}
> >
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button> <Button icon={IconPlusLarge}>{__("Add third party")}</Button>
</CreateVendorDialog> </CreateThirdPartyDialog>
)} )}
</div> </div>
</PageHeader> </PageHeader>
<SortableTable {...pagination}> <SortableTable {...pagination}>
<Thead> <Thead>
<Tr> <Tr>
<SortableTh field="NAME">{__("Vendor")}</SortableTh> <SortableTh field="NAME">{__("Third party")}</SortableTh>
<Th>{__("Accessed At")}</Th> <Th>{__("Accessed At")}</Th>
<Th>{__("Data Risk")}</Th> <Th>{__("Data Risk")}</Th>
<Th>{__("Business Risk")}</Th> <Th>{__("Business Risk")}</Th>
@@ -140,10 +140,10 @@ export default function VendorsPage(props: Props) {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{vendors?.map(vendor => ( {thirdParties?.map(thirdParty => (
<VendorRow <ThirdPartyRow
key={vendor.id} key={thirdParty.id}
vendor={vendor} thirdParty={thirdParty}
organizationId={organizationId} organizationId={organizationId}
connectionId={connectionId} connectionId={connectionId}
hasAnyAction={hasAnyAction} hasAnyAction={hasAnyAction}
@@ -155,30 +155,30 @@ export default function VendorsPage(props: Props) {
); );
} }
function VendorRow({ function ThirdPartyRow({
vendor, thirdParty,
organizationId, organizationId,
connectionId, connectionId,
hasAnyAction, hasAnyAction,
}: { }: {
vendor: Vendor; thirdParty: ThirdParty;
organizationId: string; organizationId: string;
connectionId: string; connectionId: string;
hasAnyAction: boolean; hasAnyAction: boolean;
}) { }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const latestAssessment = vendor.riskAssessments?.edges[0]?.node; const latestAssessment = thirdParty.riskAssessments?.edges[0]?.node;
const deleteVendor = useDeleteVendor(vendor, connectionId); const deleteThirdParty = useDeleteThirdParty(thirdParty, connectionId);
const vendorUrl = `/organizations/${organizationId}/vendors/${vendor.id}/overview`; const thirdPartyUrl = `/organizations/${organizationId}/third-parties/${thirdParty.id}/overview`;
return ( return (
<> <>
<Tr to={vendorUrl}> <Tr to={thirdPartyUrl}>
<Td> <Td>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Avatar name={vendor.name} src={faviconUrl(vendor.websiteUrl)} /> <Avatar name={thirdParty.name} src={faviconUrl(thirdParty.websiteUrl)} />
<div>{vendor.name}</div> <div>{thirdParty.name}</div>
</div> </div>
</Td> </Td>
<Td> <Td>
@@ -195,9 +195,9 @@ function VendorRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
{vendor.canDelete && ( {thirdParty.canDelete && (
<DropdownItem <DropdownItem
onClick={deleteVendor} onClick={deleteThirdParty}
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
> >

View File

@@ -33,52 +33,52 @@ import {
} from "react-relay"; } from "react-relay";
import { Outlet } from "react-router"; import { Outlet } from "react-router";
import type { VendorComplianceTabFragment$key } from "#/__generated__/core/VendorComplianceTabFragment.graphql"; import type { ThirdPartyComplianceTabFragment$key } from "#/__generated__/core/ThirdPartyComplianceTabFragment.graphql";
import type { VendorGraphNodeQuery } from "#/__generated__/core/VendorGraphNodeQuery.graphql"; import type { ThirdPartyGraphNodeQuery } from "#/__generated__/core/ThirdPartyGraphNodeQuery.graphql";
import { import {
useDeleteVendor, thirdPartyConnectionKey,
vendorConnectionKey, thirdPartyNodeQuery,
vendorNodeQuery, useDeleteThirdParty,
} from "#/hooks/graph/VendorGraph"; } from "#/hooks/graph/ThirdPartyGraph";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog"; import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog";
import { complianceReportsFragment } from "./tabs/VendorComplianceTab"; import { complianceReportsFragment } from "./tabs/ThirdPartyComplianceTab";
type Props = { type Props = {
queryRef: PreloadedQuery<VendorGraphNodeQuery>; queryRef: PreloadedQuery<ThirdPartyGraphNodeQuery>;
}; };
export default function VendorDetailPage(props: Props) { export default function ThirdPartyDetailPage(props: Props) {
const { node: vendor } = usePreloadedQuery(vendorNodeQuery, props.queryRef); const { node: thirdParty } = usePreloadedQuery(thirdPartyNodeQuery, props.queryRef);
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const deleteVendor = useDeleteVendor( const deleteThirdParty = useDeleteThirdParty(
vendor, thirdParty,
ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey), ConnectionHandler.getConnectionID(organizationId, thirdPartyConnectionKey),
); );
const logo = faviconUrl(vendor.websiteUrl); const logo = faviconUrl(thirdParty.websiteUrl);
const reportsCount = useFragment( const reportsCount = useFragment(
complianceReportsFragment, complianceReportsFragment,
vendor as VendorComplianceTabFragment$key, thirdParty as ThirdPartyComplianceTabFragment$key,
).complianceReports.edges.length; ).complianceReports.edges.length;
const vendorsUrl = `/organizations/${organizationId}/vendors`; const thirdPartiesUrl = `/organizations/${organizationId}/third-parties`;
const baseVendorUrl const baseThirdPartyUrl
= `/organizations/${organizationId}/vendors/${vendor.id}`; = `/organizations/${organizationId}/third-parties/${thirdParty.id}`;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Breadcrumb <Breadcrumb
items={[ items={[
{ {
label: __("Vendors"), label: __("Third parties"),
to: vendorsUrl, to: thirdPartiesUrl,
}, },
{ {
label: vendor.name ?? "", label: thirdParty.name ?? "",
}, },
]} ]}
/> />
@@ -87,26 +87,26 @@ export default function VendorDetailPage(props: Props) {
{logo && ( {logo && (
<img <img
src={logo} src={logo}
alt={vendor.name ?? ""} alt={thirdParty.name ?? ""}
className="shadow-mid rounded-2xl" className="shadow-mid rounded-2xl"
/> />
)} )}
<div className="text-2xl">{vendor.name}</div> <div className="text-2xl">{thirdParty.name}</div>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{vendor.canAssess && ( {thirdParty.canAssess && (
<ImportAssessmentDialog vendorId={vendor.id}> <ImportAssessmentDialog thirdPartyId={thirdParty.id}>
<Button icon={IconPageTextLine} variant="secondary"> <Button icon={IconPageTextLine} variant="secondary">
{__("Assessment From Website")} {__("Assessment From Website")}
</Button> </Button>
</ImportAssessmentDialog> </ImportAssessmentDialog>
)} )}
{vendor.canDelete && ( {thirdParty.canDelete && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
onClick={deleteVendor} onClick={deleteThirdParty}
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
@@ -116,20 +116,20 @@ export default function VendorDetailPage(props: Props) {
</div> </div>
<Tabs> <Tabs>
<TabLink to={`${baseVendorUrl}/overview`}>{__("Overview")}</TabLink> <TabLink to={`${baseThirdPartyUrl}/overview`}>{__("Overview")}</TabLink>
<TabLink to={`${baseVendorUrl}/certifications`}> <TabLink to={`${baseThirdPartyUrl}/certifications`}>
{__("Certifications")} {__("Certifications")}
</TabLink> </TabLink>
<TabLink to={`${baseVendorUrl}/compliance`}> <TabLink to={`${baseThirdPartyUrl}/compliance`}>
{__("Compliance reports")} {__("Compliance reports")}
{reportsCount > 0 && <TabBadge>{reportsCount}</TabBadge>} {reportsCount > 0 && <TabBadge>{reportsCount}</TabBadge>}
</TabLink> </TabLink>
<TabLink to={`${baseVendorUrl}/risks`}>{__("Risk Assessment")}</TabLink> <TabLink to={`${baseThirdPartyUrl}/risks`}>{__("Risk Assessment")}</TabLink>
<TabLink to={`${baseVendorUrl}/contacts`}>{__("Contacts")}</TabLink> <TabLink to={`${baseThirdPartyUrl}/contacts`}>{__("Contacts")}</TabLink>
<TabLink to={`${baseVendorUrl}/services`}>{__("Services")}</TabLink> <TabLink to={`${baseThirdPartyUrl}/services`}>{__("Services")}</TabLink>
</Tabs> </Tabs>
<Outlet context={{ vendor }} /> <Outlet context={{ thirdParty }} />
</div> </div>
); );
} }

View File

@@ -22,7 +22,7 @@ import type {
CommonThirdPartyCombobox_commonThirdParty$key, CommonThirdPartyCombobox_commonThirdParty$key,
} from "#/__generated__/core/CommonThirdPartyCombobox_commonThirdParty.graphql"; } from "#/__generated__/core/CommonThirdPartyCombobox_commonThirdParty.graphql";
import type { CommonThirdPartyComboboxQuery } from "#/__generated__/core/CommonThirdPartyComboboxQuery.graphql"; import type { CommonThirdPartyComboboxQuery } from "#/__generated__/core/CommonThirdPartyComboboxQuery.graphql";
import type { CreateVendorInput } from "#/__generated__/core/VendorGraphCreateMutation.graphql"; import type { CreateThirdPartyInput } from "#/__generated__/core/ThirdPartyGraphCreateMutation.graphql";
export type CommonThirdPartyRef export type CommonThirdPartyRef
= CommonThirdPartyCombobox_commonThirdParty$data; = CommonThirdPartyCombobox_commonThirdParty$data;
@@ -59,7 +59,7 @@ export const commonThirdPartiesQuery = graphql`
interface CommonThirdPartyComboboxProps { interface CommonThirdPartyComboboxProps {
queryRef: PreloadedQuery<CommonThirdPartyComboboxQuery>; queryRef: PreloadedQuery<CommonThirdPartyComboboxQuery>;
onSelect: (thridParty: Omit<CreateVendorInput, "organizationId">) => void; onSelect: (thridParty: Omit<CreateThirdPartyInput, "organizationId">) => void;
} }
export function CommonThirdPartyCombobox({ export function CommonThirdPartyCombobox({

View File

@@ -33,20 +33,20 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
type Props = { type Props = {
children: ReactNode; children: ReactNode;
connectionId: string; connectionId: string;
vendorId: string; thirdPartyId: string;
}; };
const createContactMutation = graphql` const createContactMutation = graphql`
mutation CreateContactDialogMutation( mutation CreateContactDialogMutation(
$input: CreateVendorContactInput! $input: CreateThirdPartyContactInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
createVendorContact(input: $input) { createThirdPartyContact(input: $input) {
vendorContactEdge @prependEdge(connections: $connections) { thirdPartyContactEdge @prependEdge(connections: $connections) {
node { node {
canUpdate: permission(action: "core:vendor-contact:update") canUpdate: permission(action: "core:thirdParty-contact:update")
canDelete: permission(action: "core:vendor-contact:delete") canDelete: permission(action: "core:thirdParty-contact:delete")
...VendorContactsTabFragment_contact ...ThirdPartyContactsTabFragment_contact
} }
} }
} }
@@ -58,7 +58,7 @@ const phoneRegex = /^\+[0-9]{8,15}$/;
export function CreateContactDialog({ export function CreateContactDialog({
children, children,
connectionId, connectionId,
vendorId, thirdPartyId,
}: Props) { }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
@@ -107,7 +107,7 @@ export function CreateContactDialog({
await createContact({ await createContact({
variables: { variables: {
input: { input: {
vendorId, thirdPartyId,
...cleanData, ...cleanData,
}, },
connections: [connectionId], connections: [connectionId],

View File

@@ -35,18 +35,18 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
type Props = { type Props = {
children: ReactNode; children: ReactNode;
connection: string; connection: string;
vendorId: string; thirdPartyId: string;
}; };
const createRiskAssessmentMutation = graphql` const createRiskAssessmentMutation = graphql`
mutation CreateRiskAssessmentDialogMutation( mutation CreateRiskAssessmentDialogMutation(
$input: CreateVendorRiskAssessmentInput! $input: CreateThirdPartyRiskAssessmentInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
createVendorRiskAssessment(input: $input) { createThirdPartyRiskAssessment(input: $input) {
vendorRiskAssessmentEdge @prependEdge(connections: $connections) { thirdPartyRiskAssessmentEdge @prependEdge(connections: $connections) {
node { node {
...VendorRiskAssessmentTabFragment_assessment ...ThirdPartyRiskAssessmentTabFragment_assessment
} }
} }
} }
@@ -65,7 +65,7 @@ const schema = z.object({
export function CreateRiskAssessmentDialog({ export function CreateRiskAssessmentDialog({
children, children,
connection, connection,
vendorId, thirdPartyId,
}: Props) { }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
@@ -92,7 +92,7 @@ export function CreateRiskAssessmentDialog({
input: { input: {
...data, ...data,
notes: data.notes || null, notes: data.notes || null,
vendorId, thirdPartyId,
expiresAt: nextYear.toISOString(), expiresAt: nextYear.toISOString(),
}, },
connections: [connection], connections: [connection],

View File

@@ -33,18 +33,18 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
type Props = { type Props = {
children: ReactNode; children: ReactNode;
connectionId: string; connectionId: string;
vendorId: string; thirdPartyId: string;
}; };
const createServiceMutation = graphql` const createServiceMutation = graphql`
mutation CreateServiceDialogMutation( mutation CreateServiceDialogMutation(
$input: CreateVendorServiceInput! $input: CreateThirdPartyServiceInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
createVendorService(input: $input) { createThirdPartyService(input: $input) {
vendorServiceEdge @prependEdge(connections: $connections) { thirdPartyServiceEdge @prependEdge(connections: $connections) {
node { node {
...VendorServicesTabFragment_service ...ThirdPartyServicesTabFragment_service
} }
} }
} }
@@ -54,7 +54,7 @@ const createServiceMutation = graphql`
export function CreateServiceDialog({ export function CreateServiceDialog({
children, children,
connectionId, connectionId,
vendorId, thirdPartyId,
}: Props) { }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
@@ -86,7 +86,7 @@ export function CreateServiceDialog({
await createService({ await createService({
variables: { variables: {
input: { input: {
vendorId, thirdPartyId,
...cleanData, ...cleanData,
}, },
connections: [connectionId], connections: [connectionId],

View File

@@ -27,8 +27,8 @@ import { useQueryLoader } from "react-relay";
import { useDebounceCallback } from "usehooks-ts"; import { useDebounceCallback } from "usehooks-ts";
import type { CommonThirdPartyComboboxQuery } from "#/__generated__/core/CommonThirdPartyComboboxQuery.graphql"; import type { CommonThirdPartyComboboxQuery } from "#/__generated__/core/CommonThirdPartyComboboxQuery.graphql";
import type { CreateVendorInput } from "#/__generated__/core/VendorGraphCreateMutation.graphql"; import type { CreateThirdPartyInput } from "#/__generated__/core/ThirdPartyGraphCreateMutation.graphql";
import { useCreateVendorMutation } from "#/hooks/graph/VendorGraph"; import { useCreateThirdPartyMutation } from "#/hooks/graph/ThirdPartyGraph";
import { import {
commonThirdPartiesQuery, commonThirdPartiesQuery,
@@ -41,19 +41,19 @@ type Props = {
connection: string; connection: string;
}; };
export function CreateVendorDialog({ export function CreateThirdPartyDialog({
children, children,
organizationId, organizationId,
connection, connection,
}: Props) { }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const [createVendor] = useCreateVendorMutation(); const [createThirdParty] = useCreateThirdPartyMutation();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [queryRef, loadQuery] const [queryRef, loadQuery]
= useQueryLoader<CommonThirdPartyComboboxQuery>(commonThirdPartiesQuery); = useQueryLoader<CommonThirdPartyComboboxQuery>(commonThirdPartiesQuery);
const onSelect = async (thirdParty: Omit<CreateVendorInput, "organizationId"> | string) => { const onSelect = async (thirdParty: Omit<CreateThirdPartyInput, "organizationId"> | string) => {
const input const input
= typeof thirdParty === "string" = typeof thirdParty === "string"
? { ? {
@@ -65,7 +65,7 @@ export function CreateVendorDialog({
...thirdParty, ...thirdParty,
organizationId, organizationId,
}; };
await createVendor({ await createThirdParty({
variables: { variables: {
input, input,
connections: [connection], connections: [connection],
@@ -95,9 +95,9 @@ export function CreateVendorDialog({
}; };
return ( return (
<Dialog ref={dialogRef} trigger={children} title={__("Add a vendor")}> <Dialog ref={dialogRef} trigger={children} title={__("Add a third party")}>
<DialogContent className="p-6"> <DialogContent className="p-6">
<Combobox onSearch={handleSearch} placeholder={__("Type vendor's name")}> <Combobox onSearch={handleSearch} placeholder={__("Type third party's name")}>
{searchQuery.trim().length >= 2 && queryRef && ( {searchQuery.trim().length >= 2 && queryRef && (
<Suspense> <Suspense>
<CommonThirdPartyCombobox <CommonThirdPartyCombobox
@@ -109,7 +109,7 @@ export function CreateVendorDialog({
{searchQuery.trim().length >= 2 && ( {searchQuery.trim().length >= 2 && (
<ComboboxItem onClick={() => void onSelect(searchQuery.trim())}> <ComboboxItem onClick={() => void onSelect(searchQuery.trim())}>
<IconPlusLarge size={20} /> <IconPlusLarge size={20} />
{__("Create a new vendor")} {__("Create a new third party")}
{" "} {" "}
: :
{searchQuery} {searchQuery}

View File

@@ -28,24 +28,24 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const deleteBusinessAssociateAgreementMutation = graphql` const deleteBusinessAssociateAgreementMutation = graphql`
mutation DeleteBusinessAssociateAgreementDialogMutation( mutation DeleteBusinessAssociateAgreementDialogMutation(
$input: DeleteVendorBusinessAssociateAgreementInput! $input: DeleteThirdPartyBusinessAssociateAgreementInput!
) { ) {
deleteVendorBusinessAssociateAgreement(input: $input) { deleteThirdPartyBusinessAssociateAgreement(input: $input) {
deletedVendorId deletedThirdPartyId
} }
} }
`; `;
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
vendorId: string; thirdPartyId: string;
fileName: string; fileName: string;
onSuccess?: () => void; onSuccess?: () => void;
}; };
export function DeleteBusinessAssociateAgreementDialog({ export function DeleteBusinessAssociateAgreementDialog({
children, children,
vendorId, thirdPartyId,
fileName, fileName,
onSuccess, onSuccess,
}: Props) { }: Props) {
@@ -61,7 +61,7 @@ export function DeleteBusinessAssociateAgreementDialog({
await mutate({ await mutate({
variables: { variables: {
input: { input: {
vendorId, thirdPartyId,
}, },
}, },
}); });

View File

@@ -28,24 +28,24 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const deleteDataPrivacyAgreementMutation = graphql` const deleteDataPrivacyAgreementMutation = graphql`
mutation DeleteDataPrivacyAgreementDialogMutation( mutation DeleteDataPrivacyAgreementDialogMutation(
$input: DeleteVendorDataPrivacyAgreementInput! $input: DeleteThirdPartyDataPrivacyAgreementInput!
) { ) {
deleteVendorDataPrivacyAgreement(input: $input) { deleteThirdPartyDataPrivacyAgreement(input: $input) {
deletedVendorId deletedThirdPartyId
} }
} }
`; `;
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
vendorId: string; thirdPartyId: string;
fileName: string; fileName: string;
onSuccess?: () => void; onSuccess?: () => void;
}; };
export function DeleteDataPrivacyAgreementDialog({ export function DeleteDataPrivacyAgreementDialog({
children, children,
vendorId, thirdPartyId,
fileName, fileName,
onSuccess, onSuccess,
}: Props) { }: Props) {
@@ -61,7 +61,7 @@ export function DeleteDataPrivacyAgreementDialog({
await mutate({ await mutate({
variables: { variables: {
input: { input: {
vendorId, thirdPartyId,
}, },
}, },
}); });

View File

@@ -31,10 +31,10 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const updateBusinessAssociateAgreementMutation = graphql` const updateBusinessAssociateAgreementMutation = graphql`
mutation EditBusinessAssociateAgreementDialogMutation( mutation EditBusinessAssociateAgreementDialogMutation(
$input: UpdateVendorBusinessAssociateAgreementInput! $input: UpdateThirdPartyBusinessAssociateAgreementInput!
) { ) {
updateVendorBusinessAssociateAgreement(input: $input) { updateThirdPartyBusinessAssociateAgreement(input: $input) {
vendorBusinessAssociateAgreement { thirdPartyBusinessAssociateAgreement {
id id
fileUrl fileUrl
validFrom validFrom
@@ -52,7 +52,7 @@ const schema = z.object({
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
vendorId: string; thirdPartyId: string;
agreement: { agreement: {
validFrom?: string | null; validFrom?: string | null;
validUntil?: string | null; validUntil?: string | null;
@@ -62,7 +62,7 @@ type Props = {
export function EditBusinessAssociateAgreementDialog({ export function EditBusinessAssociateAgreementDialog({
children, children,
vendorId, thirdPartyId,
agreement, agreement,
onSuccess, onSuccess,
}: Props) { }: Props) {
@@ -100,7 +100,7 @@ export function EditBusinessAssociateAgreementDialog({
await mutate({ await mutate({
variables: { variables: {
input: { input: {
vendorId, thirdPartyId,
validFrom: formatDatetime(data.validFrom), validFrom: formatDatetime(data.validFrom),
validUntil: formatDatetime(data.validUntil), validUntil: formatDatetime(data.validUntil),
}, },

View File

@@ -27,21 +27,21 @@ import { useEffect } from "react";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { z } from "zod"; import { z } from "zod";
import type { VendorContactsTabFragment_contact$data } from "#/__generated__/core/VendorContactsTabFragment_contact.graphql"; import type { ThirdPartyContactsTabFragment_contact$data } from "#/__generated__/core/ThirdPartyContactsTabFragment_contact.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
type Props = { type Props = {
contactId: string; contactId: string;
contact: VendorContactsTabFragment_contact$data; contact: ThirdPartyContactsTabFragment_contact$data;
onClose: () => void; onClose: () => void;
}; };
const updateContactMutation = graphql` const updateContactMutation = graphql`
mutation EditContactDialogUpdateMutation($input: UpdateVendorContactInput!) { mutation EditContactDialogUpdateMutation($input: UpdateThirdPartyContactInput!) {
updateVendorContact(input: $input) { updateThirdPartyContact(input: $input) {
vendorContact { thirdPartyContact {
...VendorContactsTabFragment_contact ...ThirdPartyContactsTabFragment_contact
} }
} }
} }

View File

@@ -31,10 +31,10 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const updateDataPrivacyAgreementMutation = graphql` const updateDataPrivacyAgreementMutation = graphql`
mutation EditDataPrivacyAgreementDialogMutation( mutation EditDataPrivacyAgreementDialogMutation(
$input: UpdateVendorDataPrivacyAgreementInput! $input: UpdateThirdPartyDataPrivacyAgreementInput!
) { ) {
updateVendorDataPrivacyAgreement(input: $input) { updateThirdPartyDataPrivacyAgreement(input: $input) {
vendorDataPrivacyAgreement { thirdPartyDataPrivacyAgreement {
id id
fileUrl fileUrl
validFrom validFrom
@@ -52,7 +52,7 @@ const schema = z.object({
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
vendorId: string; thirdPartyId: string;
agreement: { agreement: {
validFrom?: string | null; validFrom?: string | null;
validUntil?: string | null; validUntil?: string | null;
@@ -62,7 +62,7 @@ type Props = {
export function EditDataPrivacyAgreementDialog({ export function EditDataPrivacyAgreementDialog({
children, children,
vendorId, thirdPartyId,
agreement, agreement,
onSuccess, onSuccess,
}: Props) { }: Props) {
@@ -100,7 +100,7 @@ export function EditDataPrivacyAgreementDialog({
await mutate({ await mutate({
variables: { variables: {
input: { input: {
vendorId, thirdPartyId,
validFrom: formatDatetime(data.validFrom), validFrom: formatDatetime(data.validFrom),
validUntil: formatDatetime(data.validUntil), validUntil: formatDatetime(data.validUntil),
}, },

View File

@@ -40,10 +40,10 @@ type Props = {
}; };
const updateServiceMutation = graphql` const updateServiceMutation = graphql`
mutation EditServiceDialogUpdateMutation($input: UpdateVendorServiceInput!) { mutation EditServiceDialogUpdateMutation($input: UpdateThirdPartyServiceInput!) {
updateVendorService(input: $input) { updateThirdPartyService(input: $input) {
vendorService { thirdPartyService {
...VendorServicesTabFragment_service ...ThirdPartyServicesTabFragment_service
} }
} }
} }

View File

@@ -33,26 +33,26 @@ const schema = z.object({
}); });
const importAssessmentMutation = graphql` const importAssessmentMutation = graphql`
mutation ImportAssessmentDialogMutation($input: AssessVendorInput!) { mutation ImportAssessmentDialogMutation($input: AssessThirdPartyInput!) {
assessVendor(input: $input) { assessThirdParty(input: $input) {
vendor { thirdParty {
id id
name name
websiteUrl websiteUrl
...useVendorFormFragment ...useThirdPartyFormFragment
...VendorComplianceTabFragment ...ThirdPartyComplianceTabFragment
...VendorRiskAssessmentTabFragment ...ThirdPartyRiskAssessmentTabFragment
} }
} }
} }
`; `;
type Props = { type Props = {
vendorId: string; thirdPartyId: string;
children: ReactNode; children: ReactNode;
}; };
export function ImportAssessmentDialog({ vendorId, children }: Props) { export function ImportAssessmentDialog({ thirdPartyId, children }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const { register, handleSubmit, reset, formState } = useFormWithSchema( const { register, handleSubmit, reset, formState } = useFormWithSchema(
@@ -66,8 +66,8 @@ export function ImportAssessmentDialog({ vendorId, children }: Props) {
const [assess, isAssessing] = useMutationWithToasts( const [assess, isAssessing] = useMutationWithToasts(
importAssessmentMutation, importAssessmentMutation,
{ {
successMessage: __("Vendor assessed successfully."), successMessage: __("Third party assessed successfully."),
errorMessage: __("Failed to assess vendor"), errorMessage: __("Failed to assess third party"),
}, },
); );
@@ -75,7 +75,7 @@ export function ImportAssessmentDialog({ vendorId, children }: Props) {
await assess({ await assess({
variables: { variables: {
input: { input: {
id: vendorId, id: thirdPartyId,
websiteUrl: data.url, websiteUrl: data.url,
}, },
}, },

View File

@@ -30,15 +30,15 @@ import { useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { z } from "zod"; import { z } from "zod";
import type { PublishVendorListDialogMutation } from "#/__generated__/core/PublishVendorListDialogMutation.graphql"; import type { PublishThirdPartyListDialogMutation } from "#/__generated__/core/PublishThirdPartyListDialogMutation.graphql";
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField"; import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
const publishMutation = graphql` const publishMutation = graphql`
mutation PublishVendorListDialogMutation( mutation PublishThirdPartyListDialogMutation(
$input: PublishVendorListInput! $input: PublishThirdPartyListInput!
) { ) {
publishVendorList(input: $input) { publishThirdPartyList(input: $input) {
documentEdge { documentEdge {
node { node {
id id
@@ -55,7 +55,7 @@ type Props = {
onPublished?: (documentId: string) => void; onPublished?: (documentId: string) => void;
}; };
export function PublishVendorListDialog({ export function PublishThirdPartyListDialog({
children, children,
organizationId, organizationId,
defaultApproverIds, defaultApproverIds,
@@ -82,7 +82,7 @@ export function PublishVendorListDialog({
}); });
const [publish, isPublishing] const [publish, isPublishing]
= useMutation<PublishVendorListDialogMutation>(publishMutation); = useMutation<PublishThirdPartyListDialogMutation>(publishMutation);
const minorRef = useRef(false); const minorRef = useRef(false);
@@ -99,13 +99,13 @@ export function PublishVendorListDialog({
}, },
}, },
onCompleted(response) { onCompleted(response) {
const documentId = response.publishVendorList?.documentEdge?.node?.id; const documentId = response.publishThirdPartyList?.documentEdge?.node?.id;
if (documentId) { if (documentId) {
toast({ toast({
title: __("Success"), title: __("Success"),
description: hasApprovers description: hasApprovers
? __("Approval requested successfully.") ? __("Approval requested successfully.")
: __("Vendors published successfully."), : __("Third parties published successfully."),
variant: "success", variant: "success",
}); });
dialogRef.current?.close(); dialogRef.current?.close();
@@ -117,7 +117,7 @@ export function PublishVendorListDialog({
toast({ toast({
title: __("Error"), title: __("Error"),
description: formatError( description: formatError(
__("Failed to publish vendors"), __("Failed to publish third parties"),
error as GraphQLError, error as GraphQLError,
), ),
variant: "error", variant: "error",
@@ -131,7 +131,7 @@ export function PublishVendorListDialog({
className="max-w-xl" className="max-w-xl"
ref={dialogRef} ref={dialogRef}
trigger={children} trigger={children}
title={__("Publish Vendors")} title={__("Publish third parties")}
> >
<form onSubmit={e => void handleSubmit(onSubmit)(e)}> <form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent padded> <DialogContent padded>

View File

@@ -33,10 +33,10 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const uploadBusinessAssociateAgreementMutation = graphql` const uploadBusinessAssociateAgreementMutation = graphql`
mutation UploadBusinessAssociateAgreementDialogMutation( mutation UploadBusinessAssociateAgreementDialogMutation(
$input: UploadVendorBusinessAssociateAgreementInput! $input: UploadThirdPartyBusinessAssociateAgreementInput!
) { ) {
uploadVendorBusinessAssociateAgreement(input: $input) { uploadThirdPartyBusinessAssociateAgreement(input: $input) {
vendorBusinessAssociateAgreement { thirdPartyBusinessAssociateAgreement {
id id
fileName fileName
fileUrl fileUrl
@@ -56,13 +56,13 @@ const schema = z.object({
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
vendorId: string; thirdPartyId: string;
onSuccess?: () => void; onSuccess?: () => void;
}; };
export function UploadBusinessAssociateAgreementDialog({ export function UploadBusinessAssociateAgreementDialog({
children, children,
vendorId, thirdPartyId,
onSuccess, onSuccess,
}: Props) { }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
@@ -109,7 +109,7 @@ export function UploadBusinessAssociateAgreementDialog({
await mutate({ await mutate({
variables: { variables: {
input: { input: {
vendorId, thirdPartyId,
fileName: data.fileName, fileName: data.fileName,
validFrom: formatDatetime(data.validFrom), validFrom: formatDatetime(data.validFrom),
validUntil: formatDatetime(data.validUntil), validUntil: formatDatetime(data.validUntil),

View File

@@ -34,11 +34,11 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const uploadComplianceReportMutation = graphql` const uploadComplianceReportMutation = graphql`
mutation UploadComplianceReportDialogMutation( mutation UploadComplianceReportDialogMutation(
$input: UploadVendorComplianceReportInput! $input: UploadThirdPartyComplianceReportInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
uploadVendorComplianceReport(input: $input) { uploadThirdPartyComplianceReport(input: $input) {
vendorComplianceReportEdge @appendEdge(connections: $connections) { thirdPartyComplianceReportEdge @appendEdge(connections: $connections) {
node { node {
id id
reportName reportName
@@ -50,7 +50,7 @@ const uploadComplianceReportMutation = graphql`
size size
downloadUrl downloadUrl
} }
canDelete: permission(action: "core:vendor-compliance-report:delete") canDelete: permission(action: "core:thirdParty-compliance-report:delete")
} }
} }
} }
@@ -64,14 +64,14 @@ const schema = z.object({
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
vendorId: string; thirdPartyId: string;
connectionId: string; connectionId: string;
onSuccess?: () => void; onSuccess?: () => void;
}; };
export function UploadComplianceReportDialog({ export function UploadComplianceReportDialog({
children, children,
vendorId, thirdPartyId,
connectionId, connectionId,
onSuccess, onSuccess,
}: Props) { }: Props) {
@@ -111,7 +111,7 @@ export function UploadComplianceReportDialog({
variables: { variables: {
connections: [connectionId], connections: [connectionId],
input: { input: {
vendorId, thirdPartyId,
reportName: uploadedFile.name, reportName: uploadedFile.name,
reportDate: `${data.reportDate}T00:00:00Z`, reportDate: `${data.reportDate}T00:00:00Z`,
validUntil: formatDatetime(data.validUntil), validUntil: formatDatetime(data.validUntil),

View File

@@ -33,10 +33,10 @@ import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const uploadDataPrivacyAgreementMutation = graphql` const uploadDataPrivacyAgreementMutation = graphql`
mutation UploadDataPrivacyAgreementDialogMutation( mutation UploadDataPrivacyAgreementDialogMutation(
$input: UploadVendorDataPrivacyAgreementInput! $input: UploadThirdPartyDataPrivacyAgreementInput!
) { ) {
uploadVendorDataPrivacyAgreement(input: $input) { uploadThirdPartyDataPrivacyAgreement(input: $input) {
vendorDataPrivacyAgreement { thirdPartyDataPrivacyAgreement {
id id
fileName fileName
fileUrl fileUrl
@@ -56,13 +56,13 @@ const schema = z.object({
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
vendorId: string; thirdPartyId: string;
onSuccess?: () => void; onSuccess?: () => void;
}; };
export function UploadDataPrivacyAgreementDialog({ export function UploadDataPrivacyAgreementDialog({
children, children,
vendorId, thirdPartyId,
onSuccess, onSuccess,
}: Props) { }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
@@ -109,7 +109,7 @@ export function UploadDataPrivacyAgreementDialog({
await mutate({ await mutate({
variables: { variables: {
input: { input: {
vendorId, thirdPartyId,
fileName: data.fileName, fileName: data.fileName,
validFrom: formatDatetime(data.validFrom), validFrom: formatDatetime(data.validFrom),
validUntil: formatDatetime(data.validUntil), validUntil: formatDatetime(data.validUntil),

View File

@@ -32,23 +32,23 @@ import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql"; import type { ThirdPartyGraphNodeQuery$data } from "#/__generated__/core/ThirdPartyGraphNodeQuery.graphql";
import { useVendorForm } from "#/hooks/forms/useVendorForm"; import { useThirdPartyForm } from "#/hooks/forms/useThirdPartyForm";
/** /**
* Vendor certifications tab * ThirdParty certifications tab
*/ */
export default function VendorCertificationsTab() { export default function ThirdPartyCertificationsTab() {
const { vendor } = useOutletContext<{ const { thirdParty } = useOutletContext<{
vendor: VendorGraphNodeQuery$data["node"]; thirdParty: ThirdPartyGraphNodeQuery$data["node"];
}>(); }>();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { control, handleSubmit } = useVendorForm(vendor); const { control, handleSubmit } = useThirdPartyForm(thirdParty);
return ( return (
<form <form
className="space-y-4" className="space-y-4"
onSubmit={vendor.canUpdate onSubmit={thirdParty.canUpdate
? e => void handleSubmit(e) ? e => void handleSubmit(e)
: undefined} : undefined}
> >
@@ -60,14 +60,14 @@ export default function VendorCertificationsTab() {
<Certifications <Certifications
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value ?? []} value={field.value ?? []}
readOnly={!vendor.canUpdate} readOnly={!thirdParty.canUpdate}
/> />
)} )}
/> />
</Card> </Card>
{vendor.canUpdate && ( {thirdParty.canUpdate && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit">{__("Update vendor")}</Button> <Button type="submit">{__("Update third party")}</Button>
</div> </div>
)} )}
</form> </form>

View File

@@ -36,20 +36,20 @@ import { useOutletContext } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { ComplianceReportListQuery } from "#/__generated__/core/ComplianceReportListQuery.graphql"; import type { ComplianceReportListQuery } from "#/__generated__/core/ComplianceReportListQuery.graphql";
import type { VendorComplianceTabFragment$key } from "#/__generated__/core/VendorComplianceTabFragment.graphql"; import type { ThirdPartyComplianceTabFragment$key } from "#/__generated__/core/ThirdPartyComplianceTabFragment.graphql";
import type { VendorComplianceTabFragment_report$key } from "#/__generated__/core/VendorComplianceTabFragment_report.graphql"; import type { ThirdPartyComplianceTabFragment_report$key } from "#/__generated__/core/ThirdPartyComplianceTabFragment_report.graphql";
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql"; import type { ThirdPartyGraphNodeQuery$data } from "#/__generated__/core/ThirdPartyGraphNodeQuery.graphql";
import { SortableTable, SortableTh } from "#/components/SortableTable"; import { SortableTable, SortableTh } from "#/components/SortableTable";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { UploadComplianceReportDialog } from "../dialogs/UploadComplianceReportDialog"; import { UploadComplianceReportDialog } from "../dialogs/UploadComplianceReportDialog";
export const complianceReportsFragment = graphql` export const complianceReportsFragment = graphql`
fragment VendorComplianceTabFragment on Vendor fragment ThirdPartyComplianceTabFragment on ThirdParty
@refetchable(queryName: "ComplianceReportListQuery") @refetchable(queryName: "ComplianceReportListQuery")
@argumentDefinitions( @argumentDefinitions(
first: { type: "Int", defaultValue: 50 } first: { type: "Int", defaultValue: 50 }
order: { type: "VendorComplianceReportOrder", defaultValue: null } order: { type: "ThirdPartyComplianceReportOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null } after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null } last: { type: "Int", defaultValue: null }
@@ -60,13 +60,13 @@ export const complianceReportsFragment = graphql`
last: $last last: $last
before: $before before: $before
orderBy: $order orderBy: $order
) @connection(key: "VendorComplianceTabFragment_complianceReports") { ) @connection(key: "ThirdPartyComplianceTabFragment_complianceReports") {
__id __id
edges { edges {
node { node {
id id
canDelete: permission(action: "core:vendor-compliance-report:delete") canDelete: permission(action: "core:thirdParty-compliance-report:delete")
...VendorComplianceTabFragment_report ...ThirdPartyComplianceTabFragment_report
} }
} }
} }
@@ -74,7 +74,7 @@ export const complianceReportsFragment = graphql`
`; `;
const complianceReportFragment = graphql` const complianceReportFragment = graphql`
fragment VendorComplianceTabFragment_report on VendorComplianceReport { fragment ThirdPartyComplianceTabFragment_report on ThirdPartyComplianceReport {
id id
reportDate reportDate
validUntil validUntil
@@ -84,43 +84,43 @@ const complianceReportFragment = graphql`
size size
downloadUrl downloadUrl
} }
canDelete: permission(action: "core:vendor-compliance-report:delete") canDelete: permission(action: "core:thirdParty-compliance-report:delete")
} }
`; `;
const deleteReportMutation = graphql` const deleteReportMutation = graphql`
mutation VendorComplianceTabDeleteReportMutation( mutation ThirdPartyComplianceTabDeleteReportMutation(
$input: DeleteVendorComplianceReportInput! $input: DeleteThirdPartyComplianceReportInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
deleteVendorComplianceReport(input: $input) { deleteThirdPartyComplianceReport(input: $input) {
deletedVendorComplianceReportId @deleteEdge(connections: $connections) deletedThirdPartyComplianceReportId @deleteEdge(connections: $connections)
} }
} }
`; `;
export default function VendorComplianceTab() { export default function ThirdPartyComplianceTab() {
const { vendor } = useOutletContext<{ const { thirdParty } = useOutletContext<{
vendor: VendorGraphNodeQuery$data["node"]; thirdParty: ThirdPartyGraphNodeQuery$data["node"];
}>(); }>();
const [data, refetch] = useRefetchableFragment< const [data, refetch] = useRefetchableFragment<
ComplianceReportListQuery, ComplianceReportListQuery,
VendorComplianceTabFragment$key ThirdPartyComplianceTabFragment$key
>(complianceReportsFragment, vendor); >(complianceReportsFragment, thirdParty);
const connectionId = data.complianceReports.__id; const connectionId = data.complianceReports.__id;
const reports = data.complianceReports.edges.map(edge => edge.node); const reports = data.complianceReports.edges.map(edge => edge.node);
const { __ } = useTranslate(); const { __ } = useTranslate();
usePageTitle(vendor.name + " - " + __("Compliance reports")); usePageTitle(thirdParty.name + " - " + __("Compliance reports"));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
title={__("Compliance reports")} title={__("Compliance reports")}
description={__("Track vendor compliance certifications and reports.")} description={__("Track third party compliance certifications and reports.")}
> >
{vendor.canUploadComplianceReport && ( {thirdParty.canUploadComplianceReport && (
<UploadComplianceReportDialog <UploadComplianceReportDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
connectionId={connectionId} connectionId={connectionId}
> >
<Button icon={IconPlusLarge}>{__("Add report")}</Button> <Button icon={IconPlusLarge}>{__("Add report")}</Button>
@@ -155,13 +155,13 @@ export default function VendorComplianceTab() {
} }
type ReportRowProps = { type ReportRowProps = {
reportKey: VendorComplianceTabFragment_report$key; reportKey: ThirdPartyComplianceTabFragment_report$key;
connectionId: string; connectionId: string;
}; };
function ReportRow(props: ReportRowProps) { function ReportRow(props: ReportRowProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const report = useFragment<VendorComplianceTabFragment_report$key>( const report = useFragment<ThirdPartyComplianceTabFragment_report$key>(
complianceReportFragment, complianceReportFragment,
props.reportKey, props.reportKey,
); );

View File

@@ -35,25 +35,25 @@ import { useFragment, useRefetchableFragment } from "react-relay";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { VendorContactsListQuery } from "#/__generated__/core/VendorContactsListQuery.graphql"; import type { ThirdPartyContactsListQuery } from "#/__generated__/core/ThirdPartyContactsListQuery.graphql";
import type { VendorContactsTabFragment$key } from "#/__generated__/core/VendorContactsTabFragment.graphql"; import type { ThirdPartyContactsTabFragment$key } from "#/__generated__/core/ThirdPartyContactsTabFragment.graphql";
import type { import type {
VendorContactsTabFragment_contact$data, ThirdPartyContactsTabFragment_contact$data,
VendorContactsTabFragment_contact$key, ThirdPartyContactsTabFragment_contact$key,
} from "#/__generated__/core/VendorContactsTabFragment_contact.graphql"; } from "#/__generated__/core/ThirdPartyContactsTabFragment_contact.graphql";
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql"; import type { ThirdPartyGraphNodeQuery$data } from "#/__generated__/core/ThirdPartyGraphNodeQuery.graphql";
import { SortableTable, SortableTh } from "#/components/SortableTable"; import { SortableTable, SortableTh } from "#/components/SortableTable";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { CreateContactDialog } from "../dialogs/CreateContactDialog"; import { CreateContactDialog } from "../dialogs/CreateContactDialog";
import { EditContactDialog } from "../dialogs/EditContactDialog"; import { EditContactDialog } from "../dialogs/EditContactDialog";
export const vendorContactsFragment = graphql` export const thirdPartyContactsFragment = graphql`
fragment VendorContactsTabFragment on Vendor fragment ThirdPartyContactsTabFragment on ThirdParty
@refetchable(queryName: "VendorContactsListQuery") @refetchable(queryName: "ThirdPartyContactsListQuery")
@argumentDefinitions( @argumentDefinitions(
first: { type: "Int", defaultValue: 50 } first: { type: "Int", defaultValue: 50 }
order: { type: "VendorContactOrder", defaultValue: null } order: { type: "ThirdPartyContactOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null } after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null } last: { type: "Int", defaultValue: null }
@@ -64,14 +64,14 @@ export const vendorContactsFragment = graphql`
last: $last last: $last
before: $before before: $before
orderBy: $order orderBy: $order
) @connection(key: "VendorContactsTabFragment_contacts") { ) @connection(key: "ThirdPartyContactsTabFragment_contacts") {
__id __id
edges { edges {
node { node {
id id
canUpdate: permission(action: "core:vendor-contact:update") canUpdate: permission(action: "core:thirdParty-contact:update")
canDelete: permission(action: "core:vendor-contact:delete") canDelete: permission(action: "core:thirdParty-contact:delete")
...VendorContactsTabFragment_contact ...ThirdPartyContactsTabFragment_contact
} }
} }
} }
@@ -79,55 +79,55 @@ export const vendorContactsFragment = graphql`
`; `;
const contactFragment = graphql` const contactFragment = graphql`
fragment VendorContactsTabFragment_contact on VendorContact { fragment ThirdPartyContactsTabFragment_contact on ThirdPartyContact {
id id
fullName fullName
email email
phone phone
role role
canUpdate: permission(action: "core:vendor-contact:update") canUpdate: permission(action: "core:thirdParty-contact:update")
canDelete: permission(action: "core:vendor-contact:delete") canDelete: permission(action: "core:thirdParty-contact:delete")
} }
`; `;
const deleteContactMutation = graphql` const deleteContactMutation = graphql`
mutation VendorContactsTabDeleteContactMutation( mutation ThirdPartyContactsTabDeleteContactMutation(
$input: DeleteVendorContactInput! $input: DeleteThirdPartyContactInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
deleteVendorContact(input: $input) { deleteThirdPartyContact(input: $input) {
deletedVendorContactId @deleteEdge(connections: $connections) deletedThirdPartyContactId @deleteEdge(connections: $connections)
} }
} }
`; `;
export default function VendorContactsTab() { export default function ThirdPartyContactsTab() {
const { vendor } = useOutletContext<{ const { thirdParty } = useOutletContext<{
vendor: VendorGraphNodeQuery$data["node"]; thirdParty: ThirdPartyGraphNodeQuery$data["node"];
}>(); }>();
const [data, refetch] = useRefetchableFragment< const [data, refetch] = useRefetchableFragment<
VendorContactsListQuery, ThirdPartyContactsListQuery,
VendorContactsTabFragment$key ThirdPartyContactsTabFragment$key
>(vendorContactsFragment, vendor); >(thirdPartyContactsFragment, thirdParty);
const connectionId = data.contacts.__id; const connectionId = data.contacts.__id;
const contacts = data.contacts.edges.map(edge => edge.node); const contacts = data.contacts.edges.map(edge => edge.node);
const { __ } = useTranslate(); const { __ } = useTranslate();
const [editingContact, setEditingContact] const [editingContact, setEditingContact]
= useState<VendorContactsTabFragment_contact$data | null>(null); = useState<ThirdPartyContactsTabFragment_contact$data | null>(null);
const hasAnyAction = contacts.some( const hasAnyAction = contacts.some(
({ canUpdate, canDelete }) => canUpdate || canDelete, ({ canUpdate, canDelete }) => canUpdate || canDelete,
); );
usePageTitle(vendor.name + " - " + __("Contacts")); usePageTitle(thirdParty.name + " - " + __("Contacts"));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
title={__("Contacts")} title={__("Contacts")}
description={__("Manage vendor contacts and their information.")} description={__("Manage third party contacts and their information.")}
> >
{vendor.canCreateContact && ( {thirdParty.canCreateContact && (
<CreateContactDialog vendorId={vendor.id} connectionId={connectionId}> <CreateContactDialog thirdPartyId={thirdParty.id} connectionId={connectionId}>
<Button icon={IconPlusLarge}>{__("Add contact")}</Button> <Button icon={IconPlusLarge}>{__("Add contact")}</Button>
</CreateContactDialog> </CreateContactDialog>
)} )}
@@ -169,14 +169,14 @@ export default function VendorContactsTab() {
} }
type ContactRowProps = { type ContactRowProps = {
contactKey: VendorContactsTabFragment_contact$key; contactKey: ThirdPartyContactsTabFragment_contact$key;
connectionId: string; connectionId: string;
onEdit: (contact: VendorContactsTabFragment_contact$data) => void; onEdit: (contact: ThirdPartyContactsTabFragment_contact$data) => void;
}; };
function ContactRow(props: ContactRowProps) { function ContactRow(props: ContactRowProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const contact = useFragment<VendorContactsTabFragment_contact$key>( const contact = useFragment<ThirdPartyContactsTabFragment_contact$key>(
contactFragment, contactFragment,
props.contactKey, props.contactKey,
); );
@@ -194,7 +194,7 @@ function ContactRow(props: ContactRowProps) {
variables: { variables: {
connections: [props.connectionId], connections: [props.connectionId],
input: { input: {
vendorContactId: contact.id, thirdPartyContactId: contact.id,
}, },
}, },
}), }),

View File

@@ -15,6 +15,7 @@
import { downloadFile, formatDate } from "@probo/helpers"; import { downloadFile, formatDate } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import type { ThirdPartyCategory } from "@probo/third-parties";
import { import {
Button, Button,
Card, Card,
@@ -25,18 +26,17 @@ import {
Input, Input,
Option, Option,
} from "@probo/ui"; } from "@probo/ui";
import type { VendorCategory } from "@probo/vendors";
import { useMemo } from "react"; import { useMemo } from "react";
import { graphql, useFragment } from "react-relay"; import { graphql, useFragment } from "react-relay";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql"; import type { ThirdPartyGraphNodeQuery$data } from "#/__generated__/core/ThirdPartyGraphNodeQuery.graphql";
import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "#/__generated__/core/VendorOverviewTabBusinessAssociateAgreementFragment.graphql"; import type { ThirdPartyOverviewTabBusinessAssociateAgreementFragment$key } from "#/__generated__/core/ThirdPartyOverviewTabBusinessAssociateAgreementFragment.graphql";
import type { VendorOverviewTabDataPrivacyAgreementFragment$key } from "#/__generated__/core/VendorOverviewTabDataPrivacyAgreementFragment.graphql"; import type { ThirdPartyOverviewTabDataPrivacyAgreementFragment$key } from "#/__generated__/core/ThirdPartyOverviewTabDataPrivacyAgreementFragment.graphql";
import { ControlledField } from "#/components/form/ControlledField"; import { ControlledField } from "#/components/form/ControlledField";
import { CountriesField } from "#/components/form/CountriesField"; import { CountriesField } from "#/components/form/CountriesField";
import { PeopleSelectField } from "#/components/form/PeopleSelectField"; import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { useVendorForm } from "#/hooks/forms/useVendorForm"; import { useThirdPartyForm } from "#/hooks/forms/useThirdPartyForm";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { DeleteBusinessAssociateAgreementDialog } from "../dialogs/DeleteBusinessAssociateAgreementDialog"; import { DeleteBusinessAssociateAgreementDialog } from "../dialogs/DeleteBusinessAssociateAgreementDialog";
@@ -46,8 +46,8 @@ import { EditDataPrivacyAgreementDialog } from "../dialogs/EditDataPrivacyAgreem
import { UploadBusinessAssociateAgreementDialog } from "../dialogs/UploadBusinessAssociateAgreementDialog"; import { UploadBusinessAssociateAgreementDialog } from "../dialogs/UploadBusinessAssociateAgreementDialog";
import { UploadDataPrivacyAgreementDialog } from "../dialogs/UploadDataPrivacyAgreementDialog"; import { UploadDataPrivacyAgreementDialog } from "../dialogs/UploadDataPrivacyAgreementDialog";
const vendorBusinessAssociateAgreementFragment = graphql` const thirdPartyBusinessAssociateAgreementFragment = graphql`
fragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor { fragment ThirdPartyOverviewTabBusinessAssociateAgreementFragment on ThirdParty {
businessAssociateAgreement { businessAssociateAgreement {
id id
fileName fileName
@@ -55,36 +55,36 @@ const vendorBusinessAssociateAgreementFragment = graphql`
validFrom validFrom
validUntil validUntil
canUpdate: permission( canUpdate: permission(
action: "core:vendor-business-associate-agreement:update" action: "core:thirdParty-business-associate-agreement:update"
) )
canDelete: permission( canDelete: permission(
action: "core:vendor-business-associate-agreement:delete" action: "core:thirdParty-business-associate-agreement:delete"
) )
} }
} }
`; `;
const vendorDataPrivacyAgreementFragment = graphql` const thirdPartyDataPrivacyAgreementFragment = graphql`
fragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor { fragment ThirdPartyOverviewTabDataPrivacyAgreementFragment on ThirdParty {
dataPrivacyAgreement { dataPrivacyAgreement {
id id
fileName fileName
fileUrl fileUrl
validFrom validFrom
validUntil validUntil
canUpdate: permission(action: "core:vendor-data-privacy-agreement:update") canUpdate: permission(action: "core:thirdParty-data-privacy-agreement:update")
canDelete: permission(action: "core:vendor-data-privacy-agreement:delete") canDelete: permission(action: "core:thirdParty-data-privacy-agreement:delete")
} }
} }
`; `;
export default function VendorOverviewTab() { export default function ThirdPartyOverviewTab() {
const { vendor } = useOutletContext<{ const { thirdParty } = useOutletContext<{
vendor: VendorGraphNodeQuery$data["node"]; thirdParty: ThirdPartyGraphNodeQuery$data["node"];
}>(); }>();
const { __ } = useTranslate(); const { __ } = useTranslate();
const vendorCategories: { value: VendorCategory; label: string }[] = [ const thirdPartyCategories: { value: ThirdPartyCategory; label: string }[] = [
{ value: "ANALYTICS", label: __("Analytics") }, { value: "ANALYTICS", label: __("Analytics") },
{ value: "CLOUD_MONITORING", label: __("Cloud Monitoring") }, { value: "CLOUD_MONITORING", label: __("Cloud Monitoring") },
{ value: "CLOUD_PROVIDER", label: __("Cloud Provider") }, { value: "CLOUD_PROVIDER", label: __("Cloud Provider") },
@@ -118,21 +118,21 @@ export default function VendorOverviewTab() {
register, register,
handleSubmit, handleSubmit,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useVendorForm(vendor); } = useThirdPartyForm(thirdParty);
const vendorWithBAA const thirdPartyWithBAA
= useFragment<VendorOverviewTabBusinessAssociateAgreementFragment$key>( = useFragment<ThirdPartyOverviewTabBusinessAssociateAgreementFragment$key>(
vendorBusinessAssociateAgreementFragment, thirdPartyBusinessAssociateAgreementFragment,
vendor, thirdParty,
); );
const businessAssociateAgreement = vendorWithBAA.businessAssociateAgreement; const businessAssociateAgreement = thirdPartyWithBAA.businessAssociateAgreement;
const vendorWithDPA const thirdPartyWithDPA
= useFragment<VendorOverviewTabDataPrivacyAgreementFragment$key>( = useFragment<ThirdPartyOverviewTabDataPrivacyAgreementFragment$key>(
vendorDataPrivacyAgreementFragment, thirdPartyDataPrivacyAgreementFragment,
vendor, thirdParty,
); );
const dataPrivacyAgreement = vendorWithDPA.dataPrivacyAgreement; const dataPrivacyAgreement = thirdPartyWithDPA.dataPrivacyAgreement;
const urls = useMemo( const urls = useMemo(
() => () =>
@@ -154,20 +154,20 @@ export default function VendorOverviewTab() {
[__], [__],
); );
usePageTitle(vendor.name + " - " + __("Overview")); usePageTitle(thirdParty.name + " - " + __("Overview"));
const isFormDisabled = isSubmitting || !vendor.canUpdate; const isFormDisabled = isSubmitting || !thirdParty.canUpdate;
return ( return (
<form <form
onSubmit={!vendor.canUpdate onSubmit={!thirdParty.canUpdate
? undefined ? undefined
: e => void handleSubmit(e)} : e => void handleSubmit(e)}
className="space-y-12" className="space-y-12"
> >
{/* Vendor Details */} {/* ThirdParty Details */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-base font-medium">{__("Vendor details")}</h2> <h2 className="text-base font-medium">{__("Third party details")}</h2>
<Card className="space-y-4" padded> <Card className="space-y-4" padded>
<Field <Field
{...register("name")} {...register("name")}
@@ -192,7 +192,7 @@ export default function VendorOverviewTab() {
error={errors.category?.message} error={errors.category?.message}
disabled={isFormDisabled} disabled={isFormDisabled}
> >
{vendorCategories.map(category => ( {thirdPartyCategories.map(category => (
<Option key={category.value} value={category.value}> <Option key={category.value} value={category.value}>
{category.label} {category.label}
</Option> </Option>
@@ -330,7 +330,7 @@ export default function VendorOverviewTab() {
</Button> </Button>
{businessAssociateAgreement.canUpdate && ( {businessAssociateAgreement.canUpdate && (
<EditBusinessAssociateAgreementDialog <EditBusinessAssociateAgreementDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
agreement={{ agreement={{
validFrom: businessAssociateAgreement.validFrom, validFrom: businessAssociateAgreement.validFrom,
validUntil: businessAssociateAgreement.validUntil, validUntil: businessAssociateAgreement.validUntil,
@@ -342,7 +342,7 @@ export default function VendorOverviewTab() {
)} )}
{businessAssociateAgreement.canDelete && ( {businessAssociateAgreement.canDelete && (
<DeleteBusinessAssociateAgreementDialog <DeleteBusinessAssociateAgreementDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
fileName={businessAssociateAgreement.fileName} fileName={businessAssociateAgreement.fileName}
onSuccess={() => window.location.reload()} onSuccess={() => window.location.reload()}
> >
@@ -352,9 +352,9 @@ export default function VendorOverviewTab() {
</> </>
) )
: ( : (
vendor.canUploadBAA && ( thirdParty.canUploadBAA && (
<UploadBusinessAssociateAgreementDialog <UploadBusinessAssociateAgreementDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
onSuccess={() => window.location.reload()} onSuccess={() => window.location.reload()}
> >
<Button variant="secondary" icon={IconPlusLarge}> <Button variant="secondary" icon={IconPlusLarge}>
@@ -404,7 +404,7 @@ export default function VendorOverviewTab() {
</Button> </Button>
{dataPrivacyAgreement.canUpdate && ( {dataPrivacyAgreement.canUpdate && (
<EditDataPrivacyAgreementDialog <EditDataPrivacyAgreementDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
agreement={{ agreement={{
validFrom: dataPrivacyAgreement.validFrom, validFrom: dataPrivacyAgreement.validFrom,
validUntil: dataPrivacyAgreement.validUntil, validUntil: dataPrivacyAgreement.validUntil,
@@ -416,7 +416,7 @@ export default function VendorOverviewTab() {
)} )}
{dataPrivacyAgreement.canDelete && ( {dataPrivacyAgreement.canDelete && (
<DeleteDataPrivacyAgreementDialog <DeleteDataPrivacyAgreementDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
fileName={dataPrivacyAgreement.fileName} fileName={dataPrivacyAgreement.fileName}
onSuccess={() => window.location.reload()} onSuccess={() => window.location.reload()}
> >
@@ -426,9 +426,9 @@ export default function VendorOverviewTab() {
</> </>
) )
: ( : (
vendor.canUploadDPA && ( thirdParty.canUploadDPA && (
<UploadDataPrivacyAgreementDialog <UploadDataPrivacyAgreementDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
onSuccess={() => window.location.reload()} onSuccess={() => window.location.reload()}
> >
<Button variant="secondary" icon={IconPlusLarge}> <Button variant="secondary" icon={IconPlusLarge}>
@@ -444,9 +444,9 @@ export default function VendorOverviewTab() {
{/* Submit */} {/* Submit */}
<div className="flex justify-end"> <div className="flex justify-end">
{vendor.canUpdate && ( {thirdParty.canUpdate && (
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
{__("Update vendor")} {__("Update third party")}
</Button> </Button>
)} )}
</div> </div>

View File

@@ -33,20 +33,20 @@ import { useFragment, useRefetchableFragment } from "react-relay";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql"; import type { ThirdPartyGraphNodeQuery$data } from "#/__generated__/core/ThirdPartyGraphNodeQuery.graphql";
import type { VendorRiskAssessmentTabFragment$key } from "#/__generated__/core/VendorRiskAssessmentTabFragment.graphql"; import type { ThirdPartyRiskAssessmentTabFragment$key } from "#/__generated__/core/ThirdPartyRiskAssessmentTabFragment.graphql";
import type { VendorRiskAssessmentTabFragment_assessment$key } from "#/__generated__/core/VendorRiskAssessmentTabFragment_assessment.graphql"; import type { ThirdPartyRiskAssessmentTabFragment_assessment$key } from "#/__generated__/core/ThirdPartyRiskAssessmentTabFragment_assessment.graphql";
import type { VendorRiskAssessmentTabQuery } from "#/__generated__/core/VendorRiskAssessmentTabQuery.graphql"; import type { ThirdPartyRiskAssessmentTabQuery } from "#/__generated__/core/ThirdPartyRiskAssessmentTabQuery.graphql";
import { SortableTable, SortableTh } from "#/components/SortableTable"; import { SortableTable, SortableTh } from "#/components/SortableTable";
import { CreateRiskAssessmentDialog } from "../dialogs/CreateRiskAssessmentDialog"; import { CreateRiskAssessmentDialog } from "../dialogs/CreateRiskAssessmentDialog";
const riskAssessmentsFragment = graphql` const riskAssessmentsFragment = graphql`
fragment VendorRiskAssessmentTabFragment on Vendor fragment ThirdPartyRiskAssessmentTabFragment on ThirdParty
@refetchable(queryName: "VendorRiskAssessmentTabQuery") @refetchable(queryName: "ThirdPartyRiskAssessmentTabQuery")
@argumentDefinitions( @argumentDefinitions(
first: { type: "Int", defaultValue: 50 } first: { type: "Int", defaultValue: 50 }
order: { type: "VendorRiskAssessmentOrder", defaultValue: null } order: { type: "ThirdPartyRiskAssessmentOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null } after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null } last: { type: "Int", defaultValue: null }
@@ -59,12 +59,12 @@ const riskAssessmentsFragment = graphql`
last: $last last: $last
before: $before before: $before
orderBy: $order orderBy: $order
) @connection(key: "VendorRiskAssessmentTabFragment_riskAssessments") { ) @connection(key: "ThirdPartyRiskAssessmentTabFragment_riskAssessments") {
__id __id
edges { edges {
node { node {
id id
...VendorRiskAssessmentTabFragment_assessment ...ThirdPartyRiskAssessmentTabFragment_assessment
} }
} }
pageInfo { pageInfo {
@@ -76,7 +76,7 @@ const riskAssessmentsFragment = graphql`
`; `;
const riskAssessmentFragment = graphql` const riskAssessmentFragment = graphql`
fragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment { fragment ThirdPartyRiskAssessmentTabFragment_assessment on ThirdPartyRiskAssessment {
id id
createdAt createdAt
expiresAt expiresAt
@@ -86,27 +86,27 @@ const riskAssessmentFragment = graphql`
} }
`; `;
export default function VendorRiskAssessmentTab() { export default function ThirdPartyRiskAssessmentTab() {
const { vendor } = useOutletContext<{ const { thirdParty } = useOutletContext<{
vendor: VendorGraphNodeQuery$data["node"]; thirdParty: ThirdPartyGraphNodeQuery$data["node"];
}>(); }>();
const [data, refetch] = useRefetchableFragment< const [data, refetch] = useRefetchableFragment<
VendorRiskAssessmentTabQuery, ThirdPartyRiskAssessmentTabQuery,
VendorRiskAssessmentTabFragment$key ThirdPartyRiskAssessmentTabFragment$key
>(riskAssessmentsFragment, vendor); >(riskAssessmentsFragment, thirdParty);
const assessments = data.riskAssessments.edges.map(edge => edge.node); const assessments = data.riskAssessments.edges.map(edge => edge.node);
const { __ } = useTranslate(); const { __ } = useTranslate();
const [expanded, setExpanded] = useState<string | null>(null); const [expanded, setExpanded] = useState<string | null>(null);
usePageTitle(vendor.name + " - " + __("Risk Assessments")); usePageTitle(thirdParty.name + " - " + __("Risk Assessments"));
if (assessments.length === 0) { if (assessments.length === 0) {
return ( return (
<div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2"> <div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2">
{__("No risk assessments found")} {__("No risk assessments found")}
{vendor.canCreateRiskAssessment && ( {thirdParty.canCreateRiskAssessment && (
<CreateRiskAssessmentDialog <CreateRiskAssessmentDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
connection={data.riskAssessments.__id} connection={data.riskAssessments.__id}
> >
<Button icon={IconPlusLarge} variant="secondary"> <Button icon={IconPlusLarge} variant="secondary">
@@ -134,9 +134,9 @@ export default function VendorRiskAssessmentTab() {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{vendor.canCreateRiskAssessment && ( {thirdParty.canCreateRiskAssessment && (
<CreateRiskAssessmentDialog <CreateRiskAssessmentDialog
vendorId={vendor.id} thirdPartyId={thirdParty.id}
connection={data.riskAssessments.__id} connection={data.riskAssessments.__id}
> >
<TrButton colspan={5} onClick={() => {}}> <TrButton colspan={5} onClick={() => {}}>
@@ -163,7 +163,7 @@ export default function VendorRiskAssessmentTab() {
} }
type AssessmentRowProps = { type AssessmentRowProps = {
assessmentKey: VendorRiskAssessmentTabFragment_assessment$key; assessmentKey: ThirdPartyRiskAssessmentTabFragment_assessment$key;
onClick: (id: string) => void; onClick: (id: string) => void;
isExpanded: boolean; isExpanded: boolean;
}; };
@@ -171,7 +171,7 @@ type AssessmentRowProps = {
function AssessmentRow(props: AssessmentRowProps) { function AssessmentRow(props: AssessmentRowProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const assessment const assessment
= useFragment<VendorRiskAssessmentTabFragment_assessment$key>( = useFragment<ThirdPartyRiskAssessmentTabFragment_assessment$key>(
riskAssessmentFragment, riskAssessmentFragment,
props.assessmentKey, props.assessmentKey,
); );

View File

@@ -35,25 +35,25 @@ import { useFragment, useRefetchableFragment } from "react-relay";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { VendorGraphNodeQuery$data } from "#/__generated__/core/VendorGraphNodeQuery.graphql"; import type { ThirdPartyGraphNodeQuery$data } from "#/__generated__/core/ThirdPartyGraphNodeQuery.graphql";
import type { VendorServicesListQuery } from "#/__generated__/core/VendorServicesListQuery.graphql"; import type { ThirdPartyServicesListQuery } from "#/__generated__/core/ThirdPartyServicesListQuery.graphql";
import type { VendorServicesTabFragment$key } from "#/__generated__/core/VendorServicesTabFragment.graphql"; import type { ThirdPartyServicesTabFragment$key } from "#/__generated__/core/ThirdPartyServicesTabFragment.graphql";
import type { import type {
VendorServicesTabFragment_service$data, ThirdPartyServicesTabFragment_service$data,
VendorServicesTabFragment_service$key, ThirdPartyServicesTabFragment_service$key,
} from "#/__generated__/core/VendorServicesTabFragment_service.graphql"; } from "#/__generated__/core/ThirdPartyServicesTabFragment_service.graphql";
import { SortableTable, SortableTh } from "#/components/SortableTable"; import { SortableTable, SortableTh } from "#/components/SortableTable";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { CreateServiceDialog } from "../dialogs/CreateServiceDialog"; import { CreateServiceDialog } from "../dialogs/CreateServiceDialog";
import { EditServiceDialog } from "../dialogs/EditServiceDialog"; import { EditServiceDialog } from "../dialogs/EditServiceDialog";
export const vendorServicesFragment = graphql` export const thirdPartyServicesFragment = graphql`
fragment VendorServicesTabFragment on Vendor fragment ThirdPartyServicesTabFragment on ThirdParty
@refetchable(queryName: "VendorServicesListQuery") @refetchable(queryName: "ThirdPartyServicesListQuery")
@argumentDefinitions( @argumentDefinitions(
first: { type: "Int", defaultValue: 50 } first: { type: "Int", defaultValue: 50 }
order: { type: "VendorServiceOrder", defaultValue: null } order: { type: "ThirdPartyServiceOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null } after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null } last: { type: "Int", defaultValue: null }
@@ -64,14 +64,14 @@ export const vendorServicesFragment = graphql`
last: $last last: $last
before: $before before: $before
orderBy: $order orderBy: $order
) @connection(key: "VendorServicesTabFragment_services") { ) @connection(key: "ThirdPartyServicesTabFragment_services") {
__id __id
edges { edges {
node { node {
id id
canUpdate: permission(action: "core:vendor-service:update") canUpdate: permission(action: "core:thirdParty-service:update")
canDelete: permission(action: "core:vendor-service:delete") canDelete: permission(action: "core:thirdParty-service:delete")
...VendorServicesTabFragment_service ...ThirdPartyServicesTabFragment_service
} }
} }
} }
@@ -79,53 +79,53 @@ export const vendorServicesFragment = graphql`
`; `;
const serviceFragment = graphql` const serviceFragment = graphql`
fragment VendorServicesTabFragment_service on VendorService { fragment ThirdPartyServicesTabFragment_service on ThirdPartyService {
id id
name name
description description
canUpdate: permission(action: "core:vendor-service:update") canUpdate: permission(action: "core:thirdParty-service:update")
canDelete: permission(action: "core:vendor-service:delete") canDelete: permission(action: "core:thirdParty-service:delete")
} }
`; `;
const deleteServiceMutation = graphql` const deleteServiceMutation = graphql`
mutation VendorServicesTabDeleteServiceMutation( mutation ThirdPartyServicesTabDeleteServiceMutation(
$input: DeleteVendorServiceInput! $input: DeleteThirdPartyServiceInput!
$connections: [ID!]! $connections: [ID!]!
) { ) {
deleteVendorService(input: $input) { deleteThirdPartyService(input: $input) {
deletedVendorServiceId @deleteEdge(connections: $connections) deletedThirdPartyServiceId @deleteEdge(connections: $connections)
} }
} }
`; `;
export default function VendorServicesTab() { export default function ThirdPartyServicesTab() {
const { vendor } = useOutletContext<{ const { thirdParty } = useOutletContext<{
vendor: VendorGraphNodeQuery$data["node"]; thirdParty: ThirdPartyGraphNodeQuery$data["node"];
}>(); }>();
const [data, refetch] = useRefetchableFragment< const [data, refetch] = useRefetchableFragment<
VendorServicesListQuery, ThirdPartyServicesListQuery,
VendorServicesTabFragment$key ThirdPartyServicesTabFragment$key
>(vendorServicesFragment, vendor); >(thirdPartyServicesFragment, thirdParty);
const connectionId = data.services.__id; const connectionId = data.services.__id;
const services = data.services.edges.map(edge => edge.node); const services = data.services.edges.map(edge => edge.node);
const { __ } = useTranslate(); const { __ } = useTranslate();
const [editingService, setEditingService] const [editingService, setEditingService]
= useState<VendorServicesTabFragment_service$data | null>(null); = useState<ThirdPartyServicesTabFragment_service$data | null>(null);
const hasAnyAction = services.some( const hasAnyAction = services.some(
({ canUpdate, canDelete }) => canUpdate || canDelete, ({ canUpdate, canDelete }) => canUpdate || canDelete,
); );
usePageTitle(vendor.name + " - " + __("Services")); usePageTitle(thirdParty.name + " - " + __("Services"));
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
title={__("Services")} title={__("Services")}
description={__("Manage services provided by this vendor.")} description={__("Manage services provided by this third party.")}
> >
{vendor.canCreateService && ( {thirdParty.canCreateService && (
<CreateServiceDialog vendorId={vendor.id} connectionId={connectionId}> <CreateServiceDialog thirdPartyId={thirdParty.id} connectionId={connectionId}>
<Button icon={IconPlusLarge}>{__("Add service")}</Button> <Button icon={IconPlusLarge}>{__("Add service")}</Button>
</CreateServiceDialog> </CreateServiceDialog>
)} )}
@@ -165,14 +165,14 @@ export default function VendorServicesTab() {
} }
type ServiceRowProps = { type ServiceRowProps = {
serviceKey: VendorServicesTabFragment_service$key; serviceKey: ThirdPartyServicesTabFragment_service$key;
connectionId: string; connectionId: string;
onEdit: (service: VendorServicesTabFragment_service$data) => void; onEdit: (service: ThirdPartyServicesTabFragment_service$data) => void;
}; };
function ServiceRow(props: ServiceRowProps) { function ServiceRow(props: ServiceRowProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const service = useFragment<VendorServicesTabFragment_service$key>( const service = useFragment<ThirdPartyServicesTabFragment_service$key>(
serviceFragment, serviceFragment,
props.serviceKey, props.serviceKey,
); );
@@ -190,7 +190,7 @@ function ServiceRow(props: ServiceRowProps) {
variables: { variables: {
connections: [props.connectionId], connections: [props.connectionId],
input: { input: {
vendorServiceId: service.id, thirdPartyServiceId: service.id,
}, },
}, },
}), }),

View File

@@ -47,7 +47,7 @@ import { rightsRequestRoutes } from "./routes/rightsRequestRoutes";
import { riskRoutes } from "./routes/riskRoutes"; import { riskRoutes } from "./routes/riskRoutes";
import { statementsOfApplicabilityRoutes } from "./routes/statementsOfApplicabilityRoutes"; import { statementsOfApplicabilityRoutes } from "./routes/statementsOfApplicabilityRoutes";
import { taskRoutes } from "./routes/taskRoutes"; import { taskRoutes } from "./routes/taskRoutes";
import { vendorRoutes } from "./routes/vendorRoutes"; import { thirdPartyRoutes } from "./routes/thirdPartyRoutes";
const routes = [ const routes = [
{ {
@@ -291,7 +291,7 @@ const routes = [
...riskRoutes, ...riskRoutes,
...measureRoutes, ...measureRoutes,
...documentsRoutes, ...documentsRoutes,
...vendorRoutes, ...thirdPartyRoutes,
...frameworkRoutes, ...frameworkRoutes,
...taskRoutes, ...taskRoutes,
...assetRoutes, ...assetRoutes,

View File

@@ -20,43 +20,43 @@ import {
} from "@probo/routes"; } from "@probo/routes";
import { loadQuery } from "react-relay"; import { loadQuery } from "react-relay";
import type { VendorGraphListQuery } from "#/__generated__/core/VendorGraphListQuery.graphql"; import type { ThirdPartyGraphListQuery } from "#/__generated__/core/ThirdPartyGraphListQuery.graphql";
import type { VendorGraphNodeQuery } from "#/__generated__/core/VendorGraphNodeQuery.graphql"; import type { ThirdPartyGraphNodeQuery } from "#/__generated__/core/ThirdPartyGraphNodeQuery.graphql";
import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton"; import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton"; import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { coreEnvironment } from "#/environments"; import { coreEnvironment } from "#/environments";
import { vendorNodeQuery, vendorsQuery } from "#/hooks/graph/VendorGraph"; import { thirdPartiesQuery, thirdPartyNodeQuery } from "#/hooks/graph/ThirdPartyGraph";
export const vendorRoutes = [ export const thirdPartyRoutes = [
{ {
path: "vendors", path: "third-parties",
Fallback: PageSkeleton, Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ organizationId }) => loader: loaderFromQueryLoader(({ organizationId }) =>
loadQuery<VendorGraphListQuery>(coreEnvironment, vendorsQuery, { loadQuery<ThirdPartyGraphListQuery>(coreEnvironment, thirdPartiesQuery, {
organizationId: organizationId, organizationId: organizationId,
}), }),
), ),
Component: withQueryRef( Component: withQueryRef(
lazy(() => import("#/pages/organizations/vendors/VendorsPage")), lazy(() => import("#/pages/organizations/third-parties/ThirdPartiesPage")),
), ),
}, },
{ {
path: "vendors/:vendorId", path: "third-parties/:thirdPartyId",
Fallback: PageSkeleton, Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ vendorId }) => loader: loaderFromQueryLoader(({ thirdPartyId }) =>
loadQuery<VendorGraphNodeQuery>(coreEnvironment, vendorNodeQuery, { loadQuery<ThirdPartyGraphNodeQuery>(coreEnvironment, thirdPartyNodeQuery, {
vendorId: vendorId, thirdPartyId: thirdPartyId,
}), }),
), ),
Component: withQueryRef( Component: withQueryRef(
lazy(() => import("../pages/organizations/vendors/VendorDetailPage")), lazy(() => import("../pages/organizations/third-parties/ThirdPartyDetailPage")),
), ),
children: [ children: [
{ {
path: "overview", path: "overview",
Fallback: LinkCardSkeleton, Fallback: LinkCardSkeleton,
Component: lazy( Component: lazy(
() => import("../pages/organizations/vendors/tabs/VendorOverviewTab"), () => import("../pages/organizations/third-parties/tabs/ThirdPartyOverviewTab"),
), ),
}, },
{ {
@@ -64,7 +64,7 @@ export const vendorRoutes = [
Fallback: LinkCardSkeleton, Fallback: LinkCardSkeleton,
Component: lazy( Component: lazy(
() => () =>
import("../pages/organizations/vendors/tabs/VendorCertificationsTab"), import("../pages/organizations/third-parties/tabs/ThirdPartyCertificationsTab"),
), ),
}, },
{ {
@@ -72,7 +72,7 @@ export const vendorRoutes = [
Fallback: LinkCardSkeleton, Fallback: LinkCardSkeleton,
Component: lazy( Component: lazy(
() => () =>
import("../pages/organizations/vendors/tabs/VendorComplianceTab"), import("../pages/organizations/third-parties/tabs/ThirdPartyComplianceTab"),
), ),
}, },
{ {
@@ -80,21 +80,21 @@ export const vendorRoutes = [
Fallback: LinkCardSkeleton, Fallback: LinkCardSkeleton,
Component: lazy( Component: lazy(
() => () =>
import("../pages/organizations/vendors/tabs/VendorRiskAssessmentTab"), import("../pages/organizations/third-parties/tabs/ThirdPartyRiskAssessmentTab"),
), ),
}, },
{ {
path: "contacts", path: "contacts",
Fallback: LinkCardSkeleton, Fallback: LinkCardSkeleton,
Component: lazy( Component: lazy(
() => import("../pages/organizations/vendors/tabs/VendorContactsTab"), () => import("../pages/organizations/third-parties/tabs/ThirdPartyContactsTab"),
), ),
}, },
{ {
path: "services", path: "services",
Fallback: LinkCardSkeleton, Fallback: LinkCardSkeleton,
Component: lazy( Component: lazy(
() => import("../pages/organizations/vendors/tabs/VendorServicesTab"), () => import("../pages/organizations/third-parties/tabs/ThirdPartyServicesTab"),
), ),
}, },
], ],

View File

@@ -13,7 +13,7 @@
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
// Command common-third-parties-import seeds the common_third_parties table from // Command common-third-parties-import seeds the common_third_parties table from
// packages/vendors/data.json. It is idempotent: re-running upserts on conflict // packages/thirdParties/data.json. It is idempotent: re-running upserts on conflict
// (lower(name)) so existing rows keep their id and created_at. // (lower(name)) so existing rows keep their id and created_at.
// //
// When -fetch-logos is set, the tool inspects each third party's website to // When -fetch-logos is set, the tool inspects each third party's website to
@@ -419,15 +419,15 @@ func loadThirdParties(path string) ([]thirdPartyData, error) {
return thirdParties, nil return thirdParties, nil
} }
func parseCategory(tp thirdPartyData) coredata.VendorCategory { func parseCategory(tp thirdPartyData) coredata.ThirdPartyCategory {
if tp.Category == nil || *tp.Category == "" { if tp.Category == nil || *tp.Category == "" {
return coredata.VendorCategoryOther return coredata.ThirdPartyCategoryOther
} }
var c coredata.VendorCategory var c coredata.ThirdPartyCategory
if err := c.Scan(*tp.Category); err != nil { if err := c.Scan(*tp.Category); err != nil {
fmt.Fprintf(os.Stderr, "warning: third party %q has unknown category %q, falling back to OTHER\n", tp.Name, *tp.Category) fmt.Fprintf(os.Stderr, "warning: third party %q has unknown category %q, falling back to OTHER\n", tp.Name, *tp.Category)
return coredata.VendorCategoryOther return coredata.ThirdPartyCategoryOther
} }
return c return c

View File

@@ -339,41 +339,41 @@ ORDER BY a.name ASC;
return "", err return "", err
} }
vendorRows, err := tx.Query( thirdPartyRows, err := tx.Query(
ctx, ctx,
` `
SELECT SELECT
av.asset_id, av.asset_id,
v.name v.name
FROM asset_vendors av FROM asset_third_parties av
JOIN vendors v ON v.id = av.vendor_id JOIN third_parties v ON v.id = av.third_party_id
WHERE av.snapshot_id = @snapshot_id WHERE av.snapshot_id = @snapshot_id
ORDER BY v.name ASC; ORDER BY v.name ASC;
`, `,
pgx.NamedArgs{"snapshot_id": snapshotID}, pgx.NamedArgs{"snapshot_id": snapshotID},
) )
if err != nil { if err != nil {
return "", fmt.Errorf("cannot load snapshot asset vendors: %w", err) return "", fmt.Errorf("cannot load snapshot asset thirdParties: %w", err)
} }
defer vendorRows.Close() defer thirdPartyRows.Close()
vendorsByAsset := make(map[string][]string) thirdPartiesByAsset := make(map[string][]string)
for vendorRows.Next() { for thirdPartyRows.Next() {
var assetID, vendorName string var assetID, thirdPartyName string
if err := vendorRows.Scan(&assetID, &vendorName); err != nil { if err := thirdPartyRows.Scan(&assetID, &thirdPartyName); err != nil {
return "", fmt.Errorf("cannot scan vendor: %w", err) return "", fmt.Errorf("cannot scan thirdParty: %w", err)
} }
vendorsByAsset[assetID] = append(vendorsByAsset[assetID], vendorName) thirdPartiesByAsset[assetID] = append(thirdPartiesByAsset[assetID], thirdPartyName)
} }
if err := vendorRows.Err(); err != nil { if err := thirdPartyRows.Err(); err != nil {
return "", err return "", err
} }
assetRows := make([]docgen.AssetListRow, len(assets)) assetRows := make([]docgen.AssetListRow, len(assets))
for i, a := range assets { for i, a := range assets {
vendors := "-" thirdParties := "-"
if v, ok := vendorsByAsset[a.id]; ok && len(v) > 0 { if v, ok := thirdPartiesByAsset[a.id]; ok && len(v) > 0 {
vendors = strings.Join(v, ", ") thirdParties = strings.Join(v, ", ")
} }
assetRows[i] = docgen.AssetListRow{ assetRows[i] = docgen.AssetListRow{
@@ -382,7 +382,7 @@ ORDER BY v.name ASC;
Amount: a.amount, Amount: a.amount,
DataTypesStored: a.dataTypesStored, DataTypesStored: a.dataTypesStored,
Owner: a.ownerName, Owner: a.ownerName,
Vendors: vendors, ThirdParties: thirdParties,
} }
} }

View File

@@ -335,49 +335,49 @@ ORDER BY d.name ASC;
return "", err return "", err
} }
// Load vendors for each datum in this snapshot. // Load thirdParties for each datum in this snapshot.
vendorRows, err := tx.Query( thirdPartyRows, err := tx.Query(
ctx, ctx,
` `
SELECT SELECT
dv.datum_id, dv.datum_id,
v.name v.name
FROM data_vendors dv FROM data_third_parties dv
JOIN vendors v ON v.id = dv.vendor_id JOIN third_parties v ON v.id = dv.third_party_id
WHERE dv.snapshot_id = @snapshot_id WHERE dv.snapshot_id = @snapshot_id
ORDER BY v.name ASC; ORDER BY v.name ASC;
`, `,
pgx.NamedArgs{"snapshot_id": snapshotID}, pgx.NamedArgs{"snapshot_id": snapshotID},
) )
if err != nil { if err != nil {
return "", fmt.Errorf("cannot load snapshot data vendors: %w", err) return "", fmt.Errorf("cannot load snapshot data thirdParties: %w", err)
} }
defer vendorRows.Close() defer thirdPartyRows.Close()
vendorsByDatum := make(map[string][]string) thirdPartiesByDatum := make(map[string][]string)
for vendorRows.Next() { for thirdPartyRows.Next() {
var datumID, vendorName string var datumID, thirdPartyName string
if err := vendorRows.Scan(&datumID, &vendorName); err != nil { if err := thirdPartyRows.Scan(&datumID, &thirdPartyName); err != nil {
return "", fmt.Errorf("cannot scan vendor: %w", err) return "", fmt.Errorf("cannot scan thirdParty: %w", err)
} }
vendorsByDatum[datumID] = append(vendorsByDatum[datumID], vendorName) thirdPartiesByDatum[datumID] = append(thirdPartiesByDatum[datumID], thirdPartyName)
} }
if err := vendorRows.Err(); err != nil { if err := thirdPartyRows.Err(); err != nil {
return "", err return "", err
} }
dataRows := make([]docgen.DataListRow, len(data)) dataRows := make([]docgen.DataListRow, len(data))
for i, d := range data { for i, d := range data {
vendors := "-" thirdParties := "-"
if v, ok := vendorsByDatum[d.id]; ok && len(v) > 0 { if v, ok := thirdPartiesByDatum[d.id]; ok && len(v) > 0 {
vendors = strings.Join(v, ", ") thirdParties = strings.Join(v, ", ")
} }
dataRows[i] = docgen.DataListRow{ dataRows[i] = docgen.DataListRow{
Name: d.name, Name: d.name,
Classification: formatClassificationString(d.classification), Classification: formatClassificationString(d.classification),
Owner: d.ownerName, Owner: d.ownerName,
Vendors: vendors, ThirdParties: thirdParties,
} }
} }

View File

@@ -403,7 +403,7 @@ ORDER BY pa.name ASC;
return "", 0, nil return "", 0, nil
} }
vendorMap, err := loadVendorsForSnapshot(ctx, tx, snapshotID) thirdPartyMap, err := loadThirdPartiesForSnapshot(ctx, tx, snapshotID)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
@@ -415,9 +415,9 @@ ORDER BY pa.name ASC;
dpo = p.dpoName dpo = p.dpoName
} }
vendors := "None" thirdParties := "None"
if v, ok := vendorMap[p.id]; ok && len(v) > 0 { if v, ok := thirdPartyMap[p.id]; ok && len(v) > 0 {
vendors = strings.Join(v, ", ") thirdParties = strings.Join(v, ", ")
} }
listRows[i] = docgen.ProcessingActivityListRow{ listRows[i] = docgen.ProcessingActivityListRow{
@@ -440,7 +440,7 @@ ORDER BY pa.name ASC;
LastReviewDate: formatDateOrNotSpecified(p.lastReviewDate), LastReviewDate: formatDateOrNotSpecified(p.lastReviewDate),
NextReviewDate: formatDateOrNotSpecified(p.nextReviewDate), NextReviewDate: formatDateOrNotSpecified(p.nextReviewDate),
DataProtectionOfficer: dpo, DataProtectionOfficer: dpo,
Vendors: vendors, ThirdParties: thirdParties,
} }
} }
@@ -457,20 +457,20 @@ ORDER BY pa.name ASC;
return content, len(listRows), nil return content, len(listRows), nil
} }
func loadVendorsForSnapshot(ctx context.Context, tx pg.Tx, snapshotID string) (map[gid.GID][]string, error) { func loadThirdPartiesForSnapshot(ctx context.Context, tx pg.Tx, snapshotID string) (map[gid.GID][]string, error) {
rows, err := tx.Query( rows, err := tx.Query(
ctx, ctx,
` `
SELECT pav.processing_activity_id, v.name SELECT pav.processing_activity_id, v.name
FROM processing_activity_vendors pav FROM processing_activity_third_parties pav
INNER JOIN vendors v ON v.id = pav.vendor_id INNER JOIN third_parties v ON v.id = pav.third_party_id
WHERE pav.snapshot_id = @snapshot_id WHERE pav.snapshot_id = @snapshot_id
ORDER BY pav.processing_activity_id, v.name; ORDER BY pav.processing_activity_id, v.name;
`, `,
pgx.NamedArgs{"snapshot_id": snapshotID}, pgx.NamedArgs{"snapshot_id": snapshotID},
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot load snapshot vendors: %w", err) return nil, fmt.Errorf("cannot load snapshot thirdParties: %w", err)
} }
defer rows.Close() defer rows.Close()
@@ -479,7 +479,7 @@ ORDER BY pav.processing_activity_id, v.name;
var paID gid.GID var paID gid.GID
var name string var name string
if err := rows.Scan(&paID, &name); err != nil { if err := rows.Scan(&paID, &name); err != nil {
return nil, fmt.Errorf("cannot scan vendor row: %w", err) return nil, fmt.Errorf("cannot scan thirdParty row: %w", err)
} }
result[paID] = append(result[paID], name) result[paID] = append(result[paID], name)
} }

View File

@@ -12,9 +12,9 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
// Command migrate-vendor-snapshots-to-documents creates documents and document // Command migrate-thirdParty-snapshots-to-documents creates documents and document
// versions from existing vendor snapshots. For each organization that has vendor // versions from existing thirdParty snapshots. For each organization that has thirdParty
// snapshots, it generates a vendor list document using the same ProseMirror // snapshots, it generates a thirdParty list document using the same ProseMirror
// builder as the publish flow. // builder as the publish flow.
package main package main
@@ -72,22 +72,22 @@ func run() error {
return migrate(ctx, pgClient, dryRun) return migrate(ctx, pgClient, dryRun)
} }
type orgWithVendorSnapshots struct { type orgWithThirdPartySnapshots struct {
organizationID gid.GID organizationID gid.GID
tenantID gid.TenantID tenantID gid.TenantID
organizationName string organizationName string
} }
type vendorSnapshot struct { type thirdPartySnapshot struct {
snapshotID string snapshotID string
publishedAt time.Time publishedAt time.Time
} }
func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error { func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error {
var orgs []orgWithVendorSnapshots var orgs []orgWithThirdPartySnapshots
err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
var err error var err error
orgs, err = loadOrgsWithVendorSnapshots(ctx, conn) orgs, err = loadOrgsWithThirdPartySnapshots(ctx, conn)
return err return err
}) })
if err != nil { if err != nil {
@@ -95,7 +95,7 @@ func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error {
} }
if len(orgs) == 0 { if len(orgs) == 0 {
fmt.Println("no organizations with vendor snapshots to migrate") fmt.Println("no organizations with thirdParty snapshots to migrate")
return nil return nil
} }
@@ -107,14 +107,14 @@ func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error {
if dryRun { if dryRun {
var count int var count int
err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error { err := pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
snapshots, err := loadVendorSnapshots(ctx, conn, org.organizationID) snapshots, err := loadThirdPartySnapshots(ctx, conn, org.organizationID)
count = len(snapshots) count = len(snapshots)
return err return err
}) })
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("would migrate org %s (%s) — %d vendor snapshot(s)\n", fmt.Printf("would migrate org %s (%s) — %d thirdParty snapshot(s)\n",
org.organizationID, org.organizationName, count) org.organizationID, org.organizationName, count)
continue continue
} }
@@ -143,8 +143,8 @@ func migrate(ctx context.Context, pgClient *pg.Client, dryRun bool) error {
return nil return nil
} }
func migrateOrg(ctx context.Context, tx pg.Tx, org orgWithVendorSnapshots) error { func migrateOrg(ctx context.Context, tx pg.Tx, org orgWithThirdPartySnapshots) error {
snapshots, err := loadVendorSnapshots(ctx, tx, org.organizationID) snapshots, err := loadThirdPartySnapshots(ctx, tx, org.organizationID)
if err != nil { if err != nil {
return err return err
} }
@@ -186,15 +186,15 @@ INSERT INTO documents (
_, err = tx.Exec( _, err = tx.Exec(
ctx, ctx,
`INSERT INTO generated_documents (organization_id, tenant_id, vendors_document_id, created_at, updated_at) `INSERT INTO generated_documents (organization_id, tenant_id, third_parties_document_id, created_at, updated_at)
VALUES (@organization_id, @tenant_id, @vendors_document_id, @created_at, @updated_at) VALUES (@organization_id, @tenant_id, @third_parties_document_id, @created_at, @updated_at)
ON CONFLICT (organization_id) DO UPDATE SET vendors_document_id = @vendors_document_id, updated_at = @updated_at`, ON CONFLICT (organization_id) DO UPDATE SET third_parties_document_id = @third_parties_document_id, updated_at = @updated_at`,
pgx.NamedArgs{ pgx.NamedArgs{
"organization_id": org.organizationID, "organization_id": org.organizationID,
"tenant_id": org.tenantID, "tenant_id": org.tenantID,
"vendors_document_id": documentID, "third_parties_document_id": documentID,
"created_at": now, "created_at": now,
"updated_at": now, "updated_at": now,
}, },
) )
if err != nil { if err != nil {
@@ -234,7 +234,7 @@ INSERT INTO document_versions (
"tenant_id": org.tenantID, "tenant_id": org.tenantID,
"organization_id": org.organizationID, "organization_id": org.organizationID,
"document_id": documentID, "document_id": documentID,
"title": "Vendors", "title": "ThirdParties",
"major": major + 1, "major": major + 1,
"content": content, "content": content,
"published_at": snap.publishedAt, "published_at": snap.publishedAt,
@@ -251,7 +251,7 @@ INSERT INTO document_versions (
return nil return nil
} }
func loadOrgsWithVendorSnapshots(ctx context.Context, conn pg.Querier) ([]orgWithVendorSnapshots, error) { func loadOrgsWithThirdPartySnapshots(ctx context.Context, conn pg.Querier) ([]orgWithThirdPartySnapshots, error) {
rows, err := conn.Query( rows, err := conn.Query(
ctx, ctx,
` `
@@ -263,7 +263,7 @@ SELECT DISTINCT
FROM organizations o FROM organizations o
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 FROM generated_documents gd SELECT 1 FROM generated_documents gd
WHERE gd.organization_id = o.id AND gd.vendors_document_id IS NOT NULL WHERE gd.organization_id = o.id AND gd.third_parties_document_id IS NOT NULL
) )
AND EXISTS ( AND EXISTS (
SELECT 1 FROM snapshots s SELECT 1 FROM snapshots s
@@ -273,13 +273,13 @@ ORDER BY o.created_at;
`, `,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot query organizations with vendor snapshots: %w", err) return nil, fmt.Errorf("cannot query organizations with thirdParty snapshots: %w", err)
} }
defer rows.Close() defer rows.Close()
var result []orgWithVendorSnapshots var result []orgWithThirdPartySnapshots
for rows.Next() { for rows.Next() {
var o orgWithVendorSnapshots var o orgWithThirdPartySnapshots
var createdAt time.Time var createdAt time.Time
if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil { if err := rows.Scan(&o.organizationID, &o.tenantID, &o.organizationName, &createdAt); err != nil {
return nil, fmt.Errorf("cannot scan organization: %w", err) return nil, fmt.Errorf("cannot scan organization: %w", err)
@@ -290,7 +290,7 @@ ORDER BY o.created_at;
return result, rows.Err() return result, rows.Err()
} }
func loadVendorSnapshots(ctx context.Context, conn pg.Querier, organizationID gid.GID) ([]vendorSnapshot, error) { func loadThirdPartySnapshots(ctx context.Context, conn pg.Querier, organizationID gid.GID) ([]thirdPartySnapshot, error) {
rows, err := conn.Query( rows, err := conn.Query(
ctx, ctx,
` `
@@ -305,13 +305,13 @@ ORDER BY s.created_at ASC;
pgx.NamedArgs{"organization_id": organizationID}, pgx.NamedArgs{"organization_id": organizationID},
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot query vendor snapshots for org %s: %w", organizationID, err) return nil, fmt.Errorf("cannot query thirdParty snapshots for org %s: %w", organizationID, err)
} }
defer rows.Close() defer rows.Close()
var result []vendorSnapshot var result []thirdPartySnapshot
for rows.Next() { for rows.Next() {
var s vendorSnapshot var s thirdPartySnapshot
if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil { if err := rows.Scan(&s.snapshotID, &s.publishedAt); err != nil {
return nil, fmt.Errorf("cannot scan snapshot: %w", err) return nil, fmt.Errorf("cannot scan snapshot: %w", err)
} }
@@ -321,7 +321,7 @@ ORDER BY s.created_at ASC;
return result, rows.Err() return result, rows.Err()
} }
type vendorInfo struct { type thirdPartyInfo struct {
id string id string
name string name string
category string category string
@@ -352,7 +352,7 @@ func buildSnapshotContent(
orgName string, orgName string,
publishedAt time.Time, publishedAt time.Time,
) (string, error) { ) (string, error) {
vendorRows, err := tx.Query( thirdPartyRows, err := tx.Query(
ctx, ctx,
` `
SELECT SELECT
@@ -376,7 +376,7 @@ SELECT
v.countries, v.countries,
COALESCE(bo.full_name, 'Not assigned'), COALESCE(bo.full_name, 'Not assigned'),
COALESCE(so.full_name, 'Not assigned') COALESCE(so.full_name, 'Not assigned')
FROM vendors v FROM third_parties v
LEFT JOIN iam_membership_profiles bo ON bo.id = v.business_owner_profile_id LEFT JOIN iam_membership_profiles bo ON bo.id = v.business_owner_profile_id
LEFT JOIN iam_membership_profiles so ON so.id = v.security_owner_profile_id LEFT JOIN iam_membership_profiles so ON so.id = v.security_owner_profile_id
WHERE v.snapshot_id = @snapshot_id WHERE v.snapshot_id = @snapshot_id
@@ -385,14 +385,14 @@ ORDER BY v.name ASC;
pgx.NamedArgs{"snapshot_id": snapshotID}, pgx.NamedArgs{"snapshot_id": snapshotID},
) )
if err != nil { if err != nil {
return "", fmt.Errorf("cannot load snapshot vendors: %w", err) return "", fmt.Errorf("cannot load snapshot thirdParties: %w", err)
} }
defer vendorRows.Close() defer thirdPartyRows.Close()
var vendors []vendorInfo var thirdParties []thirdPartyInfo
for vendorRows.Next() { for thirdPartyRows.Next() {
var v vendorInfo var v thirdPartyInfo
if err := vendorRows.Scan( if err := thirdPartyRows.Scan(
&v.id, &v.name, &v.category, &v.id, &v.name, &v.category,
&v.legalName, &v.description, &v.headquarterAddress, &v.legalName, &v.description, &v.headquarterAddress,
&v.websiteURL, &v.privacyPolicyURL, &v.serviceLevelAgreementURL, &v.websiteURL, &v.privacyPolicyURL, &v.serviceLevelAgreementURL,
@@ -402,52 +402,52 @@ ORDER BY v.name ASC;
&v.certifications, &v.countries, &v.certifications, &v.countries,
&v.businessOwnerName, &v.securityOwnerName, &v.businessOwnerName, &v.securityOwnerName,
); err != nil { ); err != nil {
return "", fmt.Errorf("cannot scan vendor: %w", err) return "", fmt.Errorf("cannot scan thirdParty: %w", err)
} }
vendors = append(vendors, v) thirdParties = append(thirdParties, v)
} }
if err := vendorRows.Err(); err != nil { if err := thirdPartyRows.Err(); err != nil {
return "", err return "", err
} }
vendorIDs := make([]string, len(vendors)) thirdPartyIDs := make([]string, len(thirdParties))
for i, v := range vendors { for i, v := range thirdParties {
vendorIDs[i] = v.id thirdPartyIDs[i] = v.id
} }
servicesByVendor, err := loadSnapshotServices(ctx, tx, snapshotID, vendorIDs) servicesByThirdParty, err := loadSnapshotServices(ctx, tx, snapshotID, thirdPartyIDs)
if err != nil { if err != nil {
return "", err return "", err
} }
contactsByVendor, err := loadSnapshotContacts(ctx, tx, snapshotID, vendorIDs) contactsByThirdParty, err := loadSnapshotContacts(ctx, tx, snapshotID, thirdPartyIDs)
if err != nil { if err != nil {
return "", err return "", err
} }
assessmentsByVendor, err := loadSnapshotRiskAssessments(ctx, tx, snapshotID, vendorIDs) assessmentsByThirdParty, err := loadSnapshotRiskAssessments(ctx, tx, snapshotID, thirdPartyIDs)
if err != nil { if err != nil {
return "", err return "", err
} }
reportsByVendor, err := loadSnapshotComplianceReports(ctx, tx, snapshotID, vendorIDs) reportsByThirdParty, err := loadSnapshotComplianceReports(ctx, tx, snapshotID, thirdPartyIDs)
if err != nil { if err != nil {
return "", err return "", err
} }
baaByVendor, err := loadSnapshotBAAs(ctx, tx, snapshotID, vendorIDs) baaByThirdParty, err := loadSnapshotBAAs(ctx, tx, snapshotID, thirdPartyIDs)
if err != nil { if err != nil {
return "", err return "", err
} }
dpaByVendor, err := loadSnapshotDPAs(ctx, tx, snapshotID, vendorIDs) dpaByThirdParty, err := loadSnapshotDPAs(ctx, tx, snapshotID, thirdPartyIDs)
if err != nil { if err != nil {
return "", err return "", err
} }
rows := make([]docgen.VendorListRow, 0, len(vendors)) rows := make([]docgen.ThirdPartyListRow, 0, len(thirdParties))
for _, v := range vendors { for _, v := range thirdParties {
row := docgen.VendorListRow{ row := docgen.ThirdPartyListRow{
Name: v.name, Name: v.name,
LegalName: deref(v.legalName), LegalName: deref(v.legalName),
Description: deref(v.description), Description: deref(v.description),
@@ -467,99 +467,99 @@ ORDER BY v.name ASC;
Countries: joinOrDefault(v.countries), Countries: joinOrDefault(v.countries),
BusinessOwner: v.businessOwnerName, BusinessOwner: v.businessOwnerName,
SecurityOwner: v.securityOwnerName, SecurityOwner: v.securityOwnerName,
Services: servicesByVendor[v.id], Services: servicesByThirdParty[v.id],
Contacts: contactsByVendor[v.id], Contacts: contactsByThirdParty[v.id],
RiskAssessments: assessmentsByVendor[v.id], RiskAssessments: assessmentsByThirdParty[v.id],
ComplianceReports: reportsByVendor[v.id], ComplianceReports: reportsByThirdParty[v.id],
BusinessAssociateAgreement: baaByVendor[v.id], BusinessAssociateAgreement: baaByThirdParty[v.id],
DataPrivacyAgreement: dpaByVendor[v.id], DataPrivacyAgreement: dpaByThirdParty[v.id],
} }
rows = append(rows, row) rows = append(rows, row)
} }
docData := docgen.VendorListData{ docData := docgen.ThirdPartyListData{
Title: "Vendors", Title: "ThirdParties",
OrganizationName: orgName, OrganizationName: orgName,
CreatedAt: publishedAt, CreatedAt: publishedAt,
TotalVendors: len(rows), TotalThirdParties: len(rows),
Rows: rows, Rows: rows,
} }
return probo.BuildVendorListDocument(docData) return probo.BuildThirdPartyListDocument(docData)
} }
func loadSnapshotServices(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string][]docgen.VendorListService, error) { func loadSnapshotServices(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string][]docgen.ThirdPartyListService, error) {
rows, err := tx.Query(ctx, rows, err := tx.Query(ctx,
`SELECT vs.vendor_id, vs.name, COALESCE(vs.description, 'Not specified') `SELECT vs.third_party_id, vs.name, COALESCE(vs.description, 'Not specified')
FROM vendor_services vs FROM third_party_services vs
WHERE vs.snapshot_id = @snapshot_id AND vs.vendor_id = ANY(@vendor_ids) WHERE vs.snapshot_id = @snapshot_id AND vs.third_party_id = ANY(@third_party_ids)
ORDER BY vs.vendor_id, vs.name ASC`, ORDER BY vs.third_party_id, vs.name ASC`,
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs}) pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs})
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot load snapshot services: %w", err) return nil, fmt.Errorf("cannot load snapshot services: %w", err)
} }
defer rows.Close() defer rows.Close()
result := make(map[string][]docgen.VendorListService) result := make(map[string][]docgen.ThirdPartyListService)
for rows.Next() { for rows.Next() {
var vendorID, name, desc string var thirdPartyID, name, desc string
if err := rows.Scan(&vendorID, &name, &desc); err != nil { if err := rows.Scan(&thirdPartyID, &name, &desc); err != nil {
return nil, fmt.Errorf("cannot scan service: %w", err) return nil, fmt.Errorf("cannot scan service: %w", err)
} }
result[vendorID] = append(result[vendorID], docgen.VendorListService{Name: name, Description: desc}) result[thirdPartyID] = append(result[thirdPartyID], docgen.ThirdPartyListService{Name: name, Description: desc})
} }
return result, rows.Err() return result, rows.Err()
} }
func loadSnapshotContacts(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string][]docgen.VendorListContact, error) { func loadSnapshotContacts(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string][]docgen.ThirdPartyListContact, error) {
rows, err := tx.Query(ctx, rows, err := tx.Query(ctx,
`SELECT vc.vendor_id, `SELECT vc.third_party_id,
COALESCE(vc.full_name, 'Not specified'), COALESCE(vc.full_name, 'Not specified'),
COALESCE(vc.email, 'Not specified'), COALESCE(vc.email, 'Not specified'),
COALESCE(vc.phone, 'Not specified'), COALESCE(vc.phone, 'Not specified'),
COALESCE(vc.role, 'Not specified') COALESCE(vc.role, 'Not specified')
FROM vendor_contacts vc FROM third_party_contacts vc
WHERE vc.snapshot_id = @snapshot_id AND vc.vendor_id = ANY(@vendor_ids) WHERE vc.snapshot_id = @snapshot_id AND vc.third_party_id = ANY(@third_party_ids)
ORDER BY vc.vendor_id, vc.full_name ASC`, ORDER BY vc.third_party_id, vc.full_name ASC`,
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs}) pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs})
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot load snapshot contacts: %w", err) return nil, fmt.Errorf("cannot load snapshot contacts: %w", err)
} }
defer rows.Close() defer rows.Close()
result := make(map[string][]docgen.VendorListContact) result := make(map[string][]docgen.ThirdPartyListContact)
for rows.Next() { for rows.Next() {
var vendorID, name, email, phone, role string var thirdPartyID, name, email, phone, role string
if err := rows.Scan(&vendorID, &name, &email, &phone, &role); err != nil { if err := rows.Scan(&thirdPartyID, &name, &email, &phone, &role); err != nil {
return nil, fmt.Errorf("cannot scan contact: %w", err) return nil, fmt.Errorf("cannot scan contact: %w", err)
} }
result[vendorID] = append(result[vendorID], docgen.VendorListContact{ result[thirdPartyID] = append(result[thirdPartyID], docgen.ThirdPartyListContact{
FullName: name, Email: email, Phone: phone, Role: role, FullName: name, Email: email, Phone: phone, Role: role,
}) })
} }
return result, rows.Err() return result, rows.Err()
} }
func loadSnapshotRiskAssessments(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string][]docgen.VendorListRiskAssessment, error) { func loadSnapshotRiskAssessments(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string][]docgen.ThirdPartyListRiskAssessment, error) {
rows, err := tx.Query(ctx, rows, err := tx.Query(ctx,
`SELECT vra.vendor_id, vra.created_at, vra.expires_at, vra.data_sensitivity, vra.business_impact, COALESCE(vra.notes, 'Not specified') `SELECT vra.third_party_id, vra.created_at, vra.expires_at, vra.data_sensitivity, vra.business_impact, COALESCE(vra.notes, 'Not specified')
FROM vendor_risk_assessments vra FROM third_party_risk_assessments vra
WHERE vra.snapshot_id = @snapshot_id AND vra.vendor_id = ANY(@vendor_ids) WHERE vra.snapshot_id = @snapshot_id AND vra.third_party_id = ANY(@third_party_ids)
ORDER BY vra.vendor_id, vra.created_at DESC`, ORDER BY vra.third_party_id, vra.created_at DESC`,
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs}) pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs})
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot load snapshot risk assessments: %w", err) return nil, fmt.Errorf("cannot load snapshot risk assessments: %w", err)
} }
defer rows.Close() defer rows.Close()
result := make(map[string][]docgen.VendorListRiskAssessment) result := make(map[string][]docgen.ThirdPartyListRiskAssessment)
for rows.Next() { for rows.Next() {
var vendorID, sensitivity, impact, notes string var thirdPartyID, sensitivity, impact, notes string
var assessedAt, expiresAt time.Time var assessedAt, expiresAt time.Time
if err := rows.Scan(&vendorID, &assessedAt, &expiresAt, &sensitivity, &impact, &notes); err != nil { if err := rows.Scan(&thirdPartyID, &assessedAt, &expiresAt, &sensitivity, &impact, &notes); err != nil {
return nil, fmt.Errorf("cannot scan risk assessment: %w", err) return nil, fmt.Errorf("cannot scan risk assessment: %w", err)
} }
result[vendorID] = append(result[vendorID], docgen.VendorListRiskAssessment{ result[thirdPartyID] = append(result[thirdPartyID], docgen.ThirdPartyListRiskAssessment{
AssessedAt: assessedAt.Format("2006-01-02"), AssessedAt: assessedAt.Format("2006-01-02"),
ExpiresAt: expiresAt.Format("2006-01-02"), ExpiresAt: expiresAt.Format("2006-01-02"),
DataSensitivity: sensitivity, DataSensitivity: sensitivity,
@@ -570,81 +570,81 @@ func loadSnapshotRiskAssessments(ctx context.Context, tx pg.Tx, snapshotID strin
return result, rows.Err() return result, rows.Err()
} }
func loadSnapshotComplianceReports(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string][]docgen.VendorListComplianceReport, error) { func loadSnapshotComplianceReports(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string][]docgen.ThirdPartyListComplianceReport, error) {
rows, err := tx.Query(ctx, rows, err := tx.Query(ctx,
`SELECT vcr.vendor_id, vcr.report_name, vcr.report_date, vcr.valid_until `SELECT vcr.third_party_id, vcr.report_name, vcr.report_date, vcr.valid_until
FROM vendor_compliance_reports vcr FROM third_party_compliance_reports vcr
WHERE vcr.snapshot_id = @snapshot_id AND vcr.vendor_id = ANY(@vendor_ids) WHERE vcr.snapshot_id = @snapshot_id AND vcr.third_party_id = ANY(@third_party_ids)
ORDER BY vcr.vendor_id, vcr.report_date DESC`, ORDER BY vcr.third_party_id, vcr.report_date DESC`,
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs}) pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs})
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot load snapshot compliance reports: %w", err) return nil, fmt.Errorf("cannot load snapshot compliance reports: %w", err)
} }
defer rows.Close() defer rows.Close()
result := make(map[string][]docgen.VendorListComplianceReport) result := make(map[string][]docgen.ThirdPartyListComplianceReport)
for rows.Next() { for rows.Next() {
var vendorID, name string var thirdPartyID, name string
var reportDate time.Time var reportDate time.Time
var validUntil *time.Time var validUntil *time.Time
if err := rows.Scan(&vendorID, &name, &reportDate, &validUntil); err != nil { if err := rows.Scan(&thirdPartyID, &name, &reportDate, &validUntil); err != nil {
return nil, fmt.Errorf("cannot scan compliance report: %w", err) return nil, fmt.Errorf("cannot scan compliance report: %w", err)
} }
vu := "Not specified" vu := "Not specified"
if validUntil != nil { if validUntil != nil {
vu = validUntil.Format("2006-01-02") vu = validUntil.Format("2006-01-02")
} }
result[vendorID] = append(result[vendorID], docgen.VendorListComplianceReport{ result[thirdPartyID] = append(result[thirdPartyID], docgen.ThirdPartyListComplianceReport{
ReportName: name, ReportDate: reportDate.Format("2006-01-02"), ValidUntil: vu, ReportName: name, ReportDate: reportDate.Format("2006-01-02"), ValidUntil: vu,
}) })
} }
return result, rows.Err() return result, rows.Err()
} }
func loadSnapshotBAAs(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string]*docgen.VendorListAgreement, error) { func loadSnapshotBAAs(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string]*docgen.ThirdPartyListAgreement, error) {
rows, err := tx.Query(ctx, rows, err := tx.Query(ctx,
`SELECT vbaa.vendor_id, vbaa.valid_from, vbaa.valid_until `SELECT vbaa.third_party_id, vbaa.valid_from, vbaa.valid_until
FROM vendor_business_associate_agreements vbaa FROM third_party_business_associate_agreements vbaa
WHERE vbaa.snapshot_id = @snapshot_id AND vbaa.vendor_id = ANY(@vendor_ids)`, WHERE vbaa.snapshot_id = @snapshot_id AND vbaa.third_party_id = ANY(@third_party_ids)`,
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs}) pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs})
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot load snapshot BAAs: %w", err) return nil, fmt.Errorf("cannot load snapshot BAAs: %w", err)
} }
defer rows.Close() defer rows.Close()
result := make(map[string]*docgen.VendorListAgreement) result := make(map[string]*docgen.ThirdPartyListAgreement)
for rows.Next() { for rows.Next() {
var vendorID string var thirdPartyID string
var validFrom, validUntil *time.Time var validFrom, validUntil *time.Time
if err := rows.Scan(&vendorID, &validFrom, &validUntil); err != nil { if err := rows.Scan(&thirdPartyID, &validFrom, &validUntil); err != nil {
return nil, fmt.Errorf("cannot scan BAA: %w", err) return nil, fmt.Errorf("cannot scan BAA: %w", err)
} }
result[vendorID] = &docgen.VendorListAgreement{ result[thirdPartyID] = &docgen.ThirdPartyListAgreement{
ValidFrom: fmtTime(validFrom), ValidUntil: fmtTime(validUntil), ValidFrom: fmtTime(validFrom), ValidUntil: fmtTime(validUntil),
} }
} }
return result, rows.Err() return result, rows.Err()
} }
func loadSnapshotDPAs(ctx context.Context, tx pg.Tx, snapshotID string, vendorIDs []string) (map[string]*docgen.VendorListAgreement, error) { func loadSnapshotDPAs(ctx context.Context, tx pg.Tx, snapshotID string, thirdPartyIDs []string) (map[string]*docgen.ThirdPartyListAgreement, error) {
rows, err := tx.Query(ctx, rows, err := tx.Query(ctx,
`SELECT vdpa.vendor_id, vdpa.valid_from, vdpa.valid_until `SELECT vdpa.third_party_id, vdpa.valid_from, vdpa.valid_until
FROM vendor_data_privacy_agreements vdpa FROM third_party_data_privacy_agreements vdpa
WHERE vdpa.snapshot_id = @snapshot_id AND vdpa.vendor_id = ANY(@vendor_ids)`, WHERE vdpa.snapshot_id = @snapshot_id AND vdpa.third_party_id = ANY(@third_party_ids)`,
pgx.NamedArgs{"snapshot_id": snapshotID, "vendor_ids": vendorIDs}) pgx.NamedArgs{"snapshot_id": snapshotID, "third_party_ids": thirdPartyIDs})
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot load snapshot DPAs: %w", err) return nil, fmt.Errorf("cannot load snapshot DPAs: %w", err)
} }
defer rows.Close() defer rows.Close()
result := make(map[string]*docgen.VendorListAgreement) result := make(map[string]*docgen.ThirdPartyListAgreement)
for rows.Next() { for rows.Next() {
var vendorID string var thirdPartyID string
var validFrom, validUntil *time.Time var validFrom, validUntil *time.Time
if err := rows.Scan(&vendorID, &validFrom, &validUntil); err != nil { if err := rows.Scan(&thirdPartyID, &validFrom, &validUntil); err != nil {
return nil, fmt.Errorf("cannot scan DPA: %w", err) return nil, fmt.Errorf("cannot scan DPA: %w", err)
} }
result[vendorID] = &docgen.VendorListAgreement{ result[thirdPartyID] = &docgen.ThirdPartyListAgreement{
ValidFrom: fmtTime(validFrom), ValidUntil: fmtTime(validUntil), ValidFrom: fmtTime(validFrom), ValidUntil: fmtTime(validUntil),
} }
} }

View File

@@ -22,12 +22,12 @@ The `pages/` folder **is** the route tree. Every route segment maps to a folder
// Bad — separate routes/ folder duplicates pages/ structure // Bad — separate routes/ folder duplicates pages/ structure
src/ src/
routes/ routes/
vendorRoutes.ts # route definitions for vendors thirdPartyRoutes.ts # route definitions for third parties
assetRoutes.ts # route definitions for assets assetRoutes.ts # route definitions for assets
pages/ pages/
organizations/ organizations/
vendors/ third-parties/
VendorsPage.tsx ThirdPartiesPage.tsx
assets/ assets/
AssetsPage.tsx AssetsPage.tsx
``` ```
@@ -37,9 +37,9 @@ src/
src/ src/
pages/ pages/
organizations/ organizations/
vendors/ third-parties/
routes.ts # route definitions for vendors routes.ts # route definitions for third parties
VendorsPage.tsx ThirdPartiesPage.tsx
assets/ assets/
routes.ts # route definitions for assets routes.ts # route definitions for assets
AssetsPage.tsx AssetsPage.tsx
@@ -77,11 +77,11 @@ Use the correct suffix so the role is clear from the file name alone:
```text ```text
// Bad — a layout route named as a "Page" // Bad — a layout route named as a "Page"
VendorDetailPage.tsx # renders <Outlet />, wraps child routes ThirdPartyDetailPage.tsx # renders <Outlet />, wraps child routes
CookieBannerConfigPage.tsx # renders tabs + <Outlet /> CookieBannerConfigPage.tsx # renders tabs + <Outlet />
// Good — layout routes use the "Layout" suffix // Good — layout routes use the "Layout" suffix
VendorDetailLayout.tsx ThirdPartyDetailLayout.tsx
CookieBannerConfigLayout.tsx CookieBannerConfigLayout.tsx
``` ```
@@ -142,26 +142,26 @@ export default function CookieBannerLayout() {
Contains route objects for the current folder's feature, exported as a named array and spread into the parent. Keep imports minimal — only `lazy`, skeleton components, and typing. Contains route objects for the current folder's feature, exported as a named array and spread into the parent. Keep imports minimal — only `lazy`, skeleton components, and typing.
```ts ```ts
// pages/organizations/vendors/routes.ts // pages/organizations/third-parties/routes.ts
import { lazy } from "@probo/react-lazy"; import { lazy } from "@probo/react-lazy";
import type { AppRoute } from "@probo/routes"; import type { AppRoute } from "@probo/routes";
import { VendorsPageSkeleton } from "./VendorsPageSkeleton"; import { ThirdPartiesPageSkeleton } from "./ThirdPartiesPageSkeleton";
export const vendorRoutes = [ export const thirdPartyRoutes = [
{ {
path: "vendors", path: "third-parties",
Fallback: VendorsPageSkeleton, Fallback: ThirdPartiesPageSkeleton,
Component: lazy(() => import("./VendorsPageLoader")), Component: lazy(() => import("./ThirdPartiesPageLoader")),
}, },
{ {
path: "vendors/:vendorId", path: "third-parties/:thirdPartyId",
Fallback: VendorsPageSkeleton, Fallback: ThirdPartiesPageSkeleton,
Component: lazy(() => import("./VendorDetailLayoutLoader")), Component: lazy(() => import("./ThirdPartyDetailLayoutLoader")),
children: [ children: [
{ {
path: "overview", path: "overview",
Component: lazy(() => import("./overview/VendorOverviewPage")), Component: lazy(() => import("./overview/ThirdPartyOverviewPage")),
}, },
], ],
}, },
@@ -173,35 +173,35 @@ export const vendorRoutes = [
The loader is the **lazy bundle entry point**. It sets up providers, triggers the Relay query, shows a skeleton until the query resolves, then renders the page. The loader is the **lazy bundle entry point**. It sets up providers, triggers the Relay query, shows a skeleton until the query resolves, then renders the page.
```tsx ```tsx
// pages/organizations/vendors/VendorsPageLoader.tsx // pages/organizations/third-parties/ThirdPartiesPageLoader.tsx
import { Suspense, useEffect } from "react"; import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay"; import { useQueryLoader } from "react-relay";
import type { VendorsPageQuery } from "#/__generated__/core/VendorsPageQuery.graphql"; import type { ThirdPartiesPageQuery } from "#/__generated__/core/ThirdPartiesPageQuery.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import VendorsPage, { vendorsPageQuery } from "./VendorsPage"; import ThirdPartiesPage, { thirdPartiesPageQuery } from "./ThirdPartiesPage";
import { VendorsPageSkeleton } from "./VendorsPageSkeleton"; import { ThirdPartiesPageSkeleton } from "./ThirdPartiesPageSkeleton";
function VendorsPageQueryLoader() { function ThirdPartiesPageQueryLoader() {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<VendorsPageQuery>(vendorsPageQuery); const [queryRef, loadQuery] = useQueryLoader<ThirdPartiesPageQuery>(thirdPartiesPageQuery);
useEffect(() => { useEffect(() => {
loadQuery({ organizationId }); loadQuery({ organizationId });
}, [loadQuery, organizationId]); }, [loadQuery, organizationId]);
if (!queryRef) { if (!queryRef) {
return <VendorsPageSkeleton />; return <ThirdPartiesPageSkeleton />;
} }
return <VendorsPage queryRef={queryRef} /> return <ThirdPartiesPage queryRef={queryRef} />
} }
export default function VendorsPageLoader() { export default function ThirdPartiesPageLoader() {
return ( return (
<CoreRelayProvider> <CoreRelayProvider>
<VendorsPageQueryLoader /> <ThirdPartiesPageQueryLoader />
</CoreRelayProvider> </CoreRelayProvider>
); );
} }
@@ -212,9 +212,9 @@ export default function VendorsPageLoader() {
Receives the `queryRef` from the loader and renders the UI. Default export so `lazy()` can import it. Receives the `queryRef` from the loader and renders the UI. Default export so `lazy()` can import it.
```tsx ```tsx
// pages/organizations/vendors/VendorsPage.tsx // pages/organizations/third-parties/ThirdPartiesPage.tsx
export default function VendorsPage({ queryRef }: VendorsPageProps) { export default function ThirdPartiesPage({ queryRef }: ThirdPartiesPageProps) {
const data = usePreloadedQuery(vendorsPageQuery, queryRef); const data = usePreloadedQuery(thirdPartiesPageQuery, queryRef);
return (/* … */); return (/* … */);
} }
``` ```
@@ -224,8 +224,8 @@ export default function VendorsPage({ queryRef }: VendorsPageProps) {
A lightweight loading placeholder. Keep it free of data-fetching logic so it loads instantly. A lightweight loading placeholder. Keep it free of data-fetching logic so it loads instantly.
```tsx ```tsx
// pages/organizations/vendors/VendorsPageSkeleton.tsx // pages/organizations/third-parties/ThirdPartiesPageSkeleton.tsx
export function VendorsPageSkeleton() { export function ThirdPartiesPageSkeleton() {
return (/* pulse / skeleton UI */); return (/* pulse / skeleton UI */);
} }
``` ```
@@ -235,8 +235,8 @@ export function VendorsPageSkeleton() {
Rendered by the route error boundary when the page throws. Rendered by the route error boundary when the page throws.
```tsx ```tsx
// pages/organizations/vendors/VendorsPageError.tsx // pages/organizations/third-parties/ThirdPartiesPageError.tsx
export function VendorsPageError() { export function ThirdPartiesPageError() {
const error = useRouteError(); const error = useRouteError();
return (/* error UI */); return (/* error UI */);
} }
@@ -244,24 +244,24 @@ export function VendorsPageError() {
## File naming ## File naming
Component files (`.tsx` that export a React component) use **PascalCase**: `VendorsPage.tsx`, `VendorContactRow.tsx`, `VendorsPageSkeleton.tsx`. Component files (`.tsx` that export a React component) use **PascalCase**: `ThirdPartiesPage.tsx`, `ThirdPartyContactRow.tsx`, `ThirdPartiesPageSkeleton.tsx`.
All other helper files (utilities, hooks, constants, configuration) use **camelCase**: `routes.ts`, `useVendorFilters.ts`, `formatCurrency.ts`, `constants.ts`. All other helper files (utilities, hooks, constants, configuration) use **camelCase**: `routes.ts`, `useThirdPartyFilters.ts`, `formatCurrency.ts`, `constants.ts`.
### Do / don't: file naming ### Do / don't: file naming
```text ```text
// Bad — helper file in PascalCase // Bad — helper file in PascalCase
pages/organizations/vendors/FormatVendorStatus.ts pages/organizations/third-parties/FormatThirdPartyStatus.ts
pages/organizations/vendors/UseVendorFilters.ts pages/organizations/third-parties/UseThirdPartyFilters.ts
pages/organizations/vendors/Routes.ts pages/organizations/third-parties/Routes.ts
// Good — helpers are camelCase, components are PascalCase // Good — helpers are camelCase, components are PascalCase
pages/organizations/vendors/formatVendorStatus.ts pages/organizations/third-parties/formatThirdPartyStatus.ts
pages/organizations/vendors/useVendorFilters.ts pages/organizations/third-parties/useThirdPartyFilters.ts
pages/organizations/vendors/routes.ts pages/organizations/third-parties/routes.ts
pages/organizations/vendors/VendorsPage.tsx pages/organizations/third-parties/ThirdPartiesPage.tsx
pages/organizations/vendors/VendorsPageSkeleton.tsx pages/organizations/third-parties/ThirdPartiesPageSkeleton.tsx
``` ```
## `_components` folder ## `_components` folder
@@ -270,7 +270,7 @@ Sub-components that are used **only** by a single page live in a `_components/`
| Situation | Where the component lives | | Situation | Where the component lives |
| ------------------------------------------ | ---------------------------------------------------------------------------------- | | ------------------------------------------ | ---------------------------------------------------------------------------------- |
| Used by one page only | `pages/organizations/vendors/_components/` | | Used by one page only | `pages/organizations/third-parties/_components/` |
| Used by multiple pages in the same feature | Nearest common ancestor's `_components/` (e.g. `pages/organizations/_components/`) | | Used by multiple pages in the same feature | Nearest common ancestor's `_components/` (e.g. `pages/organizations/_components/`) |
| Reusable UI primitive | `@probo/ui` package | | Reusable UI primitive | `@probo/ui` package |
@@ -278,8 +278,8 @@ Sub-components that are used **only** by a single page live in a `_components/`
```text ```text
// Bad — shared component buried in a single page's _components // Bad — shared component buried in a single page's _components
pages/organizations/vendors/_components/StatusBadge.tsx # also used by risks page pages/organizations/third-parties/_components/StatusBadge.tsx # also used by risks page
pages/organizations/risks/SomeRiskPage.tsx # imports ../../vendors/_components/StatusBadge pages/organizations/risks/SomeRiskPage.tsx # imports ../../third-parties/_components/StatusBadge
// Good — shared component hoisted to common ancestor // Good — shared component hoisted to common ancestor
pages/organizations/_components/StatusBadge.tsx pages/organizations/_components/StatusBadge.tsx
@@ -287,10 +287,10 @@ pages/organizations/_components/StatusBadge.tsx
```text ```text
// Bad — page-specific helper placed in a global folder // Bad — page-specific helper placed in a global folder
src/components/VendorContactRow.tsx # only used by VendorContactsTab src/components/ThirdPartyContactRow.tsx # only used by ThirdPartyContactsTab
// Good — scoped to the page that uses it // Good — scoped to the page that uses it
pages/organizations/vendors/_components/VendorContactRow.tsx pages/organizations/third-parties/_components/ThirdPartyContactRow.tsx
``` ```
## Child-route folder naming ## Child-route folder naming
@@ -303,40 +303,40 @@ Folders that contain child-route pages are named after the **resource or concept
// Bad — folder named after a UI element // Bad — folder named after a UI element
configuration/ configuration/
tabs/ # "tabs" is a UI component, not a resource tabs/ # "tabs" is a UI component, not a resource
VendorOverviewTab.tsx ThirdPartyOverviewTab.tsx
VendorComplianceTab.tsx ThirdPartyComplianceTab.tsx
// Good — folders named after the resource each child route represents // Good — folders named after the resource each child route represents
configuration/ configuration/
overview/ overview/
VendorOverviewPage.tsx ThirdPartyOverviewPage.tsx
compliance/ compliance/
VendorCompliancePage.tsx ThirdPartyCompliancePage.tsx
``` ```
This also means child-route components use the `*Page` suffix (not `*Tab`), because they are pages in their own right — the fact that a tab bar navigates between them is an implementation detail of the parent layout. This also means child-route components use the `*Page` suffix (not `*Tab`), because they are pages in their own right — the fact that a tab bar navigates between them is an implementation detail of the parent layout.
## Full example tree ## Full example tree
Target layout for a `vendors` feature under `pages/organizations/`: Target layout for a `third-parties` feature under `pages/organizations/`:
```text ```text
pages/organizations/vendors/ pages/organizations/third-parties/
routes.ts # route definitions for vendors routes.ts # route definitions for third parties
VendorsPageLoader.tsx # lazy entry — providers + Suspense + query loader ThirdPartiesPageLoader.tsx # lazy entry — providers + Suspense + query loader
VendorsPage.tsx # page component (usePreloadedQuery) ThirdPartiesPage.tsx # page component (usePreloadedQuery)
VendorsPageSkeleton.tsx # loading fallback ThirdPartiesPageSkeleton.tsx # loading fallback
VendorDetailLayoutLoader.tsx # lazy entry for detail layout ThirdPartyDetailLayoutLoader.tsx # lazy entry for detail layout
VendorDetailLayout.tsx # layout — breadcrumbs, tabs, <Outlet /> ThirdPartyDetailLayout.tsx # layout — breadcrumbs, tabs, <Outlet />
VendorDetailLayoutSkeleton.tsx # detail loading fallback ThirdPartyDetailLayoutSkeleton.tsx # detail loading fallback
NewVendorPage.tsx # mutation-only page — default export, wraps itself in the Relay provider NewThirdPartyPage.tsx # mutation-only page — default export, wraps itself in the Relay provider
_components/ # sub-components used only by vendor pages _components/ # sub-components used only by third party pages
VendorContactRow.tsx ThirdPartyContactRow.tsx
VendorRiskSummary.tsx ThirdPartyRiskSummary.tsx
overview/ # child route: /vendors/:vendorId/overview overview/ # child route: /third-parties/:thirdPartyId/overview
VendorOverviewPage.tsx ThirdPartyOverviewPage.tsx
compliance/ # child route: /vendors/:vendorId/compliance compliance/ # child route: /third-parties/:thirdPartyId/compliance
VendorCompliancePage.tsx ThirdPartyCompliancePage.tsx
contacts/ # child route: /vendors/:vendorId/contacts contacts/ # child route: /third-parties/:thirdPartyId/contacts
VendorContactsPage.tsx ThirdPartyContactsPage.tsx
``` ```

View File

@@ -8,18 +8,18 @@ Policy-based authorization in `pkg/iam/` using an evaluation model similar to AW
**Policy** — a named collection of statements: **Policy** — a named collection of statements:
```go ```go
policy.NewPolicy("vendor-crud", "Vendor CRUD", policy.NewPolicy("thirdParty-crud", "ThirdParty CRUD",
policy.Allow(ActionVendorGet, ActionVendorList).WithSID("read-vendors"), policy.Allow(ActionThirdPartyGet, ActionThirdPartyList).WithSID("read-thirdParties"),
policy.Deny(ActionVendorDelete).WithSID("deny-vendor-delete"), policy.Deny(ActionThirdPartyDelete).WithSID("deny-thirdParty-delete"),
).WithDescription("Standard vendor access") ).WithDescription("Standard third party access")
``` ```
**Statement** — a single permission rule with effect (allow/deny), actions, optional resources, and optional conditions. **Statement** — a single permission rule with effect (allow/deny), actions, optional resources, and optional conditions.
**Action format** — `SERVICE:RESOURCE:OPERATION` with wildcard support: **Action format** — `SERVICE:RESOURCE:OPERATION` with wildcard support:
``` ```
core:vendor:create # specific action core:thirdParty:create # specific action
core:vendor:* # all vendor actions core:thirdParty:* # all third party actions
core:* # all core actions core:* # all core actions
* # everything * # everything
``` ```
@@ -39,8 +39,8 @@ The evaluator processes all statements against a request:
```go ```go
err := iamService.Authorizer.Authorize(ctx, iam.AuthorizeParams{ err := iamService.Authorizer.Authorize(ctx, iam.AuthorizeParams{
Principal: identityID, // who Principal: identityID, // who
Resource: vendorID, // what Resource: thirdPartyID, // what
Action: probo.ActionVendorGet, // which action Action: probo.ActionThirdPartyGet, // which action
ResourceAttributes: map[string]string{}, // optional extra attributes ResourceAttributes: map[string]string{}, // optional extra attributes
}) })
``` ```
@@ -78,8 +78,8 @@ Conditions constrain when a statement applies. All conditions must be satisfied.
// Users can only access resources in their organization // Users can only access resources in their organization
organizationCondition := policy.Equals("principal.organization_id", "resource.organization_id") organizationCondition := policy.Equals("principal.organization_id", "resource.organization_id")
policy.Allow(ActionVendorGet). policy.Allow(ActionThirdPartyGet).
WithSID("view-vendor"). WithSID("view-thirdParty").
When(organizationCondition) When(organizationCondition)
``` ```
@@ -97,14 +97,14 @@ Key paths use `principal.ATTR` or `resource.ATTR` (e.g., `principal.organization
Resources that support authorization must implement this interface in `pkg/coredata/`: Resources that support authorization must implement this interface in `pkg/coredata/`:
```go ```go
func (v *Vendor) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { func (v *ThirdParty) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) {
q := `SELECT organization_id FROM vendors WHERE id = $1 LIMIT 1;` q := `SELECT organization_id FROM thirdParties WHERE id = $1 LIMIT 1;`
var organizationID gid.GID var organizationID gid.GID
if err := conn.QueryRow(ctx, q, v.ID).Scan(&organizationID); err != nil { if err := conn.QueryRow(ctx, q, v.ID).Scan(&organizationID); err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceNotFound return nil, ErrResourceNotFound
} }
return nil, fmt.Errorf("cannot query vendor authorization attributes: %w", err) return nil, fmt.Errorf("cannot query third party authorization attributes: %w", err)
} }
return map[string]string{"organization_id": organizationID.String()}, nil return map[string]string{"organization_id": organizationID.String()}, nil
} }
@@ -126,14 +126,14 @@ var (
**GraphQL resolvers** use `AuthorizeFunc` from `pkg/server/api/authz/`: **GraphQL resolvers** use `AuthorizeFunc` from `pkg/server/api/authz/`:
```go ```go
if err := authorize(ctx, vendorID, probo.ActionVendorGet); err != nil { if err := authorize(ctx, thirdPartyID, probo.ActionThirdPartyGet); err != nil {
return nil, err return nil, err
} }
``` ```
**MCP resolvers** use `MustAuthorize` which panics (caught by middleware): **MCP resolvers** use `MustAuthorize` which panics (caught by middleware):
```go ```go
r.MustAuthorize(ctx, input.ID, probo.ActionVendorGet) r.MustAuthorize(ctx, input.ID, probo.ActionThirdPartyGet)
``` ```
## File locations ## File locations
@@ -155,11 +155,11 @@ IAM actions live in `pkg/iam/iam_actions.go`, probo actions in `pkg/probo/action
```go ```go
const ( const (
ActionVendorGet = "core:vendor:get" ActionThirdPartyGet = "core:thirdParty:get"
ActionVendorList = "core:vendor:list" ActionThirdPartyList = "core:thirdParty:list"
ActionVendorCreate = "core:vendor:create" ActionThirdPartyCreate = "core:thirdParty:create"
ActionVendorUpdate = "core:vendor:update" ActionThirdPartyUpdate = "core:thirdParty:update"
ActionVendorDelete = "core:vendor:delete" ActionThirdPartyDelete = "core:thirdParty:delete"
) )
``` ```

View File

@@ -13,18 +13,18 @@ Follow the [seven rules of a great Git commit message](https://cbea.ms/git-commi
The subject line should complete the sentence: "If applied, this commit will *your subject line here*". The subject line should complete the sentence: "If applied, this commit will *your subject line here*".
``` ```
Add vendor assessment agent for third-party reviews Add third-party assessment agent for third-party reviews
The existing changelog generator only covers internal changes. The existing changelog generator only covers internal changes.
This introduces a dedicated agent that evaluates third-party This introduces a dedicated agent that evaluates third-party
vendors against our compliance criteria, producing a structured thirdParties against our compliance criteria, producing a structured
risk report. risk report.
``` ```
Not every commit needs a body -- a single line is fine when the change is self-explanatory: Not every commit needs a body -- a single line is fine when the change is self-explanatory:
``` ```
Fix typo in vendor assessment prompt Fix typo in third-party assessment prompt
``` ```
## Signing and Authorship ## Signing and Authorship

View File

@@ -43,7 +43,7 @@ Two patterns in `e2e/internal/factory/`:
**Builder pattern (preferred):** **Builder pattern (preferred):**
```go ```go
vendorID := factory.NewVendor(owner). thirdPartyID := factory.NewThirdParty(owner).
WithName("Stripe"). WithName("Stripe").
WithCategory("CLOUD_PROVIDER"). WithCategory("CLOUD_PROVIDER").
Create() Create()
@@ -59,7 +59,7 @@ controlID := factory.NewControl(owner, frameworkID).
**Simple factory functions:** **Simple factory functions:**
```go ```go
vendorID := factory.CreateVendor(c, factory.Attrs{"name": "Acme"}) thirdPartyID := factory.CreateThirdParty(c, factory.Attrs{"name": "Acme"})
taskID := factory.CreateTask(c, &measureID, factory.Attrs{"name": "Task 1"}) taskID := factory.CreateTask(c, &measureID, factory.Attrs{"name": "Task 1"})
``` ```
@@ -70,7 +70,7 @@ Use `factory.SafeName("prefix")` for unique names and `factory.SafeEmail()` for
Every test and subtest **must** call `t.Parallel()`. One test file per entity in `e2e/console/`. Function naming: `TestEntity_Operation`. Every test and subtest **must** call `t.Parallel()`. One test file per entity in `e2e/console/`. Function naming: `TestEntity_Operation`.
```go ```go
func TestVendor_Create(t *testing.T) { func TestThirdParty_Create(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
@@ -78,9 +78,9 @@ func TestVendor_Create(t *testing.T) {
t.Parallel() t.Parallel()
const query = ` const query = `
mutation CreateVendor($input: CreateVendorInput!) { mutation CreateThirdParty($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { thirdPartyEdge {
node { id name } node { id name }
} }
} }
@@ -88,25 +88,25 @@ func TestVendor_Create(t *testing.T) {
` `
var result struct { var result struct {
CreateVendor struct { CreateThirdParty struct {
VendorEdge struct { ThirdPartyEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} `json:"node"` } `json:"node"`
} `json:"vendorEdge"` } `json:"thirdPartyEdge"`
} `json:"createVendor"` } `json:"createThirdParty"`
} }
err := owner.Execute(query, map[string]any{ err := owner.Execute(query, map[string]any{
"input": map[string]any{ "input": map[string]any{
"organizationId": owner.GetOrganizationID().String(), "organizationId": owner.GetOrganizationID().String(),
"name": factory.SafeName("Vendor"), "name": factory.SafeName("ThirdParty"),
}, },
}, &result) }, &result)
require.NoError(t, err) require.NoError(t, err)
assert.NotEmpty(t, result.CreateVendor.VendorEdge.Node.ID) assert.NotEmpty(t, result.CreateThirdParty.ThirdPartyEdge.Node.ID)
}) })
} }
``` ```
@@ -139,13 +139,13 @@ t.Run("other org cannot access", func(t *testing.T) {
owner1 := testutil.NewClient(t, testutil.RoleOwner) owner1 := testutil.NewClient(t, testutil.RoleOwner)
owner2 := testutil.NewClient(t, testutil.RoleOwner) owner2 := testutil.NewClient(t, testutil.RoleOwner)
vendorID := factory.NewVendor(owner1).WithName("Vendor").Create() thirdPartyID := factory.NewThirdParty(owner1).WithName("ThirdParty").Create()
var result struct { var result struct {
Node *struct{ ID string } `json:"node"` Node *struct{ ID string } `json:"node"`
} }
err := owner2.Execute(nodeQuery, map[string]any{"id": vendorID}, &result) err := owner2.Execute(nodeQuery, map[string]any{"id": thirdPartyID}, &result)
testutil.AssertNodeNotAccessible(t, err, result.Node == nil, "Vendor") testutil.AssertNodeNotAccessible(t, err, result.Node == nil, "ThirdParty")
}) })
``` ```
@@ -214,7 +214,7 @@ for _, tt := range tests {
```go ```go
err := owner.ExecuteWithFile( err := owner.ExecuteWithFile(
uploadQuery, uploadQuery,
map[string]any{"input": map[string]any{"vendorId": vendorID, "file": nil}}, map[string]any{"input": map[string]any{"thirdPartyId": thirdPartyID, "file": nil}},
"input.file", "input.file",
testutil.UploadFile{ testutil.UploadFile{
Filename: "report.pdf", Filename: "report.pdf",

View File

@@ -131,7 +131,7 @@ if errors.As(err, &ve) {
- Constructors: `New*` (e.g. `NewService`, `NewServer`, `NewBridge`) - Constructors: `New*` (e.g. `NewService`, `NewServer`, `NewBridge`)
- Config structs: `*Config` suffix (e.g. `APIConfig`, `PgConfig`, `TrustCenterConfig`) - Config structs: `*Config` suffix (e.g. `APIConfig`, `PgConfig`, `TrustCenterConfig`)
- Request structs: `*Request` suffix (e.g. `UpdateTrustCenterRequest`) - Request structs: `*Request` suffix (e.g. `UpdateTrustCenterRequest`)
- Unexported types for internal data: lowercase (e.g. `vendorInfo`, `ctxKey`) - Unexported types for internal data: lowercase (e.g. `thirdPartyInfo`, `ctxKey`)
## Functional options and Config structs ## Functional options and Config structs

View File

@@ -7,9 +7,9 @@ Schema-first GraphQL using [gqlgen](https://gqlgen.com/). The schema is hand-wri
Each API's schema lives in `pkg/server/api/{api}/v1/graphql/` as multiple `.graphql` files, one per coredata model: Each API's schema lives in `pkg/server/api/{api}/v1/graphql/` as multiple `.graphql` files, one per coredata model:
- `base.graphql` — directives, scalars, Node interface, PageInfo, OrderDirection, root Query/Mutation/Organization types - `base.graphql` — directives, scalars, Node interface, PageInfo, OrderDirection, root Query/Mutation/Organization types
- Entity files (e.g., `vendor.graphql`, `control.graphql`) — use `extend type Mutation` to add their mutations. - Entity files (e.g., `thirdParty.graphql`, `control.graphql`) — use `extend type Mutation` to add their mutations.
gqlgen's `follow-schema` layout generates one resolver file per schema file (e.g., `vendor.resolvers.go`). Types that get extended across files (Organization, Mutation, Viewer, TrustCenter) must be defined in `base.graphql`. gqlgen's `follow-schema` layout generates one resolver file per schema file (e.g., `thirdParty.resolvers.go`). Types that get extended across files (Organization, Mutation, Viewer, TrustCenter) must be defined in `base.graphql`.
### `extend type` restrictions ### `extend type` restrictions
@@ -20,18 +20,18 @@ gqlgen's `follow-schema` layout generates one resolver file per schema file (e.g
**Always define a custom Go type for connection types** using the `@goModel` directive. The model path points to the `types` package for the relevant API. The `totalCount` field must use `@goField(forceResolver: true)`. Edge types do not need `@goModel`. **Always define a custom Go type for connection types** using the `@goModel` directive. The model path points to the `types` package for the relevant API. The `totalCount` field must use `@goField(forceResolver: true)`. Edge types do not need `@goModel`.
```graphql ```graphql
type VendorConnection type ThirdPartyConnection
@goModel( @goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.VendorConnection" model: "go.probo.inc/probo/pkg/server/api/console/v1/types.ThirdPartyConnection"
) { ) {
totalCount: Int! @goField(forceResolver: true) totalCount: Int! @goField(forceResolver: true)
edges: [VendorEdge!]! edges: [ThirdPartyEdge!]!
pageInfo: PageInfo! pageInfo: PageInfo!
} }
type VendorEdge { type ThirdPartyEdge {
cursor: CursorKey! cursor: CursorKey!
node: Vendor! node: ThirdParty!
} }
``` ```
@@ -42,12 +42,12 @@ Without `@goModel`, gqlgen generates a default struct that lacks the custom fiel
Map GraphQL enums to existing Go types using `@goModel` on the enum and `@goEnum` on each value: Map GraphQL enums to existing Go types using `@goModel` on the enum and `@goEnum` on each value:
```graphql ```graphql
enum VendorOrderField enum ThirdPartyOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.VendorOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.ThirdPartyOrderField") {
CREATED_AT CREATED_AT
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldCreatedAt") @goEnum(value: "go.probo.inc/probo/pkg/coredata.ThirdPartyOrderFieldCreatedAt")
NAME NAME
@goEnum(value: "go.probo.inc/probo/pkg/coredata.VendorOrderFieldName") @goEnum(value: "go.probo.inc/probo/pkg/coredata.ThirdPartyOrderFieldName")
} }
``` ```
@@ -88,14 +88,14 @@ Connection fields on parent types use standard Relay arguments:
```graphql ```graphql
type Organization { type Organization {
vendors( thirdParties(
first: Int first: Int
after: CursorKey after: CursorKey
last: Int last: Int
before: CursorKey before: CursorKey
orderBy: VendorOrder orderBy: ThirdPartyOrder
filter: VendorFilter filter: ThirdPartyFilter
): VendorConnection! ): ThirdPartyConnection!
} }
``` ```
@@ -105,11 +105,11 @@ Each connection type lives in `types/*_connection.go` and follows this structure
```go ```go
type ( type (
VendorOrderBy OrderBy[coredata.VendorOrderField] ThirdPartyOrderBy OrderBy[coredata.ThirdPartyOrderField]
VendorConnection struct { ThirdPartyConnection struct {
TotalCount int TotalCount int
Edges []*VendorEdge Edges []*ThirdPartyEdge
PageInfo PageInfo PageInfo PageInfo
Resolver any Resolver any
@@ -117,17 +117,17 @@ type (
} }
) )
func NewVendorConnection( func NewThirdPartyConnection(
p *page.Page[*coredata.Vendor, coredata.VendorOrderField], p *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField],
parentType any, parentType any,
parentID gid.GID, parentID gid.GID,
) *VendorConnection { ) *ThirdPartyConnection {
edges := make([]*VendorEdge, len(p.Data)) edges := make([]*ThirdPartyEdge, len(p.Data))
for i, v := range p.Data { for i, v := range p.Data {
edges[i] = NewVendorEdge(v, p.Cursor.OrderBy.Field) edges[i] = NewThirdPartyEdge(v, p.Cursor.OrderBy.Field)
} }
return &VendorConnection{ return &ThirdPartyConnection{
Edges: edges, Edges: edges,
PageInfo: *NewPageInfo(p), PageInfo: *NewPageInfo(p),
@@ -136,13 +136,13 @@ func NewVendorConnection(
} }
} }
func NewVendorEdge( func NewThirdPartyEdge(
v *coredata.Vendor, v *coredata.ThirdParty,
orderBy coredata.VendorOrderField, orderBy coredata.ThirdPartyOrderField,
) *VendorEdge { ) *ThirdPartyEdge {
return &VendorEdge{ return &ThirdPartyEdge{
Cursor: v.CursorKey(orderBy), Cursor: v.CursorKey(orderBy),
Node: NewVendor(v), Node: NewThirdParty(v),
} }
} }
``` ```

View File

@@ -8,7 +8,7 @@ MCP tools are defined in `pkg/server/api/mcp/v1/specification.yaml` and generate
- `specification.yaml` — tool definitions, input/output schemas, component schemas - `specification.yaml` — tool definitions, input/output schemas, component schemas
- `resolver.go` — `Resolver` struct, `MustAuthorize`, service accessors - `resolver.go` — `Resolver` struct, `MustAuthorize`, service accessors
- `helpers.go` — pagination helpers, `UnwrapOmittable` - `helpers.go` — pagination helpers, `UnwrapOmittable`
- `types/*.go` (except `types/types.go`) — type conversion helpers (`NewVendor()`, `NewListVendorsOutput()`, etc.) - `types/*.go` (except `types/types.go`) — type conversion helpers (`NewThirdParty()`, `NewListThirdPartiesOutput()`, etc.)
- `schema.resolvers.go` — tool implementation bodies (stubs generated, you edit the bodies) - `schema.resolvers.go` — tool implementation bodies (stubs generated, you edit the bodies)
**Generated** (do not edit): **Generated** (do not edit):
@@ -24,16 +24,16 @@ go generate ./pkg/server/api/mcp/v1
```yaml ```yaml
tools: tools:
- name: listVendors - name: listThirdParties
description: List all vendors for the organization description: List all thirdParties for the organization
hints: hints:
readonly: true readonly: true
idempotent: true idempotent: true
destructive: false destructive: false
inputSchema: inputSchema:
$ref: "#/components/schemas/ListVendorsInput" $ref: "#/components/schemas/ListThirdPartiesInput"
outputSchema: outputSchema:
$ref: "#/components/schemas/ListVendorsOutput" $ref: "#/components/schemas/ListThirdPartiesOutput"
``` ```
Input/output schemas reference `components/schemas`. Map custom Go types with the `go.probo.inc/mcpgen/type` extension: Input/output schemas reference `components/schemas`. Map custom Go types with the `go.probo.inc/mcpgen/type` extension:
@@ -51,11 +51,11 @@ components:
Generated stubs follow this pattern: Generated stubs follow this pattern:
```go ```go
func (r *Resolver) ListVendorsTool( func (r *Resolver) ListThirdPartiesTool(
ctx context.Context, ctx context.Context,
req *mcp.CallToolRequest, req *mcp.CallToolRequest,
input *types.ListVendorsInput, input *types.ListThirdPartiesInput,
) (*mcp.CallToolResult, types.ListVendorsOutput, error) ) (*mcp.CallToolResult, types.ListThirdPartiesOutput, error)
``` ```
First return is always `nil`. Errors are either returned (for recoverable) or panicked (for authorization and unexpected failures). First return is always `nil`. Errors are either returned (for recoverable) or panicked (for authorization and unexpected failures).
@@ -65,24 +65,24 @@ First return is always `nil`. Errors are either returned (for recoverable) or pa
Use `MustAuthorize` which panics on failure (caught by middleware): Use `MustAuthorize` which panics on failure (caught by middleware):
```go ```go
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionVendorList) r.MustAuthorize(ctx, input.OrganizationID, probo.ActionThirdPartyList)
``` ```
## Common resolver patterns ## Common resolver patterns
**List with pagination:** **List with pagination:**
```go ```go
func (r *Resolver) ListVendorsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListVendorsInput) (*mcp.CallToolResult, types.ListVendorsOutput, error) { func (r *Resolver) ListThirdPartiesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListThirdPartiesInput) (*mcp.CallToolResult, types.ListThirdPartiesOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionVendorList) r.MustAuthorize(ctx, input.OrganizationID, probo.ActionThirdPartyList)
prb := r.ProboService(ctx, input.OrganizationID) prb := r.ProboService(ctx, input.OrganizationID)
pageOrderBy := page.OrderBy[coredata.VendorOrderField]{ pageOrderBy := page.OrderBy[coredata.ThirdPartyOrderField]{
Field: coredata.VendorOrderFieldCreatedAt, Field: coredata.ThirdPartyOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc, Direction: page.OrderDirectionDesc,
} }
if input.OrderBy != nil { if input.OrderBy != nil {
pageOrderBy = page.OrderBy[coredata.VendorOrderField]{ pageOrderBy = page.OrderBy[coredata.ThirdPartyOrderField]{
Field: input.OrderBy.Field, Field: input.OrderBy.Field,
Direction: input.OrderBy.Direction, Direction: input.OrderBy.Direction,
} }
@@ -90,12 +90,12 @@ func (r *Resolver) ListVendorsTool(ctx context.Context, req *mcp.CallToolRequest
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
page, err := prb.Vendors.ListForOrganizationID(ctx, input.OrganizationID, cursor, coredata.NewVendorFilter(nil, nil)) page, err := prb.ThirdParties.ListForOrganizationID(ctx, input.OrganizationID, cursor, coredata.NewThirdPartyFilter(nil, nil))
if err != nil { if err != nil {
panic(fmt.Errorf("cannot list vendors: %w", err)) panic(fmt.Errorf("cannot list thirdParties: %w", err))
} }
return nil, types.NewListVendorsOutput(page), nil return nil, types.NewListThirdPartiesOutput(page), nil
} }
``` ```
@@ -156,8 +156,8 @@ Description: UnwrapOmittable(input.Description),
Live in `types/*.go` (not the generated `types/types.go`). One file per entity: Live in `types/*.go` (not the generated `types/types.go`). One file per entity:
```go ```go
func NewVendor(v *coredata.Vendor) *Vendor { func NewThirdParty(v *coredata.ThirdParty) *ThirdParty {
return &Vendor{ return &ThirdParty{
ID: v.ID, ID: v.ID,
OrganizationID: v.OrganizationID, OrganizationID: v.OrganizationID,
Name: v.Name, Name: v.Name,
@@ -166,21 +166,21 @@ func NewVendor(v *coredata.Vendor) *Vendor {
} }
} }
func NewListVendorsOutput(vendorPage *page.Page[*coredata.Vendor, coredata.VendorOrderField]) ListVendorsOutput { func NewListThirdPartiesOutput(thirdPartyPage *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField]) ListThirdPartiesOutput {
vendors := make([]*Vendor, 0, len(vendorPage.Data)) thirdParties := make([]*ThirdParty, 0, len(thirdPartyPage.Data))
for _, v := range vendorPage.Data { for _, v := range thirdPartyPage.Data {
vendors = append(vendors, NewVendor(v)) thirdParties = append(thirdParties, NewThirdParty(v))
} }
var nextCursor *page.CursorKey var nextCursor *page.CursorKey
if len(vendorPage.Data) > 0 { if len(thirdPartyPage.Data) > 0 {
cursorKey := vendorPage.Data[len(vendorPage.Data)-1].CursorKey(vendorPage.Cursor.OrderBy.Field) cursorKey := thirdPartyPage.Data[len(thirdPartyPage.Data)-1].CursorKey(thirdPartyPage.Cursor.OrderBy.Field)
nextCursor = &cursorKey nextCursor = &cursorKey
} }
return ListVendorsOutput{ return ListThirdPartiesOutput{
NextCursor: nextCursor, NextCursor: nextCursor,
Vendors: vendors, ThirdParties: thirdParties,
} }
} }
``` ```

View File

@@ -79,10 +79,10 @@ export function UserCard({ name }: UserCardProps) {
```tsx ```tsx
// Good — destructure in body when parameter-level destructuring would exceed the line-length limit // Good — destructure in body when parameter-level destructuring would exceed the line-length limit
export function VendorComplianceOverviewPanel( export function ThirdPartyComplianceOverviewPanel(
props: VendorComplianceOverviewPanelProps, props: ThirdPartyComplianceOverviewPanelProps,
) { ) {
const { className, vendorKey, onStatusChange } = props; const { className, thirdPartyKey, onStatusChange } = props;
// … // …
} }
``` ```
@@ -139,11 +139,11 @@ export function Thing({ label }: ThingProps) {
```tsx ```tsx
// Good — rare exception: route entry default export (names still clear in module) // Good — rare exception: route entry default export (names still clear in module)
type VendorsPageProps = { type ThirdPartiesPageProps = {
queryRef: PreloadedQuery<VendorsQuery>; queryRef: PreloadedQuery<ThirdPartiesQuery>;
}; };
export default function VendorsPage({ queryRef }: VendorsPageProps) { export default function ThirdPartiesPage({ queryRef }: ThirdPartiesPageProps) {
// … // …
} }
``` ```
@@ -204,7 +204,7 @@ Use props for:
### Hooks for data and URL-derived identity ### Hooks for data and URL-derived identity
- **Fetched data:** Colocate Relay fragments and queries per [`contrib/claude/relay.md`](relay.md) (`useFragment`, `useLazyLoadQuery`, `usePreloadedQuery`, etc.) in the component that needs the data. - **Fetched data:** Colocate Relay fragments and queries per [`contrib/claude/relay.md`](relay.md) (`useFragment`, `useLazyLoadQuery`, `usePreloadedQuery`, etc.) in the component that needs the data.
- **Route parameters:** Call `useParams()` (or a small `useOrganizationId()`-style hook) **inside** the component that needs the id — avoid drilling `organizationId` / `vendorId` from a parent that only read the URL to pass them down. - **Route parameters:** Call `useParams()` (or a small `useOrganizationId()`-style hook) **inside** the component that needs the id — avoid drilling `organizationId` / `thirdPartyId` from a parent that only read the URL to pass them down.
### Relay: framework wiring is not “business data props” ### Relay: framework wiring is not “business data props”
@@ -214,36 +214,36 @@ Relay sometimes requires **opaque handles** on props: e.g. **`queryRef`** for `u
```tsx ```tsx
// Bad — parent only needed the param to pass it down // Bad — parent only needed the param to pass it down
function VendorLayout() { function ThirdPartyLayout() {
const { vendorId } = useParams(); const { thirdPartyId } = useParams();
return ( return (
<main> <main>
<VendorSummary vendorId={vendorId!} /> <ThirdPartySummary thirdPartyId={thirdPartyId!} />
</main> </main>
); );
} }
function VendorSummary({ vendorId }: { vendorId: string }) { function ThirdPartySummary({ thirdPartyId }: { thirdPartyId: string }) {
return <div>{/* … */}</div>; return <div>{/* … */}</div>;
} }
``` ```
```tsx ```tsx
// Good — component that needs the id reads it (or uses a dedicated hook) // Good — component that needs the id reads it (or uses a dedicated hook)
function VendorLayout() { function ThirdPartyLayout() {
return ( return (
<main> <main>
<VendorSummary /> <ThirdPartySummary />
</main> </main>
); );
} }
function VendorSummary() { function ThirdPartySummary() {
const { vendorId } = useParams(); const { thirdPartyId } = useParams();
if (vendorId == null) { if (thirdPartyId == null) {
return null; return null;
} }
return <div>{/* use vendorId in a hook / query … */}</div>; return <div>{/* use thirdPartyId in a hook / query … */}</div>;
} }
``` ```
@@ -251,13 +251,13 @@ function VendorSummary() {
```tsx ```tsx
// Bad — parent loaded data and passes fields as props // Bad — parent loaded data and passes fields as props
function VendorPage() { function ThirdPartyPage() {
const vendor = useLazyLoadQuery(/* … */); const thirdParty = useLazyLoadQuery(/* … */);
return ( return (
<VendorHeader <ThirdPartyHeader
name={vendor.name} name={thirdParty.name}
riskScore={vendor.riskScore} riskScore={thirdParty.riskScore}
updatedAt={vendor.updatedAt} updatedAt={thirdParty.updatedAt}
/> />
); );
} }
@@ -265,24 +265,24 @@ function VendorPage() {
```tsx ```tsx
// Good — header colocates its fragment and reads via useFragment // Good — header colocates its fragment and reads via useFragment
const vendorHeaderFragment = graphql` const thirdPartyHeaderFragment = graphql`
fragment VendorHeader_vendor on Vendor { fragment ThirdPartyHeader_thirdParty on ThirdParty {
name name
riskScore riskScore
updatedAt updatedAt
} }
`; `;
interface VendorHeaderProps { interface ThirdPartyHeaderProps {
className?: string; className?: string;
vendorKey: VendorHeader_vendor$key; thirdPartyKey: ThirdPartyHeader_thirdParty$key;
} }
export function VendorHeader({ className, vendorKey }: VendorHeaderProps) { export function ThirdPartyHeader({ className, thirdPartyKey }: ThirdPartyHeaderProps) {
const vendor = useFragment(vendorHeaderFragment, vendorKey); const thirdParty = useFragment(thirdPartyHeaderFragment, thirdPartyKey);
return ( return (
<header className={className}> <header className={className}>
{/* render from vendor … */} {/* render from thirdParty … */}
</header> </header>
); );
} }

View File

@@ -191,7 +191,7 @@ Fragments colocate data requirements with the component that reads them:
```tsx ```tsx
const contactFragment = graphql` const contactFragment = graphql`
fragment ContactRow_contactFragment on VendorContact { fragment ContactRow_contactFragment on ThirdPartyContact {
id id
fullName fullName
email email
@@ -199,8 +199,8 @@ const contactFragment = graphql`
role role
createdAt createdAt
updatedAt updatedAt
canUpdate: permission(action: "core:vendor-contact:update") canUpdate: permission(action: "core:thirdParty-contact:update")
canDelete: permission(action: "core:vendor-contact:delete") canDelete: permission(action: "core:thirdParty-contact:delete")
} }
`; `;
@@ -215,12 +215,12 @@ function ContactRow(props: { contactKey: ContactRow_contactFragment$key }) {
For lists that support sorting and pagination, use `@refetchable` with `@argumentDefinitions`: For lists that support sorting and pagination, use `@refetchable` with `@argumentDefinitions`:
```tsx ```tsx
const vendorContactsFragment = graphql` const thirdPartyContactsFragment = graphql`
fragment VendorContactsTabFragment on Vendor fragment ThirdPartyContactsTabFragment on ThirdParty
@refetchable(queryName: "VendorContactsListQuery") @refetchable(queryName: "ThirdPartyContactsListQuery")
@argumentDefinitions( @argumentDefinitions(
first: { type: "Int", defaultValue: 50 } first: { type: "Int", defaultValue: 50 }
order: { type: "VendorContactOrder", defaultValue: null } order: { type: "ThirdPartyContactOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null } after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null } before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null } last: { type: "Int", defaultValue: null }
@@ -231,18 +231,18 @@ const vendorContactsFragment = graphql`
last: $last last: $last
before: $before before: $before
orderBy: $order orderBy: $order
) @connection(key: "VendorContactsTabFragment_contacts") { ) @connection(key: "ThirdPartyContactsTabFragment_contacts") {
__id __id
edges { edges {
node { node {
...VendorContactsTabFragment_contact ...ThirdPartyContactsTabFragment_contact
} }
} }
} }
} }
`; `;
const [data, refetch] = useRefetchableFragment(vendorContactsFragment, vendor); const [data, refetch] = useRefetchableFragment(thirdPartyContactsFragment, thirdParty);
const connectionId = data.contacts.__id; const connectionId = data.contacts.__id;
``` ```
@@ -251,9 +251,9 @@ const connectionId = data.contacts.__id;
Use `usePaginationFragment` for cursor-based Relay pagination: Use `usePaginationFragment` for cursor-based Relay pagination:
```tsx ```tsx
const pagination = usePaginationFragment(paginatedVendorsFragment, data.node); const pagination = usePaginationFragment(paginatedThirdPartiesFragment, data.node);
const vendors = pagination.data.vendors?.edges.map(edge => edge.node); const thirdParties = pagination.data.thirdParties?.edges.map(edge => edge.node);
const connectionId = pagination.data.vendors.__id; const connectionId = pagination.data.thirdParties.__id;
``` ```
The `@connection(key: "...", filters: [...])` directive on the fragment tells Relay how to manage the paginated list in the store. The `filters` array controls which variables affect the connection identity. The `@connection(key: "...", filters: [...])` directive on the fragment tells Relay how to manage the paginated list in the store. The `filters` array controls which variables affect the connection identity.
@@ -294,7 +294,7 @@ createCookieBanner({ variables: { ... } });
#### Examples #### Examples
```tsx ```tsx
const [deleteVendor] = useMutation<VendorGraphDeleteMutation>(deleteVendorMutation); const [deleteThirdParty] = useMutation<ThirdPartyGraphDeleteMutation>(deleteThirdPartyMutation);
``` ```
For mutations with user feedback, combine with `useToast` and use `onCompleted`/`onError` callbacks: For mutations with user feedback, combine with `useToast` and use `onCompleted`/`onError` callbacks:
@@ -415,9 +415,9 @@ This is useful for dialogs, drawers, or other components rendered outside the su
```tsx ```tsx
// Add new edge to a connection // Add new edge to a connection
const createMutation = graphql` const createMutation = graphql`
mutation CreateVendorMutation($input: CreateVendorInput!, $connections: [ID!]!) { mutation CreateThirdPartyMutation($input: CreateThirdPartyInput!, $connections: [ID!]!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge @prependEdge(connections: $connections) { thirdPartyEdge @prependEdge(connections: $connections) {
node { node {
id id
name name
@@ -429,19 +429,19 @@ const createMutation = graphql`
// Remove an edge from a connection // Remove an edge from a connection
const deleteMutation = graphql` const deleteMutation = graphql`
mutation DeleteVendorMutation($input: DeleteVendorInput!, $connections: [ID!]!) { mutation DeleteThirdPartyMutation($input: DeleteThirdPartyInput!, $connections: [ID!]!) {
deleteVendor(input: $input) { deleteThirdParty(input: $input) {
deletedVendorId @deleteEdge(connections: $connections) deletedThirdPartyId @deleteEdge(connections: $connections)
} }
} }
`; `;
// Update in-place (Relay matches by id — no directive needed) // Update in-place (Relay matches by id — no directive needed)
const updateMutation = graphql` const updateMutation = graphql`
mutation UpdateContactMutation($input: UpdateVendorContactInput!) { mutation UpdateContactMutation($input: UpdateThirdPartyContactInput!) {
updateVendorContact(input: $input) { updateThirdPartyContact(input: $input) {
vendorContact { thirdPartyContact {
...VendorContactsTabFragment_contact ...ThirdPartyContactsTabFragment_contact
} }
} }
} }
@@ -505,15 +505,15 @@ Destructive mutations (delete) are wrapped with a confirmation dialog:
```tsx ```tsx
const confirm = useConfirm(); const confirm = useConfirm();
const [deleteVendor] = useMutation<DeleteVendorMutation>(deleteVendorMutation); const [deleteThirdParty] = useMutation<DeleteThirdPartyMutation>(deleteThirdPartyMutation);
return () => { return () => {
confirm( confirm(
() => () =>
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
deleteVendor({ deleteThirdParty({
variables: { variables: {
input: { vendorId: vendor.id! }, input: { thirdPartyId: thirdParty.id! },
connections: [connectionId], connections: [connectionId],
}, },
onCompleted() { onCompleted() {
@@ -534,14 +534,14 @@ return () => {
GraphQL operations are colocated with the components that use them. See [`contrib/claude/app-arborescence.md`](app-arborescence.md) for the full folder layout. GraphQL operations are colocated with the components that use them. See [`contrib/claude/app-arborescence.md`](app-arborescence.md) for the full folder layout.
``` ```
pages/organizations/vendors/ pages/organizations/third-parties/
VendorsPage.tsx # query + pagination fragment ThirdPartiesPage.tsx # query + pagination fragment
_components/ _components/
CreateContactDialog.tsx # create mutation CreateContactDialog.tsx # create mutation
EditContactDialog.tsx # update mutation EditContactDialog.tsx # update mutation
tabs/ tabs/
VendorContactsTab.tsx # refetchable fragment + item fragment ThirdPartyContactsTab.tsx # refetchable fragment + item fragment
VendorComplianceTab.tsx ThirdPartyComplianceTab.tsx
``` ```
Component-specific operations (queries, fragments, mutations) are defined inline in the component file that uses them. Shared sub-components live in `_components/` next to the page (scoped to the nearest common ancestor). Component-specific operations (queries, fragments, mutations) are defined inline in the component file that uses them. Shared sub-components live in `_components/` next to the page (scoped to the nearest common ancestor).

View File

@@ -7,7 +7,7 @@ Custom fluent validation API in `pkg/validator/`. Used in every service method t
Create a validator, chain `Check()` calls for each field, then call `Error()` to get accumulated errors: Create a validator, chain `Check()` calls for each field, then call `Error()` to get accumulated errors:
```go ```go
func (req *CreateVendorRequest) Validate() error { func (req *CreateThirdPartyRequest) Validate() error {
v := validator.New() v := validator.New()
v.Check(req.OrganizationID, "organization_id", v.Check(req.OrganizationID, "organization_id",
@@ -19,7 +19,7 @@ func (req *CreateVendorRequest) Validate() error {
validator.SafeTextNoNewLine(TitleMaxLength), validator.SafeTextNoNewLine(TitleMaxLength),
) )
v.Check(req.Category, "category", v.Check(req.Category, "category",
validator.OneOfSlice(coredata.VendorCategories()), validator.OneOfSlice(coredata.ThirdPartyCategories()),
) )
return v.Error() return v.Error()
@@ -81,7 +81,7 @@ v.CheckEach(ids, "ids", func(index int, item any) {
gidValue := item.(gid.GID) gidValue := item.(gid.GID)
v.Check(gidValue, fmt.Sprintf("ids[%d]", index), v.Check(gidValue, fmt.Sprintf("ids[%d]", index),
validator.Required(), validator.Required(),
validator.GID(coredata.VendorEntityType), validator.GID(coredata.ThirdPartyEntityType),
) )
}) })
``` ```
@@ -135,7 +135,7 @@ Validation errors flow naturally through Go's error interface:
3. GraphQL/HTTP handlers convert `ValidationErrors` to appropriate response format 3. GraphQL/HTTP handlers convert `ValidationErrors` to appropriate response format
```go ```go
func (s *Service) CreateVendor(ctx context.Context, req CreateVendorRequest) (*coredata.Vendor, error) { func (s *Service) CreateThirdParty(ctx context.Context, req CreateThirdPartyRequest) (*coredata.ThirdParty, error) {
if err := req.Validate(); err != nil { if err := req.Validate(); err != nil {
return nil, err return nil, err
} }

View File

@@ -480,7 +480,7 @@ create_risk \
SECURITY MITIGATED 1 5 SECURITY MITIGATED 1 5
create_risk \ create_risk \
"Third-party SaaS vendor data breach" \ "Third-party SaaS data breach" \
OPERATIONAL TRANSFERRED 3 4 OPERATIONAL TRANSFERRED 3 4
create_risk \ create_risk \
"Cloud region outage causing service disruption" \ "Cloud region outage causing service disruption" \
@@ -514,7 +514,7 @@ create_risk \
"Breach notification deadline missed" \ "Breach notification deadline missed" \
COMPLIANCE MITIGATED 1 5 COMPLIANCE MITIGATED 1 5
create_risk \ create_risk \
"Inadequate data processing agreements with vendors" \ "Inadequate data processing agreements with third parties" \
COMPLIANCE MITIGATED 3 3 COMPLIANCE MITIGATED 3 3
create_risk \ create_risk \
"Employee data retained beyond legal period" \ "Employee data retained beyond legal period" \
@@ -553,17 +553,17 @@ create_risk \
echo " 35 risks created" echo " 35 risks created"
echo " Creating vendors..." echo " Creating third parties..."
create_vendor() { create_third_party() {
local name="$1" local name="$1"
local description="$2" local description="$2"
local resp local resp
resp=$(prb_api "createVendor: $name" ' resp=$(prb_api "createThirdParty: $name" '
mutation($input: CreateVendorInput!) { mutation($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { thirdPartyEdge {
node { id } node { id }
} }
} }
@@ -574,55 +574,55 @@ create_vendor() {
description="$description" \ description="$description" \
)") )")
local id local id
id=$(echo "$resp" | jq -r '.data.createVendor.vendorEdge.node.id // empty') id=$(echo "$resp" | jq -r '.data.createThirdParty.thirdPartyEdge.node.id // empty')
if [ -z "$id" ]; then if [ -z "$id" ]; then
echo "ERROR (createVendor: $name): no vendor id in response" >&2 echo "ERROR (createThirdParty: $name): no third party id in response" >&2
exit 1 exit 1
fi fi
} }
create_vendor "Amazon Web Services" \ create_third_party "Amazon Web Services" \
"Cloud infrastructure and compute" "Cloud infrastructure and compute"
create_vendor "Google Cloud Platform" \ create_third_party "Google Cloud Platform" \
"BigQuery analytics and AI services" "BigQuery analytics and AI services"
create_vendor "Google Workspace" \ create_third_party "Google Workspace" \
"Email, calendar, and productivity suite" "Email, calendar, and productivity suite"
create_vendor "Microsoft 365" \ create_third_party "Microsoft 365" \
"Office productivity and collaboration" "Office productivity and collaboration"
create_vendor "Datadog" \ create_third_party "Datadog" \
"Application monitoring and observability" "Application monitoring and observability"
create_vendor "PagerDuty" \ create_third_party "PagerDuty" \
"Incident management and on-call scheduling" "Incident management and on-call scheduling"
create_vendor "Slack" \ create_third_party "Slack" \
"Team communication and messaging" "Team communication and messaging"
create_vendor "GitHub" \ create_third_party "GitHub" \
"Source code management and CI/CD" "Source code management and CI/CD"
create_vendor "Stripe" \ create_third_party "Stripe" \
"Payment processing and billing" "Payment processing and billing"
create_vendor "Salesforce" \ create_third_party "Salesforce" \
"Customer relationship management" "Customer relationship management"
create_vendor "HubSpot" \ create_third_party "HubSpot" \
"Marketing automation and CRM" "Marketing automation and CRM"
create_vendor "Notion" \ create_third_party "Notion" \
"Documentation and knowledge management" "Documentation and knowledge management"
create_vendor "1Password" \ create_third_party "1Password" \
"Enterprise password management" "Enterprise password management"
create_vendor "Okta" \ create_third_party "Okta" \
"Identity and access management" "Identity and access management"
create_vendor "CrowdStrike" \ create_third_party "CrowdStrike" \
"Endpoint protection and threat intelligence" "Endpoint protection and threat intelligence"
create_vendor "Vanta" \ create_third_party "Vanta" \
"Compliance automation and monitoring" "Compliance automation and monitoring"
create_vendor "Jira" \ create_third_party "Jira" \
"Project management and issue tracking" "Project management and issue tracking"
create_vendor "Cloudflare" \ create_third_party "Cloudflare" \
"CDN, DNS, and DDoS protection" "CDN, DNS, and DDoS protection"
create_vendor "Twilio SendGrid" \ create_third_party "Twilio SendGrid" \
"Transactional email delivery" "Transactional email delivery"
create_vendor "Snowflake" \ create_third_party "Snowflake" \
"Cloud data warehouse" "Cloud data warehouse"
echo " 20 vendors created" echo " 20 third parties created"
echo " Creating measures..." echo " Creating measures..."
@@ -696,7 +696,7 @@ echo ""
echo " Created:" echo " Created:"
echo " 3 frameworks, 43 controls" echo " 3 frameworks, 43 controls"
echo " 35 risks" echo " 35 risks"
echo " 20 vendors" echo " 20 third parties"
echo " 15 measures" echo " 15 measures"
echo " 8 people" echo " 8 people"
echo "" echo ""

View File

@@ -27,8 +27,8 @@ func TestAuditLog_List(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a vendor to generate an audit log entry. // Create a thirdParty to generate an audit log entry.
factory.NewVendor(owner).WithName(factory.SafeName("AuditVendor")).Create() factory.NewThirdParty(owner).WithName(factory.SafeName("AuditThirdParty")).Create()
const query = ` const query = `
query($orgId: ID!) { query($orgId: ID!) {
@@ -78,20 +78,20 @@ func TestAuditLog_List(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1) assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1)
// Find the vendor create entry. // Find the thirdParty create entry.
found := false found := false
for _, edge := range result.Node.AuditLogEntries.Edges { for _, edge := range result.Node.AuditLogEntries.Edges {
if edge.Node.Action == "core:vendor:create" { if edge.Node.Action == "core:thirdParty:create" {
found = true found = true
assert.Equal(t, "USER", edge.Node.ActorType) assert.Equal(t, "USER", edge.Node.ActorType)
assert.Equal(t, "Vendor", edge.Node.ResourceType) assert.Equal(t, "ThirdParty", edge.Node.ResourceType)
assert.NotEmpty(t, edge.Node.ActorID) assert.NotEmpty(t, edge.Node.ActorID)
assert.NotEmpty(t, edge.Node.ResourceID) assert.NotEmpty(t, edge.Node.ResourceID)
assert.NotEmpty(t, edge.Node.CreatedAt) assert.NotEmpty(t, edge.Node.CreatedAt)
break break
} }
} }
assert.True(t, found, "expected to find core:vendor:create audit log entry") assert.True(t, found, "expected to find core:thirdParty:create audit log entry")
} }
func TestAuditLog_Filter(t *testing.T) { func TestAuditLog_Filter(t *testing.T) {
@@ -99,7 +99,7 @@ func TestAuditLog_Filter(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Create different resources to generate different audit log entries. // Create different resources to generate different audit log entries.
factory.NewVendor(owner).WithName(factory.SafeName("FilterVendor")).Create() factory.NewThirdParty(owner).WithName(factory.SafeName("FilterThirdParty")).Create()
const query = ` const query = `
query($orgId: ID!, $filter: AuditLogEntryFilter) { query($orgId: ID!, $filter: AuditLogEntryFilter) {
@@ -139,12 +139,12 @@ func TestAuditLog_Filter(t *testing.T) {
err := owner.Execute(query, map[string]any{ err := owner.Execute(query, map[string]any{
"orgId": owner.GetOrganizationID().String(), "orgId": owner.GetOrganizationID().String(),
"filter": map[string]any{"action": "core:vendor:create"}, "filter": map[string]any{"action": "core:thirdParty:create"},
}, &result) }, &result)
require.NoError(t, err) require.NoError(t, err)
assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1) assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1)
for _, edge := range result.Node.AuditLogEntries.Edges { for _, edge := range result.Node.AuditLogEntries.Edges {
assert.Equal(t, "core:vendor:create", edge.Node.Action) assert.Equal(t, "core:thirdParty:create", edge.Node.Action)
} }
}) })
@@ -167,12 +167,12 @@ func TestAuditLog_Filter(t *testing.T) {
err := owner.Execute(query, map[string]any{ err := owner.Execute(query, map[string]any{
"orgId": owner.GetOrganizationID().String(), "orgId": owner.GetOrganizationID().String(),
"filter": map[string]any{"resourceType": "Vendor"}, "filter": map[string]any{"resourceType": "ThirdParty"},
}, &result) }, &result)
require.NoError(t, err) require.NoError(t, err)
assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1) assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1)
for _, edge := range result.Node.AuditLogEntries.Edges { for _, edge := range result.Node.AuditLogEntries.Edges {
assert.Equal(t, "Vendor", edge.Node.ResourceType) assert.Equal(t, "ThirdParty", edge.Node.ResourceType)
} }
}) })
} }
@@ -182,7 +182,7 @@ func TestAuditLog_RBAC(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Generate an audit log entry. // Generate an audit log entry.
factory.NewVendor(owner).WithName(factory.SafeName("RBACVendor")).Create() factory.NewThirdParty(owner).WithName(factory.SafeName("RBACThirdParty")).Create()
const query = ` const query = `
query($orgId: ID!) { query($orgId: ID!) {
@@ -253,8 +253,8 @@ func TestAuditLog_TenantIsolation(t *testing.T) {
org1Owner := testutil.NewClient(t, testutil.RoleOwner) org1Owner := testutil.NewClient(t, testutil.RoleOwner)
org2Owner := testutil.NewClient(t, testutil.RoleOwner) org2Owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a vendor in org1 to generate audit log entries. // Create a thirdParty in org1 to generate audit log entries.
factory.NewVendor(org1Owner).WithName(factory.SafeName("IsoVendor")).Create() factory.NewThirdParty(org1Owner).WithName(factory.SafeName("IsoThirdParty")).Create()
const query = ` const query = `
query($orgId: ID!) { query($orgId: ID!) {
@@ -275,7 +275,7 @@ func TestAuditLog_TenantIsolation(t *testing.T) {
} }
` `
// org2 should not see org1's audit log entries about vendors. // org2 should not see org1's audit log entries about thirdParties.
var result struct { var result struct {
Node struct { Node struct {
AuditLogEntries struct { AuditLogEntries struct {
@@ -298,9 +298,9 @@ func TestAuditLog_TenantIsolation(t *testing.T) {
for _, edge := range result.Node.AuditLogEntries.Edges { for _, edge := range result.Node.AuditLogEntries.Edges {
// org2 may have its own audit log entries (from user/org creation), // org2 may have its own audit log entries (from user/org creation),
// but should never see org1's vendor entries. // but should never see org1's thirdParty entries.
if edge.Node.ResourceType == "Vendor" { if edge.Node.ResourceType == "ThirdParty" {
t.Fatalf("org2 should not see org1's vendor audit log entries, but found: %s", edge.Node.Action) t.Fatalf("org2 should not see org1's thirdParty audit log entries, but found: %s", edge.Node.Action)
} }
} }
} }

View File

@@ -174,32 +174,32 @@ const (
} }
}` }`
createVendorMutation = ` createThirdPartyMutation = `
mutation CreateVendor($input: CreateVendorInput!) { mutation CreateThirdParty($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { node { id } } thirdPartyEdge { node { id } }
} }
}` }`
updateVendorMutation = ` updateThirdPartyMutation = `
mutation UpdateVendor($input: UpdateVendorInput!) { mutation UpdateThirdParty($input: UpdateThirdPartyInput!) {
updateVendor(input: $input) { updateThirdParty(input: $input) {
vendor { id } thirdParty { id }
} }
}` }`
deleteVendorMutation = ` deleteThirdPartyMutation = `
mutation DeleteVendor($input: DeleteVendorInput!) { mutation DeleteThirdParty($input: DeleteThirdPartyInput!) {
deleteVendor(input: $input) { deleteThirdParty(input: $input) {
deletedVendorId deletedThirdPartyId
} }
}` }`
listVendorsQuery = ` listThirdPartiesQuery = `
query GetVendors($id: ID!) { query GetThirdParties($id: ID!) {
node(id: $id) { node(id: $id) {
... on Organization { ... on Organization {
vendors(first: 10) { totalCount } thirdParties(first: 10) { totalCount }
} }
} }
}` }`
@@ -304,7 +304,7 @@ func TestRBAC(t *testing.T) {
measureID := factory.NewMeasure(owner).WithName("RBAC Test Measure").Create() measureID := factory.NewMeasure(owner).WithName("RBAC Test Measure").Create()
taskID := factory.NewTask(owner, measureID).WithName("RBAC Test Task").Create() taskID := factory.NewTask(owner, measureID).WithName("RBAC Test Task").Create()
riskID := factory.NewRisk(owner).WithName("RBAC Test Risk").Create() riskID := factory.NewRisk(owner).WithName("RBAC Test Risk").Create()
vendorID := factory.NewVendor(owner).WithName("RBAC Test Vendor").Create() thirdPartyID := factory.NewThirdParty(owner).WithName("RBAC Test ThirdParty").Create()
accessSourceID := factory.NewAccessSource(owner, owner.GetOrganizationID().String()).WithName("RBAC Test Source").Create() accessSourceID := factory.NewAccessSource(owner, owner.GetOrganizationID().String()).WithName("RBAC Test Source").Create()
accessReviewCampaignID := factory.NewAccessReviewCampaign(owner, owner.GetOrganizationID().String()).WithName("RBAC Test Campaign").Create() accessReviewCampaignID := factory.NewAccessReviewCampaign(owner, owner.GetOrganizationID().String()).WithName("RBAC Test Campaign").Create()
@@ -936,123 +936,123 @@ func TestRBAC(t *testing.T) {
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "owner can create vendor", name: "owner can create thirdParty",
role: "owner", role: "owner",
client: owner, client: owner,
query: createVendorMutation, query: createThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Vendor")}} return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("ThirdParty")}}
}, },
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "admin can create vendor", name: "admin can create thirdParty",
role: "admin", role: "admin",
client: admin, client: admin,
query: createVendorMutation, query: createThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Vendor")}} return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("ThirdParty")}}
}, },
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "viewer cannot create vendor", name: "viewer cannot create thirdParty",
role: "viewer", role: "viewer",
client: viewer, client: viewer,
query: createVendorMutation, query: createThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("Vendor")}} return map[string]any{"input": map[string]any{"organizationId": owner.GetOrganizationID().String(), "name": factory.SafeName("ThirdParty")}}
}, },
shouldAllow: false, shouldAllow: false,
}, },
{ {
name: "owner can update vendor", name: "owner can update thirdParty",
role: "owner", role: "owner",
client: owner, client: owner,
query: updateVendorMutation, query: updateThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": vendorID, "name": factory.SafeName("Updated Vendor")}} return map[string]any{"input": map[string]any{"id": thirdPartyID, "name": factory.SafeName("Updated ThirdParty")}}
}, },
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "admin can update vendor", name: "admin can update thirdParty",
role: "admin", role: "admin",
client: admin, client: admin,
query: updateVendorMutation, query: updateThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": vendorID, "name": factory.SafeName("Updated Vendor")}} return map[string]any{"input": map[string]any{"id": thirdPartyID, "name": factory.SafeName("Updated ThirdParty")}}
}, },
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "viewer cannot update vendor", name: "viewer cannot update thirdParty",
role: "viewer", role: "viewer",
client: viewer, client: viewer,
query: updateVendorMutation, query: updateThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"input": map[string]any{"id": vendorID, "name": factory.SafeName("Updated Vendor")}} return map[string]any{"input": map[string]any{"id": thirdPartyID, "name": factory.SafeName("Updated ThirdParty")}}
}, },
shouldAllow: false, shouldAllow: false,
}, },
{ {
name: "owner can delete vendor", name: "owner can delete thirdParty",
role: "owner", role: "owner",
client: owner, client: owner,
query: deleteVendorMutation, query: deleteThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
id := factory.NewVendor(owner).WithName(factory.SafeName("ToDelete")).Create() id := factory.NewThirdParty(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"vendorId": id}} return map[string]any{"input": map[string]any{"thirdPartyId": id}}
}, },
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "admin can delete vendor", name: "admin can delete thirdParty",
role: "admin", role: "admin",
client: admin, client: admin,
query: deleteVendorMutation, query: deleteThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
id := factory.NewVendor(owner).WithName(factory.SafeName("ToDelete")).Create() id := factory.NewThirdParty(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"vendorId": id}} return map[string]any{"input": map[string]any{"thirdPartyId": id}}
}, },
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "viewer cannot delete vendor", name: "viewer cannot delete thirdParty",
role: "viewer", role: "viewer",
client: viewer, client: viewer,
query: deleteVendorMutation, query: deleteThirdPartyMutation,
variables: func() map[string]any { variables: func() map[string]any {
id := factory.NewVendor(owner).WithName(factory.SafeName("ToDelete")).Create() id := factory.NewThirdParty(owner).WithName(factory.SafeName("ToDelete")).Create()
return map[string]any{"input": map[string]any{"vendorId": id}} return map[string]any{"input": map[string]any{"thirdPartyId": id}}
}, },
shouldAllow: false, shouldAllow: false,
}, },
{ {
name: "owner can list vendors", name: "owner can list third parties",
role: "owner", role: "owner",
client: owner, client: owner,
query: listVendorsQuery, query: listThirdPartiesQuery,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()} return map[string]any{"id": owner.GetOrganizationID().String()}
}, },
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "admin can list vendors", name: "admin can list third parties",
role: "admin", role: "admin",
client: admin, client: admin,
query: listVendorsQuery, query: listThirdPartiesQuery,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()} return map[string]any{"id": owner.GetOrganizationID().String()}
}, },
shouldAllow: true, shouldAllow: true,
}, },
{ {
name: "viewer can list vendors", name: "viewer can list third parties",
role: "viewer", role: "viewer",
client: viewer, client: viewer,
query: listVendorsQuery, query: listThirdPartiesQuery,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{"id": owner.GetOrganizationID().String()} return map[string]any{"id": owner.GetOrganizationID().String()}
}, },

View File

@@ -23,15 +23,15 @@ import (
"go.probo.inc/probo/e2e/internal/testutil" "go.probo.inc/probo/e2e/internal/testutil"
) )
func TestVendorComplianceReport_Upload(t *testing.T) { func TestThirdPartyComplianceReport_Upload(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
vendorID := factory.NewVendor(owner).WithName("Compliance Report Upload Vendor").Create() thirdPartyID := factory.NewThirdParty(owner).WithName("Compliance Report Upload ThirdParty").Create()
const query = ` const query = `
mutation UploadVendorComplianceReport($input: UploadVendorComplianceReportInput!) { mutation UploadThirdPartyComplianceReport($input: UploadThirdPartyComplianceReportInput!) {
uploadVendorComplianceReport(input: $input) { uploadThirdPartyComplianceReport(input: $input) {
vendorComplianceReportEdge { thirdPartyComplianceReportEdge {
node { node {
id id
reportName reportName
@@ -46,27 +46,27 @@ func TestVendorComplianceReport_Upload(t *testing.T) {
pdfContent := []byte("%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF") pdfContent := []byte("%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF")
var result struct { var result struct {
UploadVendorComplianceReport struct { UploadThirdPartyComplianceReport struct {
VendorComplianceReportEdge struct { ThirdPartyComplianceReportEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
ReportName string `json:"reportName"` ReportName string `json:"reportName"`
ReportDate string `json:"reportDate"` ReportDate string `json:"reportDate"`
ValidUntil *string `json:"validUntil"` ValidUntil *string `json:"validUntil"`
} `json:"node"` } `json:"node"`
} `json:"vendorComplianceReportEdge"` } `json:"thirdPartyComplianceReportEdge"`
} `json:"uploadVendorComplianceReport"` } `json:"uploadThirdPartyComplianceReport"`
} }
err := owner.ExecuteWithFile( err := owner.ExecuteWithFile(
query, query,
map[string]any{ map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"reportName": "SOC 2 Type II", "reportName": "SOC 2 Type II",
"reportDate": "2024-01-01T00:00:00Z", "reportDate": "2024-01-01T00:00:00Z",
"validUntil": "2025-01-01T00:00:00Z", "validUntil": "2025-01-01T00:00:00Z",
"file": nil, "file": nil,
}, },
}, "input.file", testutil.UploadFile{ }, "input.file", testutil.UploadFile{
Filename: "soc2-report.pdf", Filename: "soc2-report.pdf",
@@ -77,23 +77,23 @@ func TestVendorComplianceReport_Upload(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
node := result.UploadVendorComplianceReport.VendorComplianceReportEdge.Node node := result.UploadThirdPartyComplianceReport.ThirdPartyComplianceReportEdge.Node
assert.NotEmpty(t, node.ID) assert.NotEmpty(t, node.ID)
assert.Equal(t, "SOC 2 Type II", node.ReportName) assert.Equal(t, "SOC 2 Type II", node.ReportName)
assert.NotEmpty(t, node.ReportDate) assert.NotEmpty(t, node.ReportDate)
assert.NotNil(t, node.ValidUntil) assert.NotNil(t, node.ValidUntil)
} }
func TestVendorComplianceReport_List(t *testing.T) { func TestThirdPartyComplianceReport_List(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
vendorID := factory.NewVendor(owner).WithName("Compliance Report List Vendor").Create() thirdPartyID := factory.NewThirdParty(owner).WithName("Compliance Report List ThirdParty").Create()
pdfContent := []byte("%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF") pdfContent := []byte("%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF")
uploadQuery := ` uploadQuery := `
mutation UploadVendorComplianceReport($input: UploadVendorComplianceReportInput!) { mutation UploadThirdPartyComplianceReport($input: UploadThirdPartyComplianceReportInput!) {
uploadVendorComplianceReport(input: $input) { uploadThirdPartyComplianceReport(input: $input) {
vendorComplianceReportEdge { thirdPartyComplianceReportEdge {
node { id } node { id }
} }
} }
@@ -101,23 +101,23 @@ func TestVendorComplianceReport_List(t *testing.T) {
` `
var uploadResult struct { var uploadResult struct {
UploadVendorComplianceReport struct { UploadThirdPartyComplianceReport struct {
VendorComplianceReportEdge struct { ThirdPartyComplianceReportEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorComplianceReportEdge"` } `json:"thirdPartyComplianceReportEdge"`
} `json:"uploadVendorComplianceReport"` } `json:"uploadThirdPartyComplianceReport"`
} }
err := owner.ExecuteWithFile( err := owner.ExecuteWithFile(
uploadQuery, uploadQuery,
map[string]any{ map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"reportName": "ISO 27001", "reportName": "ISO 27001",
"reportDate": "2024-06-01T00:00:00Z", "reportDate": "2024-06-01T00:00:00Z",
"file": nil, "file": nil,
}, },
}, "input.file", testutil.UploadFile{ }, "input.file", testutil.UploadFile{
Filename: "iso27001.pdf", Filename: "iso27001.pdf",
@@ -128,13 +128,13 @@ func TestVendorComplianceReport_List(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
reportID := uploadResult.UploadVendorComplianceReport.VendorComplianceReportEdge.Node.ID reportID := uploadResult.UploadThirdPartyComplianceReport.ThirdPartyComplianceReportEdge.Node.ID
require.NotEmpty(t, reportID) require.NotEmpty(t, reportID)
const listQuery = ` const listQuery = `
query($id: ID!) { query($id: ID!) {
node(id: $id) { node(id: $id) {
... on Vendor { ... on ThirdParty {
id id
complianceReports(first: 10) { complianceReports(first: 10) {
edges { edges {
@@ -165,7 +165,7 @@ func TestVendorComplianceReport_List(t *testing.T) {
} `json:"node"` } `json:"node"`
} }
err = owner.Execute(listQuery, map[string]any{"id": vendorID}, &listResult) err = owner.Execute(listQuery, map[string]any{"id": thirdPartyID}, &listResult)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, listResult.Node.ComplianceReports.Edges, 1) require.Len(t, listResult.Node.ComplianceReports.Edges, 1)
@@ -173,17 +173,17 @@ func TestVendorComplianceReport_List(t *testing.T) {
assert.Equal(t, "ISO 27001", listResult.Node.ComplianceReports.Edges[0].Node.ReportName) assert.Equal(t, "ISO 27001", listResult.Node.ComplianceReports.Edges[0].Node.ReportName)
} }
func TestVendorComplianceReport_Delete(t *testing.T) { func TestThirdPartyComplianceReport_Delete(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
vendorID := factory.NewVendor(owner).WithName("Compliance Report Delete Vendor").Create() thirdPartyID := factory.NewThirdParty(owner).WithName("Compliance Report Delete ThirdParty").Create()
pdfContent := []byte("%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF") pdfContent := []byte("%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF")
uploadQuery := ` uploadQuery := `
mutation UploadVendorComplianceReport($input: UploadVendorComplianceReportInput!) { mutation UploadThirdPartyComplianceReport($input: UploadThirdPartyComplianceReportInput!) {
uploadVendorComplianceReport(input: $input) { uploadThirdPartyComplianceReport(input: $input) {
vendorComplianceReportEdge { thirdPartyComplianceReportEdge {
node { id } node { id }
} }
} }
@@ -191,21 +191,21 @@ func TestVendorComplianceReport_Delete(t *testing.T) {
` `
var uploadResult struct { var uploadResult struct {
UploadVendorComplianceReport struct { UploadThirdPartyComplianceReport struct {
VendorComplianceReportEdge struct { ThirdPartyComplianceReportEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorComplianceReportEdge"` } `json:"thirdPartyComplianceReportEdge"`
} `json:"uploadVendorComplianceReport"` } `json:"uploadThirdPartyComplianceReport"`
} }
err := owner.ExecuteWithFile(uploadQuery, map[string]any{ err := owner.ExecuteWithFile(uploadQuery, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"reportName": "PCI DSS", "reportName": "PCI DSS",
"reportDate": "2024-03-01T00:00:00Z", "reportDate": "2024-03-01T00:00:00Z",
"file": nil, "file": nil,
}, },
}, "input.file", testutil.UploadFile{ }, "input.file", testutil.UploadFile{
Filename: "pci-dss.pdf", Filename: "pci-dss.pdf",
@@ -214,21 +214,21 @@ func TestVendorComplianceReport_Delete(t *testing.T) {
}, &uploadResult) }, &uploadResult)
require.NoError(t, err) require.NoError(t, err)
reportID := uploadResult.UploadVendorComplianceReport.VendorComplianceReportEdge.Node.ID reportID := uploadResult.UploadThirdPartyComplianceReport.ThirdPartyComplianceReportEdge.Node.ID
require.NotEmpty(t, reportID) require.NotEmpty(t, reportID)
const deleteQuery = ` const deleteQuery = `
mutation DeleteVendorComplianceReport($input: DeleteVendorComplianceReportInput!) { mutation DeleteThirdPartyComplianceReport($input: DeleteThirdPartyComplianceReportInput!) {
deleteVendorComplianceReport(input: $input) { deleteThirdPartyComplianceReport(input: $input) {
deletedVendorComplianceReportId deletedThirdPartyComplianceReportId
} }
} }
` `
var deleteResult struct { var deleteResult struct {
DeleteVendorComplianceReport struct { DeleteThirdPartyComplianceReport struct {
DeletedVendorComplianceReportID string `json:"deletedVendorComplianceReportId"` DeletedThirdPartyComplianceReportID string `json:"deletedThirdPartyComplianceReportId"`
} `json:"deleteVendorComplianceReport"` } `json:"deleteThirdPartyComplianceReport"`
} }
err = owner.Execute( err = owner.Execute(
@@ -241,5 +241,5 @@ func TestVendorComplianceReport_Delete(t *testing.T) {
&deleteResult, &deleteResult,
) )
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, reportID, deleteResult.DeleteVendorComplianceReport.DeletedVendorComplianceReportID) assert.Equal(t, reportID, deleteResult.DeleteThirdPartyComplianceReport.DeletedThirdPartyComplianceReportID)
} }

View File

@@ -25,17 +25,17 @@ import (
"go.probo.inc/probo/e2e/internal/testutil" "go.probo.inc/probo/e2e/internal/testutil"
) )
func TestVendorContact_Create(t *testing.T) { func TestThirdPartyContact_Create(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a vendor first // Create a thirdParty first
vendorID := factory.NewVendor(owner).WithName("Contact Test Vendor").Create() thirdPartyID := factory.NewThirdParty(owner).WithName("Contact Test ThirdParty").Create()
query := ` query := `
mutation CreateVendorContact($input: CreateVendorContactInput!) { mutation CreateThirdPartyContact($input: CreateThirdPartyContactInput!) {
createVendorContact(input: $input) { createThirdPartyContact(input: $input) {
vendorContactEdge { thirdPartyContactEdge {
node { node {
id id
fullName fullName
@@ -49,8 +49,8 @@ func TestVendorContact_Create(t *testing.T) {
` `
var result struct { var result struct {
CreateVendorContact struct { CreateThirdPartyContact struct {
VendorContactEdge struct { ThirdPartyContactEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
FullName string `json:"fullName"` FullName string `json:"fullName"`
@@ -58,39 +58,39 @@ func TestVendorContact_Create(t *testing.T) {
Phone string `json:"phone"` Phone string `json:"phone"`
Role string `json:"role"` Role string `json:"role"`
} `json:"node"` } `json:"node"`
} `json:"vendorContactEdge"` } `json:"thirdPartyContactEdge"`
} `json:"createVendorContact"` } `json:"createThirdPartyContact"`
} }
err := owner.Execute(query, map[string]any{ err := owner.Execute(query, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"fullName": "John Doe", "fullName": "John Doe",
"email": fmt.Sprintf("john.doe.%d@vendor.com", time.Now().UnixNano()), "email": fmt.Sprintf("john.doe.%d@thirdParty.com", time.Now().UnixNano()),
"phone": "+1-555-123-4567", "phone": "+1-555-123-4567",
"role": "Account Manager", "role": "Account Manager",
}, },
}, &result) }, &result)
require.NoError(t, err) require.NoError(t, err)
contact := result.CreateVendorContact.VendorContactEdge.Node contact := result.CreateThirdPartyContact.ThirdPartyContactEdge.Node
assert.NotEmpty(t, contact.ID) assert.NotEmpty(t, contact.ID)
assert.Equal(t, "John Doe", contact.FullName) assert.Equal(t, "John Doe", contact.FullName)
assert.Equal(t, "+1-555-123-4567", contact.Phone) assert.Equal(t, "+1-555-123-4567", contact.Phone)
assert.Equal(t, "Account Manager", contact.Role) assert.Equal(t, "Account Manager", contact.Role)
} }
func TestVendorContact_Update(t *testing.T) { func TestThirdPartyContact_Update(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a vendor and contact // Create a thirdParty and contact
vendorID := factory.NewVendor(owner).WithName("Update Contact Vendor").Create() thirdPartyID := factory.NewThirdParty(owner).WithName("Update Contact ThirdParty").Create()
createQuery := ` createQuery := `
mutation CreateVendorContact($input: CreateVendorContactInput!) { mutation CreateThirdPartyContact($input: CreateThirdPartyContactInput!) {
createVendorContact(input: $input) { createThirdPartyContact(input: $input) {
vendorContactEdge { thirdPartyContactEdge {
node { node {
id id
} }
@@ -100,30 +100,30 @@ func TestVendorContact_Update(t *testing.T) {
` `
var createResult struct { var createResult struct {
CreateVendorContact struct { CreateThirdPartyContact struct {
VendorContactEdge struct { ThirdPartyContactEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorContactEdge"` } `json:"thirdPartyContactEdge"`
} `json:"createVendorContact"` } `json:"createThirdPartyContact"`
} }
err := owner.Execute(createQuery, map[string]any{ err := owner.Execute(createQuery, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"fullName": "Initial Name", "fullName": "Initial Name",
"email": fmt.Sprintf("initial.%d@vendor.com", time.Now().UnixNano()), "email": fmt.Sprintf("initial.%d@thirdParty.com", time.Now().UnixNano()),
}, },
}, &createResult) }, &createResult)
require.NoError(t, err) require.NoError(t, err)
contactID := createResult.CreateVendorContact.VendorContactEdge.Node.ID contactID := createResult.CreateThirdPartyContact.ThirdPartyContactEdge.Node.ID
query := ` query := `
mutation UpdateVendorContact($input: UpdateVendorContactInput!) { mutation UpdateThirdPartyContact($input: UpdateThirdPartyContactInput!) {
updateVendorContact(input: $input) { updateThirdPartyContact(input: $input) {
vendorContact { thirdPartyContact {
id id
fullName fullName
phone phone
@@ -134,14 +134,14 @@ func TestVendorContact_Update(t *testing.T) {
` `
var result struct { var result struct {
UpdateVendorContact struct { UpdateThirdPartyContact struct {
VendorContact struct { ThirdPartyContact struct {
ID string `json:"id"` ID string `json:"id"`
FullName string `json:"fullName"` FullName string `json:"fullName"`
Phone string `json:"phone"` Phone string `json:"phone"`
Role string `json:"role"` Role string `json:"role"`
} `json:"vendorContact"` } `json:"thirdPartyContact"`
} `json:"updateVendorContact"` } `json:"updateThirdPartyContact"`
} }
err = owner.Execute(query, map[string]any{ err = owner.Execute(query, map[string]any{
@@ -154,23 +154,23 @@ func TestVendorContact_Update(t *testing.T) {
}, &result) }, &result)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, contactID, result.UpdateVendorContact.VendorContact.ID) assert.Equal(t, contactID, result.UpdateThirdPartyContact.ThirdPartyContact.ID)
assert.Equal(t, "Updated Name", result.UpdateVendorContact.VendorContact.FullName) assert.Equal(t, "Updated Name", result.UpdateThirdPartyContact.ThirdPartyContact.FullName)
assert.Equal(t, "+1-555-999-8888", result.UpdateVendorContact.VendorContact.Phone) assert.Equal(t, "+1-555-999-8888", result.UpdateThirdPartyContact.ThirdPartyContact.Phone)
assert.Equal(t, "Senior Account Manager", result.UpdateVendorContact.VendorContact.Role) assert.Equal(t, "Senior Account Manager", result.UpdateThirdPartyContact.ThirdPartyContact.Role)
} }
func TestVendorContact_Delete(t *testing.T) { func TestThirdPartyContact_Delete(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
vendorID := factory.NewVendor(owner).WithName("Delete Contact Vendor").Create() thirdPartyID := factory.NewThirdParty(owner).WithName("Delete Contact ThirdParty").Create()
// Create a contact to delete // Create a contact to delete
createQuery := ` createQuery := `
mutation CreateVendorContact($input: CreateVendorContactInput!) { mutation CreateThirdPartyContact($input: CreateThirdPartyContactInput!) {
createVendorContact(input: $input) { createThirdPartyContact(input: $input) {
vendorContactEdge { thirdPartyContactEdge {
node { node {
id id
} }
@@ -180,61 +180,61 @@ func TestVendorContact_Delete(t *testing.T) {
` `
var createResult struct { var createResult struct {
CreateVendorContact struct { CreateThirdPartyContact struct {
VendorContactEdge struct { ThirdPartyContactEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorContactEdge"` } `json:"thirdPartyContactEdge"`
} `json:"createVendorContact"` } `json:"createThirdPartyContact"`
} }
err := owner.Execute(createQuery, map[string]any{ err := owner.Execute(createQuery, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"fullName": fmt.Sprintf("Contact to Delete %d", time.Now().UnixNano()), "fullName": fmt.Sprintf("Contact to Delete %d", time.Now().UnixNano()),
"email": fmt.Sprintf("delete.%d@vendor.com", time.Now().UnixNano()), "email": fmt.Sprintf("delete.%d@thirdParty.com", time.Now().UnixNano()),
}, },
}, &createResult) }, &createResult)
require.NoError(t, err) require.NoError(t, err)
contactID := createResult.CreateVendorContact.VendorContactEdge.Node.ID contactID := createResult.CreateThirdPartyContact.ThirdPartyContactEdge.Node.ID
deleteQuery := ` deleteQuery := `
mutation DeleteVendorContact($input: DeleteVendorContactInput!) { mutation DeleteThirdPartyContact($input: DeleteThirdPartyContactInput!) {
deleteVendorContact(input: $input) { deleteThirdPartyContact(input: $input) {
deletedVendorContactId deletedThirdPartyContactId
} }
} }
` `
var result struct { var result struct {
DeleteVendorContact struct { DeleteThirdPartyContact struct {
DeletedVendorContactID string `json:"deletedVendorContactId"` DeletedThirdPartyContactID string `json:"deletedThirdPartyContactId"`
} `json:"deleteVendorContact"` } `json:"deleteThirdPartyContact"`
} }
err = owner.Execute(deleteQuery, map[string]any{ err = owner.Execute(deleteQuery, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorContactId": contactID, "thirdPartyContactId": contactID,
}, },
}, &result) }, &result)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, contactID, result.DeleteVendorContact.DeletedVendorContactID) assert.Equal(t, contactID, result.DeleteThirdPartyContact.DeletedThirdPartyContactID)
} }
func TestVendorContact_List(t *testing.T) { func TestThirdPartyContact_List(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
vendorID := factory.NewVendor(owner).WithName("List Contacts Vendor").Create() thirdPartyID := factory.NewThirdParty(owner).WithName("List Contacts ThirdParty").Create()
// Create multiple contacts // Create multiple contacts
for i := range 3 { for i := range 3 {
query := ` query := `
mutation CreateVendorContact($input: CreateVendorContactInput!) { mutation CreateThirdPartyContact($input: CreateThirdPartyContactInput!) {
createVendorContact(input: $input) { createThirdPartyContact(input: $input) {
vendorContactEdge { thirdPartyContactEdge {
node { node {
id id
} }
@@ -245,18 +245,18 @@ func TestVendorContact_List(t *testing.T) {
_, err := owner.Do(query, map[string]any{ _, err := owner.Do(query, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"fullName": fmt.Sprintf("Contact %d", i), "fullName": fmt.Sprintf("Contact %d", i),
"email": fmt.Sprintf("contact.%d.%d@vendor.com", i, time.Now().UnixNano()), "email": fmt.Sprintf("contact.%d.%d@thirdParty.com", i, time.Now().UnixNano()),
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
} }
query := ` query := `
query GetVendorContacts($id: ID!) { query GetThirdPartyContacts($id: ID!) {
node(id: $id) { node(id: $id) {
... on Vendor { ... on ThirdParty {
contacts(first: 10) { contacts(first: 10) {
edges { edges {
node { node {
@@ -286,7 +286,7 @@ func TestVendorContact_List(t *testing.T) {
} }
err := owner.Execute(query, map[string]any{ err := owner.Execute(query, map[string]any{
"id": vendorID, "id": thirdPartyID,
}, &result) }, &result)
require.NoError(t, err) require.NoError(t, err)
assert.GreaterOrEqual(t, len(result.Node.Contacts.Edges), 3) assert.GreaterOrEqual(t, len(result.Node.Contacts.Edges), 3)

View File

@@ -23,7 +23,7 @@ import (
"go.probo.inc/probo/e2e/internal/testutil" "go.probo.inc/probo/e2e/internal/testutil"
) )
func TestVendor_PublishVendorList(t *testing.T) { func TestThirdParty_PublishThirdPartyList(t *testing.T) {
t.Parallel() t.Parallel()
t.Run( t.Run(
@@ -32,11 +32,11 @@ func TestVendor_PublishVendorList(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
factory.CreateVendor(owner, factory.Attrs{"name": "Test Vendor"}) factory.CreateThirdParty(owner, factory.Attrs{"name": "Test ThirdParty"})
const query = ` const query = `
mutation($input: PublishVendorListInput!) { mutation($input: PublishThirdPartyListInput!) {
publishVendorList(input: $input) { publishThirdPartyList(input: $input) {
documentEdge { documentEdge {
node { node {
id id
@@ -60,7 +60,7 @@ func TestVendor_PublishVendorList(t *testing.T) {
` `
var result struct { var result struct {
PublishVendorList struct { PublishThirdPartyList struct {
DocumentEdge struct { DocumentEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
@@ -79,7 +79,7 @@ func TestVendor_PublishVendorList(t *testing.T) {
Content string `json:"content"` Content string `json:"content"`
} `json:"node"` } `json:"node"`
} `json:"documentVersionEdge"` } `json:"documentVersionEdge"`
} `json:"publishVendorList"` } `json:"publishThirdPartyList"`
} }
err := owner.Execute( err := owner.Execute(
@@ -94,12 +94,12 @@ func TestVendor_PublishVendorList(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
doc := result.PublishVendorList.DocumentEdge.Node doc := result.PublishThirdPartyList.DocumentEdge.Node
assert.NotEmpty(t, doc.ID) assert.NotEmpty(t, doc.ID)
assert.Equal(t, "GENERATED", doc.WriteMode) assert.Equal(t, "GENERATED", doc.WriteMode)
assert.Equal(t, "ACTIVE", doc.Status) assert.Equal(t, "ACTIVE", doc.Status)
ver := result.PublishVendorList.DocumentVersionEdge.Node ver := result.PublishThirdPartyList.DocumentVersionEdge.Node
assert.NotEmpty(t, ver.ID) assert.NotEmpty(t, ver.ID)
assert.Equal(t, "REGISTER", ver.DocumentType) assert.Equal(t, "REGISTER", ver.DocumentType)
assert.Equal(t, "PUBLISHED", ver.Status) assert.Equal(t, "PUBLISHED", ver.Status)
@@ -117,8 +117,8 @@ func TestVendor_PublishVendorList(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
const query = ` const query = `
mutation($input: PublishVendorListInput!) { mutation($input: PublishThirdPartyListInput!) {
publishVendorList(input: $input) { publishThirdPartyList(input: $input) {
documentEdge { documentEdge {
node { id writeMode } node { id writeMode }
} }
@@ -130,7 +130,7 @@ func TestVendor_PublishVendorList(t *testing.T) {
` `
var result struct { var result struct {
PublishVendorList struct { PublishThirdPartyList struct {
DocumentEdge struct { DocumentEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
@@ -144,7 +144,7 @@ func TestVendor_PublishVendorList(t *testing.T) {
Major int `json:"major"` Major int `json:"major"`
} `json:"node"` } `json:"node"`
} `json:"documentVersionEdge"` } `json:"documentVersionEdge"`
} `json:"publishVendorList"` } `json:"publishThirdPartyList"`
} }
err := owner.Execute( err := owner.Execute(
@@ -160,11 +160,11 @@ func TestVendor_PublishVendorList(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
doc := result.PublishVendorList.DocumentEdge.Node doc := result.PublishThirdPartyList.DocumentEdge.Node
assert.NotEmpty(t, doc.ID) assert.NotEmpty(t, doc.ID)
assert.Equal(t, "GENERATED", doc.WriteMode) assert.Equal(t, "GENERATED", doc.WriteMode)
ver := result.PublishVendorList.DocumentVersionEdge.Node ver := result.PublishThirdPartyList.DocumentVersionEdge.Node
assert.NotEmpty(t, ver.ID) assert.NotEmpty(t, ver.ID)
assert.Equal(t, "PENDING_APPROVAL", ver.Status) assert.Equal(t, "PENDING_APPROVAL", ver.Status)
}, },
@@ -176,11 +176,11 @@ func TestVendor_PublishVendorList(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
factory.CreateVendor(owner, factory.Attrs{"name": "Reuse Vendor"}) factory.CreateThirdParty(owner, factory.Attrs{"name": "Reuse ThirdParty"})
const query = ` const query = `
mutation($input: PublishVendorListInput!) { mutation($input: PublishThirdPartyListInput!) {
publishVendorList(input: $input) { publishThirdPartyList(input: $input) {
documentEdge { node { id } } documentEdge { node { id } }
documentVersionEdge { node { id major } } documentVersionEdge { node { id major } }
} }
@@ -188,7 +188,7 @@ func TestVendor_PublishVendorList(t *testing.T) {
` `
var r1, r2 struct { var r1, r2 struct {
PublishVendorList struct { PublishThirdPartyList struct {
DocumentEdge struct { DocumentEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
@@ -200,7 +200,7 @@ func TestVendor_PublishVendorList(t *testing.T) {
Major int `json:"major"` Major int `json:"major"`
} `json:"node"` } `json:"node"`
} `json:"documentVersionEdge"` } `json:"documentVersionEdge"`
} `json:"publishVendorList"` } `json:"publishThirdPartyList"`
} }
input := map[string]any{ input := map[string]any{
@@ -217,26 +217,26 @@ func TestVendor_PublishVendorList(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, assert.Equal(t,
r1.PublishVendorList.DocumentEdge.Node.ID, r1.PublishThirdPartyList.DocumentEdge.Node.ID,
r2.PublishVendorList.DocumentEdge.Node.ID, r2.PublishThirdPartyList.DocumentEdge.Node.ID,
"should reuse same document", "should reuse same document",
) )
assert.Equal(t, 1, r1.PublishVendorList.DocumentVersionEdge.Node.Major) assert.Equal(t, 1, r1.PublishThirdPartyList.DocumentVersionEdge.Node.Major)
assert.Equal(t, 2, r2.PublishVendorList.DocumentVersionEdge.Node.Major) assert.Equal(t, 2, r2.PublishThirdPartyList.DocumentVersionEdge.Node.Major)
}, },
) )
t.Run( t.Run(
"organization vendorsDocument links to published document", "organization thirdPartiesDocument links to published document",
func(t *testing.T) { func(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
factory.CreateVendor(owner, factory.Attrs{"name": "Linked Vendor"}) factory.CreateThirdParty(owner, factory.Attrs{"name": "Linked ThirdParty"})
const publishQuery = ` const publishQuery = `
mutation($input: PublishVendorListInput!) { mutation($input: PublishThirdPartyListInput!) {
publishVendorList(input: $input) { publishThirdPartyList(input: $input) {
documentEdge { node { id } } documentEdge { node { id } }
documentVersionEdge { node { id } } documentVersionEdge { node { id } }
} }
@@ -244,7 +244,7 @@ func TestVendor_PublishVendorList(t *testing.T) {
` `
var publishResult struct { var publishResult struct {
PublishVendorList struct { PublishThirdPartyList struct {
DocumentEdge struct { DocumentEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
@@ -255,7 +255,7 @@ func TestVendor_PublishVendorList(t *testing.T) {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"documentVersionEdge"` } `json:"documentVersionEdge"`
} `json:"publishVendorList"` } `json:"publishThirdPartyList"`
} }
err := owner.Execute( err := owner.Execute(
@@ -270,14 +270,14 @@ func TestVendor_PublishVendorList(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
docID := publishResult.PublishVendorList.DocumentEdge.Node.ID docID := publishResult.PublishThirdPartyList.DocumentEdge.Node.ID
const orgQuery = ` const orgQuery = `
query($id: ID!) { query($id: ID!) {
node(id: $id) { node(id: $id) {
... on Organization { ... on Organization {
id id
vendorsDocument { id } thirdPartiesDocument { id }
} }
} }
} }
@@ -285,10 +285,10 @@ func TestVendor_PublishVendorList(t *testing.T) {
var orgResult struct { var orgResult struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
VendorsDocument *struct { ThirdPartiesDocument *struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"vendorsDocument"` } `json:"thirdPartiesDocument"`
} `json:"node"` } `json:"node"`
} }
@@ -298,23 +298,23 @@ func TestVendor_PublishVendorList(t *testing.T) {
&orgResult, &orgResult,
) )
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, orgResult.Node.VendorsDocument) require.NotNil(t, orgResult.Node.ThirdPartiesDocument)
assert.Equal(t, docID, orgResult.Node.VendorsDocument.ID) assert.Equal(t, docID, orgResult.Node.ThirdPartiesDocument.ID)
}, },
) )
} }
func TestVendor_PublishVendorList_RBAC(t *testing.T) { func TestThirdParty_PublishThirdPartyList_RBAC(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
factory.CreateVendor(owner, factory.Attrs{"name": "RBAC Vendor"}) factory.CreateThirdParty(owner, factory.Attrs{"name": "RBAC ThirdParty"})
const query = ` const query = `
mutation($input: PublishVendorListInput!) { mutation($input: PublishThirdPartyListInput!) {
publishVendorList(input: $input) { publishThirdPartyList(input: $input) {
documentEdge { node { id } } documentEdge { node { id } }
documentVersionEdge { node { id } } documentVersionEdge { node { id } }
} }
@@ -322,7 +322,7 @@ func TestVendor_PublishVendorList_RBAC(t *testing.T) {
` `
t.Run( t.Run(
"viewer cannot publish vendor list", "viewer cannot publish thirdParty list",
func(t *testing.T) { func(t *testing.T) {
t.Parallel() t.Parallel()

View File

@@ -22,15 +22,15 @@ import (
"go.probo.inc/probo/e2e/internal/testutil" "go.probo.inc/probo/e2e/internal/testutil"
) )
func TestVendorService_Create(t *testing.T) { func TestThirdPartyService_Create(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a vendor first // Create a thirdParty first
createVendorMutation := ` createThirdPartyMutation := `
mutation CreateVendor($input: CreateVendorInput!) { mutation CreateThirdParty($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { thirdPartyEdge {
node { node {
id id
} }
@@ -39,129 +39,129 @@ func TestVendorService_Create(t *testing.T) {
} }
` `
var createVendorResult struct { var createThirdPartyResult struct {
CreateVendor struct { CreateThirdParty struct {
VendorEdge struct { ThirdPartyEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorEdge"` } `json:"thirdPartyEdge"`
} `json:"createVendor"` } `json:"createThirdParty"`
} }
err := owner.Execute(createVendorMutation, map[string]any{ err := owner.Execute(createThirdPartyMutation, map[string]any{
"input": map[string]any{ "input": map[string]any{
"organizationId": owner.GetOrganizationID().String(), "organizationId": owner.GetOrganizationID().String(),
"name": "AWS", "name": "AWS",
"category": "CLOUD_PROVIDER", "category": "CLOUD_PROVIDER",
}, },
}, &createVendorResult) }, &createThirdPartyResult)
require.NoError(t, err) require.NoError(t, err)
vendorID := createVendorResult.CreateVendor.VendorEdge.Node.ID thirdPartyID := createThirdPartyResult.CreateThirdParty.ThirdPartyEdge.Node.ID
tests := []struct { tests := []struct {
name string name string
role testutil.TestRole role testutil.TestRole
variables func() map[string]any variables func() map[string]any
check func(t *testing.T, err error, m *struct { check func(t *testing.T, err error, m *struct {
CreateVendorService struct { CreateThirdPartyService struct {
VendorServiceEdge struct { ThirdPartyServiceEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"node"` } `json:"node"`
} `json:"vendorServiceEdge"` } `json:"thirdPartyServiceEdge"`
} `json:"createVendorService"` } `json:"createThirdPartyService"`
}) })
}{ }{
{ {
name: "Owner can create vendor service", name: "Owner can create thirdParty service",
role: testutil.RoleOwner, role: testutil.RoleOwner,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{ return map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"name": "Amazon S3", "name": "Amazon S3",
"description": "Simple Storage Service", "description": "Simple Storage Service",
}, },
} }
}, },
check: func(t *testing.T, err error, m *struct { check: func(t *testing.T, err error, m *struct {
CreateVendorService struct { CreateThirdPartyService struct {
VendorServiceEdge struct { ThirdPartyServiceEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"node"` } `json:"node"`
} `json:"vendorServiceEdge"` } `json:"thirdPartyServiceEdge"`
} `json:"createVendorService"` } `json:"createThirdPartyService"`
}) { }) {
require.NoError(t, err) require.NoError(t, err)
assert.NotEmpty(t, m.CreateVendorService.VendorServiceEdge.Node.ID) assert.NotEmpty(t, m.CreateThirdPartyService.ThirdPartyServiceEdge.Node.ID)
assert.Equal(t, "Amazon S3", m.CreateVendorService.VendorServiceEdge.Node.Name) assert.Equal(t, "Amazon S3", m.CreateThirdPartyService.ThirdPartyServiceEdge.Node.Name)
assert.Equal(t, "Simple Storage Service", *m.CreateVendorService.VendorServiceEdge.Node.Description) assert.Equal(t, "Simple Storage Service", *m.CreateThirdPartyService.ThirdPartyServiceEdge.Node.Description)
}, },
}, },
{ {
name: "Admin can create vendor service", name: "Admin can create thirdParty service",
role: testutil.RoleAdmin, role: testutil.RoleAdmin,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{ return map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"name": "Amazon EC2", "name": "Amazon EC2",
"description": "Elastic Compute Cloud", "description": "Elastic Compute Cloud",
}, },
} }
}, },
check: func(t *testing.T, err error, m *struct { check: func(t *testing.T, err error, m *struct {
CreateVendorService struct { CreateThirdPartyService struct {
VendorServiceEdge struct { ThirdPartyServiceEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"node"` } `json:"node"`
} `json:"vendorServiceEdge"` } `json:"thirdPartyServiceEdge"`
} `json:"createVendorService"` } `json:"createThirdPartyService"`
}) { }) {
require.NoError(t, err) require.NoError(t, err)
}, },
}, },
{ {
name: "Viewer cannot create vendor service", name: "Viewer cannot create thirdParty service",
role: testutil.RoleViewer, role: testutil.RoleViewer,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{ return map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"name": "Should Fail", "name": "Should Fail",
}, },
} }
}, },
check: func(t *testing.T, err error, m *struct { check: func(t *testing.T, err error, m *struct {
CreateVendorService struct { CreateThirdPartyService struct {
VendorServiceEdge struct { ThirdPartyServiceEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"node"` } `json:"node"`
} `json:"vendorServiceEdge"` } `json:"thirdPartyServiceEdge"`
} `json:"createVendorService"` } `json:"createThirdPartyService"`
}) { }) {
require.Error(t, err, "Viewer should not be able to create vendor service") require.Error(t, err, "Viewer should not be able to create thirdParty service")
}, },
}, },
} }
createVendorServiceMutation := ` createThirdPartyServiceMutation := `
mutation CreateVendorService($input: CreateVendorServiceInput!) { mutation CreateThirdPartyService($input: CreateThirdPartyServiceInput!) {
createVendorService(input: $input) { createThirdPartyService(input: $input) {
vendorServiceEdge { thirdPartyServiceEdge {
node { node {
id id
name name
@@ -182,32 +182,32 @@ func TestVendorService_Create(t *testing.T) {
} }
var m struct { var m struct {
CreateVendorService struct { CreateThirdPartyService struct {
VendorServiceEdge struct { ThirdPartyServiceEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"node"` } `json:"node"`
} `json:"vendorServiceEdge"` } `json:"thirdPartyServiceEdge"`
} `json:"createVendorService"` } `json:"createThirdPartyService"`
} }
err := client.Execute(createVendorServiceMutation, tt.variables(), &m) err := client.Execute(createThirdPartyServiceMutation, tt.variables(), &m)
tt.check(t, err, &m) tt.check(t, err, &m)
}) })
} }
} }
func TestVendorService_Update(t *testing.T) { func TestThirdPartyService_Update(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a vendor first // Create a thirdParty first
createVendorMutation := ` createThirdPartyMutation := `
mutation CreateVendor($input: CreateVendorInput!) { mutation CreateThirdParty($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { thirdPartyEdge {
node { node {
id id
} }
@@ -216,32 +216,32 @@ func TestVendorService_Update(t *testing.T) {
} }
` `
var createVendorResult struct { var createThirdPartyResult struct {
CreateVendor struct { CreateThirdParty struct {
VendorEdge struct { ThirdPartyEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorEdge"` } `json:"thirdPartyEdge"`
} `json:"createVendor"` } `json:"createThirdParty"`
} }
err := owner.Execute(createVendorMutation, map[string]any{ err := owner.Execute(createThirdPartyMutation, map[string]any{
"input": map[string]any{ "input": map[string]any{
"organizationId": owner.GetOrganizationID().String(), "organizationId": owner.GetOrganizationID().String(),
"name": "Google Cloud", "name": "Google Cloud",
"category": "CLOUD_PROVIDER", "category": "CLOUD_PROVIDER",
}, },
}, &createVendorResult) }, &createThirdPartyResult)
require.NoError(t, err) require.NoError(t, err)
vendorID := createVendorResult.CreateVendor.VendorEdge.Node.ID thirdPartyID := createThirdPartyResult.CreateThirdParty.ThirdPartyEdge.Node.ID
// Create a vendor service // Create a thirdParty service
createServiceMutation := ` createServiceMutation := `
mutation CreateVendorService($input: CreateVendorServiceInput!) { mutation CreateThirdPartyService($input: CreateThirdPartyServiceInput!) {
createVendorService(input: $input) { createThirdPartyService(input: $input) {
vendorServiceEdge { thirdPartyServiceEdge {
node { node {
id id
} }
@@ -251,42 +251,42 @@ func TestVendorService_Update(t *testing.T) {
` `
var createServiceResult struct { var createServiceResult struct {
CreateVendorService struct { CreateThirdPartyService struct {
VendorServiceEdge struct { ThirdPartyServiceEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorServiceEdge"` } `json:"thirdPartyServiceEdge"`
} `json:"createVendorService"` } `json:"createThirdPartyService"`
} }
err = owner.Execute(createServiceMutation, map[string]any{ err = owner.Execute(createServiceMutation, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"name": "Cloud Storage", "name": "Cloud Storage",
"description": "Initial description", "description": "Initial description",
}, },
}, &createServiceResult) }, &createServiceResult)
require.NoError(t, err) require.NoError(t, err)
serviceID := createServiceResult.CreateVendorService.VendorServiceEdge.Node.ID serviceID := createServiceResult.CreateThirdPartyService.ThirdPartyServiceEdge.Node.ID
tests := []struct { tests := []struct {
name string name string
role testutil.TestRole role testutil.TestRole
variables func() map[string]any variables func() map[string]any
check func(t *testing.T, err error, m *struct { check func(t *testing.T, err error, m *struct {
UpdateVendorService struct { UpdateThirdPartyService struct {
VendorService struct { ThirdPartyService struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"vendorService"` } `json:"thirdPartyService"`
} `json:"updateVendorService"` } `json:"updateThirdPartyService"`
}) })
}{ }{
{ {
name: "Owner can update vendor service", name: "Owner can update thirdParty service",
role: testutil.RoleOwner, role: testutil.RoleOwner,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{ return map[string]any{
@@ -298,22 +298,22 @@ func TestVendorService_Update(t *testing.T) {
} }
}, },
check: func(t *testing.T, err error, m *struct { check: func(t *testing.T, err error, m *struct {
UpdateVendorService struct { UpdateThirdPartyService struct {
VendorService struct { ThirdPartyService struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"vendorService"` } `json:"thirdPartyService"`
} `json:"updateVendorService"` } `json:"updateThirdPartyService"`
}) { }) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, serviceID, m.UpdateVendorService.VendorService.ID) assert.Equal(t, serviceID, m.UpdateThirdPartyService.ThirdPartyService.ID)
assert.Equal(t, "Updated Cloud Storage", m.UpdateVendorService.VendorService.Name) assert.Equal(t, "Updated Cloud Storage", m.UpdateThirdPartyService.ThirdPartyService.Name)
assert.Equal(t, "Updated description", *m.UpdateVendorService.VendorService.Description) assert.Equal(t, "Updated description", *m.UpdateThirdPartyService.ThirdPartyService.Description)
}, },
}, },
{ {
name: "Admin can update vendor service", name: "Admin can update thirdParty service",
role: testutil.RoleAdmin, role: testutil.RoleAdmin,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{ return map[string]any{
@@ -324,19 +324,19 @@ func TestVendorService_Update(t *testing.T) {
} }
}, },
check: func(t *testing.T, err error, m *struct { check: func(t *testing.T, err error, m *struct {
UpdateVendorService struct { UpdateThirdPartyService struct {
VendorService struct { ThirdPartyService struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"vendorService"` } `json:"thirdPartyService"`
} `json:"updateVendorService"` } `json:"updateThirdPartyService"`
}) { }) {
require.NoError(t, err) require.NoError(t, err)
}, },
}, },
{ {
name: "Viewer cannot update vendor service", name: "Viewer cannot update thirdParty service",
role: testutil.RoleViewer, role: testutil.RoleViewer,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{ return map[string]any{
@@ -347,23 +347,23 @@ func TestVendorService_Update(t *testing.T) {
} }
}, },
check: func(t *testing.T, err error, m *struct { check: func(t *testing.T, err error, m *struct {
UpdateVendorService struct { UpdateThirdPartyService struct {
VendorService struct { ThirdPartyService struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"vendorService"` } `json:"thirdPartyService"`
} `json:"updateVendorService"` } `json:"updateThirdPartyService"`
}) { }) {
require.Error(t, err, "Viewer should not be able to update vendor service") require.Error(t, err, "Viewer should not be able to update thirdParty service")
}, },
}, },
} }
updateVendorServiceMutation := ` updateThirdPartyServiceMutation := `
mutation UpdateVendorService($input: UpdateVendorServiceInput!) { mutation UpdateThirdPartyService($input: UpdateThirdPartyServiceInput!) {
updateVendorService(input: $input) { updateThirdPartyService(input: $input) {
vendorService { thirdPartyService {
id id
name name
description description
@@ -382,30 +382,30 @@ func TestVendorService_Update(t *testing.T) {
} }
var m struct { var m struct {
UpdateVendorService struct { UpdateThirdPartyService struct {
VendorService struct { ThirdPartyService struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
} `json:"vendorService"` } `json:"thirdPartyService"`
} `json:"updateVendorService"` } `json:"updateThirdPartyService"`
} }
err := client.Execute(updateVendorServiceMutation, tt.variables(), &m) err := client.Execute(updateThirdPartyServiceMutation, tt.variables(), &m)
tt.check(t, err, &m) tt.check(t, err, &m)
}) })
} }
} }
func TestVendorService_Delete(t *testing.T) { func TestThirdPartyService_Delete(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a vendor first // Create a thirdParty first
createVendorMutation := ` createThirdPartyMutation := `
mutation CreateVendor($input: CreateVendorInput!) { mutation CreateThirdParty($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { thirdPartyEdge {
node { node {
id id
} }
@@ -414,32 +414,32 @@ func TestVendorService_Delete(t *testing.T) {
} }
` `
var createVendorResult struct { var createThirdPartyResult struct {
CreateVendor struct { CreateThirdParty struct {
VendorEdge struct { ThirdPartyEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorEdge"` } `json:"thirdPartyEdge"`
} `json:"createVendor"` } `json:"createThirdParty"`
} }
err := owner.Execute(createVendorMutation, map[string]any{ err := owner.Execute(createThirdPartyMutation, map[string]any{
"input": map[string]any{ "input": map[string]any{
"organizationId": owner.GetOrganizationID().String(), "organizationId": owner.GetOrganizationID().String(),
"name": "Azure", "name": "Azure",
"category": "CLOUD_PROVIDER", "category": "CLOUD_PROVIDER",
}, },
}, &createVendorResult) }, &createThirdPartyResult)
require.NoError(t, err) require.NoError(t, err)
vendorID := createVendorResult.CreateVendor.VendorEdge.Node.ID thirdPartyID := createThirdPartyResult.CreateThirdParty.ThirdPartyEdge.Node.ID
createService := func() string { createService := func() string {
createServiceMutation := ` createServiceMutation := `
mutation CreateVendorService($input: CreateVendorServiceInput!) { mutation CreateThirdPartyService($input: CreateThirdPartyServiceInput!) {
createVendorService(input: $input) { createThirdPartyService(input: $input) {
vendorServiceEdge { thirdPartyServiceEdge {
node { node {
id id
} }
@@ -449,24 +449,24 @@ func TestVendorService_Delete(t *testing.T) {
` `
var m struct { var m struct {
CreateVendorService struct { CreateThirdPartyService struct {
VendorServiceEdge struct { ThirdPartyServiceEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorServiceEdge"` } `json:"thirdPartyServiceEdge"`
} `json:"createVendorService"` } `json:"createThirdPartyService"`
} }
err := owner.Execute(createServiceMutation, map[string]any{ err := owner.Execute(createServiceMutation, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"name": "Service to delete", "name": "Service to delete",
}, },
}, &m) }, &m)
require.NoError(t, err) require.NoError(t, err)
return m.CreateVendorService.VendorServiceEdge.Node.ID return m.CreateThirdPartyService.ThirdPartyServiceEdge.Node.ID
} }
tests := []struct { tests := []struct {
@@ -474,73 +474,73 @@ func TestVendorService_Delete(t *testing.T) {
role testutil.TestRole role testutil.TestRole
variables func(serviceID string) map[string]any variables func(serviceID string) map[string]any
check func(t *testing.T, err error, serviceID string, m *struct { check func(t *testing.T, err error, serviceID string, m *struct {
DeleteVendorService struct { DeleteThirdPartyService struct {
DeletedVendorServiceID string `json:"deletedVendorServiceId"` DeletedThirdPartyServiceID string `json:"deletedThirdPartyServiceId"`
} `json:"deleteVendorService"` } `json:"deleteThirdPartyService"`
}) })
}{ }{
{ {
name: "Viewer cannot delete vendor service", name: "Viewer cannot delete thirdParty service",
role: testutil.RoleViewer, role: testutil.RoleViewer,
variables: func(serviceID string) map[string]any { variables: func(serviceID string) map[string]any {
return map[string]any{ return map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorServiceId": serviceID, "thirdPartyServiceId": serviceID,
}, },
} }
}, },
check: func(t *testing.T, err error, serviceID string, m *struct { check: func(t *testing.T, err error, serviceID string, m *struct {
DeleteVendorService struct { DeleteThirdPartyService struct {
DeletedVendorServiceID string `json:"deletedVendorServiceId"` DeletedThirdPartyServiceID string `json:"deletedThirdPartyServiceId"`
} `json:"deleteVendorService"` } `json:"deleteThirdPartyService"`
}) { }) {
require.Error(t, err, "Viewer should not be able to delete vendor service") require.Error(t, err, "Viewer should not be able to delete thirdParty service")
}, },
}, },
{ {
name: "Admin can delete vendor service", name: "Admin can delete thirdParty service",
role: testutil.RoleAdmin, role: testutil.RoleAdmin,
variables: func(serviceID string) map[string]any { variables: func(serviceID string) map[string]any {
return map[string]any{ return map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorServiceId": serviceID, "thirdPartyServiceId": serviceID,
}, },
} }
}, },
check: func(t *testing.T, err error, serviceID string, m *struct { check: func(t *testing.T, err error, serviceID string, m *struct {
DeleteVendorService struct { DeleteThirdPartyService struct {
DeletedVendorServiceID string `json:"deletedVendorServiceId"` DeletedThirdPartyServiceID string `json:"deletedThirdPartyServiceId"`
} `json:"deleteVendorService"` } `json:"deleteThirdPartyService"`
}) { }) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, serviceID, m.DeleteVendorService.DeletedVendorServiceID) assert.Equal(t, serviceID, m.DeleteThirdPartyService.DeletedThirdPartyServiceID)
}, },
}, },
{ {
name: "Owner can delete vendor service", name: "Owner can delete thirdParty service",
role: testutil.RoleOwner, role: testutil.RoleOwner,
variables: func(serviceID string) map[string]any { variables: func(serviceID string) map[string]any {
return map[string]any{ return map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorServiceId": serviceID, "thirdPartyServiceId": serviceID,
}, },
} }
}, },
check: func(t *testing.T, err error, serviceID string, m *struct { check: func(t *testing.T, err error, serviceID string, m *struct {
DeleteVendorService struct { DeleteThirdPartyService struct {
DeletedVendorServiceID string `json:"deletedVendorServiceId"` DeletedThirdPartyServiceID string `json:"deletedThirdPartyServiceId"`
} `json:"deleteVendorService"` } `json:"deleteThirdPartyService"`
}) { }) {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, serviceID, m.DeleteVendorService.DeletedVendorServiceID) assert.Equal(t, serviceID, m.DeleteThirdPartyService.DeletedThirdPartyServiceID)
}, },
}, },
} }
deleteVendorServiceMutation := ` deleteThirdPartyServiceMutation := `
mutation DeleteVendorService($input: DeleteVendorServiceInput!) { mutation DeleteThirdPartyService($input: DeleteThirdPartyServiceInput!) {
deleteVendorService(input: $input) { deleteThirdPartyService(input: $input) {
deletedVendorServiceId deletedThirdPartyServiceId
} }
} }
` `
@@ -557,26 +557,26 @@ func TestVendorService_Delete(t *testing.T) {
} }
var m struct { var m struct {
DeleteVendorService struct { DeleteThirdPartyService struct {
DeletedVendorServiceID string `json:"deletedVendorServiceId"` DeletedThirdPartyServiceID string `json:"deletedThirdPartyServiceId"`
} `json:"deleteVendorService"` } `json:"deleteThirdPartyService"`
} }
err := client.Execute(deleteVendorServiceMutation, tt.variables(serviceID), &m) err := client.Execute(deleteThirdPartyServiceMutation, tt.variables(serviceID), &m)
tt.check(t, err, serviceID, &m) tt.check(t, err, serviceID, &m)
}) })
} }
} }
func TestVendorService_List(t *testing.T) { func TestThirdPartyService_List(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a vendor first // Create a thirdParty first
createVendorMutation := ` createThirdPartyMutation := `
mutation CreateVendor($input: CreateVendorInput!) { mutation CreateThirdParty($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { thirdPartyEdge {
node { node {
id id
} }
@@ -585,32 +585,32 @@ func TestVendorService_List(t *testing.T) {
} }
` `
var createVendorResult struct { var createThirdPartyResult struct {
CreateVendor struct { CreateThirdParty struct {
VendorEdge struct { ThirdPartyEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorEdge"` } `json:"thirdPartyEdge"`
} `json:"createVendor"` } `json:"createThirdParty"`
} }
err := owner.Execute(createVendorMutation, map[string]any{ err := owner.Execute(createThirdPartyMutation, map[string]any{
"input": map[string]any{ "input": map[string]any{
"organizationId": owner.GetOrganizationID().String(), "organizationId": owner.GetOrganizationID().String(),
"name": "Vendor for Services", "name": "ThirdParty for Services",
"category": "CLOUD_PROVIDER", "category": "CLOUD_PROVIDER",
}, },
}, &createVendorResult) }, &createThirdPartyResult)
require.NoError(t, err) require.NoError(t, err)
vendorID := createVendorResult.CreateVendor.VendorEdge.Node.ID thirdPartyID := createThirdPartyResult.CreateThirdParty.ThirdPartyEdge.Node.ID
// Create multiple services // Create multiple services
createServiceMutation := ` createServiceMutation := `
mutation CreateVendorService($input: CreateVendorServiceInput!) { mutation CreateThirdPartyService($input: CreateThirdPartyServiceInput!) {
createVendorService(input: $input) { createThirdPartyService(input: $input) {
vendorServiceEdge { thirdPartyServiceEdge {
node { node {
id id
} }
@@ -622,19 +622,19 @@ func TestVendorService_List(t *testing.T) {
services := []string{"Service A", "Service B", "Service C"} services := []string{"Service A", "Service B", "Service C"}
for _, name := range services { for _, name := range services {
var m struct { var m struct {
CreateVendorService struct { CreateThirdPartyService struct {
VendorServiceEdge struct { ThirdPartyServiceEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorServiceEdge"` } `json:"thirdPartyServiceEdge"`
} `json:"createVendorService"` } `json:"createThirdPartyService"`
} }
err := owner.Execute(createServiceMutation, map[string]any{ err := owner.Execute(createServiceMutation, map[string]any{
"input": map[string]any{ "input": map[string]any{
"vendorId": vendorID, "thirdPartyId": thirdPartyID,
"name": name, "name": name,
}, },
}, &m) }, &m)
require.NoError(t, err) require.NoError(t, err)
@@ -659,11 +659,11 @@ func TestVendorService_List(t *testing.T) {
}) })
}{ }{
{ {
name: "Owner can list vendor services", name: "Owner can list thirdParty services",
role: testutil.RoleOwner, role: testutil.RoleOwner,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{ return map[string]any{
"id": vendorID, "id": thirdPartyID,
} }
}, },
check: func(t *testing.T, err error, q *struct { check: func(t *testing.T, err error, q *struct {
@@ -684,11 +684,11 @@ func TestVendorService_List(t *testing.T) {
}, },
}, },
{ {
name: "Viewer can list vendor services", name: "Viewer can list thirdParty services",
role: testutil.RoleViewer, role: testutil.RoleViewer,
variables: func() map[string]any { variables: func() map[string]any {
return map[string]any{ return map[string]any{
"id": vendorID, "id": thirdPartyID,
} }
}, },
check: func(t *testing.T, err error, q *struct { check: func(t *testing.T, err error, q *struct {
@@ -709,10 +709,10 @@ func TestVendorService_List(t *testing.T) {
}, },
} }
listVendorServicesQuery := ` listThirdPartyServicesQuery := `
query ListVendorServices($id: ID!) { query ListThirdPartyServices($id: ID!) {
node(id: $id) { node(id: $id) {
... on Vendor { ... on ThirdParty {
id id
services(first: 10) { services(first: 10) {
edges { edges {
@@ -750,7 +750,7 @@ func TestVendorService_List(t *testing.T) {
} `json:"node"` } `json:"node"`
} }
err := client.Execute(listVendorServicesQuery, tt.variables(), &q) err := client.Execute(listThirdPartyServicesQuery, tt.variables(), &q)
tt.check(t, err, &q) tt.check(t, err, &q)
}) })
} }

View File

@@ -142,7 +142,7 @@ func CreateUser(c *testutil.Client, attrs ...Attrs) string {
return result.CreateUser.ProfileEdge.Node.ID return result.CreateUser.ProfileEdge.Node.ID
} }
func CreateVendor(c *testutil.Client, attrs ...Attrs) string { func CreateThirdParty(c *testutil.Client, attrs ...Attrs) string {
c.T.Helper() c.T.Helper()
var a Attrs var a Attrs
@@ -151,9 +151,9 @@ func CreateVendor(c *testutil.Client, attrs ...Attrs) string {
} }
const query = ` const query = `
mutation($input: CreateVendorInput!) { mutation($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { thirdPartyEdge {
node { id } node { id }
} }
} }
@@ -162,7 +162,7 @@ func CreateVendor(c *testutil.Client, attrs ...Attrs) string {
input := map[string]any{ input := map[string]any{
"organizationId": c.GetOrganizationID().String(), "organizationId": c.GetOrganizationID().String(),
"name": a.getString("name", SafeName("Vendor")), "name": a.getString("name", SafeName("ThirdParty")),
} }
if desc := a.getStringPtr("description"); desc != nil { if desc := a.getStringPtr("description"); desc != nil {
input["description"] = *desc input["description"] = *desc
@@ -175,19 +175,19 @@ func CreateVendor(c *testutil.Client, attrs ...Attrs) string {
} }
var result struct { var result struct {
CreateVendor struct { CreateThirdParty struct {
VendorEdge struct { ThirdPartyEdge struct {
Node struct { Node struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"node"` } `json:"node"`
} `json:"vendorEdge"` } `json:"thirdPartyEdge"`
} `json:"createVendor"` } `json:"createThirdParty"`
} }
err := c.Execute(query, map[string]any{"input": input}, &result) err := c.Execute(query, map[string]any{"input": input}, &result)
require.NoError(c.T, err, "createVendor mutation failed") require.NoError(c.T, err, "createThirdParty mutation failed")
return result.CreateVendor.VendorEdge.Node.ID return result.CreateThirdParty.ThirdPartyEdge.Node.ID
} }
func CreateFramework(c *testutil.Client, attrs ...Attrs) string { func CreateFramework(c *testutil.Client, attrs ...Attrs) string {
@@ -414,37 +414,37 @@ func CreateRisk(c *testutil.Client, attrs ...Attrs) string {
return result.CreateRisk.RiskEdge.Node.ID return result.CreateRisk.RiskEdge.Node.ID
} }
type VendorBuilder struct { type ThirdPartyBuilder struct {
client *testutil.Client client *testutil.Client
attrs Attrs attrs Attrs
} }
func NewVendor(c *testutil.Client) *VendorBuilder { func NewThirdParty(c *testutil.Client) *ThirdPartyBuilder {
return &VendorBuilder{client: c, attrs: Attrs{}} return &ThirdPartyBuilder{client: c, attrs: Attrs{}}
} }
func (b *VendorBuilder) WithName(name string) *VendorBuilder { func (b *ThirdPartyBuilder) WithName(name string) *ThirdPartyBuilder {
b.attrs["name"] = name b.attrs["name"] = name
return b return b
} }
func (b *VendorBuilder) WithDescription(desc string) *VendorBuilder { func (b *ThirdPartyBuilder) WithDescription(desc string) *ThirdPartyBuilder {
b.attrs["description"] = desc b.attrs["description"] = desc
return b return b
} }
func (b *VendorBuilder) WithWebsiteUrl(url string) *VendorBuilder { func (b *ThirdPartyBuilder) WithWebsiteUrl(url string) *ThirdPartyBuilder {
b.attrs["websiteUrl"] = url b.attrs["websiteUrl"] = url
return b return b
} }
func (b *VendorBuilder) WithCategory(category string) *VendorBuilder { func (b *ThirdPartyBuilder) WithCategory(category string) *ThirdPartyBuilder {
b.attrs["category"] = category b.attrs["category"] = category
return b return b
} }
func (b *VendorBuilder) Create() string { func (b *ThirdPartyBuilder) Create() string {
return CreateVendor(b.client, b.attrs) return CreateThirdParty(b.client, b.attrs)
} }
type FrameworkBuilder struct { type FrameworkBuilder struct {

View File

@@ -95,7 +95,7 @@ func TestMCP_AuditLog(t *testing.T) {
orgID := owner.GetOrganizationID().String() orgID := owner.GetOrganizationID().String()
// Creating something generates audit log entries // Creating something generates audit log entries
factory.CreateVendor(owner) factory.CreateThirdParty(owner)
var listResult struct { var listResult struct {
AuditLogEntries []struct { AuditLogEntries []struct {

View File

@@ -0,0 +1,142 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package mcp_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/e2e/internal/factory"
"go.probo.inc/probo/e2e/internal/testutil"
)
type thirdPartyContact struct {
ID string `json:"id"`
Name string `json:"name"`
Email *string `json:"email"`
Phone *string `json:"phone"`
Role *string `json:"role"`
}
func TestMCP_AddThirdPartyContact(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
thirdPartyID := factory.CreateThirdParty(owner)
var result struct {
ThirdPartyContact thirdPartyContact `json:"thirdPartyContact"`
}
mc.CallToolInto("addThirdPartyContact", map[string]any{
"thirdPartyId": thirdPartyID,
"name": "Alice Smith",
"email": "alice@example.com",
"phone": "+1-555-0100",
"role": "Account Manager",
}, &result)
assert.NotEmpty(t, result.ThirdPartyContact.ID)
assert.Equal(t, "Alice Smith", result.ThirdPartyContact.Name)
}
func TestMCP_UpdateThirdPartyContact(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
thirdPartyID := factory.CreateThirdParty(owner)
// Create
var addResult struct {
ThirdPartyContact thirdPartyContact `json:"thirdPartyContact"`
}
mc.CallToolInto("addThirdPartyContact", map[string]any{
"thirdPartyId": thirdPartyID,
"name": "Bob Jones",
"email": "bob@example.com",
}, &addResult)
require.NotEmpty(t, addResult.ThirdPartyContact.ID)
// Update
var updateResult struct {
ThirdPartyContact thirdPartyContact `json:"thirdPartyContact"`
}
mc.CallToolInto("updateThirdPartyContact", map[string]any{
"id": addResult.ThirdPartyContact.ID,
"name": "Robert Jones",
"role": "CTO",
}, &updateResult)
assert.Equal(t, addResult.ThirdPartyContact.ID, updateResult.ThirdPartyContact.ID)
assert.Equal(t, "Robert Jones", updateResult.ThirdPartyContact.Name)
}
func TestMCP_DeleteThirdPartyContact(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
thirdPartyID := factory.CreateThirdParty(owner)
// Create
var addResult struct {
ThirdPartyContact thirdPartyContact `json:"thirdPartyContact"`
}
mc.CallToolInto("addThirdPartyContact", map[string]any{
"thirdPartyId": thirdPartyID,
"name": "Contact to delete",
}, &addResult)
require.NotEmpty(t, addResult.ThirdPartyContact.ID)
// Delete
var deleteResult struct {
DeletedThirdPartyContactID string `json:"deletedThirdPartyContactId"`
}
mc.CallToolInto("deleteThirdPartyContact", map[string]any{
"id": addResult.ThirdPartyContact.ID,
}, &deleteResult)
assert.Equal(t, addResult.ThirdPartyContact.ID, deleteResult.DeletedThirdPartyContactID)
}
func TestMCP_ListThirdPartyContacts(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
thirdPartyID := factory.CreateThirdParty(owner)
// Create contacts
for i := range 3 {
var result struct {
ThirdPartyContact thirdPartyContact `json:"thirdPartyContact"`
}
mc.CallToolInto("addThirdPartyContact", map[string]any{
"thirdPartyId": thirdPartyID,
"name": factory.SafeName("Contact"),
"email": factory.SafeEmail(),
}, &result)
require.NotEmpty(t, result.ThirdPartyContact.ID)
_ = i
}
// List
var listResult struct {
ThirdPartyContacts []thirdPartyContact `json:"thirdPartyContacts"`
}
mc.CallToolInto("listThirdPartyContacts", map[string]any{
"thirdPartyId": thirdPartyID,
}, &listResult)
assert.GreaterOrEqual(t, len(listResult.ThirdPartyContacts), 3)
}

View File

@@ -0,0 +1,135 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package mcp_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/e2e/internal/factory"
"go.probo.inc/probo/e2e/internal/testutil"
)
type thirdPartyService struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
}
func TestMCP_AddThirdPartyService(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
thirdPartyID := factory.CreateThirdParty(owner)
var result struct {
ThirdPartyService thirdPartyService `json:"thirdPartyService"`
}
mc.CallToolInto("addThirdPartyService", map[string]any{
"thirdPartyId": thirdPartyID,
"name": "Cloud Storage",
"description": "Object storage service",
}, &result)
assert.NotEmpty(t, result.ThirdPartyService.ID)
assert.Equal(t, "Cloud Storage", result.ThirdPartyService.Name)
}
func TestMCP_UpdateThirdPartyService(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
thirdPartyID := factory.CreateThirdParty(owner)
// Create
var addResult struct {
ThirdPartyService thirdPartyService `json:"thirdPartyService"`
}
mc.CallToolInto("addThirdPartyService", map[string]any{
"thirdPartyId": thirdPartyID,
"name": "Original Service",
}, &addResult)
require.NotEmpty(t, addResult.ThirdPartyService.ID)
// Update
var updateResult struct {
ThirdPartyService thirdPartyService `json:"thirdPartyService"`
}
mc.CallToolInto("updateThirdPartyService", map[string]any{
"id": addResult.ThirdPartyService.ID,
"name": "Updated Service",
}, &updateResult)
assert.Equal(t, addResult.ThirdPartyService.ID, updateResult.ThirdPartyService.ID)
assert.Equal(t, "Updated Service", updateResult.ThirdPartyService.Name)
}
func TestMCP_DeleteThirdPartyService(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
thirdPartyID := factory.CreateThirdParty(owner)
// Create
var addResult struct {
ThirdPartyService thirdPartyService `json:"thirdPartyService"`
}
mc.CallToolInto("addThirdPartyService", map[string]any{
"thirdPartyId": thirdPartyID,
"name": "Service to delete",
}, &addResult)
require.NotEmpty(t, addResult.ThirdPartyService.ID)
// Delete
var deleteResult struct {
DeletedThirdPartyServiceID string `json:"deletedThirdPartyServiceId"`
}
mc.CallToolInto("deleteThirdPartyService", map[string]any{
"id": addResult.ThirdPartyService.ID,
}, &deleteResult)
assert.Equal(t, addResult.ThirdPartyService.ID, deleteResult.DeletedThirdPartyServiceID)
}
func TestMCP_ListThirdPartyServices(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
thirdPartyID := factory.CreateThirdParty(owner)
// Create services
for i := range 3 {
var result struct {
ThirdPartyService thirdPartyService `json:"thirdPartyService"`
}
mc.CallToolInto("addThirdPartyService", map[string]any{
"thirdPartyId": thirdPartyID,
"name": factory.SafeName("Service"),
}, &result)
require.NotEmpty(t, result.ThirdPartyService.ID)
_ = i
}
// List
var listResult struct {
ThirdPartyServices []thirdPartyService `json:"thirdPartyServices"`
}
mc.CallToolInto("listThirdPartyServices", map[string]any{
"thirdPartyId": thirdPartyID,
}, &listResult)
assert.GreaterOrEqual(t, len(listResult.ThirdPartyServices), 3)
}

View File

@@ -23,7 +23,7 @@ import (
"go.probo.inc/probo/e2e/internal/testutil" "go.probo.inc/probo/e2e/internal/testutil"
) )
func TestMCP_Vendor_CRUD(t *testing.T) { func TestMCP_ThirdParty_CRUD(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner) mc := testutil.NewMCPClient(t, owner)
@@ -31,67 +31,67 @@ func TestMCP_Vendor_CRUD(t *testing.T) {
// Create // Create
var addResult struct { var addResult struct {
Vendor struct { ThirdParty struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} `json:"vendor"` } `json:"third_party"`
} }
name := factory.SafeName("Vendor") name := factory.SafeName("ThirdParty")
mc.CallToolInto("addVendor", map[string]any{ mc.CallToolInto("addThirdParty", map[string]any{
"organizationId": orgID, "organizationId": orgID,
"name": name, "name": name,
}, &addResult) }, &addResult)
require.NotEmpty(t, addResult.Vendor.ID) require.NotEmpty(t, addResult.ThirdParty.ID)
assert.Equal(t, name, addResult.Vendor.Name) assert.Equal(t, name, addResult.ThirdParty.Name)
// Update // Update
var updateResult struct { var updateResult struct {
Vendor struct { ThirdParty struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} `json:"vendor"` } `json:"third_party"`
} }
mc.CallToolInto("updateVendor", map[string]any{ mc.CallToolInto("updateThirdParty", map[string]any{
"id": addResult.Vendor.ID, "id": addResult.ThirdParty.ID,
"name": "Updated Vendor", "name": "Updated ThirdParty",
}, &updateResult) }, &updateResult)
assert.Equal(t, "Updated Vendor", updateResult.Vendor.Name) assert.Equal(t, "Updated ThirdParty", updateResult.ThirdParty.Name)
// List // List
var listResult struct { var listResult struct {
Vendors []struct { ThirdParties []struct {
ID string `json:"id"` ID string `json:"id"`
} `json:"vendors"` } `json:"third_parties"`
} }
mc.CallToolInto("listVendors", map[string]any{ mc.CallToolInto("listThirdParties", map[string]any{
"organizationId": orgID, "organizationId": orgID,
}, &listResult) }, &listResult)
assert.NotEmpty(t, listResult.Vendors) assert.NotEmpty(t, listResult.ThirdParties)
// Delete // Delete
var deleteResult struct { var deleteResult struct {
DeletedVendorID string `json:"deletedVendorId"` DeletedThirdPartyID string `json:"deletedThirdPartyId"`
} }
mc.CallToolInto("deleteVendor", map[string]any{ mc.CallToolInto("deleteThirdParty", map[string]any{
"id": addResult.Vendor.ID, "id": addResult.ThirdParty.ID,
}, &deleteResult) }, &deleteResult)
assert.Equal(t, addResult.Vendor.ID, deleteResult.DeletedVendorID) assert.Equal(t, addResult.ThirdParty.ID, deleteResult.DeletedThirdPartyID)
// Update deleted vendor returns sanitized not-found error // Update deleted thirdParty returns sanitized not-found error
msg := mc.CallToolExpectToolError("updateVendor", map[string]any{ msg := mc.CallToolExpectToolError("updateThirdParty", map[string]any{
"id": addResult.Vendor.ID, "id": addResult.ThirdParty.ID,
"name": "Should Fail", "name": "Should Fail",
}) })
assert.Equal(t, "resource not found", msg) assert.Equal(t, "resource not found", msg)
} }
func TestMCP_Vendor_ValidationError(t *testing.T) { func TestMCP_ThirdParty_ValidationError(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner) mc := testutil.NewMCPClient(t, owner)
orgID := owner.GetOrganizationID().String() orgID := owner.GetOrganizationID().String()
msg := mc.CallToolExpectToolError("addVendor", map[string]any{ msg := mc.CallToolExpectToolError("addThirdParty", map[string]any{
"organizationId": orgID, "organizationId": orgID,
"name": "", "name": "",
}) })
@@ -100,16 +100,16 @@ func TestMCP_Vendor_ValidationError(t *testing.T) {
assert.NotContains(t, msg, "sql:") assert.NotContains(t, msg, "sql:")
} }
func TestMCP_Vendor_PermissionDenied(t *testing.T) { func TestMCP_ThirdParty_PermissionDenied(t *testing.T) {
t.Parallel() t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner) owner := testutil.NewClient(t, testutil.RoleOwner)
orgID := owner.GetOrganizationID().String() orgID := owner.GetOrganizationID().String()
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
viewerMC := testutil.NewMCPClient(t, viewer) viewerMC := testutil.NewMCPClient(t, viewer)
msg := viewerMC.CallToolExpectToolError("addVendor", map[string]any{ msg := viewerMC.CallToolExpectToolError("addThirdParty", map[string]any{
"organizationId": orgID, "organizationId": orgID,
"name": factory.SafeName("Vendor"), "name": factory.SafeName("ThirdParty"),
}) })
assert.Contains(t, msg, "permission denied") assert.Contains(t, msg, "permission denied")
assert.NotContains(t, msg, "pq:") assert.NotContains(t, msg, "pq:")

View File

@@ -1,142 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package mcp_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/e2e/internal/factory"
"go.probo.inc/probo/e2e/internal/testutil"
)
type vendorContact struct {
ID string `json:"id"`
Name string `json:"name"`
Email *string `json:"email"`
Phone *string `json:"phone"`
Role *string `json:"role"`
}
func TestMCP_AddVendorContact(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
vendorID := factory.CreateVendor(owner)
var result struct {
VendorContact vendorContact `json:"vendorContact"`
}
mc.CallToolInto("addVendorContact", map[string]any{
"vendorId": vendorID,
"name": "Alice Smith",
"email": "alice@example.com",
"phone": "+1-555-0100",
"role": "Account Manager",
}, &result)
assert.NotEmpty(t, result.VendorContact.ID)
assert.Equal(t, "Alice Smith", result.VendorContact.Name)
}
func TestMCP_UpdateVendorContact(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
vendorID := factory.CreateVendor(owner)
// Create
var addResult struct {
VendorContact vendorContact `json:"vendorContact"`
}
mc.CallToolInto("addVendorContact", map[string]any{
"vendorId": vendorID,
"name": "Bob Jones",
"email": "bob@example.com",
}, &addResult)
require.NotEmpty(t, addResult.VendorContact.ID)
// Update
var updateResult struct {
VendorContact vendorContact `json:"vendorContact"`
}
mc.CallToolInto("updateVendorContact", map[string]any{
"id": addResult.VendorContact.ID,
"name": "Robert Jones",
"role": "CTO",
}, &updateResult)
assert.Equal(t, addResult.VendorContact.ID, updateResult.VendorContact.ID)
assert.Equal(t, "Robert Jones", updateResult.VendorContact.Name)
}
func TestMCP_DeleteVendorContact(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
vendorID := factory.CreateVendor(owner)
// Create
var addResult struct {
VendorContact vendorContact `json:"vendorContact"`
}
mc.CallToolInto("addVendorContact", map[string]any{
"vendorId": vendorID,
"name": "Contact to delete",
}, &addResult)
require.NotEmpty(t, addResult.VendorContact.ID)
// Delete
var deleteResult struct {
DeletedVendorContactID string `json:"deletedVendorContactId"`
}
mc.CallToolInto("deleteVendorContact", map[string]any{
"id": addResult.VendorContact.ID,
}, &deleteResult)
assert.Equal(t, addResult.VendorContact.ID, deleteResult.DeletedVendorContactID)
}
func TestMCP_ListVendorContacts(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
vendorID := factory.CreateVendor(owner)
// Create contacts
for i := range 3 {
var result struct {
VendorContact vendorContact `json:"vendorContact"`
}
mc.CallToolInto("addVendorContact", map[string]any{
"vendorId": vendorID,
"name": factory.SafeName("Contact"),
"email": factory.SafeEmail(),
}, &result)
require.NotEmpty(t, result.VendorContact.ID)
_ = i
}
// List
var listResult struct {
VendorContacts []vendorContact `json:"vendorContacts"`
}
mc.CallToolInto("listVendorContacts", map[string]any{
"vendorId": vendorID,
}, &listResult)
assert.GreaterOrEqual(t, len(listResult.VendorContacts), 3)
}

View File

@@ -1,135 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package mcp_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/e2e/internal/factory"
"go.probo.inc/probo/e2e/internal/testutil"
)
type vendorService struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
}
func TestMCP_AddVendorService(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
vendorID := factory.CreateVendor(owner)
var result struct {
VendorService vendorService `json:"vendorService"`
}
mc.CallToolInto("addVendorService", map[string]any{
"vendorId": vendorID,
"name": "Cloud Storage",
"description": "Object storage service",
}, &result)
assert.NotEmpty(t, result.VendorService.ID)
assert.Equal(t, "Cloud Storage", result.VendorService.Name)
}
func TestMCP_UpdateVendorService(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
vendorID := factory.CreateVendor(owner)
// Create
var addResult struct {
VendorService vendorService `json:"vendorService"`
}
mc.CallToolInto("addVendorService", map[string]any{
"vendorId": vendorID,
"name": "Original Service",
}, &addResult)
require.NotEmpty(t, addResult.VendorService.ID)
// Update
var updateResult struct {
VendorService vendorService `json:"vendorService"`
}
mc.CallToolInto("updateVendorService", map[string]any{
"id": addResult.VendorService.ID,
"name": "Updated Service",
}, &updateResult)
assert.Equal(t, addResult.VendorService.ID, updateResult.VendorService.ID)
assert.Equal(t, "Updated Service", updateResult.VendorService.Name)
}
func TestMCP_DeleteVendorService(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
vendorID := factory.CreateVendor(owner)
// Create
var addResult struct {
VendorService vendorService `json:"vendorService"`
}
mc.CallToolInto("addVendorService", map[string]any{
"vendorId": vendorID,
"name": "Service to delete",
}, &addResult)
require.NotEmpty(t, addResult.VendorService.ID)
// Delete
var deleteResult struct {
DeletedVendorServiceID string `json:"deletedVendorServiceId"`
}
mc.CallToolInto("deleteVendorService", map[string]any{
"id": addResult.VendorService.ID,
}, &deleteResult)
assert.Equal(t, addResult.VendorService.ID, deleteResult.DeletedVendorServiceID)
}
func TestMCP_ListVendorServices(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
mc := testutil.NewMCPClient(t, owner)
vendorID := factory.CreateVendor(owner)
// Create services
for i := range 3 {
var result struct {
VendorService vendorService `json:"vendorService"`
}
mc.CallToolInto("addVendorService", map[string]any{
"vendorId": vendorID,
"name": factory.SafeName("Service"),
}, &result)
require.NotEmpty(t, result.VendorService.ID)
_ = i
}
// List
var listResult struct {
VendorServices []vendorService `json:"vendorServices"`
}
mc.CallToolInto("listVendorServices", map[string]any{
"vendorId": vendorID,
}, &listResult)
assert.GreaterOrEqual(t, len(listResult.VendorServices), 3)
}

20
package-lock.json generated
View File

@@ -35,8 +35,8 @@
"@probo/react-lazy": "1.0.0", "@probo/react-lazy": "1.0.0",
"@probo/relay": "^1.0.0", "@probo/relay": "^1.0.0",
"@probo/routes": "^1.0.0", "@probo/routes": "^1.0.0",
"@probo/third-parties": "0.0.1",
"@probo/ui": "1.0.0", "@probo/ui": "1.0.0",
"@probo/vendors": "0.0.1",
"@tanstack/react-query": "^5.76.1", "@tanstack/react-query": "^5.76.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"react": "^19.1.0", "react": "^19.1.0",
@@ -3246,6 +3246,10 @@
"resolved": "packages/routes", "resolved": "packages/routes",
"link": true "link": true
}, },
"node_modules/@probo/third-parties": {
"resolved": "packages/third-parties",
"link": true
},
"node_modules/@probo/trust": { "node_modules/@probo/trust": {
"resolved": "apps/trust", "resolved": "apps/trust",
"link": true "link": true
@@ -3258,10 +3262,6 @@
"resolved": "packages/ui", "resolved": "packages/ui",
"link": true "link": true
}, },
"node_modules/@probo/vendors": {
"resolved": "packages/vendors",
"link": true
},
"node_modules/@radix-ui/number": { "node_modules/@radix-ui/number": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -19633,6 +19633,11 @@
"relay-runtime": "^20.1.1" "relay-runtime": "^20.1.1"
} }
}, },
"packages/third-parties": {
"name": "@probo/third-parties",
"version": "0.0.1",
"license": "CC BY-SA 4.0"
},
"packages/tsconfig": { "packages/tsconfig": {
"name": "@probo/tsconfig", "name": "@probo/tsconfig",
"version": "0.0.1", "version": "0.0.1",
@@ -20010,11 +20015,6 @@
"optional": true "optional": true
} }
} }
},
"packages/vendors": {
"name": "@probo/vendors",
"version": "0.0.1",
"license": "CC BY-SA 4.0"
} }
} }
} }

View File

@@ -196,6 +196,11 @@ export class Probo implements INodeType {
value: 'task', value: 'task',
description: 'Manage tasks', description: 'Manage tasks',
}, },
{
name: 'Third Party',
value: 'thirdParty',
description: 'Manage third parties',
},
{ {
name: 'TIA', name: 'TIA',
value: 'tia', value: 'tia',
@@ -216,11 +221,6 @@ export class Probo implements INodeType {
value: 'user', value: 'user',
description: 'Manage organization users (profiles)', description: 'Manage organization users (profiles)',
}, },
{
name: 'Vendor',
value: 'vendor',
description: 'Manage vendors',
},
{ {
name: 'Webhook', name: 'Webhook',
value: 'webhook', value: 'webhook',

View File

@@ -111,8 +111,8 @@ export const description: INodeProperties[] = [
required: true, required: true,
}, },
{ {
displayName: 'Vendor IDs', displayName: 'ThirdParty IDs',
name: 'vendorIds', name: 'thirdPartyIds',
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
@@ -121,7 +121,7 @@ export const description: INodeProperties[] = [
}, },
}, },
default: '', default: '',
description: 'Comma-separated list of vendor IDs', description: 'Comma-separated list of thirdParty IDs',
}, },
{ {
displayName: 'Options', displayName: 'Options',
@@ -144,11 +144,11 @@ export const description: INodeProperties[] = [
description: 'Whether to include owner details in the response', description: 'Whether to include owner details in the response',
}, },
{ {
displayName: 'Include Vendors', displayName: 'Include ThirdParties',
name: 'includeVendors', name: 'includeThirdParties',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether to include vendors in the response', description: 'Whether to include thirdParties in the response',
}, },
], ],
}, },
@@ -164,10 +164,10 @@ export async function execute(
const ownerId = this.getNodeParameter('ownerId', itemIndex) as string; const ownerId = this.getNodeParameter('ownerId', itemIndex) as string;
const assetType = this.getNodeParameter('assetType', itemIndex) as string; const assetType = this.getNodeParameter('assetType', itemIndex) as string;
const dataTypesStored = this.getNodeParameter('dataTypesStored', itemIndex) as string; const dataTypesStored = this.getNodeParameter('dataTypesStored', itemIndex) as string;
const vendorIdsStr = this.getNodeParameter('vendorIds', itemIndex, '') as string; const thirdPartyIdsStr = this.getNodeParameter('thirdPartyIds', itemIndex, '') as string;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean; includeOwner?: boolean;
includeVendors?: boolean; includeThirdParties?: boolean;
}; };
const ownerFragment = options.includeOwner const ownerFragment = options.includeOwner
@@ -178,8 +178,8 @@ export async function execute(
}` }`
: ''; : '';
const vendorsFragment = options.includeVendors const thirdPartiesFragment = options.includeThirdParties
? `vendors(first: 100) { ? `thirdParties(first: 100) {
edges { edges {
node { node {
id id
@@ -200,7 +200,7 @@ export async function execute(
assetType assetType
dataTypesStored dataTypesStored
${ownerFragment} ${ownerFragment}
${vendorsFragment} ${thirdPartiesFragment}
createdAt createdAt
updatedAt updatedAt
} }
@@ -209,7 +209,7 @@ export async function execute(
} }
`; `;
const vendorIds = vendorIdsStr ? vendorIdsStr.split(',').map((id) => id.trim()).filter(Boolean) : undefined; const thirdPartyIds = thirdPartyIdsStr ? thirdPartyIdsStr.split(',').map((id) => id.trim()).filter(Boolean) : undefined;
const variables = { const variables = {
input: { input: {
@@ -219,7 +219,7 @@ export async function execute(
ownerId, ownerId,
assetType, assetType,
dataTypesStored, dataTypesStored,
...(vendorIds && vendorIds.length > 0 && { vendorIds }), ...(thirdPartyIds && thirdPartyIds.length > 0 && { thirdPartyIds }),
}, },
}; };

View File

@@ -51,11 +51,11 @@ export const description: INodeProperties[] = [
description: 'Whether to include owner details in the response', description: 'Whether to include owner details in the response',
}, },
{ {
displayName: 'Include Vendors', displayName: 'Include ThirdParties',
name: 'includeVendors', name: 'includeThirdParties',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether to include vendors in the response', description: 'Whether to include thirdParties in the response',
}, },
], ],
}, },
@@ -68,7 +68,7 @@ export async function execute(
const assetId = this.getNodeParameter('assetId', itemIndex) as string; const assetId = this.getNodeParameter('assetId', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean; includeOwner?: boolean;
includeVendors?: boolean; includeThirdParties?: boolean;
}; };
const ownerFragment = options.includeOwner const ownerFragment = options.includeOwner
@@ -79,8 +79,8 @@ export async function execute(
}` }`
: ''; : '';
const vendorsFragment = options.includeVendors const thirdPartiesFragment = options.includeThirdParties
? `vendors(first: 100) { ? `thirdParties(first: 100) {
edges { edges {
node { node {
id id
@@ -100,7 +100,7 @@ export async function execute(
assetType assetType
dataTypesStored dataTypesStored
${ownerFragment} ${ownerFragment}
${vendorsFragment} ${thirdPartiesFragment}
createdAt createdAt
updatedAt updatedAt
} }

View File

@@ -81,11 +81,11 @@ export const description: INodeProperties[] = [
description: 'Whether to include owner details in the response', description: 'Whether to include owner details in the response',
}, },
{ {
displayName: 'Include Vendors', displayName: 'Include ThirdParties',
name: 'includeVendors', name: 'includeThirdParties',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether to include vendors in the response', description: 'Whether to include thirdParties in the response',
}, },
], ],
}, },
@@ -100,7 +100,7 @@ export async function execute(
const limit = this.getNodeParameter('limit', itemIndex, 50) as number; const limit = this.getNodeParameter('limit', itemIndex, 50) as number;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean; includeOwner?: boolean;
includeVendors?: boolean; includeThirdParties?: boolean;
}; };
const ownerFragment = options.includeOwner const ownerFragment = options.includeOwner
@@ -111,8 +111,8 @@ export async function execute(
}` }`
: ''; : '';
const vendorsFragment = options.includeVendors const thirdPartiesFragment = options.includeThirdParties
? `vendors(first: 100) { ? `thirdParties(first: 100) {
edges { edges {
node { node {
id id
@@ -135,7 +135,7 @@ export async function execute(
assetType assetType
dataTypesStored dataTypesStored
${ownerFragment} ${ownerFragment}
${vendorsFragment} ${thirdPartiesFragment}
createdAt createdAt
updatedAt updatedAt
} }

View File

@@ -106,8 +106,8 @@ export const description: INodeProperties[] = [
description: 'The types of data stored in the asset', description: 'The types of data stored in the asset',
}, },
{ {
displayName: 'Vendor IDs', displayName: 'ThirdParty IDs',
name: 'vendorIds', name: 'thirdPartyIds',
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
@@ -116,7 +116,7 @@ export const description: INodeProperties[] = [
}, },
}, },
default: '', default: '',
description: 'Comma-separated list of vendor IDs', description: 'Comma-separated list of thirdParty IDs',
}, },
{ {
displayName: 'Options', displayName: 'Options',
@@ -139,11 +139,11 @@ export const description: INodeProperties[] = [
description: 'Whether to include owner details in the response', description: 'Whether to include owner details in the response',
}, },
{ {
displayName: 'Include Vendors', displayName: 'Include ThirdParties',
name: 'includeVendors', name: 'includeThirdParties',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether to include vendors in the response', description: 'Whether to include thirdParties in the response',
}, },
], ],
}, },
@@ -159,10 +159,10 @@ export async function execute(
const ownerId = this.getNodeParameter('ownerId', itemIndex, '') as string; const ownerId = this.getNodeParameter('ownerId', itemIndex, '') as string;
const assetType = this.getNodeParameter('assetType', itemIndex, '') as string; const assetType = this.getNodeParameter('assetType', itemIndex, '') as string;
const dataTypesStored = this.getNodeParameter('dataTypesStored', itemIndex, '') as string; const dataTypesStored = this.getNodeParameter('dataTypesStored', itemIndex, '') as string;
const vendorIdsStr = this.getNodeParameter('vendorIds', itemIndex, '') as string; const thirdPartyIdsStr = this.getNodeParameter('thirdPartyIds', itemIndex, '') as string;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean; includeOwner?: boolean;
includeVendors?: boolean; includeThirdParties?: boolean;
}; };
const ownerFragment = options.includeOwner const ownerFragment = options.includeOwner
@@ -173,8 +173,8 @@ export async function execute(
}` }`
: ''; : '';
const vendorsFragment = options.includeVendors const thirdPartiesFragment = options.includeThirdParties
? `vendors(first: 100) { ? `thirdParties(first: 100) {
edges { edges {
node { node {
id id
@@ -194,7 +194,7 @@ export async function execute(
assetType assetType
dataTypesStored dataTypesStored
${ownerFragment} ${ownerFragment}
${vendorsFragment} ${thirdPartiesFragment}
createdAt createdAt
updatedAt updatedAt
} }
@@ -208,9 +208,9 @@ export async function execute(
if (ownerId) input.ownerId = ownerId; if (ownerId) input.ownerId = ownerId;
if (assetType) input.assetType = assetType; if (assetType) input.assetType = assetType;
if (dataTypesStored) input.dataTypesStored = dataTypesStored; if (dataTypesStored) input.dataTypesStored = dataTypesStored;
if (vendorIdsStr) { if (thirdPartyIdsStr) {
const vendorIds = vendorIdsStr.split(',').map((vid) => vid.trim()).filter(Boolean); const thirdPartyIds = thirdPartyIdsStr.split(',').map((vid) => vid.trim()).filter(Boolean);
if (vendorIds.length > 0) input.vendorIds = vendorIds; if (thirdPartyIds.length > 0) input.thirdPartyIds = thirdPartyIds;
} }
const responseData = await proboApiRequest.call(this, query, { input }); const responseData = await proboApiRequest.call(this, query, { input });

View File

@@ -91,8 +91,8 @@ export const description: INodeProperties[] = [
required: true, required: true,
}, },
{ {
displayName: 'Vendor IDs', displayName: 'ThirdParty IDs',
name: 'vendorIds', name: 'thirdPartyIds',
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
@@ -101,7 +101,7 @@ export const description: INodeProperties[] = [
}, },
}, },
default: '', default: '',
description: 'Comma-separated list of vendor IDs', description: 'Comma-separated list of thirdParty IDs',
}, },
{ {
displayName: 'Options', displayName: 'Options',
@@ -124,11 +124,11 @@ export const description: INodeProperties[] = [
description: 'Whether to include owner details in the response', description: 'Whether to include owner details in the response',
}, },
{ {
displayName: 'Include Vendors', displayName: 'Include ThirdParties',
name: 'includeVendors', name: 'includeThirdParties',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether to include vendors in the response', description: 'Whether to include thirdParties in the response',
}, },
], ],
}, },
@@ -142,10 +142,10 @@ export async function execute(
const name = this.getNodeParameter('name', itemIndex) as string; const name = this.getNodeParameter('name', itemIndex) as string;
const dataClassification = this.getNodeParameter('dataClassification', itemIndex) as string; const dataClassification = this.getNodeParameter('dataClassification', itemIndex) as string;
const ownerId = this.getNodeParameter('ownerId', itemIndex) as string; const ownerId = this.getNodeParameter('ownerId', itemIndex) as string;
const vendorIdsStr = this.getNodeParameter('vendorIds', itemIndex, '') as string; const thirdPartyIdsStr = this.getNodeParameter('thirdPartyIds', itemIndex, '') as string;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean; includeOwner?: boolean;
includeVendors?: boolean; includeThirdParties?: boolean;
}; };
const ownerFragment = options.includeOwner const ownerFragment = options.includeOwner
@@ -156,8 +156,8 @@ export async function execute(
}` }`
: ''; : '';
const vendorsFragment = options.includeVendors const thirdPartiesFragment = options.includeThirdParties
? `vendors(first: 100) { ? `thirdParties(first: 100) {
edges { edges {
node { node {
id id
@@ -176,7 +176,7 @@ export async function execute(
name name
dataClassification dataClassification
${ownerFragment} ${ownerFragment}
${vendorsFragment} ${thirdPartiesFragment}
createdAt createdAt
updatedAt updatedAt
} }
@@ -185,7 +185,7 @@ export async function execute(
} }
`; `;
const vendorIds = vendorIdsStr ? vendorIdsStr.split(',').map((id) => id.trim()).filter(Boolean) : undefined; const thirdPartyIds = thirdPartyIdsStr ? thirdPartyIdsStr.split(',').map((id) => id.trim()).filter(Boolean) : undefined;
const variables = { const variables = {
input: { input: {
@@ -193,7 +193,7 @@ export async function execute(
name, name,
dataClassification, dataClassification,
ownerId, ownerId,
...(vendorIds && vendorIds.length > 0 && { vendorIds }), ...(thirdPartyIds && thirdPartyIds.length > 0 && { thirdPartyIds }),
}, },
}; };

View File

@@ -51,11 +51,11 @@ export const description: INodeProperties[] = [
description: 'Whether to include owner details in the response', description: 'Whether to include owner details in the response',
}, },
{ {
displayName: 'Include Vendors', displayName: 'Include ThirdParties',
name: 'includeVendors', name: 'includeThirdParties',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether to include vendors in the response', description: 'Whether to include thirdParties in the response',
}, },
], ],
}, },
@@ -68,7 +68,7 @@ export async function execute(
const datumId = this.getNodeParameter('datumId', itemIndex) as string; const datumId = this.getNodeParameter('datumId', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean; includeOwner?: boolean;
includeVendors?: boolean; includeThirdParties?: boolean;
}; };
const ownerFragment = options.includeOwner const ownerFragment = options.includeOwner
@@ -79,8 +79,8 @@ export async function execute(
}` }`
: ''; : '';
const vendorsFragment = options.includeVendors const thirdPartiesFragment = options.includeThirdParties
? `vendors(first: 100) { ? `thirdParties(first: 100) {
edges { edges {
node { node {
id id
@@ -98,7 +98,7 @@ export async function execute(
name name
dataClassification dataClassification
${ownerFragment} ${ownerFragment}
${vendorsFragment} ${thirdPartiesFragment}
createdAt createdAt
updatedAt updatedAt
} }

View File

@@ -81,11 +81,11 @@ export const description: INodeProperties[] = [
description: 'Whether to include owner details in the response', description: 'Whether to include owner details in the response',
}, },
{ {
displayName: 'Include Vendors', displayName: 'Include ThirdParties',
name: 'includeVendors', name: 'includeThirdParties',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether to include vendors in the response', description: 'Whether to include thirdParties in the response',
}, },
], ],
}, },
@@ -100,7 +100,7 @@ export async function execute(
const limit = this.getNodeParameter('limit', itemIndex, 50) as number; const limit = this.getNodeParameter('limit', itemIndex, 50) as number;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean; includeOwner?: boolean;
includeVendors?: boolean; includeThirdParties?: boolean;
}; };
const ownerFragment = options.includeOwner const ownerFragment = options.includeOwner
@@ -111,8 +111,8 @@ export async function execute(
}` }`
: ''; : '';
const vendorsFragment = options.includeVendors const thirdPartiesFragment = options.includeThirdParties
? `vendors(first: 100) { ? `thirdParties(first: 100) {
edges { edges {
node { node {
id id
@@ -133,7 +133,7 @@ export async function execute(
name name
dataClassification dataClassification
${ownerFragment} ${ownerFragment}
${vendorsFragment} ${thirdPartiesFragment}
createdAt createdAt
updatedAt updatedAt
} }

View File

@@ -88,8 +88,8 @@ export const description: INodeProperties[] = [
description: 'The ID of the owner (People)', description: 'The ID of the owner (People)',
}, },
{ {
displayName: 'Vendor IDs', displayName: 'ThirdParty IDs',
name: 'vendorIds', name: 'thirdPartyIds',
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
@@ -98,7 +98,7 @@ export const description: INodeProperties[] = [
}, },
}, },
default: '', default: '',
description: 'Comma-separated list of vendor IDs', description: 'Comma-separated list of thirdParty IDs',
}, },
{ {
displayName: 'Options', displayName: 'Options',
@@ -121,11 +121,11 @@ export const description: INodeProperties[] = [
description: 'Whether to include owner details in the response', description: 'Whether to include owner details in the response',
}, },
{ {
displayName: 'Include Vendors', displayName: 'Include ThirdParties',
name: 'includeVendors', name: 'includeThirdParties',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether to include vendors in the response', description: 'Whether to include thirdParties in the response',
}, },
], ],
}, },
@@ -139,10 +139,10 @@ export async function execute(
const name = this.getNodeParameter('name', itemIndex, '') as string; const name = this.getNodeParameter('name', itemIndex, '') as string;
const dataClassification = this.getNodeParameter('dataClassification', itemIndex, '') as string; const dataClassification = this.getNodeParameter('dataClassification', itemIndex, '') as string;
const ownerId = this.getNodeParameter('ownerId', itemIndex, '') as string; const ownerId = this.getNodeParameter('ownerId', itemIndex, '') as string;
const vendorIdsStr = this.getNodeParameter('vendorIds', itemIndex, '') as string; const thirdPartyIdsStr = this.getNodeParameter('thirdPartyIds', itemIndex, '') as string;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
includeOwner?: boolean; includeOwner?: boolean;
includeVendors?: boolean; includeThirdParties?: boolean;
}; };
const ownerFragment = options.includeOwner const ownerFragment = options.includeOwner
@@ -153,8 +153,8 @@ export async function execute(
}` }`
: ''; : '';
const vendorsFragment = options.includeVendors const thirdPartiesFragment = options.includeThirdParties
? `vendors(first: 100) { ? `thirdParties(first: 100) {
edges { edges {
node { node {
id id
@@ -172,7 +172,7 @@ export async function execute(
name name
dataClassification dataClassification
${ownerFragment} ${ownerFragment}
${vendorsFragment} ${thirdPartiesFragment}
createdAt createdAt
updatedAt updatedAt
} }
@@ -184,9 +184,9 @@ export async function execute(
if (name) input.name = name; if (name) input.name = name;
if (dataClassification) input.dataClassification = dataClassification; if (dataClassification) input.dataClassification = dataClassification;
if (ownerId) input.ownerId = ownerId; if (ownerId) input.ownerId = ownerId;
if (vendorIdsStr) { if (thirdPartyIdsStr) {
const vendorIds = vendorIdsStr.split(',').map((vid) => vid.trim()).filter(Boolean); const thirdPartyIds = thirdPartyIdsStr.split(',').map((vid) => vid.trim()).filter(Boolean);
if (vendorIds.length > 0) input.vendorIds = vendorIds; if (thirdPartyIds.length > 0) input.thirdPartyIds = thirdPartyIds;
} }
const responseData = await proboApiRequest.call(this, query, { input }); const responseData = await proboApiRequest.call(this, query, { input });

View File

@@ -41,7 +41,7 @@ import * as statementOfApplicability from './statementOfApplicability';
import * as task from './task'; import * as task from './task';
import * as tia from './tia'; import * as tia from './tia';
import * as trustCenter from './trustCenter'; import * as trustCenter from './trustCenter';
import * as vendor from './vendor'; import * as thirdParty from './thirdParty';
import * as webhook from './webhook'; import * as webhook from './webhook';
export interface ResourceModule { export interface ResourceModule {
@@ -83,7 +83,7 @@ export const resources: Record<string, ResourceModule> = {
task: task as ResourceModule, task: task as ResourceModule,
tia: tia as ResourceModule, tia: tia as ResourceModule,
trustCenter: trustCenter as ResourceModule, trustCenter: trustCenter as ResourceModule,
vendor: vendor as ResourceModule, thirdParty: thirdParty as ResourceModule,
webhook: webhook as ResourceModule, webhook: webhook as ResourceModule,
}; };

View File

@@ -22,7 +22,7 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
@@ -36,12 +36,12 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
default: '', default: '',
description: 'The name of the vendor', description: 'The name of the thirdParty',
required: true, required: true,
}, },
{ {
@@ -53,12 +53,12 @@ export const description: INodeProperties[] = [
}, },
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
default: '', default: '',
description: 'The description of the vendor', description: 'The description of the thirdParty',
}, },
{ {
displayName: 'Category', displayName: 'Category',
@@ -66,12 +66,12 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
default: '', default: '',
description: 'The category of the vendor', description: 'The category of the thirdParty',
}, },
{ {
displayName: 'Website URL', displayName: 'Website URL',
@@ -79,12 +79,12 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
default: '', default: '',
description: 'The website URL of the vendor', description: 'The website URL of the thirdParty',
}, },
{ {
displayName: 'Legal Name', displayName: 'Legal Name',
@@ -92,12 +92,12 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
default: '', default: '',
description: 'The legal name of the vendor', description: 'The legal name of the thirdParty',
}, },
{ {
displayName: 'Headquarter Address', displayName: 'Headquarter Address',
@@ -105,12 +105,12 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
default: '', default: '',
description: 'The headquarter address of the vendor', description: 'The headquarter address of the thirdParty',
}, },
{ {
displayName: 'Business Owner ID', displayName: 'Business Owner ID',
@@ -118,7 +118,7 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
@@ -131,7 +131,7 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
@@ -146,7 +146,7 @@ export const description: INodeProperties[] = [
default: {}, default: {},
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['create'], operation: ['create'],
}, },
}, },
@@ -200,7 +200,7 @@ export const description: INodeProperties[] = [
name: 'statusPageUrl', name: 'statusPageUrl',
type: 'string', type: 'string',
default: '', default: '',
description: 'The status page URL of the vendor', description: 'The status page URL of the thirdParty',
}, },
{ {
displayName: 'Subprocessors List URL', displayName: 'Subprocessors List URL',
@@ -252,9 +252,9 @@ export async function execute(
}; };
const query = ` const query = `
mutation CreateVendor($input: CreateVendorInput!) { mutation CreateThirdParty($input: CreateThirdPartyInput!) {
createVendor(input: $input) { createThirdParty(input: $input) {
vendorEdge { thirdPartyEdge {
node { node {
id id
name name

View File

@@ -17,17 +17,17 @@ import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [ export const description: INodeProperties[] = [
{ {
displayName: 'Vendor ID', displayName: 'ThirdParty ID',
name: 'vendorId', name: 'thirdPartyId',
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createContact'], operation: ['createContact'],
}, },
}, },
default: '', default: '',
description: 'The ID of the vendor', description: 'The ID of the thirdParty',
required: true, required: true,
}, },
{ {
@@ -36,7 +36,7 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createContact'], operation: ['createContact'],
}, },
}, },
@@ -50,7 +50,7 @@ export const description: INodeProperties[] = [
placeholder: 'name@email.com', placeholder: 'name@email.com',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createContact'], operation: ['createContact'],
}, },
}, },
@@ -63,7 +63,7 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createContact'], operation: ['createContact'],
}, },
}, },
@@ -76,7 +76,7 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createContact'], operation: ['createContact'],
}, },
}, },
@@ -89,16 +89,16 @@ export async function execute(
this: IExecuteFunctions, this: IExecuteFunctions,
itemIndex: number, itemIndex: number,
): Promise<INodeExecutionData> { ): Promise<INodeExecutionData> {
const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; const thirdPartyId = this.getNodeParameter('thirdPartyId', itemIndex) as string;
const fullName = this.getNodeParameter('fullName', itemIndex, '') as string; const fullName = this.getNodeParameter('fullName', itemIndex, '') as string;
const email = this.getNodeParameter('email', itemIndex, '') as string; const email = this.getNodeParameter('email', itemIndex, '') as string;
const phone = this.getNodeParameter('phone', itemIndex, '') as string; const phone = this.getNodeParameter('phone', itemIndex, '') as string;
const role = this.getNodeParameter('role', itemIndex, '') as string; const role = this.getNodeParameter('role', itemIndex, '') as string;
const query = ` const query = `
mutation CreateVendorContact($input: CreateVendorContactInput!) { mutation CreateThirdPartyContact($input: CreateThirdPartyContactInput!) {
createVendorContact(input: $input) { createThirdPartyContact(input: $input) {
vendorContactEdge { thirdPartyContactEdge {
node { node {
id id
fullName fullName
@@ -113,7 +113,7 @@ export async function execute(
} }
`; `;
const input: Record<string, unknown> = { vendorId }; const input: Record<string, unknown> = { thirdPartyId };
if (fullName) input.fullName = fullName; if (fullName) input.fullName = fullName;
if (email) input.email = email; if (email) input.email = email;
if (phone) input.phone = phone; if (phone) input.phone = phone;

View File

@@ -17,17 +17,17 @@ import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [ export const description: INodeProperties[] = [
{ {
displayName: 'Vendor ID', displayName: 'ThirdParty ID',
name: 'vendorId', name: 'thirdPartyId',
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createRiskAssessment'], operation: ['createRiskAssessment'],
}, },
}, },
default: '', default: '',
description: 'The ID of the vendor', description: 'The ID of the thirdParty',
required: true, required: true,
}, },
{ {
@@ -36,7 +36,7 @@ export const description: INodeProperties[] = [
type: 'dateTime', type: 'dateTime',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createRiskAssessment'], operation: ['createRiskAssessment'],
}, },
}, },
@@ -50,7 +50,7 @@ export const description: INodeProperties[] = [
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createRiskAssessment'], operation: ['createRiskAssessment'],
}, },
}, },
@@ -71,7 +71,7 @@ export const description: INodeProperties[] = [
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createRiskAssessment'], operation: ['createRiskAssessment'],
}, },
}, },
@@ -94,7 +94,7 @@ export const description: INodeProperties[] = [
}, },
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createRiskAssessment'], operation: ['createRiskAssessment'],
}, },
}, },
@@ -107,7 +107,7 @@ export async function execute(
this: IExecuteFunctions, this: IExecuteFunctions,
itemIndex: number, itemIndex: number,
): Promise<INodeExecutionData> { ): Promise<INodeExecutionData> {
const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; const thirdPartyId = this.getNodeParameter('thirdPartyId', itemIndex) as string;
const expiresAtRaw = this.getNodeParameter('expiresAt', itemIndex) as string; const expiresAtRaw = this.getNodeParameter('expiresAt', itemIndex) as string;
const dataSensitivity = this.getNodeParameter('dataSensitivity', itemIndex) as string; const dataSensitivity = this.getNodeParameter('dataSensitivity', itemIndex) as string;
const businessImpact = this.getNodeParameter('businessImpact', itemIndex) as string; const businessImpact = this.getNodeParameter('businessImpact', itemIndex) as string;
@@ -117,9 +117,9 @@ export async function execute(
const expiresAt = new Date(expiresAtRaw).toISOString(); const expiresAt = new Date(expiresAtRaw).toISOString();
const query = ` const query = `
mutation CreateVendorRiskAssessment($input: CreateVendorRiskAssessmentInput!) { mutation CreateThirdPartyRiskAssessment($input: CreateThirdPartyRiskAssessmentInput!) {
createVendorRiskAssessment(input: $input) { createThirdPartyRiskAssessment(input: $input) {
vendorRiskAssessmentEdge { thirdPartyRiskAssessmentEdge {
node { node {
id id
expiresAt expiresAt
@@ -135,7 +135,7 @@ export async function execute(
`; `;
const input: Record<string, unknown> = { const input: Record<string, unknown> = {
vendorId, thirdPartyId,
expiresAt, expiresAt,
dataSensitivity, dataSensitivity,
businessImpact, businessImpact,

View File

@@ -17,17 +17,17 @@ import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [ export const description: INodeProperties[] = [
{ {
displayName: 'Vendor ID', displayName: 'ThirdParty ID',
name: 'vendorId', name: 'thirdPartyId',
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createService'], operation: ['createService'],
}, },
}, },
default: '', default: '',
description: 'The ID of the vendor', description: 'The ID of the thirdParty',
required: true, required: true,
}, },
{ {
@@ -36,12 +36,12 @@ export const description: INodeProperties[] = [
type: 'string', type: 'string',
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createService'], operation: ['createService'],
}, },
}, },
default: '', default: '',
description: 'The name of the vendor service', description: 'The name of the thirdParty service',
required: true, required: true,
}, },
{ {
@@ -53,12 +53,12 @@ export const description: INodeProperties[] = [
}, },
displayOptions: { displayOptions: {
show: { show: {
resource: ['vendor'], resource: ['thirdParty'],
operation: ['createService'], operation: ['createService'],
}, },
}, },
default: '', default: '',
description: 'The description of the vendor service', description: 'The description of the thirdParty service',
}, },
]; ];
@@ -66,14 +66,14 @@ export async function execute(
this: IExecuteFunctions, this: IExecuteFunctions,
itemIndex: number, itemIndex: number,
): Promise<INodeExecutionData> { ): Promise<INodeExecutionData> {
const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; const thirdPartyId = this.getNodeParameter('thirdPartyId', itemIndex) as string;
const name = this.getNodeParameter('name', itemIndex) as string; const name = this.getNodeParameter('name', itemIndex) as string;
const description = this.getNodeParameter('description', itemIndex, '') as string; const description = this.getNodeParameter('description', itemIndex, '') as string;
const query = ` const query = `
mutation CreateVendorService($input: CreateVendorServiceInput!) { mutation CreateThirdPartyService($input: CreateThirdPartyServiceInput!) {
createVendorService(input: $input) { createThirdPartyService(input: $input) {
vendorServiceEdge { thirdPartyServiceEdge {
node { node {
id id
name name
@@ -87,7 +87,7 @@ export async function execute(
`; `;
const input: Record<string, unknown> = { const input: Record<string, unknown> = {
vendorId, thirdPartyId,
name, name,
}; };
if (description) input.description = description; if (description) input.description = description;

Some files were not shown because too many files have changed in this diff Show More