Rename State of Applicability to Statement of Applicability

Rename the entity across the full stack: database table
(states_of_applicability → statements_of_applicability), Go model,
GraphQL types, MCP specification, CLI commands, frontend components,
routes, and display labels. Includes a migration to rename the table
and its foreign key column.

Widen sidebar from 260px to 280px to fit the longer label.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-04-09 13:06:06 +02:00
parent 050073ab3c
commit 5fd580e995
48 changed files with 978 additions and 957 deletions

View File

@@ -34,15 +34,15 @@ import { useEffect, useMemo, useState } from "react";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { LinkedStatesOfApplicabilityCardFragment$key } from "#/__generated__/core/LinkedStatesOfApplicabilityCardFragment.graphql";
import type { LinkedStatementsOfApplicabilityCardFragment$key } from "#/__generated__/core/LinkedStatementsOfApplicabilityCardFragment.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import { LinkedStatesOfApplicabilityDialog } from "./LinkedStatesOfApplicabilityDialog";
import { LinkedStatementsOfApplicabilityDialog } from "./LinkedStatementsOfApplicabilityDialog";
const linkedStateOfApplicabilityFragment = graphql`
fragment LinkedStatesOfApplicabilityCardFragment on ApplicabilityStatement {
const linkedStatementOfApplicabilityFragment = graphql`
fragment LinkedStatementsOfApplicabilityCardFragment on ApplicabilityStatement {
id
stateOfApplicability {
statementOfApplicability {
id
name
}
@@ -57,7 +57,7 @@ const linkedStateOfApplicabilityFragment = graphql`
type AttachMutation<Params> = (p: {
variables: {
input: {
stateOfApplicabilityId: string;
statementOfApplicabilityId: string;
applicability: boolean;
justification: string | null;
} & Params;
@@ -68,7 +68,7 @@ type AttachMutation<Params> = (p: {
type DetachMutation = (p: {
variables: {
input: {
stateOfApplicabilityId: string;
statementOfApplicabilityId: string;
controlId: string;
};
connections: string[];
@@ -76,7 +76,7 @@ type DetachMutation = (p: {
}) => void;
type Props<Params> = {
statesOfApplicability: readonly (LinkedStatesOfApplicabilityCardFragment$key & {
statementsOfApplicability: readonly (LinkedStatementsOfApplicabilityCardFragment$key & {
id: string;
})[];
params: Params;
@@ -88,7 +88,7 @@ type Props<Params> = {
readOnly?: boolean;
};
export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>) {
const { __ } = useTranslate();
const [limit, setLimit] = useState<number | null>(
@@ -96,30 +96,30 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
);
const [linkedInfo, setLinkedInfo] = useState<
{ stateOfApplicabilityId: string; controlId: string }[]
{ statementOfApplicabilityId: string; controlId: string }[]
>([]);
const statesOfApplicability = useMemo(() => {
const statementsOfApplicability = useMemo(() => {
return limit
? props.statesOfApplicability.slice(0, limit)
: props.statesOfApplicability;
}, [props.statesOfApplicability, limit]);
? props.statementsOfApplicability.slice(0, limit)
: props.statementsOfApplicability;
}, [props.statementsOfApplicability, limit]);
const showMoreButton
= limit !== null && props.statesOfApplicability.length > limit;
= limit !== null && props.statementsOfApplicability.length > limit;
const variant = props.variant ?? "table";
const linkedData = linkedInfo;
const onAttach = (
stateOfApplicabilityId: string,
statementOfApplicabilityId: string,
applicability: boolean,
justification: string | null,
) => {
props.onAttach({
variables: {
input: {
stateOfApplicabilityId,
statementOfApplicabilityId,
applicability,
justification,
...props.params,
@@ -129,11 +129,11 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
});
};
const onDetach = (stateOfApplicabilityId: string, controlId: string) => {
const onDetach = (statementOfApplicabilityId: string, controlId: string) => {
props.onDetach({
variables: {
input: {
stateOfApplicabilityId,
statementOfApplicabilityId,
controlId,
},
connections: [props.connectionId],
@@ -145,7 +145,7 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
return (
<Wrapper padded className="space-y-[10px]">
{props.statesOfApplicability.map((soa, idx) => (
{props.statementsOfApplicability.map((soa, idx) => (
<LinkedInfoExtractor
key={idx}
fragment={soa}
@@ -153,8 +153,8 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
setLinkedInfo((prev) => {
const exists = prev.some(
p =>
p.stateOfApplicabilityId
=== info.stateOfApplicabilityId
p.statementOfApplicabilityId
=== info.statementOfApplicabilityId
&& p.controlId === info.controlId,
);
return exists ? prev : [...prev, info];
@@ -165,20 +165,20 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
{variant === "card" && (
<div className="flex justify-between">
<div className="text-lg font-semibold">
{__("States of Applicability")}
{__("Statements of Applicability")}
</div>
{!props.readOnly && (
<LinkedStatesOfApplicabilityDialog
<LinkedStatementsOfApplicabilityDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedStatesOfApplicability={linkedData}
linkedStatementsOfApplicability={linkedData}
onLink={onAttach}
onUnlink={onDetach}
>
<Button variant="tertiary" icon={IconPlusLarge}>
{__("Link state of applicability")}
{__("Link statement of applicability")}
</Button>
</LinkedStatesOfApplicabilityDialog>
</LinkedStatementsOfApplicabilityDialog>
)}
</div>
)}
@@ -192,36 +192,36 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
</Tr>
</Thead>
<Tbody>
{statesOfApplicability.length === 0 && (
{statementsOfApplicability.length === 0 && (
<Tr>
<Td
colSpan={props.readOnly ? 3 : 4}
className="text-center text-txt-secondary"
>
{__("No states of applicability linked")}
{__("No statements of applicability linked")}
</Td>
</Tr>
)}
{statesOfApplicability.map(soa => (
<StateOfApplicabilityRow
{statementsOfApplicability.map(soa => (
<StatementOfApplicabilityRow
key={soa.id}
stateOfApplicability={soa}
statementOfApplicability={soa}
onClick={onDetach}
readOnly={props.readOnly}
/>
))}
{variant === "table" && !props.readOnly && (
<LinkedStatesOfApplicabilityDialog
<LinkedStatementsOfApplicabilityDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedStatesOfApplicability={linkedData}
linkedStatementsOfApplicability={linkedData}
onLink={onAttach}
onUnlink={onDetach}
>
<TrButton colspan={4} icon={IconPlusLarge}>
{__("Link state of applicability")}
{__("Link statement of applicability")}
</TrButton>
</LinkedStatesOfApplicabilityDialog>
</LinkedStatementsOfApplicabilityDialog>
)}
</Tbody>
</Table>
@@ -233,7 +233,7 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
>
{sprintf(
__("Show %d more"),
props.statesOfApplicability.length - limit,
props.statementsOfApplicability.length - limit,
)}
</Button>
)}
@@ -242,48 +242,48 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
}
function LinkedInfoExtractor(props: {
fragment: LinkedStatesOfApplicabilityCardFragment$key;
fragment: LinkedStatementsOfApplicabilityCardFragment$key;
onExtracted: (info: {
stateOfApplicabilityId: string;
statementOfApplicabilityId: string;
controlId: string;
}) => void;
}) {
const { onExtracted, fragment } = props;
const data = useFragment(
linkedStateOfApplicabilityFragment,
linkedStatementOfApplicabilityFragment,
fragment,
);
useEffect(() => {
onExtracted({
stateOfApplicabilityId: data.stateOfApplicability.id,
statementOfApplicabilityId: data.statementOfApplicability.id,
controlId: data.control.id,
});
}, [data.stateOfApplicability.id, data.control.id, onExtracted]);
}, [data.statementOfApplicability.id, data.control.id, onExtracted]);
return null;
}
function StateOfApplicabilityRow(props: {
stateOfApplicability: LinkedStatesOfApplicabilityCardFragment$key & {
function StatementOfApplicabilityRow(props: {
statementOfApplicability: LinkedStatementsOfApplicabilityCardFragment$key & {
id: string;
};
onClick: (stateOfApplicabilityId: string, controlId: string) => void;
onClick: (statementOfApplicabilityId: string, controlId: string) => void;
readOnly?: boolean;
}) {
const soa = useFragment(
linkedStateOfApplicabilityFragment,
props.stateOfApplicability,
linkedStatementOfApplicabilityFragment,
props.statementOfApplicability,
);
const organizationId = useOrganizationId();
const { __ } = useTranslate();
return (
<Tr
to={`/organizations/${organizationId}/states-of-applicability/${soa.stateOfApplicability.id}`}
to={`/organizations/${organizationId}/statements-of-applicability/${soa.statementOfApplicability.id}`}
>
<Td>{soa.stateOfApplicability.name}</Td>
<Td>{soa.statementOfApplicability.name}</Td>
<Td>
<Badge variant={soa.applicability ? "success" : "danger"}>
{soa.applicability
@@ -298,7 +298,7 @@ function StateOfApplicabilityRow(props: {
variant="secondary"
onClick={() =>
props.onClick(
soa.stateOfApplicability.id,
soa.statementOfApplicability.id,
soa.control.id,
)}
icon={IconTrashCan}

View File

@@ -27,14 +27,14 @@ import { Suspense, useRef, useState } from "react";
import { useLazyLoadQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { LinkedStatesOfApplicabilityDialogQuery } from "#/__generated__/core/LinkedStatesOfApplicabilityDialogQuery.graphql";
import type { LinkedStatementsOfApplicabilityDialogQuery } from "#/__generated__/core/LinkedStatementsOfApplicabilityDialogQuery.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";
const query = graphql`
query LinkedStatesOfApplicabilityDialogQuery($organizationId: ID!) {
query LinkedStatementsOfApplicabilityDialogQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
statesOfApplicability(first: 100) {
statementsOfApplicability(first: 100) {
edges {
node {
id
@@ -48,7 +48,7 @@ const query = graphql`
`;
type LinkedSOAInfo = {
stateOfApplicabilityId: string;
statementOfApplicabilityId: string;
controlId: string;
};
@@ -56,16 +56,16 @@ type Props = {
children: ReactNode;
connectionId: string;
disabled?: boolean;
linkedStatesOfApplicability: readonly LinkedSOAInfo[];
linkedStatementsOfApplicability: readonly LinkedSOAInfo[];
onLink: (
stateOfApplicabilityId: string,
statementOfApplicabilityId: string,
applicability: boolean,
justification: string | null,
) => void;
onUnlink: (stateOfApplicabilityId: string, controlId: string) => void;
onUnlink: (statementOfApplicabilityId: string, controlId: string) => void;
};
export function LinkedStatesOfApplicabilityDialog({
export function LinkedStatementsOfApplicabilityDialog({
children,
...props
}: Props) {
@@ -75,10 +75,10 @@ export function LinkedStatesOfApplicabilityDialog({
<Dialog
ref={dialogRef}
trigger={children}
title="Link State of Applicability"
title="Link Statement of Applicability"
>
<Suspense fallback={<div>Loading...</div>}>
<LinkedStatesOfApplicabilityDialogContent
<LinkedStatementsOfApplicabilityDialogContent
{...props}
onClose={() => dialogRef.current?.close()}
/>
@@ -87,7 +87,7 @@ export function LinkedStatesOfApplicabilityDialog({
);
}
function LinkedStatesOfApplicabilityDialogContent(
function LinkedStatementsOfApplicabilityDialogContent(
props: Omit<Props, "children"> & { onClose: () => void },
) {
const { __ } = useTranslate();
@@ -99,7 +99,7 @@ function LinkedStatesOfApplicabilityDialogContent(
const [applicability, setApplicability] = useState(true);
const [justification, setJustification] = useState("");
const data = useLazyLoadQuery<LinkedStatesOfApplicabilityDialogQuery>(
const data = useLazyLoadQuery<LinkedStatementsOfApplicabilityDialogQuery>(
query,
{
organizationId,
@@ -108,18 +108,18 @@ function LinkedStatesOfApplicabilityDialogContent(
);
const linkedSOAIds = new Set(
props.linkedStatesOfApplicability.map(
soa => soa.stateOfApplicabilityId,
props.linkedStatementsOfApplicability.map(
soa => soa.statementOfApplicabilityId,
),
);
const linkedSOAMap = new Map(
props.linkedStatesOfApplicability.map(soa => [
soa.stateOfApplicabilityId,
props.linkedStatementsOfApplicability.map(soa => [
soa.statementOfApplicabilityId,
soa,
]),
);
const statesOfApplicability
= data.organization?.statesOfApplicability?.edges.map(
const statementsOfApplicability
= data.organization?.statementsOfApplicability?.edges.map(
edge => edge.node,
) ?? [];
@@ -140,11 +140,11 @@ function LinkedStatesOfApplicabilityDialogContent(
}
};
const handleUnlink = (stateOfApplicabilityId: string) => {
const linkedSOA = linkedSOAMap.get(stateOfApplicabilityId);
const handleUnlink = (statementOfApplicabilityId: string) => {
const linkedSOA = linkedSOAMap.get(statementOfApplicabilityId);
if (linkedSOA) {
props.onUnlink(
linkedSOA.stateOfApplicabilityId,
linkedSOA.statementOfApplicabilityId,
linkedSOA.controlId,
);
}
@@ -153,15 +153,15 @@ function LinkedStatesOfApplicabilityDialogContent(
return (
<>
<DialogContent padded className="space-y-4">
{statesOfApplicability.length === 0
{statementsOfApplicability.length === 0
? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="text-txt-secondary text-base mb-2">
{__("No states of applicability available")}
{__("No statements of applicability available")}
</div>
<div className="text-txt-tertiary text-sm">
{__(
"Create a state of applicability first to link it to this control",
"Create a statement of applicability first to link it to this control",
)}
</div>
</div>
@@ -170,9 +170,9 @@ function LinkedStatesOfApplicabilityDialogContent(
? (
<div className="space-y-2">
<div className="text-sm font-medium mb-2">
{__("Select a state of applicability:")}
{__("Select a statement of applicability:")}
</div>
{statesOfApplicability.map((soa) => {
{statementsOfApplicability.map((soa) => {
const isLinked = linkedSOAIds.has(soa.id);
return (
<div

View File

@@ -64,8 +64,8 @@ const fragment = graphql`
canListSnapshots: permission(action: "core:snapshot:list")
canGetTrustCenter: permission(action: "core:trust-center:get")
canUpdateOrganization: permission(action: "iam:organization:update")
canListStatesOfApplicability: permission(
action: "core:state-of-applicability:list"
canListStatementsOfApplicability: permission(
action: "core:statement-of-applicability:list"
)
canListAccessReviewCampaigns: permission(
action: "core:access-review-campaign:list"
@@ -183,11 +183,11 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
to={`${prefix}/processing-activities`}
/>
)}
{organization.canListStatesOfApplicability && (
{organization.canListStatementsOfApplicability && (
<SidebarItem
label={__("States of Applicability")}
label={__("Statements of Applicability")}
icon={IconPageCheck}
to={`${prefix}/states-of-applicability`}
to={`${prefix}/statements-of-applicability`}
/>
)}
{organization.canListRightsRequests && (

View File

@@ -42,56 +42,56 @@ import {
import { useNavigate, useParams } from "react-router";
import { z } from "zod";
import type { StateOfApplicabilityDetailPageDeleteMutation } from "#/__generated__/core/StateOfApplicabilityDetailPageDeleteMutation.graphql";
import type { StateOfApplicabilityDetailPageExportMutation } from "#/__generated__/core/StateOfApplicabilityDetailPageExportMutation.graphql";
import type { StateOfApplicabilityDetailPageQuery } from "#/__generated__/core/StateOfApplicabilityDetailPageQuery.graphql";
import type { StateOfApplicabilityDetailPageUpdateMutation } from "#/__generated__/core/StateOfApplicabilityDetailPageUpdateMutation.graphql";
import type { StatementOfApplicabilityDetailPageDeleteMutation } from "#/__generated__/core/StatementOfApplicabilityDetailPageDeleteMutation.graphql";
import type { StatementOfApplicabilityDetailPageExportMutation } from "#/__generated__/core/StatementOfApplicabilityDetailPageExportMutation.graphql";
import type { StatementOfApplicabilityDetailPageQuery } from "#/__generated__/core/StatementOfApplicabilityDetailPageQuery.graphql";
import type { StatementOfApplicabilityDetailPageUpdateMutation } from "#/__generated__/core/StatementOfApplicabilityDetailPageUpdateMutation.graphql";
import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { SnapshotBanner } from "#/components/SnapshotBanner";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import StateOfApplicabilityControlsTab from "./tabs/StateOfApplicabilityControlsTab";
import StatementOfApplicabilityControlsTab from "./tabs/StatementOfApplicabilityControlsTab";
export const stateOfApplicabilityDetailPageQuery = graphql`
query StateOfApplicabilityDetailPageQuery($stateOfApplicabilityId: ID!) {
node(id: $stateOfApplicabilityId) {
... on StateOfApplicability {
export const statementOfApplicabilityDetailPageQuery = graphql`
query StatementOfApplicabilityDetailPageQuery($statementOfApplicabilityId: ID!) {
node(id: $statementOfApplicabilityId) {
... on StatementOfApplicability {
id
name
snapshotId
createdAt
updatedAt
canUpdate: permission(action: "core:state-of-applicability:update")
canDelete: permission(action: "core:state-of-applicability:delete")
canExport: permission(action: "core:state-of-applicability:export")
canUpdate: permission(action: "core:statement-of-applicability:update")
canDelete: permission(action: "core:statement-of-applicability:delete")
canExport: permission(action: "core:statement-of-applicability:export")
owner {
id
fullName
}
...StateOfApplicabilityControlsTabFragment
...StatementOfApplicabilityControlsTabFragment
}
}
}
`;
const exportMutation = graphql`
mutation StateOfApplicabilityDetailPageExportMutation(
$input: ExportStateOfApplicabilityPDFInput!
mutation StatementOfApplicabilityDetailPageExportMutation(
$input: ExportStatementOfApplicabilityPDFInput!
) {
exportStateOfApplicabilityPDF(input: $input) {
exportStatementOfApplicabilityPDF(input: $input) {
data
}
}
`;
const updateMutation = graphql`
mutation StateOfApplicabilityDetailPageUpdateMutation(
$input: UpdateStateOfApplicabilityInput!
mutation StatementOfApplicabilityDetailPageUpdateMutation(
$input: UpdateStatementOfApplicabilityInput!
) {
updateStateOfApplicability(input: $input) {
stateOfApplicability {
updateStatementOfApplicability(input: $input) {
statementOfApplicability {
id
name
sourceId
@@ -108,74 +108,75 @@ const updateMutation = graphql`
`;
const deleteMutation = graphql`
mutation StateOfApplicabilityDetailPageDeleteMutation(
$input: DeleteStateOfApplicabilityInput!
mutation StatementOfApplicabilityDetailPageDeleteMutation(
$input: DeleteStatementOfApplicabilityInput!
$connections: [ID!]!
) {
deleteStateOfApplicability(input: $input) {
deletedStateOfApplicabilityId @deleteEdge(connections: $connections)
deleteStatementOfApplicability(input: $input) {
deletedStatementOfApplicabilityId @deleteEdge(connections: $connections)
}
}
`;
const StateOfApplicabilityConnectionKey = "StatesOfApplicabilityPage_statesOfApplicability";
const StatementOfApplicabilityConnectionKey = "StatementsOfApplicabilityPage_statementsOfApplicability";
type Props = {
queryRef: PreloadedQuery<StateOfApplicabilityDetailPageQuery>;
queryRef: PreloadedQuery<StatementOfApplicabilityDetailPageQuery>;
};
export default function StateOfApplicabilityDetailPage(props: Props) {
const { stateOfApplicabilityId, snapshotId } = useParams<{
stateOfApplicabilityId: string;
export default function StatementOfApplicabilityDetailPage(props: Props) {
const { statementOfApplicabilityId, snapshotId } = useParams<{
statementOfApplicabilityId: string;
snapshotId?: string;
}>();
const organizationId = useOrganizationId();
const data = usePreloadedQuery(stateOfApplicabilityDetailPageQuery, props.queryRef);
const stateOfApplicability = data.node;
const data = usePreloadedQuery(statementOfApplicabilityDetailPageQuery, props.queryRef);
const statementOfApplicability = data.node;
const { __ } = useTranslate();
const navigate = useNavigate();
const isSnapshotMode = Boolean(snapshotId);
const confirm = useConfirm();
const { toast } = useToast();
if (!stateOfApplicabilityId || !stateOfApplicability) {
if (!statementOfApplicabilityId || !statementOfApplicability) {
throw new Error(
"Cannot load state of applicability detail page without stateOfApplicabilityId parameter",
"Cannot load statement of applicability detail page without statementOfApplicabilityId parameter",
);
}
validateSnapshotConsistency(stateOfApplicability, snapshotId);
validateSnapshotConsistency(statementOfApplicability, snapshotId);
const connectionId = ConnectionHandler.getConnectionID(
organizationId,
StateOfApplicabilityConnectionKey,
StatementOfApplicabilityConnectionKey,
{ filter: { snapshotId: snapshotId ?? null } },
);
const [deleteStateOfApplicability] = useMutation<StateOfApplicabilityDetailPageDeleteMutation>(deleteMutation);
const [deleteStatementOfApplicability]
= useMutation<StatementOfApplicabilityDetailPageDeleteMutation>(deleteMutation);
const handleDelete = () => {
if (!stateOfApplicability.id || !stateOfApplicability.name) {
return alert(__("Failed to delete state of applicability: missing id or name"));
if (!statementOfApplicability.id || !statementOfApplicability.name) {
return alert(__("Failed to delete statement of applicability: missing id or name"));
}
confirm(
() =>
promisifyMutation(deleteStateOfApplicability)({
promisifyMutation(deleteStatementOfApplicability)({
variables: {
input: {
stateOfApplicabilityId: stateOfApplicability.id!,
statementOfApplicabilityId: statementOfApplicability.id!,
},
connections: [connectionId],
},
})
.then(() => {
void navigate(`/organizations/${organizationId}/states-of-applicability`);
void navigate(`/organizations/${organizationId}/statements-of-applicability`);
})
.catch((error) => {
toast({
title: __("Error"),
description: formatError(
__("Failed to delete state of applicability"),
__("Failed to delete statement of applicability"),
error as GraphQLError,
),
variant: "error",
@@ -186,52 +187,53 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
__(
"This will permanently delete \"%s\". This action cannot be undone.",
),
stateOfApplicability.name,
statementOfApplicability.name,
),
},
);
};
usePageTitle(stateOfApplicability.name || __("State of Applicability"));
usePageTitle(statementOfApplicability.name || __("Statement of Applicability"));
const [isEditingName, setIsEditingName] = useState(false);
const [isEditingOwner, setIsEditingOwner] = useState(false);
const [updateStateOfApplicability, isUpdating] = useMutationWithToasts<StateOfApplicabilityDetailPageUpdateMutation>(
updateMutation,
{
successMessage: __("State of Applicability updated successfully."),
errorMessage: __("Failed to update State of Applicability"),
},
);
const [updateStatementOfApplicability, isUpdating]
= useMutationWithToasts<StatementOfApplicabilityDetailPageUpdateMutation>(
updateMutation,
{
successMessage: __("Statement of Applicability updated successfully."),
errorMessage: __("Failed to update Statement of Applicability"),
},
);
const canUpdate = !isSnapshotMode && stateOfApplicability.canUpdate;
const canDelete = !isSnapshotMode && stateOfApplicability.canDelete;
const canUpdate = !isSnapshotMode && statementOfApplicability.canUpdate;
const canDelete = !isSnapshotMode && statementOfApplicability.canDelete;
const [exportStateOfApplicabilityPDF, isExporting]
= useMutationWithToasts<StateOfApplicabilityDetailPageExportMutation>(
const [exportStatementOfApplicabilityPDF, isExporting]
= useMutationWithToasts<StatementOfApplicabilityDetailPageExportMutation>(
exportMutation,
{
successMessage: __(
"State of Applicability exported successfully.",
"Statement of Applicability exported successfully.",
),
errorMessage: __("Failed to export State of Applicability"),
errorMessage: __("Failed to export Statement of Applicability"),
},
);
const handleExport = async () => {
if (!stateOfApplicability.id) return;
if (!statementOfApplicability.id) return;
await exportStateOfApplicabilityPDF({
await exportStatementOfApplicabilityPDF({
variables: {
input: {
stateOfApplicabilityId: stateOfApplicability.id,
statementOfApplicabilityId: statementOfApplicability.id,
},
},
onCompleted: (data) => {
if (data.exportStateOfApplicabilityPDF?.data) {
if (data.exportStatementOfApplicabilityPDF?.data) {
const link = window.document.createElement("a");
link.href = data.exportStateOfApplicabilityPDF.data;
link.download = `${stateOfApplicability.name || "state-of-applicability"}.pdf`;
link.href = data.exportStatementOfApplicabilityPDF.data;
link.download = `${statementOfApplicability.name || "statement-of-applicability"}.pdf`;
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
@@ -254,7 +256,7 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
reset: resetName,
} = useFormWithSchema(nameSchema, {
defaultValues: {
name: stateOfApplicability.name || "",
name: statementOfApplicability.name || "",
},
});
@@ -264,17 +266,17 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
reset: resetOwner,
} = useFormWithSchema(ownerSchema, {
defaultValues: {
ownerId: stateOfApplicability.owner?.id || "",
ownerId: statementOfApplicability.owner?.id || "",
},
});
const handleUpdateName = handleSubmitName(async (data) => {
if (!stateOfApplicability.id) return;
if (!statementOfApplicability.id) return;
await updateStateOfApplicability({
await updateStatementOfApplicability({
variables: {
input: {
id: stateOfApplicability.id,
id: statementOfApplicability.id,
name: data.name,
},
},
@@ -286,12 +288,12 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
});
const handleUpdateOwner = handleSubmitOwner(async (data) => {
if (!stateOfApplicability.id) return;
if (!statementOfApplicability.id) return;
await updateStateOfApplicability({
await updateStatementOfApplicability({
variables: {
input: {
id: stateOfApplicability.id,
id: statementOfApplicability.id,
ownerId: data.ownerId,
},
},
@@ -305,20 +307,20 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
const handleCancelNameEdit = () => {
setIsEditingName(false);
resetName({
name: stateOfApplicability.name || "",
name: statementOfApplicability.name || "",
});
};
const handleCancelOwnerEdit = () => {
setIsEditingOwner(false);
resetOwner({
ownerId: stateOfApplicability.owner?.id || "",
ownerId: statementOfApplicability.owner?.id || "",
});
};
const listUrl = snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/states-of-applicability`
: `/organizations/${organizationId}/states-of-applicability`;
? `/organizations/${organizationId}/snapshots/${snapshotId}/statements-of-applicability`
: `/organizations/${organizationId}/statements-of-applicability`;
return (
<div className="space-y-6">
@@ -326,13 +328,13 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
<Breadcrumb
items={[
{
label: __("States of Applicability"),
label: __("Statements of Applicability"),
to: listUrl,
},
{
label:
stateOfApplicability.name
|| __("State of Applicability detail"),
statementOfApplicability.name
|| __("Statement of Applicability detail"),
},
]}
/>
@@ -371,7 +373,7 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
)
: (
<div className="flex items-center gap-2">
<span>{stateOfApplicability.name || ""}</span>
<span>{statementOfApplicability.name || ""}</span>
{canUpdate && (
<Button
variant="quaternary"
@@ -383,7 +385,7 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
)
}
>
{stateOfApplicability.canExport && (
{statementOfApplicability.canExport && (
<Button
variant="secondary"
icon={IconArrowDown}
@@ -445,7 +447,7 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
: (
<div className="flex items-center gap-2">
<div className="text-sm text-txt-primary">
{stateOfApplicability.owner
{statementOfApplicability.owner
?.fullName || "-"}
</div>
{canUpdate && (
@@ -466,7 +468,7 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
{__("Created at")}
</div>
<div className="text-sm text-txt-primary">
{formatDate(stateOfApplicability.createdAt)}
{formatDate(statementOfApplicability.createdAt)}
</div>
</div>
<div>
@@ -474,21 +476,21 @@ export default function StateOfApplicabilityDetailPage(props: Props) {
{__("Updated at")}
</div>
<div className="text-sm text-txt-primary">
{formatDate(stateOfApplicability.updatedAt)}
{formatDate(statementOfApplicability.updatedAt)}
</div>
</div>
</div>
</Card>
</div>
{stateOfApplicability.id && (
{statementOfApplicability.id && (
<div className="space-y-4">
<h2 className="text-base font-medium">
{__("Statements")}
</h2>
<StateOfApplicabilityControlsTab
stateOfApplicability={
stateOfApplicability as typeof stateOfApplicability & {
<StatementOfApplicabilityControlsTab
statementOfApplicability={
statementOfApplicability as typeof statementOfApplicability & {
id: string;
}
}

View File

@@ -16,21 +16,21 @@ import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { useParams } from "react-router";
import type { StateOfApplicabilityDetailPageQuery } from "#/__generated__/core/StateOfApplicabilityDetailPageQuery.graphql";
import type { StatementOfApplicabilityDetailPageQuery } from "#/__generated__/core/StatementOfApplicabilityDetailPageQuery.graphql";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import StateOfApplicabilityDetailPage, { stateOfApplicabilityDetailPageQuery } from "./StateOfApplicabilityDetailPage";
import StatementOfApplicabilityDetailPage, { statementOfApplicabilityDetailPageQuery } from "./StatementOfApplicabilityDetailPage";
export default function StateOfApplicabilityDetailPageLoader() {
const { stateOfApplicabilityId } = useParams<{ stateOfApplicabilityId: string }>();
export default function StatementOfApplicabilityDetailPageLoader() {
const { statementOfApplicabilityId } = useParams<{ statementOfApplicabilityId: string }>();
const [queryRef, loadQuery]
= useQueryLoader<StateOfApplicabilityDetailPageQuery>(stateOfApplicabilityDetailPageQuery);
= useQueryLoader<StatementOfApplicabilityDetailPageQuery>(statementOfApplicabilityDetailPageQuery);
useEffect(() => {
if (stateOfApplicabilityId) {
loadQuery({ stateOfApplicabilityId });
if (statementOfApplicabilityId) {
loadQuery({ statementOfApplicabilityId });
}
}, [loadQuery, stateOfApplicabilityId]);
}, [loadQuery, statementOfApplicabilityId]);
if (!queryRef) {
return <PageSkeleton />;
@@ -38,7 +38,7 @@ export default function StateOfApplicabilityDetailPageLoader() {
return (
<Suspense fallback={<PageSkeleton />}>
<StateOfApplicabilityDetailPage queryRef={queryRef} />
<StatementOfApplicabilityDetailPage queryRef={queryRef} />
</Suspense>
);
}

View File

@@ -34,86 +34,86 @@ import {
} from "react-relay";
import { useParams } from "react-router";
import type { StatesOfApplicabilityPageFragment$key } from "#/__generated__/core/StatesOfApplicabilityPageFragment.graphql";
import type { StatesOfApplicabilityPagePaginationQuery } from "#/__generated__/core/StatesOfApplicabilityPagePaginationQuery.graphql";
import type { StatesOfApplicabilityPageQuery } from "#/__generated__/core/StatesOfApplicabilityPageQuery.graphql";
import type { StatementsOfApplicabilityPageFragment$key } from "#/__generated__/core/StatementsOfApplicabilityPageFragment.graphql";
import type { StatementsOfApplicabilityPagePaginationQuery } from "#/__generated__/core/StatementsOfApplicabilityPagePaginationQuery.graphql";
import type { StatementsOfApplicabilityPageQuery } from "#/__generated__/core/StatementsOfApplicabilityPageQuery.graphql";
import { SnapshotBanner } from "#/components/SnapshotBanner";
import { StateOfApplicabilityRow } from "./_components/StateOfApplicabilityRow";
import { CreateStateOfApplicabilityDialog } from "./dialogs/CreateStateOfApplicabilityDialog";
import { StatementOfApplicabilityRow } from "./_components/StatementOfApplicabilityRow";
import { CreateStatementOfApplicabilityDialog } from "./dialogs/CreateStatementOfApplicabilityDialog";
export const statesOfApplicabilityPageQuery = graphql`
query StatesOfApplicabilityPageQuery($organizationId: ID!) {
export const statementsOfApplicabilityPageQuery = graphql`
query StatementsOfApplicabilityPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
__typename
... on Organization {
id
canCreateStateOfApplicability: permission(action: "core:state-of-applicability:create")
...StatesOfApplicabilityPageFragment
canCreateStatementOfApplicability: permission(action: "core:statement-of-applicability:create")
...StatementsOfApplicabilityPageFragment
}
}
}
`;
const paginatedFragment = graphql`
fragment StatesOfApplicabilityPageFragment on Organization
@refetchable(queryName: "StatesOfApplicabilityPagePaginationQuery")
fragment StatementsOfApplicabilityPageFragment on Organization
@refetchable(queryName: "StatementsOfApplicabilityPagePaginationQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 50 }
order: {
type: "StateOfApplicabilityOrder"
type: "StatementOfApplicabilityOrder"
defaultValue: { direction: DESC, field: CREATED_AT }
}
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
filter: { type: "StateOfApplicabilityFilter", defaultValue: { snapshotId: null } }
filter: { type: "StatementOfApplicabilityFilter", defaultValue: { snapshotId: null } }
) {
statesOfApplicability(
statementsOfApplicability(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
filter: $filter
) @connection(key: "StatesOfApplicabilityPage_statesOfApplicability", filters: ["filter"]) {
) @connection(key: "StatementsOfApplicabilityPage_statementsOfApplicability", filters: ["filter"]) {
__id
edges {
node {
id
...StateOfApplicabilityRowFragment
...StatementOfApplicabilityRowFragment
}
}
}
}
`;
export default function StatesOfApplicabilityPage({
export default function StatementsOfApplicabilityPage({
queryRef,
}: {
queryRef: PreloadedQuery<StatesOfApplicabilityPageQuery>;
queryRef: PreloadedQuery<StatementsOfApplicabilityPageQuery>;
}) {
const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
usePageTitle(__("States of Applicability"));
usePageTitle(__("Statements of Applicability"));
const { organization } = usePreloadedQuery(statesOfApplicabilityPageQuery, queryRef);
const { organization } = usePreloadedQuery(statementsOfApplicabilityPageQuery, queryRef);
if (organization.__typename !== "Organization") {
throw new Error("Organization not found");
}
const {
data: { statesOfApplicability },
data: { statementsOfApplicability },
loadNext,
hasNext,
refetch,
isLoadingNext,
} = usePaginationFragment<
StatesOfApplicabilityPagePaginationQuery,
StatesOfApplicabilityPageFragment$key
StatementsOfApplicabilityPagePaginationQuery,
StatementsOfApplicabilityPageFragment$key
>(paginatedFragment, organization);
useEffect(() => {
@@ -129,24 +129,24 @@ export default function StatesOfApplicabilityPage({
<div className="space-y-6">
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
<PageHeader
title={__("States of Applicability")}
title={__("Statements of Applicability")}
description={__(
"Manage states of applicability for your organization's frameworks.",
"Manage statements of applicability for your organization's frameworks.",
)}
>
{!isSnapshotMode
&& organization.canCreateStateOfApplicability && (
<CreateStateOfApplicabilityDialog
connectionId={statesOfApplicability.__id}
&& organization.canCreateStatementOfApplicability && (
<CreateStatementOfApplicabilityDialog
connectionId={statementsOfApplicability.__id}
>
<Button icon={IconPlusLarge}>
{__("Add state of applicability")}
{__("Add statement of applicability")}
</Button>
</CreateStateOfApplicabilityDialog>
</CreateStatementOfApplicabilityDialog>
)}
</PageHeader>
{statesOfApplicability && statesOfApplicability.edges.length > 0
{statementsOfApplicability && statementsOfApplicability.edges.length > 0
? (
<Card>
<Table>
@@ -158,11 +158,11 @@ export default function StatesOfApplicabilityPage({
</Tr>
</Thead>
<Tbody>
{statesOfApplicability.edges.map(edge => (
<StateOfApplicabilityRow
{statementsOfApplicability.edges.map(edge => (
<StatementOfApplicabilityRow
key={edge.node.id}
fKey={edge.node}
connectionId={statesOfApplicability.__id}
connectionId={statementsOfApplicability.__id}
/>
))}
</Tbody>
@@ -187,11 +187,11 @@ export default function StatesOfApplicabilityPage({
<Card padded>
<div className="text-center py-12">
<h3 className="text-lg font-semibold mb-2">
{__("No states of applicability yet")}
{__("No statements of applicability yet")}
</h3>
<p className="text-txt-tertiary mb-4">
{__(
"Create your first state of applicability to get started.",
"Create your first statement of applicability to get started.",
)}
</p>
</div>

View File

@@ -15,16 +15,16 @@
import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay";
import type { StatesOfApplicabilityPageQuery } from "#/__generated__/core/StatesOfApplicabilityPageQuery.graphql";
import type { StatementsOfApplicabilityPageQuery } from "#/__generated__/core/StatementsOfApplicabilityPageQuery.graphql";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId";
import StatesOfApplicabilityPage, { statesOfApplicabilityPageQuery } from "./StatesOfApplicabilityPage";
import StatementsOfApplicabilityPage, { statementsOfApplicabilityPageQuery } from "./StatementsOfApplicabilityPage";
export default function StatesOfApplicabilityPageLoader() {
export default function StatementsOfApplicabilityPageLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery]
= useQueryLoader<StatesOfApplicabilityPageQuery>(statesOfApplicabilityPageQuery);
= useQueryLoader<StatementsOfApplicabilityPageQuery>(statementsOfApplicabilityPageQuery);
useEffect(() => {
loadQuery({ organizationId });
@@ -36,7 +36,7 @@ export default function StatesOfApplicabilityPageLoader() {
return (
<Suspense fallback={<PageSkeleton />}>
<StatesOfApplicabilityPage queryRef={queryRef} />
<StatementsOfApplicabilityPage queryRef={queryRef} />
</Suspense>
);
}

View File

@@ -27,16 +27,16 @@ import { useFragment, useMutation } from "react-relay";
import { useParams } from "react-router";
import { graphql } from "relay-runtime";
import type { StateOfApplicabilityRowDeleteMutation } from "#/__generated__/core/StateOfApplicabilityRowDeleteMutation.graphql";
import type { StateOfApplicabilityRowFragment$key } from "#/__generated__/core/StateOfApplicabilityRowFragment.graphql";
import type { StatementOfApplicabilityRowDeleteMutation } from "#/__generated__/core/StatementOfApplicabilityRowDeleteMutation.graphql";
import type { StatementOfApplicabilityRowFragment$key } from "#/__generated__/core/StatementOfApplicabilityRowFragment.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";
const fragment = graphql`
fragment StateOfApplicabilityRowFragment on StateOfApplicability {
fragment StatementOfApplicabilityRowFragment on StatementOfApplicability {
id
name
createdAt
canDelete: permission(action: "core:state-of-applicability:delete")
canDelete: permission(action: "core:statement-of-applicability:delete")
statementsInfo: applicabilityStatements {
totalCount
}
@@ -44,22 +44,22 @@ const fragment = graphql`
`;
const deleteMutation = graphql`
mutation StateOfApplicabilityRowDeleteMutation(
$input: DeleteStateOfApplicabilityInput!
mutation StatementOfApplicabilityRowDeleteMutation(
$input: DeleteStatementOfApplicabilityInput!
$connections: [ID!]!
) {
deleteStateOfApplicability(input: $input) {
deletedStateOfApplicabilityId @deleteEdge(connections: $connections)
deleteStatementOfApplicability(input: $input) {
deletedStatementOfApplicabilityId @deleteEdge(connections: $connections)
}
}
`;
type Props = {
fKey: StateOfApplicabilityRowFragment$key;
fKey: StatementOfApplicabilityRowFragment$key;
connectionId: string;
};
export function StateOfApplicabilityRow({ fKey, connectionId }: Props) {
export function StatementOfApplicabilityRow({ fKey, connectionId }: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
@@ -67,21 +67,21 @@ export function StateOfApplicabilityRow({ fKey, connectionId }: Props) {
const { toast } = useToast();
const isSnapshotMode = Boolean(snapshotId);
const stateOfApplicability = useFragment(fragment, fKey);
const canDelete = !isSnapshotMode && stateOfApplicability.canDelete;
const statementOfApplicability = useFragment(fragment, fKey);
const canDelete = !isSnapshotMode && statementOfApplicability.canDelete;
const [deleteStateOfApplicability] = useMutation<StateOfApplicabilityRowDeleteMutation>(deleteMutation);
const [deleteStatementOfApplicability] = useMutation<StatementOfApplicabilityRowDeleteMutation>(deleteMutation);
const handleDelete = () => {
if (!stateOfApplicability.id || !stateOfApplicability.name) {
return alert(__("Failed to delete state of applicability: missing id or name"));
if (!statementOfApplicability.id || !statementOfApplicability.name) {
return alert(__("Failed to delete statement of applicability: missing id or name"));
}
confirm(
() =>
promisifyMutation(deleteStateOfApplicability)({
promisifyMutation(deleteStatementOfApplicability)({
variables: {
input: {
stateOfApplicabilityId: stateOfApplicability.id,
statementOfApplicabilityId: statementOfApplicability.id,
},
connections: [connectionId],
},
@@ -89,7 +89,7 @@ export function StateOfApplicabilityRow({ fKey, connectionId }: Props) {
toast({
title: __("Error"),
description: formatError(
__("Failed to delete state of applicability"),
__("Failed to delete statement of applicability"),
error as GraphQLError,
),
variant: "error",
@@ -100,25 +100,25 @@ export function StateOfApplicabilityRow({ fKey, connectionId }: Props) {
__(
"This will permanently delete \"%s\". This action cannot be undone.",
),
stateOfApplicability.name,
statementOfApplicability.name,
),
},
);
};
const detailUrl = snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/states-of-applicability/${stateOfApplicability.id}`
: `/organizations/${organizationId}/states-of-applicability/${stateOfApplicability.id}`;
? `/organizations/${organizationId}/snapshots/${snapshotId}/statements-of-applicability/${statementOfApplicability.id}`
: `/organizations/${organizationId}/statements-of-applicability/${statementOfApplicability.id}`;
return (
<Tr to={detailUrl}>
<Td>{stateOfApplicability.name}</Td>
<Td>{statementOfApplicability.name}</Td>
<Td>
<time dateTime={stateOfApplicability.createdAt}>
{formatDate(stateOfApplicability.createdAt)}
<time dateTime={statementOfApplicability.createdAt}>
{formatDate(statementOfApplicability.createdAt)}
</time>
</Td>
<Td>{stateOfApplicability.statementsInfo?.totalCount ?? 0}</Td>
<Td>{statementOfApplicability.statementsInfo?.totalCount ?? 0}</Td>
{canDelete && (
<Td noLink width={50} className="text-end">
<ActionDropdown>

View File

@@ -38,9 +38,9 @@ import type { AddApplicabilityStatementDialogQuery } from "#/__generated__/core/
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const query = graphql`
query AddApplicabilityStatementDialogQuery($stateOfApplicabilityId: ID!, $organizationId: ID!) {
stateOfApplicability: node(id: $stateOfApplicabilityId) {
... on StateOfApplicability {
query AddApplicabilityStatementDialogQuery($statementOfApplicabilityId: ID!, $organizationId: ID!) {
statementOfApplicability: node(id: $statementOfApplicabilityId) {
... on StatementOfApplicability {
id
applicabilityStatements(first: 10000) {
edges {
@@ -138,7 +138,7 @@ const updateApplicabilityStatementMutation = graphql`
`;
export type AddApplicabilityStatementDialogRef = {
open: (stateOfApplicabilityId: string, organizationId: string, connectionId: string) => void;
open: (statementOfApplicabilityId: string, organizationId: string, connectionId: string) => void;
};
type ControlWithStatement = {
@@ -154,11 +154,11 @@ type ControlWithStatement = {
function ControlRow({
control,
stateOfApplicabilityId,
statementOfApplicabilityId,
connectionId,
}: {
control: ControlWithStatement;
stateOfApplicabilityId: string;
statementOfApplicabilityId: string;
connectionId: string;
}) {
const { __ } = useTranslate();
@@ -228,7 +228,7 @@ function ControlRow({
await createApplicabilityStatement({
variables: {
input: {
stateOfApplicabilityId,
statementOfApplicabilityId,
controlId: control.controlId,
applicability: true,
justification: null,
@@ -263,7 +263,7 @@ function ControlRow({
await createApplicabilityStatement({
variables: {
input: {
stateOfApplicabilityId,
statementOfApplicabilityId,
controlId: control.controlId,
applicability: false,
justification: justification || null,
@@ -333,11 +333,11 @@ function ControlRow({
}
function AddApplicabilityStatementDialogContent({
stateOfApplicabilityId,
statementOfApplicabilityId,
organizationId,
connectionId,
}: {
stateOfApplicabilityId: string;
statementOfApplicabilityId: string;
organizationId: string;
connectionId: string;
}) {
@@ -346,7 +346,7 @@ function AddApplicabilityStatementDialogContent({
const [collapsedFrameworks, setCollapsedFrameworks] = useState<Set<string>>(new Set());
const data = useLazyLoadQuery<AddApplicabilityStatementDialogQuery>(
query,
{ stateOfApplicabilityId, organizationId },
{ statementOfApplicabilityId, organizationId },
{ fetchPolicy: "store-or-network" },
);
@@ -355,7 +355,7 @@ function AddApplicabilityStatementDialogContent({
string,
{ id: string; applicability: boolean; justification: string | null }
>();
data.stateOfApplicability?.applicabilityStatements?.edges.forEach((edge) => {
data.statementOfApplicability?.applicabilityStatements?.edges.forEach((edge) => {
map.set(edge.node.control.id, {
id: edge.node.id,
applicability: edge.node.applicability,
@@ -363,7 +363,7 @@ function AddApplicabilityStatementDialogContent({
});
});
return map;
}, [data.stateOfApplicability?.applicabilityStatements]);
}, [data.statementOfApplicability?.applicabilityStatements]);
const allControls = useMemo(() => {
return (data.organization?.controls?.edges || []).map((edge) => {
@@ -459,7 +459,7 @@ function AddApplicabilityStatementDialogContent({
<ControlRow
key={control.controlId}
control={control}
stateOfApplicabilityId={stateOfApplicabilityId}
statementOfApplicabilityId={statementOfApplicabilityId}
connectionId={connectionId}
/>
))}
@@ -488,7 +488,7 @@ export const AddApplicabilityStatementDialog = forwardRef<
({ onClose }, ref) => {
const { __ } = useTranslate();
const dialogRef = useDialogRef();
const [stateOfApplicabilityId, setStateOfApplicabilityId] = useState<string | null>(null);
const [statementOfApplicabilityId, setStatementOfApplicabilityId] = useState<string | null>(null);
const [organizationId, setOrganizationId] = useState<string | null>(null);
const [connectionId, setConnectionId] = useState<string | null>(null);
@@ -496,7 +496,7 @@ export const AddApplicabilityStatementDialog = forwardRef<
ref,
() => ({
open: (soaId: string, orgId: string, connId: string) => {
setStateOfApplicabilityId(soaId);
setStatementOfApplicabilityId(soaId);
setOrganizationId(orgId);
setConnectionId(connId);
dialogRef.current?.open();
@@ -506,7 +506,7 @@ export const AddApplicabilityStatementDialog = forwardRef<
);
const handleClose = () => {
setStateOfApplicabilityId(null);
setStatementOfApplicabilityId(null);
setOrganizationId(null);
setConnectionId(null);
onClose?.();
@@ -518,12 +518,12 @@ export const AddApplicabilityStatementDialog = forwardRef<
className="max-w-3xl"
title={(
<Breadcrumb
items={[__("States of Applicability"), __("Add Statement")]}
items={[__("Statements of Applicability"), __("Add Statement")]}
/>
)}
onClose={handleClose}
>
{stateOfApplicabilityId && organizationId && connectionId
{statementOfApplicabilityId && organizationId && connectionId
? (
<Suspense
fallback={(
@@ -536,7 +536,7 @@ export const AddApplicabilityStatementDialog = forwardRef<
)}
>
<AddApplicabilityStatementDialogContent
stateOfApplicabilityId={stateOfApplicabilityId}
statementOfApplicabilityId={statementOfApplicabilityId}
organizationId={organizationId}
connectionId={connectionId}
/>

View File

@@ -28,19 +28,19 @@ import { graphql } from "react-relay";
import { useNavigate } from "react-router";
import { z } from "zod";
import type { CreateStateOfApplicabilityDialogMutation } from "#/__generated__/core/CreateStateOfApplicabilityDialogMutation.graphql";
import type { CreateStatementOfApplicabilityDialogMutation } from "#/__generated__/core/CreateStatementOfApplicabilityDialogMutation.graphql";
import { PeopleSelectField } from "#/components/form/PeopleSelectField";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
const createMutation = graphql`
mutation CreateStateOfApplicabilityDialogMutation(
$input: CreateStateOfApplicabilityInput!
mutation CreateStatementOfApplicabilityDialogMutation(
$input: CreateStatementOfApplicabilityInput!
$connections: [ID!]!
) {
createStateOfApplicability(input: $input) {
stateOfApplicabilityEdge @prependEdge(connections: $connections) {
createStatementOfApplicability(input: $input) {
statementOfApplicabilityEdge @prependEdge(connections: $connections) {
node {
id
name
@@ -48,8 +48,8 @@ const createMutation = graphql`
snapshotId
createdAt
updatedAt
canDelete: permission(action: "core:state-of-applicability:delete")
...StateOfApplicabilityRowFragment
canDelete: permission(action: "core:statement-of-applicability:delete")
...StatementOfApplicabilityRowFragment
}
}
}
@@ -66,7 +66,7 @@ const schema = z.object({
ownerId: z.string().min(1),
});
export function CreateStateOfApplicabilityDialog({
export function CreateStatementOfApplicabilityDialog({
children,
connectionId,
}: Props) {
@@ -84,19 +84,19 @@ export function CreateStateOfApplicabilityDialog({
);
const ref = useDialogRef();
const [createStateOfApplicability, isCreating]
= useMutationWithToasts<CreateStateOfApplicabilityDialogMutation>(
const [createStatementOfApplicability, isCreating]
= useMutationWithToasts<CreateStatementOfApplicabilityDialogMutation>(
createMutation,
{
successMessage: __(
"State of applicability created successfully.",
"Statement of applicability created successfully.",
),
errorMessage: __("Failed to create state of applicability"),
errorMessage: __("Failed to create statement of applicability"),
},
);
const onSubmit = async (data: z.infer<typeof schema>) => {
await createStateOfApplicability({
await createStatementOfApplicability({
variables: {
input: {
name: data.name,
@@ -108,11 +108,11 @@ export function CreateStateOfApplicabilityDialog({
onCompleted: (response) => {
reset();
ref.current?.close();
const stateOfApplicabilityId
= response.createStateOfApplicability.stateOfApplicabilityEdge
const statementOfApplicabilityId
= response.createStatementOfApplicability.statementOfApplicabilityEdge
.node.id;
void navigate(
`/organizations/${organizationId}/states-of-applicability/${stateOfApplicabilityId}`,
`/organizations/${organizationId}/statements-of-applicability/${statementOfApplicabilityId}`,
);
},
});
@@ -125,8 +125,8 @@ export function CreateStateOfApplicabilityDialog({
title={(
<Breadcrumb
items={[
__("States of Applicability"),
__("New State of Applicability"),
__("Statements of Applicability"),
__("New Statement of Applicability"),
]}
/>
)}

View File

@@ -121,7 +121,7 @@ export const EditControlDialog = forwardRef<EditControlDialogRef>((_props, ref)
ref={dialogRef}
className="max-w-lg"
title={
<Breadcrumb items={[__("States of Applicability"), __("Edit Statement")]} />
<Breadcrumb items={[__("Statements of Applicability"), __("Edit Statement")]} />
}
>
{control

View File

@@ -38,9 +38,9 @@ import type { LinkControlDialogQuery } from "#/__generated__/core/LinkControlDia
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
const linkControlQuery = graphql`
query LinkControlDialogQuery($stateOfApplicabilityId: ID!, $organizationId: ID!) {
stateOfApplicability: node(id: $stateOfApplicabilityId) {
... on StateOfApplicability {
query LinkControlDialogQuery($statementOfApplicabilityId: ID!, $organizationId: ID!) {
statementOfApplicability: node(id: $statementOfApplicabilityId) {
... on StatementOfApplicability {
id
applicabilityStatements(first: 10000) {
edges {
@@ -103,7 +103,7 @@ const unlinkControlMutation = graphql`
`;
export type LinkControlDialogRef = {
open: (stateOfApplicabilityId: string, organizationId: string, onUpdate?: () => void) => void;
open: (statementOfApplicabilityId: string, organizationId: string, onUpdate?: () => void) => void;
};
type Control = {
@@ -119,12 +119,12 @@ type Control = {
function ControlRow({
control,
stateOfApplicabilityId,
statementOfApplicabilityId,
isLinked,
onUpdate,
}: {
control: Control;
stateOfApplicabilityId: string;
statementOfApplicabilityId: string;
isLinked: boolean;
onUpdate?: () => void;
}) {
@@ -169,7 +169,7 @@ function ControlRow({
await linkMutate({
variables: {
input: {
stateOfApplicabilityId,
statementOfApplicabilityId,
controlId: control.controlId,
applicability: true,
justification: null,
@@ -189,7 +189,7 @@ function ControlRow({
await linkMutate({
variables: {
input: {
stateOfApplicabilityId,
statementOfApplicabilityId,
controlId: control.controlId,
applicability: false,
justification: justification || null,
@@ -259,11 +259,11 @@ function ControlRow({
}
function LinkControlDialogContent({
stateOfApplicabilityId,
statementOfApplicabilityId,
organizationId,
onUpdate,
}: {
stateOfApplicabilityId: string;
statementOfApplicabilityId: string;
organizationId: string;
onUpdate?: () => void;
}) {
@@ -272,10 +272,10 @@ function LinkControlDialogContent({
const [collapsedFrameworks, setCollapsedFrameworks] = useState<Set<string>>(new Set());
const data = useLazyLoadQuery<LinkControlDialogQuery>(
linkControlQuery,
{ stateOfApplicabilityId, organizationId },
{ statementOfApplicabilityId, organizationId },
{ fetchPolicy: "store-or-network" },
) as {
stateOfApplicability: {
statementOfApplicability: {
id: string;
applicabilityStatements?: {
edges: Array<{
@@ -309,7 +309,7 @@ function LinkControlDialogContent({
// Build a map of control ID -> applicability statement
const applicabilityMap = useMemo(() => {
const map = new Map<string, { id: string; applicability: boolean; justification: string | null }>();
data.stateOfApplicability?.applicabilityStatements?.edges.forEach((edge) => {
data.statementOfApplicability?.applicabilityStatements?.edges.forEach((edge) => {
map.set(edge.node.control.id, {
id: edge.node.id,
applicability: edge.node.applicability,
@@ -317,7 +317,7 @@ function LinkControlDialogContent({
});
});
return map;
}, [data.stateOfApplicability?.applicabilityStatements]);
}, [data.statementOfApplicability?.applicabilityStatements]);
// Merge controls with applicability info
const allControls = useMemo(() => {
@@ -410,7 +410,7 @@ function LinkControlDialogContent({
<ControlRow
key={control.controlId}
control={control}
stateOfApplicabilityId={stateOfApplicabilityId}
statementOfApplicabilityId={statementOfApplicabilityId}
isLinked={control.applicabilityStatementId !== null}
onUpdate={onUpdate}
/>
@@ -431,13 +431,13 @@ function LinkControlDialogContent({
export const LinkControlDialog = forwardRef<LinkControlDialogRef>((_props, ref) => {
const { __ } = useTranslate();
const dialogRef = useDialogRef();
const [stateOfApplicabilityId, setStateOfApplicabilityId] = useState<string | null>(null);
const [statementOfApplicabilityId, setStatementOfApplicabilityId] = useState<string | null>(null);
const [organizationId, setOrganizationId] = useState<string | null>(null);
const [onUpdateCallback, setOnUpdateCallback] = useState<(() => void) | undefined>(undefined);
useImperativeHandle(ref, () => ({
open: (soaId: string, orgId: string, callback?: () => void) => {
setStateOfApplicabilityId(soaId);
setStatementOfApplicabilityId(soaId);
setOrganizationId(orgId);
setOnUpdateCallback(() => callback);
dialogRef.current?.open();
@@ -445,7 +445,7 @@ export const LinkControlDialog = forwardRef<LinkControlDialogRef>((_props, ref)
}), [dialogRef]);
const handleClose = () => {
setStateOfApplicabilityId(null);
setStatementOfApplicabilityId(null);
setOrganizationId(null);
setOnUpdateCallback(undefined);
};
@@ -456,12 +456,12 @@ export const LinkControlDialog = forwardRef<LinkControlDialogRef>((_props, ref)
className="max-w-3xl"
title={(
<Breadcrumb
items={[__("States of Applicability"), __("Add Controls")]}
items={[__("Statements of Applicability"), __("Add Controls")]}
/>
)}
onClose={handleClose}
>
{stateOfApplicabilityId && organizationId
{statementOfApplicabilityId && organizationId
? (
<Suspense
fallback={(
@@ -471,7 +471,7 @@ export const LinkControlDialog = forwardRef<LinkControlDialogRef>((_props, ref)
)}
>
<LinkControlDialogContent
stateOfApplicabilityId={stateOfApplicabilityId}
statementOfApplicabilityId={statementOfApplicabilityId}
organizationId={organizationId}
onUpdate={onUpdateCallback}
/>

View File

@@ -32,7 +32,7 @@ import { Suspense, useCallback, useRef } from "react";
import { graphql, useFragment, useRelayEnvironment } from "react-relay";
import { fetchQuery } from "relay-runtime";
import type { StateOfApplicabilityControlsTabFragment$key } from "#/__generated__/core/StateOfApplicabilityControlsTabFragment.graphql";
import type { StatementOfApplicabilityControlsTabFragment$key } from "#/__generated__/core/StatementOfApplicabilityControlsTabFragment.graphql";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
import { useOrganizationId } from "#/hooks/useOrganizationId";
@@ -46,17 +46,17 @@ import {
} from "../dialogs/EditControlDialog";
const refetchStatementsQuery = graphql`
query StateOfApplicabilityControlsTabRefetchQuery($stateOfApplicabilityId: ID!) {
node(id: $stateOfApplicabilityId) {
... on StateOfApplicability {
...StateOfApplicabilityControlsTabFragment
query StatementOfApplicabilityControlsTabRefetchQuery($statementOfApplicabilityId: ID!) {
node(id: $statementOfApplicabilityId) {
... on StatementOfApplicability {
...StatementOfApplicabilityControlsTabFragment
}
}
}
`;
export const controlsFragment = graphql`
fragment StateOfApplicabilityControlsTabFragment on StateOfApplicability {
fragment StatementOfApplicabilityControlsTabFragment on StatementOfApplicability {
id
organization {
id
@@ -73,7 +73,7 @@ export const controlsFragment = graphql`
)
applicabilityStatements(first: 1000, orderBy: { direction: ASC, field: CONTROL_SECTION_TITLE })
@connection(key: "StateOfApplicabilityControlsTab_applicabilityStatements") {
@connection(key: "StatementOfApplicabilityControlsTab_applicabilityStatements") {
__id
edges {
node {
@@ -105,7 +105,7 @@ export const controlsFragment = graphql`
`;
const deleteApplicabilityStatementMutation = graphql`
mutation StateOfApplicabilityControlsTabDeleteMutation(
mutation StatementOfApplicabilityControlsTabDeleteMutation(
$input: DeleteApplicabilityStatementInput!
$connections: [ID!]!
) {
@@ -115,17 +115,17 @@ const deleteApplicabilityStatementMutation = graphql`
}
`;
export default function StateOfApplicabilityControlsTab({
stateOfApplicability,
export default function StatementOfApplicabilityControlsTab({
statementOfApplicability,
isSnapshotMode = false,
}: {
stateOfApplicability: StateOfApplicabilityControlsTabFragment$key & {
statementOfApplicability: StatementOfApplicabilityControlsTabFragment$key & {
id: string;
};
isSnapshotMode?: boolean;
}) {
const { __ } = useTranslate();
const data = useFragment(controlsFragment, stateOfApplicability);
const data = useFragment(controlsFragment, statementOfApplicability);
const environment = useRelayEnvironment();
const organizationId = useOrganizationId();
const addStatementDialogRef = useRef<AddApplicabilityStatementDialogRef>(null);
@@ -134,7 +134,7 @@ export default function StateOfApplicabilityControlsTab({
const handleDialogClose = useCallback(() => {
if (!data.id) return;
fetchQuery(environment, refetchStatementsQuery, {
stateOfApplicabilityId: data.id,
statementOfApplicabilityId: data.id,
}).subscribe({});
}, [environment, data.id]);

View File

@@ -45,7 +45,7 @@ import { processingActivityRoutes } from "./routes/processingActivityRoutes";
import { rightsRequestRoutes } from "./routes/rightsRequestRoutes";
import { riskRoutes } from "./routes/riskRoutes";
import { snapshotsRoutes } from "./routes/snapshotsRoutes";
import { statesOfApplicabilityRoutes } from "./routes/statesOfApplicabilityRoutes";
import { statementsOfApplicabilityRoutes } from "./routes/statementsOfApplicabilityRoutes";
import { taskRoutes } from "./routes/taskRoutes";
import { vendorRoutes } from "./routes/vendorRoutes";
@@ -288,7 +288,7 @@ const routes = [
...obligationRoutes,
...rightsRequestRoutes,
...processingActivityRoutes,
...statesOfApplicabilityRoutes,
...statementsOfApplicabilityRoutes,
...accessReviewRoutes,
...compliancePageRoutes,
...snapshotsRoutes,

View File

@@ -17,33 +17,33 @@ import type { AppRoute } from "@probo/routes";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
export const statesOfApplicabilityRoutes = [
export const statementsOfApplicabilityRoutes = [
{
path: "states-of-applicability",
path: "statements-of-applicability",
Fallback: PageSkeleton,
Component: lazy(
() => import("#/pages/organizations/states-of-applicability/StatesOfApplicabilityPageLoader"),
() => import("#/pages/organizations/statements-of-applicability/StatementsOfApplicabilityPageLoader"),
),
},
{
path: "snapshots/:snapshotId/states-of-applicability",
path: "snapshots/:snapshotId/statements-of-applicability",
Fallback: PageSkeleton,
Component: lazy(
() => import("#/pages/organizations/states-of-applicability/StatesOfApplicabilityPageLoader"),
() => import("#/pages/organizations/statements-of-applicability/StatementsOfApplicabilityPageLoader"),
),
},
{
path: "states-of-applicability/:stateOfApplicabilityId",
path: "statements-of-applicability/:statementOfApplicabilityId",
Fallback: PageSkeleton,
Component: lazy(
() => import("#/pages/organizations/states-of-applicability/StateOfApplicabilityDetailPageLoader"),
() => import("#/pages/organizations/statements-of-applicability/StatementOfApplicabilityDetailPageLoader"),
),
},
{
path: "snapshots/:snapshotId/states-of-applicability/:stateOfApplicabilityId",
path: "snapshots/:snapshotId/statements-of-applicability/:statementOfApplicabilityId",
Fallback: PageSkeleton,
Component: lazy(
() => import("#/pages/organizations/states-of-applicability/StateOfApplicabilityDetailPageLoader"),
() => import("#/pages/organizations/statements-of-applicability/StatementOfApplicabilityDetailPageLoader"),
),
},
] satisfies AppRoute[];