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:
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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"),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
@@ -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
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user