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:
@@ -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) ?? []}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
>
|
||||
Reference in New Issue
Block a user