Add compliance-portal Subprocessors page

Replace the Subprocessors placeholder with the real trust-center page from
the Figma design: subprocessor cards grouped by their backend category,
each showing a favicon logo over a blurred backdrop, the name, the
description, and the hosting regions. Country codes render through
Intl.DisplayNames and the section labels/descriptions come from a new
page-scoped i18n namespace.

Migrate the route to the per-resource folder layout (pages/subprocessors/
with its own routes.ts, loader, page, skeleton, _components, _lib, and
_locales) and drop the old flat stub. Filtering is added separately.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-30 14:27:41 +02:00
parent 502fb08054
commit f1f4c93104
14 changed files with 620 additions and 18 deletions

View File

@@ -27,9 +27,6 @@
"documents": {
"title": "Documents"
},
"subprocessors": {
"title": "Subprocessors"
},
"updates": {
"title": "Updates",
"subscribe": "Subscribe to updates"

View File

@@ -27,9 +27,6 @@
"documents": {
"title": "Documents"
},
"subprocessors": {
"title": "Sous-traitants"
},
"updates": {
"title": "Mises à jour",
"subscribe": "S'abonner aux mises à jour"

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 { useTranslation } from "react-i18next";
import type { PreloadedQuery } from "react-relay";
import { graphql, usePreloadedQuery } from "react-relay";
import { PageHeader } from "#/components/PageHeader/PageHeader";
import { SubprocessorCategorySection } from "./_components/SubprocessorCategorySection";
import type { SubprocessorNode } from "./_components/SubprocessorCategorySection";
import { SubprocessorsEmpty } from "./_components/SubprocessorsEmpty";
import { groupByCategory } from "./_lib/groupByCategory";
import type { SubprocessorsPageQuery } from "./__generated__/SubprocessorsPageQuery.graphql";
export const subprocessorsPageQuery = graphql`
query SubprocessorsPageQuery {
currentTrustCenter @required(action: THROW) {
subprocessors(first: 250) {
totalCount
edges {
node {
id
category
...SubprocessorListItem_subprocessor
}
}
}
}
}
`;
interface SubprocessorsPageProps {
queryRef: PreloadedQuery<SubprocessorsPageQuery>;
}
export function SubprocessorsPage({ queryRef }: SubprocessorsPageProps) {
const { t } = useTranslation("subprocessors");
const data = usePreloadedQuery<SubprocessorsPageQuery>(subprocessorsPageQuery, queryRef);
const { subprocessors } = data.currentTrustCenter;
const nodes: SubprocessorNode[] = subprocessors.edges.map(edge => edge.node);
const groups = groupByCategory(nodes, category => t(`categories.${category}.label`));
return (
<>
<PageHeader title={t("title")} count={subprocessors.totalCount} />
<div className="flex w-full flex-col items-center px-8 py-8">
<div className="flex w-full max-w-5xl flex-col gap-8">
{groups.length === 0
? <SubprocessorsEmpty />
: groups.map(group => (
<SubprocessorCategorySection
key={group.category}
category={group.category}
subprocessors={group.nodes}
/>
))}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { SubprocessorsPage, subprocessorsPageQuery } from "./SubprocessorsPage";
import { SubprocessorsPageSkeleton } from "./SubprocessorsPageSkeleton";
import type { SubprocessorsPageQuery } from "./__generated__/SubprocessorsPageQuery.graphql";
export default function SubprocessorsPageLoader() {
const [queryRef, loadQuery] = useQueryLoader<SubprocessorsPageQuery>(subprocessorsPageQuery);
useEffect(() => {
loadQuery({});
}, [loadQuery]);
if (!queryRef) {
return <SubprocessorsPageSkeleton />;
}
return <SubprocessorsPage queryRef={queryRef} />;
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 { HeadingSkeleton } from "@probo/ui/src/v2/typography/HeadingSkeleton";
import { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
import { HeaderBand } from "#/components/HeaderBand/HeaderBand";
const CARD_PLACEHOLDERS = ["a", "b", "c", "d", "e", "f"];
export function SubprocessorsPageSkeleton() {
return (
<>
<HeaderBand>
<div className="flex w-full flex-col gap-2">
<HeadingSkeleton size={7} className="w-64" />
</div>
</HeaderBand>
<div className="flex w-full flex-col items-center px-8 py-8">
<div className="flex w-full max-w-5xl flex-col gap-4">
<TextSkeleton size={3} className="w-48" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{CARD_PLACEHOLDERS.map(placeholder => (
<div key={placeholder} className="h-56 animate-pulse rounded-5 bg-sand-3" />
))}
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 { Text } from "@probo/ui/src/v2/typography/Text";
import { useTranslation } from "react-i18next";
import type { SubprocessorListItem_subprocessor$key } from "./__generated__/SubprocessorListItem_subprocessor.graphql";
import { SubprocessorListItem } from "./SubprocessorListItem";
// A subprocessor edge node: the list-item fragment key plus the fields the page
// reads to group and key the list.
export type SubprocessorNode = SubprocessorListItem_subprocessor$key & {
readonly id: string;
readonly category: string;
};
interface SubprocessorCategorySectionProps {
category: string;
subprocessors: readonly SubprocessorNode[];
}
// One category group: a localized header (label + description) above a
// responsive grid of subprocessor cards.
export function SubprocessorCategorySection({ category, subprocessors }: SubprocessorCategorySectionProps) {
const { t } = useTranslation("subprocessors");
return (
<section className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<Text size={3} weight="medium" color="neutral" highContrast>
{t(`categories.${category}.label`)}
</Text>
<Text size={2} color="neutral">
{t(`categories.${category}.description`)}
</Text>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{subprocessors.map(subprocessor => (
<SubprocessorListItem key={subprocessor.id} subprocessorKey={subprocessor} />
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 { BuildingsIcon, MapPinSimpleIcon } from "@phosphor-icons/react";
import { faviconUrl } from "@probo/helpers";
import { Card } from "@probo/ui/src/v2/Card/Card";
import { Text } from "@probo/ui/src/v2/typography/Text";
import { graphql, useFragment } from "react-relay";
import { useCountryLabel } from "../_lib/useCountryLabel";
import type { SubprocessorListItem_subprocessor$key } from "./__generated__/SubprocessorListItem_subprocessor.graphql";
const subprocessorListItemFragment = graphql`
fragment SubprocessorListItem_subprocessor on Subprocessor {
name
description
websiteUrl
countries
}
`;
interface SubprocessorListItemProps {
subprocessorKey: SubprocessorListItem_subprocessor$key;
}
// A single subprocessor card: a favicon logo over a blurred backdrop, the name,
// description, and the hosting regions.
export function SubprocessorListItem({ subprocessorKey }: SubprocessorListItemProps) {
const subprocessor = useFragment(subprocessorListItemFragment, subprocessorKey);
const countryLabel = useCountryLabel();
const logoUrl = faviconUrl(subprocessor.websiteUrl);
const countries = subprocessor.countries.map(countryLabel).join(", ");
return (
<Card variant="soft" size={3} padding="none">
<div className="relative flex items-center overflow-hidden p-8">
{logoUrl != null && (
<img
src={logoUrl}
alt=""
aria-hidden
className="pointer-events-none absolute inset-0 size-full scale-150 object-cover opacity-10 blur-lg"
/>
)}
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-sand-1/0 to-sand-1" />
<div className="relative z-10 flex size-10 items-center justify-center overflow-hidden rounded-2 bg-sand-1">
{logoUrl != null
? <img src={logoUrl} alt="" className="size-full object-cover" />
: <BuildingsIcon size={24} weight="duotone" className="text-sand-9" />}
</div>
</div>
<div className="flex flex-col gap-2 px-8 pb-8">
<Text size={4} weight="medium" color="neutral" highContrast>
{subprocessor.name}
</Text>
{subprocessor.description != null && subprocessor.description !== "" && (
<Text size={2} color="neutral">
{subprocessor.description}
</Text>
)}
{countries !== "" && (
<div className="flex items-start gap-1">
<MapPinSimpleIcon className="mt-0.5 size-4 shrink-0 text-gold-11" />
<Text size={1} color="gold">
{countries}
</Text>
</div>
)}
</div>
</Card>
);
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 { MagnifyingGlassIcon } from "@phosphor-icons/react";
import { Text } from "@probo/ui/src/v2/typography/Text";
import { useTranslation } from "react-i18next";
// Empty state shown when the trust center lists no subprocessors.
export function SubprocessorsEmpty() {
const { t } = useTranslation("subprocessors");
return (
<div className="flex flex-col items-center gap-5 py-8 text-center">
<MagnifyingGlassIcon className="size-6 text-sand-a8" />
<div className="flex max-w-xs flex-col items-center gap-2">
<Text size={2} weight="medium" color="faint">
{t("empty.title")}
</Text>
<Text size={2} color="faint">
{t("empty.description")}
</Text>
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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.
export interface CategoryGroup<T> {
category: string;
nodes: T[];
}
// Groups subprocessor nodes into their (non-empty) categories, ordered by the
// localized category label. Presentational only — filtering happens server-side.
export function groupByCategory<T extends { readonly category: string }>(
nodes: readonly T[],
getLabel: (category: string) => string,
): CategoryGroup<T>[] {
const groups = new Map<string, T[]>();
for (const node of nodes) {
const existing = groups.get(node.category);
if (existing) {
existing.push(node);
} else {
groups.set(node.category, [node]);
}
}
return [...groups.entries()]
.map(([category, groupNodes]) => ({ category, nodes: groupNodes }))
.sort((a, b) => getLabel(a.category).localeCompare(getLabel(b.category)));
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2026 Probo Inc <hello@probo.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 { useMemo } from "react";
import { useTranslation } from "react-i18next";
// Resolves a trust-center CountryCode to a localized display name. ISO 3166
// alpha-2 codes go through Intl.DisplayNames (locale-aware); the schema's two
// pseudo-regions (GLOBAL, EU) fall back to translated labels.
export function useCountryLabel(): (code: string) => string {
const { t, i18n } = useTranslation("subprocessors");
return useMemo(() => {
const display = new Intl.DisplayNames([i18n.language], { type: "region" });
return (code: string): string => {
if (code === "GLOBAL") {
return t("regions.global");
}
if (code === "EU") {
return t("regions.eu");
}
try {
return display.of(code) ?? code;
} catch {
return code;
}
};
}, [i18n.language, t]);
}

View File

@@ -0,0 +1,101 @@
{
"title": "Subprocessors",
"empty": {
"title": "No subprocessors listed.",
"description": "This trust center has not published any subprocessors yet."
},
"regions": {
"global": "Global",
"eu": "European Union"
},
"categories": {
"ANALYTICS": {
"label": "Analytics",
"description": "Usage analytics and product telemetry."
},
"CLOUD_MONITORING": {
"label": "Cloud Monitoring",
"description": "Observability, logging, and uptime monitoring."
},
"CLOUD_PROVIDER": {
"label": "Cloud Provider",
"description": "Cloud hosting, compute, storage, and networking."
},
"COLLABORATION": {
"label": "Collaboration",
"description": "Communication, scheduling, and teamwork tools."
},
"CUSTOMER_SUPPORT": {
"label": "Customer Support",
"description": "Help desk, ticketing, and customer messaging."
},
"DATA_STORAGE_AND_PROCESSING": {
"label": "Data Storage and Processing",
"description": "Databases, data warehouses, and processing pipelines."
},
"DOCUMENT_MANAGEMENT": {
"label": "Document Management",
"description": "Document storage, signing, and e-signature."
},
"EMPLOYEE_MANAGEMENT": {
"label": "Employee Management",
"description": "HR, payroll, and people operations."
},
"ENGINEERING": {
"label": "Engineering",
"description": "Developer tooling and engineering platforms."
},
"FINANCE": {
"label": "Finance",
"description": "Billing, payments, and accounting."
},
"IDENTITY_PROVIDER": {
"label": "Identity Provider",
"description": "Authentication, SSO, and identity management."
},
"IT": {
"label": "IT",
"description": "IT administration and device management."
},
"MARKETING": {
"label": "Marketing",
"description": "Marketing, CRM, and audience engagement."
},
"OFFICE_OPERATIONS": {
"label": "Office Operations",
"description": "Workplace and office operations."
},
"OTHER": {
"label": "Other",
"description": "Other supporting services."
},
"PASSWORD_MANAGEMENT": {
"label": "Password Management",
"description": "Credential and secret management."
},
"PRODUCT_AND_DESIGN": {
"label": "Product and Design",
"description": "Product and design tooling."
},
"PROFESSIONAL_SERVICES": {
"label": "Professional Services",
"description": "Consulting and professional services."
},
"RECRUITING": {
"label": "Recruiting",
"description": "Hiring, sourcing, and applicant tracking."
},
"SALES": {
"label": "Sales",
"description": "Sales enablement and CRM."
},
"SECURITY": {
"label": "Security",
"description": "Security, monitoring, and secure access."
},
"VERSION_CONTROL": {
"label": "Version Control",
"description": "Source control and code hosting."
}
}
}

View File

@@ -0,0 +1,101 @@
{
"title": "Sous-traitants",
"empty": {
"title": "Aucun sous-traitant répertorié.",
"description": "Ce centre de confiance n'a pas encore publié de sous-traitants."
},
"regions": {
"global": "International",
"eu": "Union européenne"
},
"categories": {
"ANALYTICS": {
"label": "Analytique",
"description": "Analyse d'usage et télémétrie produit."
},
"CLOUD_MONITORING": {
"label": "Supervision cloud",
"description": "Observabilité, journalisation et supervision."
},
"CLOUD_PROVIDER": {
"label": "Fournisseur cloud",
"description": "Hébergement, calcul, stockage et réseau cloud."
},
"COLLABORATION": {
"label": "Collaboration",
"description": "Communication, planification et travail d'équipe."
},
"CUSTOMER_SUPPORT": {
"label": "Support client",
"description": "Assistance, tickets et messagerie client."
},
"DATA_STORAGE_AND_PROCESSING": {
"label": "Stockage et traitement des données",
"description": "Bases de données, entrepôts et traitements."
},
"DOCUMENT_MANAGEMENT": {
"label": "Gestion documentaire",
"description": "Stockage, signature et e-signature de documents."
},
"EMPLOYEE_MANAGEMENT": {
"label": "Gestion des employés",
"description": "RH, paie et opérations RH."
},
"ENGINEERING": {
"label": "Ingénierie",
"description": "Outils et plateformes d'ingénierie."
},
"FINANCE": {
"label": "Finance",
"description": "Facturation, paiements et comptabilité."
},
"IDENTITY_PROVIDER": {
"label": "Fournisseur d'identité",
"description": "Authentification, SSO et gestion des identités."
},
"IT": {
"label": "Informatique",
"description": "Administration informatique et gestion des appareils."
},
"MARKETING": {
"label": "Marketing",
"description": "Marketing, CRM et engagement de l'audience."
},
"OFFICE_OPERATIONS": {
"label": "Opérations de bureau",
"description": "Opérations et gestion des bureaux."
},
"OTHER": {
"label": "Autre",
"description": "Autres services de support."
},
"PASSWORD_MANAGEMENT": {
"label": "Gestion des mots de passe",
"description": "Gestion des identifiants et des secrets."
},
"PRODUCT_AND_DESIGN": {
"label": "Produit et design",
"description": "Outils de produit et de design."
},
"PROFESSIONAL_SERVICES": {
"label": "Services professionnels",
"description": "Conseil et services professionnels."
},
"RECRUITING": {
"label": "Recrutement",
"description": "Recrutement, sourcing et suivi des candidatures."
},
"SALES": {
"label": "Ventes",
"description": "Aide à la vente et CRM."
},
"SECURITY": {
"label": "Sécurité",
"description": "Sécurité, supervision et accès sécurisé."
},
"VERSION_CONTROL": {
"label": "Gestion de versions",
"description": "Gestion de sources et hébergement de code."
}
}
}

View File

@@ -12,13 +12,15 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { useTranslation } from "react-i18next";
import { lazy } from "@probo/react-lazy";
import type { AppRoute } from "@probo/routes";
import { PageHeader } from "#/components/PageHeader/PageHeader";
import { SubprocessorsPageSkeleton } from "./SubprocessorsPageSkeleton";
// Toolbar (category/region filters, search, Download CSV) and the subprocessor
// count are deferred until the v2 Select/TextField components exist.
export default function SubprocessorsPage() {
const { t } = useTranslation();
return <PageHeader title={t("subprocessors.title")} />;
}
export const subprocessorRoutes = [
{
path: "subprocessors",
Fallback: SubprocessorsPageSkeleton,
Component: lazy(() => import("./SubprocessorsPageLoader")),
},
] satisfies AppRoute[];

View File

@@ -19,6 +19,7 @@ import { createBrowserRouter } from "react-router";
import { getPathPrefix } from "#/lib/http/pathPrefix";
import { HomePageSkeleton } from "#/pages/HomePageSkeleton";
import { MainLayoutSkeleton } from "#/pages/MainLayoutSkeleton";
import { subprocessorRoutes } from "#/pages/subprocessors/routes";
const routes = [
{
@@ -35,10 +36,7 @@ const routes = [
path: "documents",
Component: lazy(() => import("#/pages/DocumentsPage")),
},
{
path: "subprocessors",
Component: lazy(() => import("#/pages/SubprocessorsPage")),
},
...subprocessorRoutes,
{
path: "updates",
Component: lazy(() => import("#/pages/UpdatesPage")),