Add configurable compliance portal commitment cards
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é <emile@probo.com>
This commit is contained in:
@@ -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<Complianc
|
||||
<IconCheckmark1 className="size-4" />
|
||||
{__("References")}
|
||||
</TabLink>
|
||||
<TabLink to={`/organizations/${organizationId}/compliance-page/commitments`}>
|
||||
<IconShield className="size-4" />
|
||||
{__("Commitments")}
|
||||
</TabLink>
|
||||
<TabLink to={`/organizations/${organizationId}/compliance-page/audits`}>
|
||||
<IconMedal className="size-4" />
|
||||
{__("Audits")}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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<CompliancePageCommitmentsPageQuery> }) {
|
||||
const { queryRef } = props;
|
||||
|
||||
const { organization } = usePreloadedQuery<CompliancePageCommitmentsPageQuery>(
|
||||
compliancePageCommitmentsPageQuery,
|
||||
queryRef,
|
||||
);
|
||||
if (organization.__typename !== "Organization") {
|
||||
throw new Error("invalid type for node");
|
||||
}
|
||||
|
||||
return (
|
||||
<CompliancePageCommitmentGroupList
|
||||
fragmentRef={organization.compliancePage}
|
||||
trustCenterId={organization.compliancePage.id}
|
||||
canCreate={organization.compliancePage.canCreateGroup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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>(compliancePageCommitmentsPageQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryRef) {
|
||||
loadQuery({ organizationId });
|
||||
}
|
||||
});
|
||||
|
||||
if (!queryRef) return <LinkCardSkeleton />;
|
||||
|
||||
return <CompliancePageCommitmentsPage queryRef={queryRef} />;
|
||||
}
|
||||
|
||||
export default function CompliancePageCommitmentsPageLoader() {
|
||||
return (
|
||||
<CoreRelayProvider>
|
||||
<CompliancePageCommitmentsPageQueryLoader />
|
||||
</CoreRelayProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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<typeof commitmentSchema>;
|
||||
|
||||
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<string>("");
|
||||
const [commitmentId, setCommitmentId] = useState<string>("");
|
||||
|
||||
const [createCommitment, isCreating] = useMutationWithToasts<CompliancePageCommitmentDialogCreateMutation>(
|
||||
createCommitmentMutation,
|
||||
{ successMessage: __("Commitment created successfully"), errorMessage: __("Failed to create commitment") },
|
||||
);
|
||||
const [updateCommitment, isUpdating] = useMutationWithToasts<CompliancePageCommitmentDialogUpdateMutation>(
|
||||
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 (
|
||||
<Dialog ref={dialogRef} title={title} className="max-w-2xl" onClose={() => reset()}>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<div className="space-y-1.5">
|
||||
<Label>{__("Icon")}</Label>
|
||||
<ControlledSelect control={control} name="icon" placeholder={__("Select an icon")}>
|
||||
{COMMITMENT_ICON_VALUES.map(value => (
|
||||
<Option key={value} value={value}>
|
||||
{COMMITMENT_ICON_LABELS[value]}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
{...register("eyebrow")}
|
||||
label={__("Eyebrow")}
|
||||
type="text"
|
||||
error={errors.eyebrow?.message}
|
||||
placeholder={__("Small accent label above the title")}
|
||||
/>
|
||||
|
||||
<Field
|
||||
{...register("title")}
|
||||
label={__("Title")}
|
||||
type="text"
|
||||
required
|
||||
error={errors.title?.message}
|
||||
placeholder={__("Commitment headline")}
|
||||
/>
|
||||
|
||||
<Field label={__("Description")} error={errors.description?.message} required>
|
||||
<Textarea
|
||||
{...register("description")}
|
||||
placeholder={__("Supporting body copy")}
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isSubmitting} icon={isSubmitting ? Spinner : undefined}>
|
||||
{mode === "create" ? __("Add Commitment") : __("Update Commitment")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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, Spinner, Textarea, useDialogRef } from "@probo/ui";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CompliancePageCommitmentGroupDialogCreateMutation } from "#/__generated__/core/CompliancePageCommitmentGroupDialogCreateMutation.graphql";
|
||||
import type { CompliancePageCommitmentGroupDialogUpdateMutation } from "#/__generated__/core/CompliancePageCommitmentGroupDialogUpdateMutation.graphql";
|
||||
import type { CompliancePageCommitmentGroupListItemFragment$data } from "#/__generated__/core/CompliancePageCommitmentGroupListItemFragment.graphql";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
|
||||
const createGroupMutation = graphql`
|
||||
mutation CompliancePageCommitmentGroupDialogCreateMutation(
|
||||
$input: CreateCompliancePortalCommitmentGroupInput!
|
||||
) {
|
||||
createCompliancePortalCommitmentGroup(input: $input) {
|
||||
compliancePortalCommitmentGroupEdge {
|
||||
node {
|
||||
id
|
||||
title
|
||||
description
|
||||
rank
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const updateGroupMutation = graphql`
|
||||
mutation CompliancePageCommitmentGroupDialogUpdateMutation(
|
||||
$input: UpdateCompliancePortalCommitmentGroupInput!
|
||||
) {
|
||||
updateCompliancePortalCommitmentGroup(input: $input) {
|
||||
compliancePortalCommitmentGroup {
|
||||
id
|
||||
title
|
||||
description
|
||||
rank
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const groupSchema = z.object({
|
||||
title: z.string().min(1, "Title is required"),
|
||||
description: z.string().min(1, "Description is required"),
|
||||
});
|
||||
|
||||
type GroupFormData = z.infer<typeof groupSchema>;
|
||||
|
||||
export type CompliancePageCommitmentGroupDialogRef = {
|
||||
openCreate: (trustCenterId: string) => void;
|
||||
openEdit: (group: CompliancePageCommitmentGroupListItemFragment$data) => void;
|
||||
};
|
||||
|
||||
export const CompliancePageCommitmentGroupDialog = forwardRef<
|
||||
CompliancePageCommitmentGroupDialogRef,
|
||||
{ onChanged: () => void }
|
||||
>(function CompliancePageCommitmentGroupDialog({ onChanged }, ref) {
|
||||
const { __ } = useTranslate();
|
||||
const dialogRef = useDialogRef();
|
||||
const [mode, setMode] = useState<"create" | "edit">("create");
|
||||
const [trustCenterId, setTrustCenterId] = useState<string>("");
|
||||
const [groupId, setGroupId] = useState<string>("");
|
||||
|
||||
const [createGroup, isCreating] = useMutationWithToasts<CompliancePageCommitmentGroupDialogCreateMutation>(
|
||||
createGroupMutation,
|
||||
{ successMessage: __("Group created successfully"), errorMessage: __("Failed to create group") },
|
||||
);
|
||||
const [updateGroup, isUpdating] = useMutationWithToasts<CompliancePageCommitmentGroupDialogUpdateMutation>(
|
||||
updateGroupMutation,
|
||||
{ successMessage: __("Group updated successfully"), errorMessage: __("Failed to update group") },
|
||||
);
|
||||
|
||||
const { register, handleSubmit, formState: { errors }, reset } = useFormWithSchema(groupSchema, {
|
||||
defaultValues: { title: "", description: "" },
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
openCreate: (tId: string) => {
|
||||
setMode("create");
|
||||
setTrustCenterId(tId);
|
||||
reset({ title: "", description: "" });
|
||||
dialogRef.current?.open();
|
||||
},
|
||||
openEdit: (group) => {
|
||||
setMode("edit");
|
||||
setGroupId(group.id);
|
||||
reset({ title: group.title, description: group.description });
|
||||
dialogRef.current?.open();
|
||||
},
|
||||
}));
|
||||
|
||||
const onSubmit = async (data: GroupFormData) => {
|
||||
if (mode === "create") {
|
||||
await createGroup({
|
||||
variables: { input: { trustCenterId, title: data.title, description: data.description } },
|
||||
onSuccess: () => {
|
||||
reset();
|
||||
dialogRef.current?.close();
|
||||
onChanged();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await updateGroup({
|
||||
variables: { input: { id: groupId, title: data.title, description: data.description } },
|
||||
onSuccess: () => {
|
||||
reset();
|
||||
dialogRef.current?.close();
|
||||
onChanged();
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isSubmitting = isCreating || isUpdating;
|
||||
const title = mode === "create" ? __("Add Group") : __("Edit Group");
|
||||
|
||||
return (
|
||||
<Dialog ref={dialogRef} title={title} className="max-w-2xl" onClose={() => reset()}>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent padded className="space-y-6">
|
||||
<Field
|
||||
{...register("title")}
|
||||
label={__("Title")}
|
||||
type="text"
|
||||
required
|
||||
error={errors.title?.message}
|
||||
placeholder={__("e.g. Data Protection")}
|
||||
/>
|
||||
<Field label={__("Description")} error={errors.description?.message} required>
|
||||
<Textarea
|
||||
{...register("description")}
|
||||
placeholder={__("Describe what this group of commitments covers")}
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isSubmitting} icon={isSubmitting ? Spinner : undefined}>
|
||||
{mode === "create" ? __("Add Group") : __("Update Group")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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, Card, IconPlusLarge } from "@probo/ui";
|
||||
import { useRef } from "react";
|
||||
import { useRefetchableFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { CompliancePageCommitmentGroupListFragment$key } from "#/__generated__/core/CompliancePageCommitmentGroupListFragment.graphql";
|
||||
import type { CompliancePageCommitmentGroupListRefetchQuery } from "#/__generated__/core/CompliancePageCommitmentGroupListRefetchQuery.graphql";
|
||||
|
||||
import { CompliancePageCommitmentGroupDialog, type CompliancePageCommitmentGroupDialogRef } from "./CompliancePageCommitmentGroupDialog";
|
||||
import { CompliancePageCommitmentGroupListItem } from "./CompliancePageCommitmentGroupListItem";
|
||||
|
||||
const fragment = graphql`
|
||||
fragment CompliancePageCommitmentGroupListFragment on TrustCenter
|
||||
@refetchable(queryName: "CompliancePageCommitmentGroupListRefetchQuery") {
|
||||
commitmentGroups(first: 100, orderBy: { field: RANK, direction: ASC }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...CompliancePageCommitmentGroupListItemFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function CompliancePageCommitmentGroupList(props: {
|
||||
fragmentRef: CompliancePageCommitmentGroupListFragment$key;
|
||||
trustCenterId: string;
|
||||
canCreate: boolean;
|
||||
}) {
|
||||
const { fragmentRef, trustCenterId, canCreate } = props;
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const dialogRef = useRef<CompliancePageCommitmentGroupDialogRef>(null);
|
||||
|
||||
const [data, refetch] = useRefetchableFragment<
|
||||
CompliancePageCommitmentGroupListRefetchQuery,
|
||||
CompliancePageCommitmentGroupListFragment$key
|
||||
>(fragment, fragmentRef);
|
||||
|
||||
const onChanged = () => refetch({}, { fetchPolicy: "network-only" });
|
||||
|
||||
const groups = data.commitmentGroups.edges.map(edge => edge.node);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-medium">{__("Security Commitments")}</h2>
|
||||
<p className="text-sm text-txt-tertiary">
|
||||
{__("Group commitment cards into sections shown on your compliance page")}
|
||||
</p>
|
||||
</div>
|
||||
{canCreate && (
|
||||
<Button icon={IconPlusLarge} onClick={() => dialogRef.current?.openCreate(trustCenterId)}>
|
||||
{__("Add Group")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{groups.length === 0
|
||||
? (
|
||||
<Card className="p-6 text-center text-sm text-txt-secondary">
|
||||
{__("No commitment groups yet")}
|
||||
</Card>
|
||||
)
|
||||
: (
|
||||
<div className="space-y-6">
|
||||
{groups.map(group => (
|
||||
<CompliancePageCommitmentGroupListItem
|
||||
key={group.id}
|
||||
fragmentRef={group}
|
||||
onEdit={g => dialogRef.current?.openEdit(g)}
|
||||
onChanged={onChanged}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CompliancePageCommitmentGroupDialog ref={dialogRef} onChanged={onChanged} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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 { sprintf } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Card, Dialog, DialogContent, DialogFooter, IconPencil, IconPlusLarge, IconTrashCan, Spinner, Table, Tbody, Td, Th, Thead, Tr, useDialogRef } from "@probo/ui";
|
||||
import { useRef } from "react";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { CompliancePageCommitmentGroupListItemDeleteMutation } from "#/__generated__/core/CompliancePageCommitmentGroupListItemDeleteMutation.graphql";
|
||||
import type { CompliancePageCommitmentGroupListItemFragment$data, CompliancePageCommitmentGroupListItemFragment$key } from "#/__generated__/core/CompliancePageCommitmentGroupListItemFragment.graphql";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
|
||||
import { CompliancePageCommitmentDialog, type CompliancePageCommitmentDialogRef } from "./CompliancePageCommitmentDialog";
|
||||
import { CompliancePageCommitmentListItem } from "./CompliancePageCommitmentListItem";
|
||||
|
||||
const deleteGroupMutation = graphql`
|
||||
mutation CompliancePageCommitmentGroupListItemDeleteMutation(
|
||||
$input: DeleteCompliancePortalCommitmentGroupInput!
|
||||
) {
|
||||
deleteCompliancePortalCommitmentGroup(input: $input) {
|
||||
deletedCompliancePortalCommitmentGroupId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const fragment = graphql`
|
||||
fragment CompliancePageCommitmentGroupListItemFragment on CompliancePortalCommitmentGroup {
|
||||
id
|
||||
title
|
||||
description
|
||||
canUpdate: permission(action: "core:compliance-portal-commitment-group:update")
|
||||
canDelete: permission(action: "core:compliance-portal-commitment-group:delete")
|
||||
canCreateCommitment: permission(action: "core:compliance-portal-commitment:create")
|
||||
commitments(first: 100, orderBy: { field: RANK, direction: ASC }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...CompliancePageCommitmentListItemFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function CompliancePageCommitmentGroupListItem(props: {
|
||||
fragmentRef: CompliancePageCommitmentGroupListItemFragment$key;
|
||||
onEdit: (group: CompliancePageCommitmentGroupListItemFragment$data) => void;
|
||||
onChanged: () => void;
|
||||
}) {
|
||||
const { fragmentRef, onEdit, onChanged } = props;
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const group = useFragment<CompliancePageCommitmentGroupListItemFragment$key>(fragment, fragmentRef);
|
||||
const commitmentDialogRef = useRef<CompliancePageCommitmentDialogRef>(null);
|
||||
const deleteDialogRef = useDialogRef();
|
||||
|
||||
const [deleteGroup, isDeleting] = useMutationWithToasts<CompliancePageCommitmentGroupListItemDeleteMutation>(
|
||||
deleteGroupMutation,
|
||||
{ successMessage: __("Group deleted successfully"), errorMessage: __("Failed to delete group") },
|
||||
);
|
||||
|
||||
const commitments = group.commitments.edges.map(edge => edge.node);
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteGroup({
|
||||
variables: { input: { id: group.id } },
|
||||
onSuccess: () => {
|
||||
deleteDialogRef.current?.close();
|
||||
onChanged();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="space-y-4 p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-base font-medium">{group.title}</h3>
|
||||
<p className="text-sm text-txt-tertiary">{group.description}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{group.canUpdate && (
|
||||
<Button variant="secondary" icon={IconPencil} onClick={() => onEdit(group)} />
|
||||
)}
|
||||
{group.canDelete && (
|
||||
<>
|
||||
<Button variant="danger" icon={IconTrashCan} onClick={() => deleteDialogRef.current?.open()} />
|
||||
<Dialog ref={deleteDialogRef} title={__("Delete Group")} className="max-w-md">
|
||||
<DialogContent padded>
|
||||
<p className="text-txt-secondary">
|
||||
{sprintf(__("Are you sure you want to delete the group \"%s\"?"), group.title)}
|
||||
</p>
|
||||
<p className="text-txt-secondary mt-2">
|
||||
{__("All commitment cards in this group will also be deleted. This action cannot be undone.")}
|
||||
</p>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => void handleDelete()}
|
||||
disabled={isDeleting}
|
||||
icon={isDeleting ? Spinner : IconTrashCan}
|
||||
>
|
||||
{isDeleting ? __("Deleting...") : __("Delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Icon")}</Th>
|
||||
<Th>{__("Title")}</Th>
|
||||
<Th>{__("Description")}</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{commitments.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={4} className="text-center text-txt-secondary">
|
||||
{__("No commitment cards yet")}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{commitments.map(commitment => (
|
||||
<CompliancePageCommitmentListItem
|
||||
key={commitment.id}
|
||||
fragmentRef={commitment}
|
||||
onEdit={c => commitmentDialogRef.current?.openEdit(c)}
|
||||
onChanged={onChanged}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
{group.canCreateCommitment && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={IconPlusLarge}
|
||||
onClick={() => commitmentDialogRef.current?.openCreate(group.id)}
|
||||
>
|
||||
{__("Add Commitment")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<CompliancePageCommitmentDialog ref={commitmentDialogRef} onChanged={onChanged} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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 { Badge, Button, IconPencil, IconTrashCan, Spinner, Td, Tr } from "@probo/ui";
|
||||
import { useFragment } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { CompliancePageCommitmentListItemDeleteMutation } from "#/__generated__/core/CompliancePageCommitmentListItemDeleteMutation.graphql";
|
||||
import type { CompliancePageCommitmentListItemFragment$data, CompliancePageCommitmentListItemFragment$key } from "#/__generated__/core/CompliancePageCommitmentListItemFragment.graphql";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
|
||||
import { COMMITMENT_ICON_LABELS } from "../_lib/commitmentIcons";
|
||||
|
||||
const deleteCommitmentMutation = graphql`
|
||||
mutation CompliancePageCommitmentListItemDeleteMutation(
|
||||
$input: DeleteCompliancePortalCommitmentInput!
|
||||
) {
|
||||
deleteCompliancePortalCommitment(input: $input) {
|
||||
deletedCompliancePortalCommitmentId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const fragment = graphql`
|
||||
fragment CompliancePageCommitmentListItemFragment on CompliancePortalCommitment {
|
||||
id
|
||||
icon
|
||||
eyebrow
|
||||
title
|
||||
description
|
||||
canUpdate: permission(action: "core:compliance-portal-commitment:update")
|
||||
canDelete: permission(action: "core:compliance-portal-commitment:delete")
|
||||
}
|
||||
`;
|
||||
|
||||
export function CompliancePageCommitmentListItem(props: {
|
||||
fragmentRef: CompliancePageCommitmentListItemFragment$key;
|
||||
onEdit: (commitment: CompliancePageCommitmentListItemFragment$data) => void;
|
||||
onChanged: () => void;
|
||||
}) {
|
||||
const { fragmentRef, onEdit, onChanged } = props;
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const commitment = useFragment<CompliancePageCommitmentListItemFragment$key>(fragment, fragmentRef);
|
||||
|
||||
const [deleteCommitment, isDeleting] = useMutationWithToasts<CompliancePageCommitmentListItemDeleteMutation>(
|
||||
deleteCommitmentMutation,
|
||||
{ successMessage: __("Commitment deleted successfully"), errorMessage: __("Failed to delete commitment") },
|
||||
);
|
||||
|
||||
const handleDelete = async () => {
|
||||
await deleteCommitment({
|
||||
variables: { input: { id: commitment.id } },
|
||||
onSuccess: onChanged,
|
||||
});
|
||||
};
|
||||
|
||||
const iconLabel = COMMITMENT_ICON_LABELS[commitment.icon];
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td>
|
||||
<Badge variant="neutral">{iconLabel}</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col">
|
||||
{commitment.eyebrow && (
|
||||
<span className="text-xs text-txt-tertiary">{commitment.eyebrow}</span>
|
||||
)}
|
||||
<span className="font-medium">{commitment.title}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<span className="text-txt-secondary line-clamp-2">{commitment.description}</span>
|
||||
</Td>
|
||||
<Td noLink width={120} className="text-end">
|
||||
<div className="flex gap-2 justify-end">
|
||||
{commitment.canUpdate && (
|
||||
<Button variant="secondary" icon={IconPencil} onClick={() => onEdit(commitment)} />
|
||||
)}
|
||||
{commitment.canDelete && (
|
||||
<Button
|
||||
variant="danger"
|
||||
icon={isDeleting ? Spinner : IconTrashCan}
|
||||
disabled={isDeleting}
|
||||
onClick={() => void handleDelete()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// 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 { CompliancePortalCommitmentIcon } from "#/__generated__/core/CompliancePageCommitmentDialogCreateMutation.graphql";
|
||||
|
||||
// The curated icon set mirrors coredata.CompliancePortalCommitmentIcon and the
|
||||
// Phosphor icons the compliance portal renders for each value.
|
||||
export const COMMITMENT_ICON_VALUES: CompliancePortalCommitmentIcon[] = [
|
||||
"LOCK_KEY",
|
||||
"EYE_SLASH",
|
||||
"FINGERPRINT",
|
||||
"SHIELD_WARNING",
|
||||
"SHIELD_CHECK",
|
||||
"SIREN",
|
||||
"KEY",
|
||||
"LOCK",
|
||||
"CLOUD",
|
||||
"DATABASE",
|
||||
"GLOBE",
|
||||
"EYE",
|
||||
"USERS",
|
||||
"CERTIFICATE",
|
||||
"GAVEL",
|
||||
"HEARTBEAT",
|
||||
"BELL",
|
||||
"BUG",
|
||||
"CODE",
|
||||
"SERVER",
|
||||
];
|
||||
|
||||
// Human-readable label for each icon value, shown in the console picker.
|
||||
export const COMMITMENT_ICON_LABELS: Record<CompliancePortalCommitmentIcon, string> = {
|
||||
LOCK_KEY: "Lock & key",
|
||||
EYE_SLASH: "Eye slash",
|
||||
FINGERPRINT: "Fingerprint",
|
||||
SHIELD_WARNING: "Shield warning",
|
||||
SHIELD_CHECK: "Shield check",
|
||||
SIREN: "Siren",
|
||||
KEY: "Key",
|
||||
LOCK: "Lock",
|
||||
CLOUD: "Cloud",
|
||||
DATABASE: "Database",
|
||||
GLOBE: "Globe",
|
||||
EYE: "Eye",
|
||||
USERS: "Users",
|
||||
CERTIFICATE: "Certificate",
|
||||
GAVEL: "Gavel",
|
||||
HEARTBEAT: "Heartbeat",
|
||||
BELL: "Bell",
|
||||
BUG: "Bug",
|
||||
CODE: "Code",
|
||||
SERVER: "Server",
|
||||
};
|
||||
@@ -52,6 +52,11 @@ export const compliancePageRoutes = [
|
||||
Fallback: LinkCardSkeleton,
|
||||
Component: lazy(() => import("#/pages/organizations/compliance-page/references/CompliancePageReferencesPageLoader")),
|
||||
},
|
||||
{
|
||||
path: "commitments",
|
||||
Fallback: LinkCardSkeleton,
|
||||
Component: lazy(() => import("#/pages/organizations/compliance-page/commitments/CompliancePageCommitmentsPageLoader")),
|
||||
},
|
||||
{
|
||||
path: "audits",
|
||||
Fallback: LinkCardSkeleton,
|
||||
|
||||
Reference in New Issue
Block a user