From 0b146a4054af2e29e5d5bf8b680ab995c2a32773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 16 Jul 2026 13:36:44 +0200 Subject: [PATCH] Add configurable compliance portal commitment cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compliance portal home page rendered security-commitment cards from a hardcoded placeholder POJO. Back them with real, per-organization data that admins configure in the console and the portal loads over the trust center GraphQL API. Model two entities under the trust center: a commitment group (title, description, rank) and a commitment card (icon, eyebrow, title, description, rank). The card icon is a curated enum mapped to a Phosphor icon in the portal. New entities adopt the compliance_portal_ prefix as the start of the broader rename away from trust_center_ naming. Expose the groups and cards read-only on the public trust API and with full CRUD on the console API, add a Commitments tab to the compliance page, and replace the placeholder section with a Relay-driven one. Signed-off-by: Émile Ré --- .../compliance-portal/src/_locales/en-US.json | 1 + .../compliance-portal/src/_locales/fr-FR.json | 1 + .../SecurityCommitmentGroupListItem.tsx | 83 ++++ .../SecurityCommitmentListItem.tsx | 57 +++ .../SecurityCommitmentsSection.tsx | 96 ++-- .../SecurityCommitments/commitmentIcons.ts | 78 ++++ .../securityCommitments.ts | 93 ---- apps/compliance-portal/src/pages/HomePage.tsx | 3 +- .../compliance-page/CompliancePageLayout.tsx | 6 +- .../CompliancePageCommitmentsPage.tsx | 60 +++ .../CompliancePageCommitmentsPageLoader.tsx | 52 +++ .../CompliancePageCommitmentDialog.tsx | 208 +++++++++ .../CompliancePageCommitmentGroupDialog.tsx | 168 +++++++ .../CompliancePageCommitmentGroupList.tsx | 104 +++++ .../CompliancePageCommitmentGroupListItem.tsx | 173 ++++++++ .../CompliancePageCommitmentListItem.tsx | 111 +++++ .../commitments/_lib/commitmentIcons.ts | 70 +++ .../organizations/compliance-page/routes.ts | 5 + pkg/coredata/compliance_portal_commitment.go | 416 ++++++++++++++++++ .../compliance_portal_commitment_group.go | 393 +++++++++++++++++ ...nce_portal_commitment_group_order_field.go | 97 ++++ .../compliance_portal_commitment_icon.go | 130 ++++++ ...ompliance_portal_commitment_order_field.go | 97 ++++ pkg/coredata/entity_type_reg.go | 6 + pkg/coredata/migrations/20260716T084447Z.sql | 53 +++ pkg/probo/actions.go | 14 + ...pliance_portal_commitment_group_service.go | 257 +++++++++++ .../compliance_portal_commitment_service.go | 278 ++++++++++++ pkg/probo/policies.go | 1 + pkg/probo/service.go | 4 + .../console/v1/graphql/trust_center.graphql | 246 +++++++++++ .../api/console/v1/trust_center_resolvers.go | 308 ++++++++++++- .../v1/types/compliance_portal_commitment.go | 125 ++++++ .../api/trust/v1/graphql/trust_center.graphql | 92 ++++ .../api/trust/v1/trust_center_resolvers.go | 62 ++- .../v1/types/compliance_portal_commitment.go | 94 ++++ ...pliance_portal_commitment_group_service.go | 83 ++++ .../compliance_portal_commitment_service.go | 83 ++++ pkg/trust/service.go | 46 +- 39 files changed, 4079 insertions(+), 175 deletions(-) create mode 100644 apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentGroupListItem.tsx create mode 100644 apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentListItem.tsx create mode 100644 apps/compliance-portal/src/components/SecurityCommitments/commitmentIcons.ts delete mode 100644 apps/compliance-portal/src/components/SecurityCommitments/securityCommitments.ts create mode 100644 apps/console/src/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPage.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPageLoader.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentDialog.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupDialog.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupList.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentGroupListItem.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentListItem.tsx create mode 100644 apps/console/src/pages/organizations/compliance-page/commitments/_lib/commitmentIcons.ts create mode 100644 pkg/coredata/compliance_portal_commitment.go create mode 100644 pkg/coredata/compliance_portal_commitment_group.go create mode 100644 pkg/coredata/compliance_portal_commitment_group_order_field.go create mode 100644 pkg/coredata/compliance_portal_commitment_icon.go create mode 100644 pkg/coredata/compliance_portal_commitment_order_field.go create mode 100644 pkg/coredata/migrations/20260716T084447Z.sql create mode 100644 pkg/probo/compliance_portal_commitment_group_service.go create mode 100644 pkg/probo/compliance_portal_commitment_service.go create mode 100644 pkg/server/api/console/v1/types/compliance_portal_commitment.go create mode 100644 pkg/server/api/trust/v1/types/compliance_portal_commitment.go create mode 100644 pkg/trust/compliance_portal_commitment_group_service.go create mode 100644 pkg/trust/compliance_portal_commitment_service.go diff --git a/apps/compliance-portal/src/_locales/en-US.json b/apps/compliance-portal/src/_locales/en-US.json index 530470117..8abdfd02b 100644 --- a/apps/compliance-portal/src/_locales/en-US.json +++ b/apps/compliance-portal/src/_locales/en-US.json @@ -20,6 +20,7 @@ "heroDescription": "Welcome to our Compliance Portal. Find our security documentation and certifications here.", "sections": { "compliance": "Compliance", + "securityCommitments": "Security Commitments", "trustedBy": "Trusted by", "recentUpdates": "Recent updates" }, diff --git a/apps/compliance-portal/src/_locales/fr-FR.json b/apps/compliance-portal/src/_locales/fr-FR.json index c951263bc..03fcd2d1c 100644 --- a/apps/compliance-portal/src/_locales/fr-FR.json +++ b/apps/compliance-portal/src/_locales/fr-FR.json @@ -20,6 +20,7 @@ "heroDescription": "Bienvenue dans notre Compliance Portal. Retrouvez ici notre documentation et nos certifications de sécurité.", "sections": { "compliance": "Conformité", + "securityCommitments": "Engagements de sécurité", "trustedBy": "Ils nous font confiance", "recentUpdates": "Mises à jour récentes" }, diff --git a/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentGroupListItem.tsx b/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentGroupListItem.tsx new file mode 100644 index 000000000..d8e19498a --- /dev/null +++ b/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentGroupListItem.tsx @@ -0,0 +1,83 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Text } from "@probo/ui/src/v2/typography/Text"; +import { useTranslation } from "react-i18next"; +import { graphql, useFragment } from "react-relay"; + +import type { SecurityCommitmentGroupListItem_group$key } from "./__generated__/SecurityCommitmentGroupListItem_group.graphql"; +import { SecurityCommitmentListItem } from "./SecurityCommitmentListItem"; +import { securityCommitments } from "./variants"; + +const fragment = graphql` + fragment SecurityCommitmentGroupListItem_group on CompliancePortalCommitmentGroup { + title + description + commitments(first: 100) { + edges { + node { + id + ...SecurityCommitmentListItem_commitment + } + } + } + } +`; + +interface SecurityCommitmentGroupListItemProps { + groupKey: SecurityCommitmentGroupListItem_group$key; + // Only the first group carries the section eyebrow, matching the design. + showEyebrow: boolean; +} + +export function SecurityCommitmentGroupListItem({ groupKey, showEyebrow }: SecurityCommitmentGroupListItemProps) { + const { t } = useTranslation(); + const group = useFragment(fragment, groupKey); + const slots = securityCommitments(); + + const commitments = group.commitments.edges.map(edge => edge.node); + + if (commitments.length === 0) { + return null; + } + + return ( +
+
+ {showEyebrow && ( + + {t("home.sections.securityCommitments")} + + )} + + {group.title} + + + {group.description} + +
+
+ {commitments.map(commitment => ( + + ))} +
+
+ ); +} diff --git a/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentListItem.tsx b/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentListItem.tsx new file mode 100644 index 000000000..34318ffae --- /dev/null +++ b/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentListItem.tsx @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Text } from "@probo/ui/src/v2/typography/Text"; +import { graphql, useFragment } from "react-relay"; + +import { CommitmentCard } from "#/components/CommitmentCard/CommitmentCard"; + +import type { SecurityCommitmentListItem_commitment$key } from "./__generated__/SecurityCommitmentListItem_commitment.graphql"; +import { CommitmentIcon } from "./commitmentIcons"; + +const fragment = graphql` + fragment SecurityCommitmentListItem_commitment on CompliancePortalCommitment { + icon + eyebrow + title + description + } +`; + +interface SecurityCommitmentListItemProps { + commitmentKey: SecurityCommitmentListItem_commitment$key; +} + +export function SecurityCommitmentListItem({ commitmentKey }: SecurityCommitmentListItemProps) { + const commitment = useFragment(fragment, commitmentKey); + + return ( + } + eyebrow={{commitment.eyebrow}} + title={( + + {commitment.title} + + )} + description={{commitment.description}} + /> + ); +} diff --git a/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentsSection.tsx b/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentsSection.tsx index 3c871dff5..63635736c 100644 --- a/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentsSection.tsx +++ b/apps/compliance-portal/src/components/SecurityCommitments/SecurityCommitmentsSection.tsx @@ -18,54 +18,70 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import { Text } from "@probo/ui/src/v2/typography/Text"; +import { ErrorBoundary } from "@probo/ui/src/v2/ErrorBoundary/ErrorBoundary"; +import { graphql, useFragment } from "react-relay"; -import { CommitmentCard } from "#/components/CommitmentCard/CommitmentCard"; +import { InlineErrorCard } from "#/components/errors/InlineErrorCard"; -import { SECURITY_COMMITMENT_GROUPS } from "./securityCommitments"; +import type { SecurityCommitmentsSection_trustCenter$key } from "./__generated__/SecurityCommitmentsSection_trustCenter.graphql"; +import { SecurityCommitmentGroupListItem } from "./SecurityCommitmentGroupListItem"; import { securityCommitments } from "./variants"; -// "Security Commitments" section. -// -// TODO: This section renders placeholder data from a local POJO -// (./securityCommitments.ts) because there is no backend / DB structure for it -// yet. Replace it with a relay-driven fragment (and i18n copy) once available. -export function SecurityCommitmentsSection() { +// @throwOnFieldError makes a field error in this fragment throw at the read +// below, where the section's ErrorBoundary contains it. See +// contrib/claude/error-handling.md. +const securityCommitmentsSectionFragment = graphql` + fragment SecurityCommitmentsSection_trustCenter on TrustCenter @throwOnFieldError { + commitmentGroups(first: 100) { + edges { + node { + id + ...SecurityCommitmentGroupListItem_group + } + } + } + } +`; + +interface SecurityCommitmentsSectionProps { + trustCenterKey: SecurityCommitmentsSection_trustCenter$key; +} + +// "Security Commitments" section: stacked groups, each a header above a grid of +// commitment cards. Wraps its data-reading content in a boundary so a load +// failure degrades to an inline error instead of taking down the page. +export function SecurityCommitmentsSection({ trustCenterKey }: SecurityCommitmentsSectionProps) { + return ( + + window.location.reload()} /> + + )} + > + + + ); +} + +function SecurityCommitmentsSectionContent({ trustCenterKey }: SecurityCommitmentsSectionProps) { + const data = useFragment(securityCommitmentsSectionFragment, trustCenterKey); const slots = securityCommitments(); + const groups = data.commitmentGroups.edges.map(edge => edge.node); + + if (groups.length === 0) { + return null; + } + return (
- {SECURITY_COMMITMENT_GROUPS.map(group => ( -
-
- {group.eyebrow != null && ( - - {group.eyebrow} - - )} - - {group.title} - - - {group.description} - -
-
- {group.items.map(item => ( - } - eyebrow={{item.eyebrow}} - title={( - - {item.title} - - )} - description={{item.description}} - /> - ))} -
-
+ {groups.map((group, index) => ( + ))}
); diff --git a/apps/compliance-portal/src/components/SecurityCommitments/commitmentIcons.ts b/apps/compliance-portal/src/components/SecurityCommitments/commitmentIcons.ts new file mode 100644 index 000000000..c3170023d --- /dev/null +++ b/apps/compliance-portal/src/components/SecurityCommitments/commitmentIcons.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import type { Icon, IconProps } from "@phosphor-icons/react"; +import { + BellIcon, + BugIcon, + CertificateIcon, + CloudIcon, + CodeIcon, + DatabaseIcon, + EyeIcon, + EyeSlashIcon, + FingerprintIcon, + GavelIcon, + GlobeIcon, + HardDrivesIcon, + HeartbeatIcon, + KeyIcon, + LockIcon, + LockKeyIcon, + ShieldCheckIcon, + ShieldWarningIcon, + SirenIcon, + UsersIcon, +} from "@phosphor-icons/react"; +import { createElement } from "react"; + +// Maps the CompliancePortalCommitmentIcon enum (coredata) to the Phosphor icon +// rendered on each commitment card. Keep in sync with the console icon picker +// (see apps/console .../commitments/_lib/commitmentIcons.ts). +const COMMITMENT_ICONS: Record = { + LOCK_KEY: LockKeyIcon, + EYE_SLASH: EyeSlashIcon, + FINGERPRINT: FingerprintIcon, + SHIELD_WARNING: ShieldWarningIcon, + SHIELD_CHECK: ShieldCheckIcon, + SIREN: SirenIcon, + KEY: KeyIcon, + LOCK: LockIcon, + CLOUD: CloudIcon, + DATABASE: DatabaseIcon, + GLOBE: GlobeIcon, + EYE: EyeIcon, + USERS: UsersIcon, + CERTIFICATE: CertificateIcon, + GAVEL: GavelIcon, + HEARTBEAT: HeartbeatIcon, + BELL: BellIcon, + BUG: BugIcon, + CODE: CodeIcon, + SERVER: HardDrivesIcon, +}; + +// Renders the Phosphor icon mapped from a CompliancePortalCommitmentIcon enum +// value. Declared at module scope (and built via createElement) so the icon +// component is never created during a parent's render. +export function CommitmentIcon({ icon, ...props }: { icon: string } & IconProps) { + const resolved: Icon = COMMITMENT_ICONS[icon] ?? LockKeyIcon; + return createElement(resolved, props); +} diff --git a/apps/compliance-portal/src/components/SecurityCommitments/securityCommitments.ts b/apps/compliance-portal/src/components/SecurityCommitments/securityCommitments.ts deleted file mode 100644 index d6e857602..000000000 --- a/apps/compliance-portal/src/components/SecurityCommitments/securityCommitments.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2026 Probo Inc . -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import type { Icon } from "@phosphor-icons/react"; -import { EyeSlashIcon, FingerprintIcon, LockKeyIcon, ShieldWarningIcon, SirenIcon } from "@phosphor-icons/react"; - -export interface SecurityCommitmentItem { - eyebrow: string; - title: string; - description: string; - Icon: Icon; -} - -export interface SecurityCommitmentGroup { - // Only the first group carries the section eyebrow in the design. - eyebrow?: string; - title: string; - description: string; - items: SecurityCommitmentItem[]; -} - -// TODO: This content is placeholder data. There is no backend / DB structure for -// security commitments yet — replace this POJO with relay-sourced data (and move -// the copy into i18n) once the schema exists. -export const SECURITY_COMMITMENT_GROUPS: SecurityCommitmentGroup[] = [ - { - eyebrow: "Security Commitments", - title: "Data Protection", - description: - "Your data is protected with industry-leading encryption and strict privacy controls, from storage to transit to processing.", - items: [ - { - eyebrow: "Data Protection", - title: "Encrypted at rest. Encrypted in transit. Always.", - description: - "Customer data is protected with AES-256 at rest and TLS 1.3 in transit. Keys rotated on a fixed schedule.", - Icon: LockKeyIcon, - }, - { - eyebrow: "Privacy", - title: "Customer data stays customer data.", - description: - "PII is masked in non-production environments. Access requires SSO with hardware-backed keys.", - Icon: EyeSlashIcon, - }, - { - eyebrow: "Identity", - title: "Phishing-resistant authentication, by default.", - description: - "Every employee authenticates via WebAuthn. Production access is just-in-time and recorded.", - Icon: FingerprintIcon, - }, - ], - }, - { - title: "Operational Security", - description: - "We maintain continuous uptime and rapid incident response so you can rely on our platform around the clock.", - items: [ - { - eyebrow: "Threat Detection", - title: "Anomalies do not wait. Neither do we.", - description: - "24/7 SIEM monitoring with automated triage. Critical incidents page on-call within 90 seconds.", - Icon: ShieldWarningIcon, - }, - { - eyebrow: "Incident Response", - title: "Issues are caught fast. And handled.", - description: - "Security incidents are investigated within hours. Customers notified within 24 hours of a confirmed breach.", - Icon: SirenIcon, - }, - ], - }, -]; diff --git a/apps/compliance-portal/src/pages/HomePage.tsx b/apps/compliance-portal/src/pages/HomePage.tsx index 3d2a70f3f..f204d1f08 100644 --- a/apps/compliance-portal/src/pages/HomePage.tsx +++ b/apps/compliance-portal/src/pages/HomePage.tsx @@ -39,6 +39,7 @@ export const homePageQuery = graphql` ...OrganizationContactInfo_organization } ...ComplianceFrameworksSection_trustCenter + ...SecurityCommitmentsSection_trustCenter ...TrustedBySection_trustCenter ...RecentUpdatesSection_trustCenter } @@ -66,7 +67,7 @@ export function HomePage({ queryRef }: HomePageProps) {
- +
diff --git a/apps/console/src/pages/organizations/compliance-page/CompliancePageLayout.tsx b/apps/console/src/pages/organizations/compliance-page/CompliancePageLayout.tsx index 1591fce7a..51f720563 100644 --- a/apps/console/src/pages/organizations/compliance-page/CompliancePageLayout.tsx +++ b/apps/console/src/pages/organizations/compliance-page/CompliancePageLayout.tsx @@ -20,7 +20,7 @@ import { usePageTitle } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; -import { Badge, Button, IconBell2, IconCheckmark1, IconFolder2, IconMedal, IconPageTextLine, IconPencil, IconPeopleAdd, IconSettingsGear2, IconStore, PageHeader, TabLink, Tabs } from "@probo/ui"; +import { Badge, Button, IconBell2, IconCheckmark1, IconFolder2, IconMedal, IconPageTextLine, IconPencil, IconPeopleAdd, IconSettingsGear2, IconShield, IconStore, PageHeader, TabLink, Tabs } from "@probo/ui"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { Outlet } from "react-router"; import { graphql } from "relay-runtime"; @@ -107,6 +107,10 @@ export function CompliancePageLayout(props: { queryRef: PreloadedQuery {__("References")} + + + {__("Commitments")} + {__("Audits")} diff --git a/apps/console/src/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPage.tsx b/apps/console/src/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPage.tsx new file mode 100644 index 000000000..e358c650c --- /dev/null +++ b/apps/console/src/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPage.tsx @@ -0,0 +1,60 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay"; + +import type { CompliancePageCommitmentsPageQuery } from "#/__generated__/core/CompliancePageCommitmentsPageQuery.graphql"; + +import { CompliancePageCommitmentGroupList } from "./_components/CompliancePageCommitmentGroupList"; + +export const compliancePageCommitmentsPageQuery = graphql` + query CompliancePageCommitmentsPageQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + __typename + ... on Organization { + compliancePage: trustCenter @required(action: THROW) { + id + canCreateGroup: permission(action: "core:compliance-portal-commitment-group:create") + ...CompliancePageCommitmentGroupListFragment + } + } + } + } +`; + +export function CompliancePageCommitmentsPage(props: { queryRef: PreloadedQuery }) { + const { queryRef } = props; + + const { organization } = usePreloadedQuery( + compliancePageCommitmentsPageQuery, + queryRef, + ); + if (organization.__typename !== "Organization") { + throw new Error("invalid type for node"); + } + + return ( + + ); +} diff --git a/apps/console/src/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPageLoader.tsx b/apps/console/src/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPageLoader.tsx new file mode 100644 index 000000000..892b78c5f --- /dev/null +++ b/apps/console/src/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPageLoader.tsx @@ -0,0 +1,52 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { useEffect } from "react"; +import { useQueryLoader } from "react-relay"; + +import type { CompliancePageCommitmentsPageQuery } from "#/__generated__/core/CompliancePageCommitmentsPageQuery.graphql"; +import { LinkCardSkeleton } from "#/components/skeletons/LinkCardSkeleton"; +import { useOrganizationId } from "#/hooks/useOrganizationId"; +import { CoreRelayProvider } from "#/providers/CoreRelayProvider"; + +import { CompliancePageCommitmentsPage, compliancePageCommitmentsPageQuery } from "./CompliancePageCommitmentsPage"; + +function CompliancePageCommitmentsPageQueryLoader() { + const organizationId = useOrganizationId(); + const [queryRef, loadQuery] = useQueryLoader(compliancePageCommitmentsPageQuery); + + useEffect(() => { + if (!queryRef) { + loadQuery({ organizationId }); + } + }); + + if (!queryRef) return ; + + return ; +} + +export default function CompliancePageCommitmentsPageLoader() { + return ( + + + + ); +} diff --git a/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentDialog.tsx b/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentDialog.tsx new file mode 100644 index 000000000..e1d3bcb29 --- /dev/null +++ b/apps/console/src/pages/organizations/compliance-page/commitments/_components/CompliancePageCommitmentDialog.tsx @@ -0,0 +1,208 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { useTranslate } from "@probo/i18n"; +import { Button, Dialog, DialogContent, DialogFooter, Field, Label, Option, Spinner, Textarea, useDialogRef } from "@probo/ui"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { graphql } from "relay-runtime"; +import { z } from "zod"; + +import type { CompliancePageCommitmentDialogCreateMutation, CompliancePortalCommitmentIcon } from "#/__generated__/core/CompliancePageCommitmentDialogCreateMutation.graphql"; +import type { CompliancePageCommitmentDialogUpdateMutation } from "#/__generated__/core/CompliancePageCommitmentDialogUpdateMutation.graphql"; +import type { CompliancePageCommitmentListItemFragment$data } from "#/__generated__/core/CompliancePageCommitmentListItemFragment.graphql"; +import { ControlledSelect } from "#/components/form/ControlledField"; +import { useFormWithSchema } from "#/hooks/useFormWithSchema"; +import { useMutationWithToasts } from "#/hooks/useMutationWithToasts"; + +import { COMMITMENT_ICON_LABELS, COMMITMENT_ICON_VALUES } from "../_lib/commitmentIcons"; + +const createCommitmentMutation = graphql` + mutation CompliancePageCommitmentDialogCreateMutation( + $input: CreateCompliancePortalCommitmentInput! + ) { + createCompliancePortalCommitment(input: $input) { + compliancePortalCommitmentEdge { + node { + id + icon + eyebrow + title + description + rank + } + } + } + } +`; + +const updateCommitmentMutation = graphql` + mutation CompliancePageCommitmentDialogUpdateMutation( + $input: UpdateCompliancePortalCommitmentInput! + ) { + updateCompliancePortalCommitment(input: $input) { + compliancePortalCommitment { + id + icon + eyebrow + title + description + rank + } + } + } +`; + +const commitmentSchema = z.object({ + icon: z.string().min(1, "Icon is required"), + eyebrow: z.string(), + title: z.string().min(1, "Title is required"), + description: z.string().min(1, "Description is required"), +}); + +type CommitmentFormData = z.infer; + +export type CompliancePageCommitmentDialogRef = { + openCreate: (groupId: string) => void; + openEdit: (commitment: CompliancePageCommitmentListItemFragment$data) => void; +}; + +export const CompliancePageCommitmentDialog = forwardRef< + CompliancePageCommitmentDialogRef, + { onChanged: () => void } +>(function CompliancePageCommitmentDialog({ onChanged }, ref) { + const { __ } = useTranslate(); + const dialogRef = useDialogRef(); + const [mode, setMode] = useState<"create" | "edit">("create"); + const [groupId, setGroupId] = useState(""); + const [commitmentId, setCommitmentId] = useState(""); + + const [createCommitment, isCreating] = useMutationWithToasts( + createCommitmentMutation, + { successMessage: __("Commitment created successfully"), errorMessage: __("Failed to create commitment") }, + ); + const [updateCommitment, isUpdating] = useMutationWithToasts( + updateCommitmentMutation, + { successMessage: __("Commitment updated successfully"), errorMessage: __("Failed to update commitment") }, + ); + + const { register, handleSubmit, control, formState: { errors }, reset } = useFormWithSchema(commitmentSchema, { + defaultValues: { icon: COMMITMENT_ICON_VALUES[0], eyebrow: "", title: "", description: "" }, + }); + + useImperativeHandle(ref, () => ({ + openCreate: (gId: string) => { + setMode("create"); + setGroupId(gId); + reset({ icon: COMMITMENT_ICON_VALUES[0], eyebrow: "", title: "", description: "" }); + dialogRef.current?.open(); + }, + openEdit: (commitment) => { + setMode("edit"); + setCommitmentId(commitment.id); + reset({ + icon: commitment.icon, + eyebrow: commitment.eyebrow, + title: commitment.title, + description: commitment.description, + }); + dialogRef.current?.open(); + }, + })); + + const onSubmit = async (data: CommitmentFormData) => { + const icon = data.icon as CompliancePortalCommitmentIcon; + + if (mode === "create") { + await createCommitment({ + variables: { + input: { groupId, icon, eyebrow: data.eyebrow, title: data.title, description: data.description }, + }, + onSuccess: () => { + reset(); + dialogRef.current?.close(); + onChanged(); + }, + }); + } else { + await updateCommitment({ + variables: { + input: { id: commitmentId, icon, eyebrow: data.eyebrow, title: data.title, description: data.description }, + }, + onSuccess: () => { + reset(); + dialogRef.current?.close(); + onChanged(); + }, + }); + } + }; + + const isSubmitting = isCreating || isUpdating; + const title = mode === "create" ? __("Add Commitment") : __("Edit Commitment"); + + return ( + reset()}> +
void handleSubmit(onSubmit)(e)}> + +
+ + + {COMMITMENT_ICON_VALUES.map(value => ( + + ))} + +
+ + + + + + +