Add processing activity, DPIA and TIA publish to document system
Replace the old PDF/snapshot-based exports for processing activities, Data Protection Impact Assessments and Transfer Impact Assessments with the publish document system. Includes GraphQL mutations, MCP tools, CLI commands, n8n operations, frontend publish dialogs, e2e tests, and prosemirror register templates that mirror the previous PDF layouts. Each register lives as a generated DocumentTypeRegister document on the organization, reused across publishes (the major version bumps on every republish). Approvers can be passed in to create a draft pending approval; otherwise the version is published immediately. The frontend ProcessingActivities page exposes a Publish dropdown per register and a Document link button per active tab, pre-fills the previous default approvers, and navigates to the published document on success. Remove snapshot mode entirely from these three entities: drop snapshotId and sourceId from GraphQL schemas, types, filters, resolvers, MCP spec, frontend routes and pages; remove SnapshotsTypeProcessingActivities from the snapshot registry and delete the ProcessingActivities.Snapshot, ProcessingActivitySnapshotter interface and *.InsertProcessingActivitySnapshots methods. The snapshot_id columns remain in the database but are now filtered out with snapshot_id IS NULL. Add Get/Upsert/Clear GeneratedDocumentID methods on each entity type (ProcessingActivity, DataProtectionImpactAssessment, TransferImpactAssessment) backed by new columns in the generated_documents table, matching the Finding/Obligation pattern. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -29,27 +29,42 @@ export type ProcessingActivityDPIAResidualRisk = "LOW" | "MEDIUM" | "HIGH";
|
||||
export const processingActivitiesQuery = graphql`
|
||||
query ProcessingActivityGraphListQuery(
|
||||
$organizationId: ID!
|
||||
$snapshotId: ID
|
||||
) {
|
||||
node(id: $organizationId) {
|
||||
... on Organization {
|
||||
canCreateProcessingActivity: permission(
|
||||
action: "core:processing-activity:create"
|
||||
)
|
||||
canExportProcessingActivities: permission(
|
||||
action: "core:processing-activity:export"
|
||||
canPublishProcessingActivities: permission(
|
||||
action: "core:processing-activity:publish"
|
||||
)
|
||||
canExportDataProtectionImpactAssessments: permission(
|
||||
action: "core:data-protection-impact-assessment:export"
|
||||
canPublishDataProtectionImpactAssessments: permission(
|
||||
action: "core:data-protection-impact-assessment:publish"
|
||||
)
|
||||
canExportTransferImpactAssessments: permission(
|
||||
action: "core:transfer-impact-assessment:export"
|
||||
canPublishTransferImpactAssessments: permission(
|
||||
action: "core:transfer-impact-assessment:publish"
|
||||
)
|
||||
...ProcessingActivitiesPageFragment @arguments(snapshotId: $snapshotId)
|
||||
processingActivitiesDocument {
|
||||
id
|
||||
defaultApprovers {
|
||||
id
|
||||
}
|
||||
}
|
||||
dataProtectionImpactAssessmentsDocument {
|
||||
id
|
||||
defaultApprovers {
|
||||
id
|
||||
}
|
||||
}
|
||||
transferImpactAssessmentsDocument {
|
||||
id
|
||||
defaultApprovers {
|
||||
id
|
||||
}
|
||||
}
|
||||
...ProcessingActivitiesPageFragment
|
||||
...ProcessingActivitiesPageDPIAFragment
|
||||
@arguments(snapshotId: $snapshotId)
|
||||
...ProcessingActivitiesPageTIAFragment
|
||||
@arguments(snapshotId: $snapshotId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +75,6 @@ export const processingActivityNodeQuery = graphql`
|
||||
node(id: $processingActivityId) {
|
||||
... on ProcessingActivity {
|
||||
id
|
||||
snapshotId
|
||||
name
|
||||
purpose
|
||||
dataSubjectCategory
|
||||
|
||||
@@ -12,12 +12,7 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import {
|
||||
downloadFile,
|
||||
promisifyMutation,
|
||||
sprintf,
|
||||
toDateInput,
|
||||
} from "@probo/helpers";
|
||||
import { promisifyMutation, sprintf } from "@probo/helpers";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
@@ -25,14 +20,12 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
IconArrowDown,
|
||||
IconChevronDown,
|
||||
IconPageTextLine,
|
||||
IconPlusLarge,
|
||||
IconTrashCan,
|
||||
IconUpload,
|
||||
PageHeader,
|
||||
Spinner,
|
||||
TabItem,
|
||||
Table,
|
||||
Tabs,
|
||||
@@ -52,7 +45,7 @@ import {
|
||||
usePaginationFragment,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
|
||||
import type {
|
||||
ProcessingActivitiesPageDPIAFragment$data,
|
||||
@@ -68,8 +61,6 @@ import type {
|
||||
} from "#/__generated__/core/ProcessingActivitiesPageTIAFragment.graphql";
|
||||
import type { ProcessingActivityGraphDeleteMutation } from "#/__generated__/core/ProcessingActivityGraphDeleteMutation.graphql";
|
||||
import type { ProcessingActivityGraphListQuery } from "#/__generated__/core/ProcessingActivityGraphListQuery.graphql";
|
||||
import { SnapshotBanner } from "#/components/SnapshotBanner";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
import type { NodeOf } from "#/types";
|
||||
|
||||
@@ -84,6 +75,9 @@ import {
|
||||
} from "../../../hooks/graph/ProcessingActivityGraph";
|
||||
|
||||
import { CreateProcessingActivityDialog } from "./dialogs/CreateProcessingActivityDialog";
|
||||
import { PublishDataProtectionImpactAssessmentListDialog } from "./dialogs/PublishDataProtectionImpactAssessmentListDialog";
|
||||
import { PublishProcessingActivityListDialog } from "./dialogs/PublishProcessingActivityListDialog";
|
||||
import { PublishTransferImpactAssessmentListDialog } from "./dialogs/PublishTransferImpactAssessmentListDialog";
|
||||
|
||||
interface ProcessingActivitiesPageProps {
|
||||
queryRef: PreloadedQuery<ProcessingActivityGraphListQuery>;
|
||||
@@ -95,22 +89,15 @@ const processingActivitiesPageFragment = graphql`
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 10 }
|
||||
after: { type: "CursorKey" }
|
||||
snapshotId: { type: "ID", defaultValue: null }
|
||||
) {
|
||||
id
|
||||
processingActivities(
|
||||
first: $first
|
||||
after: $after
|
||||
filter: { snapshotId: $snapshotId }
|
||||
)
|
||||
processingActivities(first: $first, after: $after)
|
||||
@connection(
|
||||
key: "ProcessingActivitiesPage_processingActivities"
|
||||
filters: ["filter"]
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
snapshotId
|
||||
name
|
||||
purpose
|
||||
dataSubjectCategory
|
||||
@@ -139,17 +126,11 @@ const dpiaListPageFragment = graphql`
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 10 }
|
||||
after: { type: "CursorKey" }
|
||||
snapshotId: { type: "ID", defaultValue: null }
|
||||
) {
|
||||
id
|
||||
dataProtectionImpactAssessments(
|
||||
first: $first
|
||||
after: $after
|
||||
filter: { snapshotId: $snapshotId }
|
||||
)
|
||||
dataProtectionImpactAssessments(first: $first, after: $after)
|
||||
@connection(
|
||||
key: "ProcessingActivitiesPage_dataProtectionImpactAssessments"
|
||||
filters: ["filter"]
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
@@ -177,17 +158,11 @@ const tiaListPageFragment = graphql`
|
||||
@argumentDefinitions(
|
||||
first: { type: "Int", defaultValue: 10 }
|
||||
after: { type: "CursorKey" }
|
||||
snapshotId: { type: "ID", defaultValue: null }
|
||||
) {
|
||||
id
|
||||
transferImpactAssessments(
|
||||
first: $first
|
||||
after: $after
|
||||
filter: { snapshotId: $snapshotId }
|
||||
)
|
||||
transferImpactAssessments(first: $first, after: $after)
|
||||
@connection(
|
||||
key: "ProcessingActivitiesPage_transferImpactAssessments"
|
||||
filters: ["filter"]
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
@@ -209,43 +184,12 @@ const tiaListPageFragment = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const exportProcessingActivitiesPDFMutation = graphql`
|
||||
mutation ProcessingActivitiesPageExportPDFMutation(
|
||||
$input: ExportProcessingActivitiesPDFInput!
|
||||
) {
|
||||
exportProcessingActivitiesPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportDataProtectionImpactAssessmentsPDFMutation = graphql`
|
||||
mutation ProcessingActivitiesPageExportDPIAPDFMutation(
|
||||
$input: ExportDataProtectionImpactAssessmentsPDFInput!
|
||||
) {
|
||||
exportDataProtectionImpactAssessmentsPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportTransferImpactAssessmentsPDFMutation = graphql`
|
||||
mutation ProcessingActivitiesPageExportTIAPDFMutation(
|
||||
$input: ExportTransferImpactAssessmentsPDFInput!
|
||||
) {
|
||||
exportTransferImpactAssessmentsPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function ProcessingActivitiesPage({
|
||||
queryRef,
|
||||
}: ProcessingActivitiesPageProps) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState<"activities" | "dpia" | "tia">(
|
||||
"activities",
|
||||
);
|
||||
@@ -254,6 +198,17 @@ export default function ProcessingActivitiesPage({
|
||||
|
||||
const organization = usePreloadedQuery(processingActivitiesQuery, queryRef);
|
||||
|
||||
const paDocument = organization.node.processingActivitiesDocument;
|
||||
const dpiaDocument = organization.node.dataProtectionImpactAssessmentsDocument;
|
||||
const tiaDocument = organization.node.transferImpactAssessmentsDocument;
|
||||
const paDefaultApproverIds = (paDocument?.defaultApprovers ?? []).map(a => a.id);
|
||||
const dpiaDefaultApproverIds = (dpiaDocument?.defaultApprovers ?? []).map(a => a.id);
|
||||
const tiaDefaultApproverIds = (tiaDocument?.defaultApprovers ?? []).map(a => a.id);
|
||||
|
||||
const goToDocument = (documentId: string) => {
|
||||
void navigate(`/organizations/${organizationId}/documents/${documentId}`);
|
||||
};
|
||||
|
||||
const {
|
||||
data: activitiesData,
|
||||
loadNext: loadNextActivities,
|
||||
@@ -287,7 +242,6 @@ export default function ProcessingActivitiesPage({
|
||||
const connectionId = ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
ProcessingActivitiesConnectionKey,
|
||||
{ filter: { snapshotId: snapshotId || null } },
|
||||
);
|
||||
const activities
|
||||
= activitiesData?.processingActivities?.edges?.map(edge => edge.node)
|
||||
@@ -300,178 +254,24 @@ export default function ProcessingActivitiesPage({
|
||||
= tiaData?.transferImpactAssessments?.edges?.map(edge => edge.node)
|
||||
?? [];
|
||||
|
||||
const hasAnyAction
|
||||
= !isSnapshotMode
|
||||
&& activities.some(({ canUpdate, canDelete }) => canUpdate || canDelete);
|
||||
const hasAnyAction = activities.some(
|
||||
({ canUpdate, canDelete }) => canUpdate || canDelete,
|
||||
);
|
||||
|
||||
const canExportPDF = organization.node.canExportProcessingActivities;
|
||||
|
||||
const [exportPDF, isExportingPDF] = useMutationWithToasts<{
|
||||
response: {
|
||||
exportProcessingActivitiesPDF?: {
|
||||
data: string;
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: string;
|
||||
filter: { snapshotId: string } | null;
|
||||
};
|
||||
};
|
||||
}>(exportProcessingActivitiesPDFMutation, {
|
||||
successMessage: __("PDF download started."),
|
||||
errorMessage: __("Failed to generate PDF"),
|
||||
});
|
||||
|
||||
const handleExportPDF = async () => {
|
||||
await exportPDF({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organizationId,
|
||||
filter: snapshotId ? { snapshotId } : null,
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.exportProcessingActivitiesPDF?.data) {
|
||||
downloadFile(
|
||||
data.exportProcessingActivitiesPDF.data,
|
||||
`processing-activities-${toDateInput(new Date().toISOString())}.pdf`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const canExportDPIAPDF
|
||||
= organization.node.canExportDataProtectionImpactAssessments;
|
||||
|
||||
const [exportDPIAPDF, isExportingDPIAPDF] = useMutationWithToasts<{
|
||||
response: {
|
||||
exportDataProtectionImpactAssessmentsPDF?: {
|
||||
data: string;
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: string;
|
||||
filter: { snapshotId: string } | null;
|
||||
};
|
||||
};
|
||||
}>(exportDataProtectionImpactAssessmentsPDFMutation, {
|
||||
successMessage: __("PDF download started."),
|
||||
errorMessage: __("Failed to generate PDF"),
|
||||
});
|
||||
|
||||
const handleExportDPIAPDF = async () => {
|
||||
await exportDPIAPDF({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organizationId,
|
||||
filter: snapshotId ? { snapshotId } : null,
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.exportDataProtectionImpactAssessmentsPDF?.data) {
|
||||
downloadFile(
|
||||
data.exportDataProtectionImpactAssessmentsPDF.data,
|
||||
`data-protection-impact-assessments-${toDateInput(new Date().toISOString())}.pdf`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const canExportTIAPDF
|
||||
= organization.node.canExportTransferImpactAssessments;
|
||||
|
||||
const [exportTIAPDF, isExportingTIAPDF] = useMutationWithToasts<{
|
||||
response: {
|
||||
exportTransferImpactAssessmentsPDF?: {
|
||||
data: string;
|
||||
};
|
||||
};
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: string;
|
||||
filter: { snapshotId: string } | null;
|
||||
};
|
||||
};
|
||||
}>(exportTransferImpactAssessmentsPDFMutation, {
|
||||
successMessage: __("PDF download started."),
|
||||
errorMessage: __("Failed to generate PDF"),
|
||||
});
|
||||
|
||||
const handleExportTIAPDF = async () => {
|
||||
await exportTIAPDF({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId: organizationId,
|
||||
filter: snapshotId ? { snapshotId } : null,
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.exportTransferImpactAssessmentsPDF?.data) {
|
||||
downloadFile(
|
||||
data.exportTransferImpactAssessmentsPDF.data,
|
||||
`transfer-impact-assessments-${toDateInput(new Date().toISOString())}.pdf`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
const canPublishProcessingActivities
|
||||
= organization.node.canPublishProcessingActivities;
|
||||
const canPublishDPIA
|
||||
= organization.node.canPublishDataProtectionImpactAssessments;
|
||||
const canPublishTIA
|
||||
= organization.node.canPublishTransferImpactAssessments;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{isSnapshotMode && snapshotId && (
|
||||
<SnapshotBanner snapshotId={snapshotId} />
|
||||
)}
|
||||
<PageHeader
|
||||
title={__("Processing Activities")}
|
||||
description={__("Manage your processing activities under GDPR")}
|
||||
>
|
||||
{(canExportPDF || canExportDPIAPDF || canExportTIAPDF) && (
|
||||
<Dropdown
|
||||
toggle={(
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={IconArrowDown}
|
||||
iconAfter={IconChevronDown}
|
||||
>
|
||||
{__("Export")}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{canExportPDF && (
|
||||
<DropdownItem
|
||||
onClick={() => void handleExportPDF()}
|
||||
disabled={isExportingPDF}
|
||||
icon={isExportingPDF ? Spinner : undefined}
|
||||
>
|
||||
{__("Processing Activities")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{canExportDPIAPDF && (
|
||||
<DropdownItem
|
||||
onClick={() => void handleExportDPIAPDF()}
|
||||
disabled={isExportingDPIAPDF}
|
||||
icon={isExportingDPIAPDF ? Spinner : undefined}
|
||||
>
|
||||
{__("Data Protection Impact Assessments")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{canExportTIAPDF && (
|
||||
<DropdownItem
|
||||
onClick={() => void handleExportTIAPDF()}
|
||||
disabled={isExportingTIAPDF}
|
||||
icon={isExportingTIAPDF ? Spinner : undefined}
|
||||
>
|
||||
{__("Transfer Impact Assessments")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</Dropdown>
|
||||
)}
|
||||
{!isSnapshotMode
|
||||
&& activeTab === "activities"
|
||||
{activeTab === "activities"
|
||||
&& organization.node.canCreateProcessingActivity && (
|
||||
<CreateProcessingActivityDialog
|
||||
organizationId={organizationId}
|
||||
@@ -505,6 +305,84 @@ export default function ProcessingActivitiesPage({
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
{activeTab === "activities" && (
|
||||
<>
|
||||
{paDocument?.id && (
|
||||
<Button variant="secondary" asChild>
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/documents/${paDocument.id}`}
|
||||
>
|
||||
<IconPageTextLine size={16} />
|
||||
{__("Document")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{canPublishProcessingActivities && (
|
||||
<PublishProcessingActivityListDialog
|
||||
organizationId={organizationId}
|
||||
defaultApproverIds={paDefaultApproverIds}
|
||||
onPublished={goToDocument}
|
||||
>
|
||||
<Button variant="secondary" icon={IconUpload}>
|
||||
{__("Publish")}
|
||||
</Button>
|
||||
</PublishProcessingActivityListDialog>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeTab === "dpia" && (
|
||||
<>
|
||||
{dpiaDocument?.id && (
|
||||
<Button variant="secondary" asChild>
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/documents/${dpiaDocument.id}`}
|
||||
>
|
||||
<IconPageTextLine size={16} />
|
||||
{__("Document")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{canPublishDPIA && (
|
||||
<PublishDataProtectionImpactAssessmentListDialog
|
||||
organizationId={organizationId}
|
||||
defaultApproverIds={dpiaDefaultApproverIds}
|
||||
onPublished={goToDocument}
|
||||
>
|
||||
<Button variant="secondary" icon={IconUpload}>
|
||||
{__("Publish")}
|
||||
</Button>
|
||||
</PublishDataProtectionImpactAssessmentListDialog>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{activeTab === "tia" && (
|
||||
<>
|
||||
{tiaDocument?.id && (
|
||||
<Button variant="secondary" asChild>
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/documents/${tiaDocument.id}`}
|
||||
>
|
||||
<IconPageTextLine size={16} />
|
||||
{__("Document")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{canPublishTIA && (
|
||||
<PublishTransferImpactAssessmentListDialog
|
||||
organizationId={organizationId}
|
||||
defaultApproverIds={tiaDefaultApproverIds}
|
||||
onPublished={goToDocument}
|
||||
>
|
||||
<Button variant="secondary" icon={IconUpload}>
|
||||
{__("Publish")}
|
||||
</Button>
|
||||
</PublishTransferImpactAssessmentListDialog>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeTab === "activities" && (
|
||||
<>
|
||||
{activities.length > 0
|
||||
@@ -706,8 +584,6 @@ function ActivityRow({
|
||||
}) {
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
const [deleteActivity] = useMutation<ProcessingActivityGraphDeleteMutation>(deleteProcessingActivityMutation);
|
||||
const confirm = useConfirm();
|
||||
|
||||
@@ -734,9 +610,7 @@ function ActivityRow({
|
||||
};
|
||||
|
||||
const activityUrl
|
||||
= isSnapshotMode && snapshotId
|
||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/processing-activities/${activity.id}`
|
||||
: `/organizations/${organizationId}/processing-activities/${activity.id}`;
|
||||
= `/organizations/${organizationId}/processing-activities/${activity.id}`;
|
||||
|
||||
return (
|
||||
<Tr to={activityUrl}>
|
||||
@@ -790,13 +664,9 @@ function DPIARow({
|
||||
}) {
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
|
||||
const activityUrl
|
||||
= isSnapshotMode && snapshotId
|
||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/processing-activities/${dpia.processingActivity.id}#dpia`
|
||||
: `/organizations/${organizationId}/processing-activities/${dpia.processingActivity.id}#dpia`;
|
||||
= `/organizations/${organizationId}/processing-activities/${dpia.processingActivity.id}#dpia`;
|
||||
|
||||
return (
|
||||
<Tr to={activityUrl}>
|
||||
@@ -848,13 +718,9 @@ function TIARow({
|
||||
>;
|
||||
}) {
|
||||
const organizationId = useOrganizationId();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
|
||||
const activityUrl
|
||||
= isSnapshotMode && snapshotId
|
||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/processing-activities/${tia.processingActivity.id}#tia`
|
||||
: `/organizations/${organizationId}/processing-activities/${tia.processingActivity.id}#tia`;
|
||||
= `/organizations/${organizationId}/processing-activities/${tia.processingActivity.id}#tia`;
|
||||
|
||||
return (
|
||||
<Tr to={activityUrl}>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import {
|
||||
formatDatetime,
|
||||
toDateInput,
|
||||
validateSnapshotConsistency,
|
||||
} from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
@@ -43,13 +42,11 @@ import {
|
||||
type PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { ProcessingActivityGraphNodeQuery } from "#/__generated__/core/ProcessingActivityGraphNodeQuery.graphql";
|
||||
import { PeopleSelectField } from "#/components/form/PeopleSelectField";
|
||||
import { VendorsMultiSelectField } from "#/components/form/VendorsMultiSelectField";
|
||||
import { SnapshotBanner } from "#/components/SnapshotBanner";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
@@ -121,8 +118,6 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const organizationId = useOrganizationId();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
|
||||
// Get initial tab from URL hash
|
||||
const getInitialTab = (): "overview" | "dpia" | "tia" => {
|
||||
@@ -156,8 +151,6 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
window.location.hash = activeTab === "overview" ? "" : activeTab;
|
||||
}, [activeTab]);
|
||||
|
||||
validateSnapshotConsistency(activity, snapshotId);
|
||||
|
||||
const updateActivity = useUpdateProcessingActivity();
|
||||
const createDPIA = useCreateDataProtectionImpactAssessment();
|
||||
const updateDPIA = useUpdateDataProtectionImpactAssessment();
|
||||
@@ -199,7 +192,6 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
const connectionId = ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
ProcessingActivitiesConnectionKey,
|
||||
{ filter: { snapshotId: snapshotId || null } },
|
||||
);
|
||||
|
||||
const deleteActivity = useDeleteProcessingActivity(
|
||||
@@ -425,15 +417,10 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
});
|
||||
|
||||
const breadcrumbProcessingActivitiesUrl
|
||||
= isSnapshotMode && snapshotId
|
||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/processing-activities`
|
||||
: `/organizations/${organizationId}/processing-activities`;
|
||||
= `/organizations/${organizationId}/processing-activities`;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{isSnapshotMode && snapshotId && (
|
||||
<SnapshotBanner snapshotId={snapshotId} />
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
@@ -444,7 +431,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
{ label: activity.name! },
|
||||
]}
|
||||
/>
|
||||
{!isSnapshotMode && activity.canDelete && (
|
||||
{activity.canDelete && (
|
||||
<ActionDropdown>
|
||||
<DropdownItem onClick={deleteActivity} variant="danger">
|
||||
{__("Delete")}
|
||||
@@ -489,7 +476,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
{...register("name")}
|
||||
error={formState.errors.name?.message}
|
||||
required
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -508,7 +495,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="w-full"
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
>
|
||||
<RoleOptions />
|
||||
</Select>
|
||||
@@ -527,7 +514,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
{...register("purpose")}
|
||||
placeholder={__("Describe the purpose of processing")}
|
||||
rows={3}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -535,14 +522,14 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
label={__("Data Subject Category")}
|
||||
{...register("dataSubjectCategory")}
|
||||
placeholder={__("e.g., employees, customers, prospects")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={__("Personal Data Category")}
|
||||
{...register("personalDataCategory")}
|
||||
placeholder={__("e.g., contact details, financial data")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -563,7 +550,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="w-full"
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
>
|
||||
<SpecialOrCriminalDataOptions />
|
||||
</Select>
|
||||
@@ -580,7 +567,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
label={__("Consent Evidence Link")}
|
||||
{...register("consentEvidenceLink")}
|
||||
placeholder={__("Link to consent evidence if applicable")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -599,7 +586,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="w-full"
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
>
|
||||
<LawfulBasisOptions />
|
||||
</Select>
|
||||
@@ -620,7 +607,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
id="lastReviewDate"
|
||||
type="date"
|
||||
{...register("lastReviewDate")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -632,7 +619,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
id="nextReviewDate"
|
||||
type="date"
|
||||
{...register("nextReviewDate")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -641,7 +628,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
control={control}
|
||||
name="dataProtectionOfficerId"
|
||||
label={__("Data Protection Officer")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -650,14 +637,14 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
label={__("Recipients")}
|
||||
{...register("recipients")}
|
||||
placeholder={__("Who receives the data")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={__("Location")}
|
||||
{...register("location")}
|
||||
placeholder={__("Where is the data processed")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
@@ -670,7 +657,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
<Checkbox
|
||||
checked={field.value ?? false}
|
||||
onChange={field.onChange}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
<span>
|
||||
{__("Data is transferred internationally")}
|
||||
@@ -694,7 +681,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="w-full"
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
>
|
||||
<TransferSafeguardsOptions />
|
||||
</Select>
|
||||
@@ -711,7 +698,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
label={__("Retention Period")}
|
||||
{...register("retentionPeriod")}
|
||||
placeholder={__("How long is data retained")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -720,7 +707,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
{...register("securityMeasures")}
|
||||
placeholder={__("Technical and organizational measures")}
|
||||
rows={3}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -740,7 +727,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="w-full"
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
>
|
||||
<DataProtectionImpactAssessmentOptions />
|
||||
</Select>
|
||||
@@ -772,7 +759,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="w-full"
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
>
|
||||
<TransferImpactAssessmentOptions />
|
||||
</Select>
|
||||
@@ -796,24 +783,22 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
name="vendorIds"
|
||||
selectedVendors={vendors}
|
||||
label={__("Vendors")}
|
||||
disabled={isSnapshotMode || !activity.canUpdate}
|
||||
disabled={!activity.canUpdate}
|
||||
/>
|
||||
|
||||
{!isSnapshotMode && (
|
||||
<div className="flex justify-end pt-4">
|
||||
{activity.canUpdate && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
>
|
||||
{formState.isSubmitting
|
||||
? __("Saving...")
|
||||
: __("Save Changes")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end pt-4">
|
||||
{activity.canUpdate && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
>
|
||||
{formState.isSubmitting
|
||||
? __("Saving...")
|
||||
: __("Save Changes")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -829,7 +814,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
<h2 className="text-xl font-semibold mb-6 text-center">
|
||||
{__("Data Protection Impact Assessment")}
|
||||
</h2>
|
||||
{!isSnapshotMode && activity.canCreateDPIA && (
|
||||
{activity.canCreateDPIA && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setShowDpiaForm(true)}
|
||||
@@ -845,8 +830,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
<h2 className="text-xl font-semibold">
|
||||
{__("Data Protection Impact Assessment")}
|
||||
</h2>
|
||||
{!isSnapshotMode
|
||||
&& activity?.dataProtectionImpactAssessment?.id
|
||||
{activity?.dataProtectionImpactAssessment?.id
|
||||
&& !dpiaDeleted
|
||||
&& activity.dataProtectionImpactAssessment.canDelete && (
|
||||
<Button variant="danger" onClick={deleteDPIA}>
|
||||
@@ -867,7 +851,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Describe the processing activity and its purpose",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
|
||||
disabled={!canCreateOrUpdateDPIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -882,7 +866,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Explain why the processing is necessary and proportionate",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
|
||||
disabled={!canCreateOrUpdateDPIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -897,7 +881,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Describe the potential risks to data subjects",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
|
||||
disabled={!canCreateOrUpdateDPIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -912,7 +896,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Describe measures to mitigate the identified risks",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
|
||||
disabled={!canCreateOrUpdateDPIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -930,7 +914,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="w-full"
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
|
||||
disabled={!canCreateOrUpdateDPIA}
|
||||
>
|
||||
<Option value="LOW">{__("Low")}</Option>
|
||||
<Option value="MEDIUM">{__("Medium")}</Option>
|
||||
@@ -940,37 +924,35 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isSnapshotMode && (
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
{(!activity?.dataProtectionImpactAssessment?.id
|
||||
|| dpiaDeleted) && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setShowDpiaForm(false)}
|
||||
>
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{(activity?.dataProtectionImpactAssessment?.id
|
||||
&& !dpiaDeleted
|
||||
? activity.dataProtectionImpactAssessment.canUpdate
|
||||
: activity.canCreateDPIA) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={dpiaSubmitting}
|
||||
>
|
||||
{dpiaSubmitting
|
||||
? __("Saving...")
|
||||
: activity?.dataProtectionImpactAssessment?.id
|
||||
&& !dpiaDeleted
|
||||
? __("Update DPIA")
|
||||
: __("Create DPIA")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
{(!activity?.dataProtectionImpactAssessment?.id
|
||||
|| dpiaDeleted) && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setShowDpiaForm(false)}
|
||||
>
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{(activity?.dataProtectionImpactAssessment?.id
|
||||
&& !dpiaDeleted
|
||||
? activity.dataProtectionImpactAssessment.canUpdate
|
||||
: activity.canCreateDPIA) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={dpiaSubmitting}
|
||||
>
|
||||
{dpiaSubmitting
|
||||
? __("Saving...")
|
||||
: activity?.dataProtectionImpactAssessment?.id
|
||||
&& !dpiaDeleted
|
||||
? __("Update DPIA")
|
||||
: __("Create DPIA")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
@@ -988,7 +970,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
<h2 className="text-xl font-semibold mb-6 text-center">
|
||||
{__("Transfer Impact Assessment")}
|
||||
</h2>
|
||||
{!isSnapshotMode && activity.canCreateTIA && (
|
||||
{activity.canCreateTIA && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setShowTiaForm(true)}
|
||||
@@ -1004,8 +986,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
<h2 className="text-xl font-semibold">
|
||||
{__("Transfer Impact Assessment")}
|
||||
</h2>
|
||||
{!isSnapshotMode
|
||||
&& activity?.transferImpactAssessment?.id
|
||||
{activity?.transferImpactAssessment?.id
|
||||
&& !tiaDeleted
|
||||
&& activity.transferImpactAssessment.canDelete && (
|
||||
<Button variant="danger" onClick={deleteTIA}>
|
||||
@@ -1026,7 +1007,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Describe the data subjects involved in the transfer",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
|
||||
disabled={!canCreateOrUpdateTIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1041,7 +1022,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Describe the legal mechanism for the transfer (e.g., SCCs, BCRs, adequacy decision)",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
|
||||
disabled={!canCreateOrUpdateTIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1054,7 +1035,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Describe the nature and details of the data transfer",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
|
||||
disabled={!canCreateOrUpdateTIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1069,7 +1050,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Assess the risks related to the local laws of the destination country",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
|
||||
disabled={!canCreateOrUpdateTIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1084,40 +1065,38 @@ export default function ProcessingActivityDetailsPage(props: Props) {
|
||||
"Describe any supplementary measures taken to ensure adequate protection",
|
||||
)}
|
||||
rows={4}
|
||||
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
|
||||
disabled={!canCreateOrUpdateTIA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isSnapshotMode && (
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
{(!activity?.transferImpactAssessment?.id
|
||||
|| tiaDeleted) && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setShowTiaForm(false)}
|
||||
>
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{(activity?.transferImpactAssessment?.id && !tiaDeleted
|
||||
? activity.transferImpactAssessment.canUpdate
|
||||
: activity.canCreateTIA) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={tiaSubmitting}
|
||||
>
|
||||
{tiaSubmitting
|
||||
? __("Saving...")
|
||||
: activity?.transferImpactAssessment?.id
|
||||
&& !tiaDeleted
|
||||
? __("Update TIA")
|
||||
: __("Create TIA")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
{(!activity?.transferImpactAssessment?.id
|
||||
|| tiaDeleted) && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setShowTiaForm(false)}
|
||||
>
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{(activity?.transferImpactAssessment?.id && !tiaDeleted
|
||||
? activity.transferImpactAssessment.canUpdate
|
||||
: activity.canCreateTIA) && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={tiaSubmitting}
|
||||
>
|
||||
{tiaSubmitting
|
||||
? __("Saving...")
|
||||
: activity?.transferImpactAssessment?.id
|
||||
&& !tiaDeleted
|
||||
? __("Update TIA")
|
||||
: __("Create TIA")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
IconSend,
|
||||
IconUpload,
|
||||
useDialogRef,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PublishDataProtectionImpactAssessmentListDialogMutation } from "#/__generated__/core/PublishDataProtectionImpactAssessmentListDialogMutation.graphql";
|
||||
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
|
||||
const publishMutation = graphql`
|
||||
mutation PublishDataProtectionImpactAssessmentListDialogMutation(
|
||||
$input: PublishDataProtectionImpactAssessmentListInput!
|
||||
) {
|
||||
publishDataProtectionImpactAssessmentList(input: $input) {
|
||||
documentEdge {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
organizationId: string;
|
||||
defaultApproverIds?: string[];
|
||||
onPublished?: (documentId: string) => void;
|
||||
};
|
||||
|
||||
export function PublishDataProtectionImpactAssessmentListDialog({
|
||||
children,
|
||||
organizationId,
|
||||
defaultApproverIds,
|
||||
onPublished,
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
const schema = useMemo(() => z.object({
|
||||
approverIds: z.array(z.string()),
|
||||
}), []);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useFormWithSchema(schema, {
|
||||
defaultValues: {
|
||||
approverIds: defaultApproverIds ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const [publish, isPublishing]
|
||||
= useMutation<PublishDataProtectionImpactAssessmentListDialogMutation>(publishMutation);
|
||||
|
||||
const approverIds = watch("approverIds");
|
||||
const hasApprovers = approverIds.length > 0;
|
||||
|
||||
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||
publish({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId,
|
||||
approverIds: data.approverIds.length > 0 ? data.approverIds : undefined,
|
||||
},
|
||||
},
|
||||
onCompleted(response) {
|
||||
const documentId = response.publishDataProtectionImpactAssessmentList?.documentEdge?.node?.id;
|
||||
if (documentId) {
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: hasApprovers
|
||||
? __("Approval requested successfully.")
|
||||
: __("Data Protection Impact Assessments published successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
dialogRef.current?.close();
|
||||
reset();
|
||||
onPublished?.(documentId);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to publish Data Protection Impact Assessments"),
|
||||
error as GraphQLError,
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className="max-w-xl"
|
||||
ref={dialogRef}
|
||||
trigger={children}
|
||||
title={__("Publish Data Protection Impact Assessments")}
|
||||
>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent padded>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{__("Select approvers to request approval before publishing, or publish directly without approvers.")}
|
||||
</p>
|
||||
<PeopleMultiSelectField
|
||||
name="approverIds"
|
||||
label={__("Approvers")}
|
||||
control={control}
|
||||
organizationId={organizationId}
|
||||
placeholder={__("Add approvers...")}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
icon={hasApprovers ? IconSend : IconUpload}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
{hasApprovers ? __("Request approval") : __("Publish")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
IconSend,
|
||||
IconUpload,
|
||||
useDialogRef,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PublishProcessingActivityListDialogMutation } from "#/__generated__/core/PublishProcessingActivityListDialogMutation.graphql";
|
||||
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
|
||||
const publishMutation = graphql`
|
||||
mutation PublishProcessingActivityListDialogMutation(
|
||||
$input: PublishProcessingActivityListInput!
|
||||
) {
|
||||
publishProcessingActivityList(input: $input) {
|
||||
documentEdge {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
organizationId: string;
|
||||
defaultApproverIds?: string[];
|
||||
onPublished?: (documentId: string) => void;
|
||||
};
|
||||
|
||||
export function PublishProcessingActivityListDialog({
|
||||
children,
|
||||
organizationId,
|
||||
defaultApproverIds,
|
||||
onPublished,
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
const schema = useMemo(() => z.object({
|
||||
approverIds: z.array(z.string()),
|
||||
}), []);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useFormWithSchema(schema, {
|
||||
defaultValues: {
|
||||
approverIds: defaultApproverIds ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const [publish, isPublishing]
|
||||
= useMutation<PublishProcessingActivityListDialogMutation>(publishMutation);
|
||||
|
||||
const approverIds = watch("approverIds");
|
||||
const hasApprovers = approverIds.length > 0;
|
||||
|
||||
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||
publish({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId,
|
||||
approverIds: data.approverIds.length > 0 ? data.approverIds : undefined,
|
||||
},
|
||||
},
|
||||
onCompleted(response) {
|
||||
const documentId = response.publishProcessingActivityList?.documentEdge?.node?.id;
|
||||
if (documentId) {
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: hasApprovers
|
||||
? __("Approval requested successfully.")
|
||||
: __("Processing activities published successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
dialogRef.current?.close();
|
||||
reset();
|
||||
onPublished?.(documentId);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to publish processing activities"),
|
||||
error as GraphQLError,
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className="max-w-xl"
|
||||
ref={dialogRef}
|
||||
trigger={children}
|
||||
title={__("Publish Processing Activities")}
|
||||
>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent padded>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{__("Select approvers to request approval before publishing, or publish directly without approvers.")}
|
||||
</p>
|
||||
<PeopleMultiSelectField
|
||||
name="approverIds"
|
||||
label={__("Approvers")}
|
||||
control={control}
|
||||
organizationId={organizationId}
|
||||
placeholder={__("Add approvers...")}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
icon={hasApprovers ? IconSend : IconUpload}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
{hasApprovers ? __("Request approval") : __("Publish")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
IconSend,
|
||||
IconUpload,
|
||||
useDialogRef,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PublishTransferImpactAssessmentListDialogMutation } from "#/__generated__/core/PublishTransferImpactAssessmentListDialogMutation.graphql";
|
||||
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
|
||||
const publishMutation = graphql`
|
||||
mutation PublishTransferImpactAssessmentListDialogMutation(
|
||||
$input: PublishTransferImpactAssessmentListInput!
|
||||
) {
|
||||
publishTransferImpactAssessmentList(input: $input) {
|
||||
documentEdge {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
organizationId: string;
|
||||
defaultApproverIds?: string[];
|
||||
onPublished?: (documentId: string) => void;
|
||||
};
|
||||
|
||||
export function PublishTransferImpactAssessmentListDialog({
|
||||
children,
|
||||
organizationId,
|
||||
defaultApproverIds,
|
||||
onPublished,
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
const schema = useMemo(() => z.object({
|
||||
approverIds: z.array(z.string()),
|
||||
}), []);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useFormWithSchema(schema, {
|
||||
defaultValues: {
|
||||
approverIds: defaultApproverIds ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const [publish, isPublishing]
|
||||
= useMutation<PublishTransferImpactAssessmentListDialogMutation>(publishMutation);
|
||||
|
||||
const approverIds = watch("approverIds");
|
||||
const hasApprovers = approverIds.length > 0;
|
||||
|
||||
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||
publish({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId,
|
||||
approverIds: data.approverIds.length > 0 ? data.approverIds : undefined,
|
||||
},
|
||||
},
|
||||
onCompleted(response) {
|
||||
const documentId = response.publishTransferImpactAssessmentList?.documentEdge?.node?.id;
|
||||
if (documentId) {
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: hasApprovers
|
||||
? __("Approval requested successfully.")
|
||||
: __("Transfer Impact Assessments published successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
dialogRef.current?.close();
|
||||
reset();
|
||||
onPublished?.(documentId);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to publish Transfer Impact Assessments"),
|
||||
error as GraphQLError,
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className="max-w-xl"
|
||||
ref={dialogRef}
|
||||
trigger={children}
|
||||
title={__("Publish Transfer Impact Assessments")}
|
||||
>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent padded>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{__("Select approvers to request approval before publishing, or publish directly without approvers.")}
|
||||
</p>
|
||||
<PeopleMultiSelectField
|
||||
name="approverIds"
|
||||
label={__("Approvers")}
|
||||
control={control}
|
||||
organizationId={organizationId}
|
||||
placeholder={__("Add approvers...")}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
icon={hasApprovers ? IconSend : IconUpload}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
{hasApprovers ? __("Request approval") : __("Publish")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -39,27 +39,6 @@ export const processingActivityRoutes = [
|
||||
processingActivitiesQuery,
|
||||
{
|
||||
organizationId: organizationId,
|
||||
snapshotId: null,
|
||||
},
|
||||
),
|
||||
),
|
||||
Component: withQueryRef(
|
||||
lazy(
|
||||
() =>
|
||||
import("#/pages/organizations/processingActivities/ProcessingActivitiesPage"),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "snapshots/:snapshotId/processing-activities",
|
||||
Fallback: PageSkeleton,
|
||||
loader: loaderFromQueryLoader(({ organizationId, snapshotId }) =>
|
||||
loadQuery<ProcessingActivityGraphListQuery>(
|
||||
coreEnvironment,
|
||||
processingActivitiesQuery,
|
||||
{
|
||||
organizationId: organizationId,
|
||||
snapshotId,
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -89,23 +68,4 @@ export const processingActivityRoutes = [
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "snapshots/:snapshotId/processing-activities/:activityId",
|
||||
Fallback: PageSkeleton,
|
||||
loader: loaderFromQueryLoader(({ activityId }) =>
|
||||
loadQuery<ProcessingActivityGraphNodeQuery>(
|
||||
coreEnvironment,
|
||||
processingActivityNodeQuery,
|
||||
{
|
||||
processingActivityId: activityId,
|
||||
},
|
||||
),
|
||||
),
|
||||
Component: withQueryRef(
|
||||
lazy(
|
||||
() =>
|
||||
import("#/pages/organizations/processingActivities/ProcessingActivityDetailsPage"),
|
||||
),
|
||||
),
|
||||
},
|
||||
] satisfies AppRoute[];
|
||||
|
||||
Reference in New Issue
Block a user