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

@@ -48,7 +48,7 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
import { EditableTable } from "../table/EditableTable";
import { PeopleCell } from "../table/PeopleCell";
import { VendorsCell } from "../table/VendorsCell";
import { ThirdPartiesCell } from "../table/ThirdPartiesCell";
type Props = {
connectionId: string;
@@ -65,7 +65,7 @@ const schema = z.object({
amount: z.coerce.number().min(1, "Amount is required"),
assetType: z.enum(["PHYSICAL", "VIRTUAL"]),
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"),
organizationId: z.string().trim().min(1, "Organization is required"),
});
@@ -75,7 +75,7 @@ const defaultValue = {
amount: 0,
assetType: "VIRTUAL",
ownerId: "",
vendorIds: [],
thirdPartyIds: [],
dataTypesStored: "",
organizationId: "",
} satisfies z.infer<typeof schema>;
@@ -99,7 +99,7 @@ export function AssetsTable(props: Props) {
__("Data Types stored"),
__("Amount"),
__("Owner"),
__("Vendors"),
__("Third parties"),
]}
schema={schema}
updateMutation={updateAssetMutation}
@@ -154,10 +154,10 @@ export function AssetsTable(props: Props) {
defaultValue={item?.owner}
organizationId={organizationId}
/>
<VendorsCell
name="vendorIds"
<ThirdPartiesCell
name="thirdPartyIds"
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>{__("Amount")}</Th>
<Th>{__("Owner")}</Th>
<Th>{__("Vendors")}</Th>
<Th>{__("Third parties")}</Th>
</Tr>
</Thead>
<Tbody>
@@ -65,7 +65,7 @@ export function ReadOnlyAssetsTable(props: Props) {
function AssetRow({ entry }: { entry: AssetEntry }) {
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const vendors = entry.vendors?.edges.map(edge => edge.node) ?? [];
const thirdParties = entry.thirdParties?.edges.map(edge => edge.node) ?? [];
return (
<Tr to={`/organizations/${organizationId}/assets/${entry.id}`}>
@@ -78,27 +78,27 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
<Td>{entry.amount}</Td>
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td>
<Td>
{vendors.length > 0
{thirdParties.length > 0
? (
<div className="flex flex-wrap gap-1">
{vendors.slice(0, 3).map(vendor => (
{thirdParties.slice(0, 3).map(thirdParty => (
<Badge
key={vendor.id}
key={thirdParty.id}
variant="neutral"
className="flex items-center gap-1"
>
<Avatar
name={vendor.name}
src={faviconUrl(vendor.websiteUrl)}
name={thirdParty.name}
src={faviconUrl(thirdParty.websiteUrl)}
size="s"
/>
<span className="text-xs">{vendor.name}</span>
<span className="text-xs">{thirdParty.name}</span>
</Badge>
))}
{vendors.length > 3 && (
{thirdParties.length > 3 && (
<Badge variant="neutral" className="text-xs">
+
{vendors.length - 3}
{thirdParties.length - 3}
</Badge>
)}
</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 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;
name: string;
websiteUrl: string | null | undefined;
@@ -32,13 +32,13 @@ type Props<T extends FieldValues = FieldValues> = {
name: string;
label?: string;
error?: string;
selectedVendors?: Vendor[];
selectedThirdParties?: ThirdParty[];
} & ComponentProps<typeof Field>;
export function VendorsMultiSelectField<T extends FieldValues = FieldValues>({
export function ThirdPartiesMultiSelectField<T extends FieldValues = FieldValues>({
organizationId,
control,
selectedVendors = [],
selectedThirdParties = [],
...props
}: Props<T>) {
return (
@@ -46,31 +46,31 @@ export function VendorsMultiSelectField<T extends FieldValues = FieldValues>({
<Suspense
fallback={<Select variant="editor" disabled placeholder="Loading..." />}
>
<VendorsMultiSelectWithQuery
<ThirdPartiesMultiSelectWithQuery
organizationId={organizationId}
control={control}
name={props.name}
disabled={props.disabled}
selectedVendors={selectedVendors}
selectedThirdParties={selectedThirdParties}
/>
</Suspense>
</Field>
);
}
function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedVendors">,
function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedThirdParties">,
) {
const { __ } = useTranslate();
const { name, organizationId, control, selectedVendors = [] } = props;
const vendors = useVendors(organizationId);
const { name, organizationId, control, selectedThirdParties = [] } = props;
const thirdParties = useThirdParties(organizationId);
const [isOpen, setIsOpen] = useState(false);
const allVendors = [...vendors];
const allThirdParties = [...thirdParties];
if (props.disabled) {
selectedVendors.forEach((selectedVendor) => {
if (!allVendors.find(v => v.id === selectedVendor.id)) {
allVendors.push(selectedVendor);
selectedThirdParties.forEach((selectedThirdParty) => {
if (!allThirdParties.find(v => v.id === selectedThirdParty.id)) {
allThirdParties.push(selectedThirdParty);
}
});
}
@@ -81,49 +81,49 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
control={control}
name={name as Path<T>}
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 availableVendors = allVendors.filter(v => !selectedVendorIds.includes(v.id));
const selectedThirdParties = allThirdParties.filter(v => selectedThirdPartyIds.includes(v.id));
const availableThirdParties = allThirdParties.filter(v => !selectedThirdPartyIds.includes(v.id));
const handleAddVendor = (vendorId: string) => {
const newValue = [...selectedVendorIds, vendorId];
const handleAddThirdParty = (thirdPartyId: string) => {
const newValue = [...selectedThirdPartyIds, thirdPartyId];
field.onChange(newValue);
setIsOpen(false);
};
const handleRemoveVendor = (vendorId: string) => {
const newValue = selectedVendorIds.filter((id: string) => id !== vendorId);
const handleRemoveThirdParty = (thirdPartyId: string) => {
const newValue = selectedThirdPartyIds.filter((id: string) => id !== thirdPartyId);
field.onChange(newValue);
};
return (
<div className="space-y-2">
{availableVendors.length > 0 && !props.disabled && (
{availableThirdParties.length > 0 && !props.disabled && (
<Select
disabled={props.disabled}
id={name}
variant="editor"
placeholder={__("Add vendors...")}
onValueChange={handleAddVendor}
key={`${selectedVendorIds.length}-${vendors.length}`}
placeholder={__("Add third parties...")}
onValueChange={handleAddThirdParty}
key={`${selectedThirdPartyIds.length}-${thirdParties.length}`}
className="w-full"
value=""
open={isOpen}
onOpenChange={setIsOpen}
>
{availableVendors.map(vendor => (
<Option key={vendor.id} value={vendor.id} className="flex gap-2">
{availableThirdParties.map(thirdParty => (
<Option key={thirdParty.id} value={thirdParty.id} className="flex gap-2">
<Avatar
name={vendor.name}
src={faviconUrl(vendor.websiteUrl)}
name={thirdParty.name}
src={faviconUrl(thirdParty.websiteUrl)}
size="s"
/>
<div className="flex flex-col">
<span>{vendor.name}</span>
{vendor.websiteUrl && (
<span>{thirdParty.name}</span>
{thirdParty.websiteUrl && (
<span className="text-xs text-txt-secondary">
{vendor.websiteUrl}
{thirdParty.websiteUrl}
</span>
)}
</div>
@@ -132,21 +132,21 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
</Select>
)}
{selectedVendors.length > 0 && (
{selectedThirdParties.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedVendors.map(vendor => (
<Badge key={vendor.id} variant="neutral" className="flex items-center gap-2">
{selectedThirdParties.map(thirdParty => (
<Badge key={thirdParty.id} variant="neutral" className="flex items-center gap-2">
<Avatar
name={vendor.name}
src={faviconUrl(vendor.websiteUrl)}
name={thirdParty.name}
src={faviconUrl(thirdParty.websiteUrl)}
size="s"
/>
<span>{vendor.name}</span>
<span>{thirdParty.name}</span>
{!props.disabled && (
<Button
variant="tertiary"
icon={IconCrossLargeX}
onClick={() => handleRemoveVendor(vendor.id)}
onClick={() => handleRemoveThirdParty(thirdParty.id)}
className="h-4 w-4 p-0 hover:bg-transparent"
/>
)}
@@ -155,9 +155,9 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
</div>
)}
{selectedVendors.length === 0 && availableVendors.length === 0 && (
{selectedThirdParties.length === 0 && availableThirdParties.length === 0 && (
<div className="text-sm text-txt-secondary py-2">
{__("No vendors available")}
{__("No third parties available")}
</div>
)}
</div>

View File

@@ -15,11 +15,11 @@
import { faviconUrl } from "@probo/helpers";
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 { vendorsSelectQuery } from "#/hooks/graph/VendorGraph";
import { thirdPartiesSelectQuery } from "#/hooks/graph/ThirdPartyGraph";
type Vendor = {
type ThirdParty = {
id: string;
name: string;
websiteUrl: string | null | undefined;
@@ -27,47 +27,47 @@ type Vendor = {
type Props = {
name: string;
defaultValue?: Vendor[];
defaultValue?: ThirdParty[];
organizationId: string;
};
const empty = [] as Vendor[];
const empty = [] as ThirdParty[];
export function VendorsCell(props: Props) {
export function ThirdPartiesCell(props: Props) {
return (
<GraphQLCell<VendorGraphSelectQuery, Vendor>
<GraphQLCell<ThirdPartyGraphSelectQuery, ThirdParty>
multiple
name={props.name}
query={vendorsSelectQuery}
query={thirdPartiesSelectQuery}
variables={{
organizationId: props.organizationId,
}}
items={data =>
data.organization?.vendors?.edges?.map(edge => edge.node) ?? []}
data.organization?.thirdParties?.edges?.map(edge => edge.node) ?? []}
itemRenderer={({ item, onRemove }) => (
<VendorBadge vendor={item} onRemove={onRemove} />
<ThirdPartyBadge thirdParty={item} onRemove={onRemove} />
)}
defaultValue={props.defaultValue ?? empty}
/>
);
}
function VendorBadge({
vendor,
function ThirdPartyBadge({
thirdParty,
onRemove,
}: {
vendor: Vendor;
onRemove?: (v: Vendor) => void;
thirdParty: ThirdParty;
onRemove?: (v: ThirdParty) => void;
}) {
return (
<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">
{vendor.name}
{thirdParty.name}
</span>
{onRemove && (
<button
onClick={() => onRemove(vendor)}
onClick={() => onRemove(thirdParty)}
className="size-4 hover:text-txt-primary cursor-pointer"
type="button"
>