Fix processing activities & risks permissions pages handling

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2026-01-02 11:23:46 +01:00
committed by Bryan Frimin
parent de0e0b596d
commit e80766509a
15 changed files with 694 additions and 275 deletions

View File

@@ -64,8 +64,7 @@ import type {
ProcessingActivitiesPageTIAFragment$key,
ProcessingActivitiesPageTIAFragment$data,
} from "/__generated__/core/ProcessingActivitiesPageTIAFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use, useState } from "react";
import { useState } from "react";
import type { ProcessingActivityGraphListQuery } from "/__generated__/core/ProcessingActivityGraphListQuery.graphql";
interface ProcessingActivitiesPageProps {
@@ -106,6 +105,8 @@ const processingActivitiesPageFragment = graphql`
internationalTransfers
createdAt
updatedAt
canUpdate: permission(action: "core:processing-activity:update")
canDelete: permission(action: "core:processing-activity:delete")
}
}
pageInfo {
@@ -237,7 +238,6 @@ export default function ProcessingActivitiesPage({
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const [activeTab, setActiveTab] = useState<"activities" | "dpia" | "tia">(
"activities",
);
@@ -292,8 +292,7 @@ export default function ProcessingActivitiesPage({
const hasAnyAction =
!isSnapshotMode &&
(isAuthorized("ProcessingActivity", "updateProcessingActivity") ||
isAuthorized("ProcessingActivity", "deleteProcessingActivity"));
activities.some(({ canUpdate, canDelete }) => canUpdate || canDelete);
const canExportPDF = isAuthorized(
"ProcessingActivity",
@@ -467,7 +466,7 @@ export default function ProcessingActivitiesPage({
)}
{!isSnapshotMode &&
activeTab === "activities" &&
isAuthorized("Organization", "createProcessingActivity") && (
organization.node.canCreateProcessingActivity && (
<CreateProcessingActivityDialog
organizationId={organizationId}
connectionId={connectionId}
@@ -677,7 +676,6 @@ function ActivityRow({
const isSnapshotMode = Boolean(snapshotId);
const [deleteActivity] = useMutation(deleteProcessingActivityMutation);
const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => {
confirm(
@@ -729,7 +727,7 @@ function ActivityRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
{isAuthorized("ProcessingActivity", "deleteProcessingActivity") && (
{activity.canDelete && (
<DropdownItem
icon={IconTrashCan}
variant="danger"

View File

@@ -58,8 +58,7 @@ import {
} from "../../../components/form/ProcessingActivityEnumOptions";
import type { ProcessingActivityGraphNodeQuery } from "/__generated__/core/ProcessingActivityGraphNodeQuery.graphql";
import { use, useState, useEffect } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import { useState, useEffect } from "react";
const updateProcessingActivitySchema = z.object({
name: z.string().min(1, "Name is required"),
@@ -99,17 +98,16 @@ type Props = {
};
export default function ProcessingActivityDetailsPage(props: Props) {
const data = usePreloadedQuery<ProcessingActivityGraphNodeQuery>(
processingActivityNodeQuery,
props.queryRef,
);
const activity = data.node;
const { node: activity } =
usePreloadedQuery<ProcessingActivityGraphNodeQuery>(
processingActivityNodeQuery,
props.queryRef,
);
const { __ } = useTranslate();
const { toast } = useToast();
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
// Get initial tab from URL hash
const getInitialTab = (): "overview" | "dpia" | "tia" => {
@@ -132,6 +130,13 @@ export default function ProcessingActivityDetailsPage(props: Props) {
const [dpiaDeleted, setDpiaDeleted] = useState(false);
const [tiaDeleted, setTiaDeleted] = useState(false);
const canCreateOrUpdateDPIA = activity.dataProtectionImpactAssessment
? activity.dataProtectionImpactAssessment.canUpdate
: activity.canCreateDPIA;
const canCreateOrUpdateTIA = activity.transferImpactAssessment
? activity.transferImpactAssessment.canUpdate
: activity.canCreateTIA;
useEffect(() => {
window.location.hash = activeTab === "overview" ? "" : activeTab;
}, [activeTab]);
@@ -422,14 +427,13 @@ export default function ProcessingActivityDetailsPage(props: Props) {
{ label: activity.name! },
]}
/>
{!isSnapshotMode &&
isAuthorized("ProcessingActivity", "deleteProcessingActivity") && (
<ActionDropdown>
<DropdownItem onClick={deleteActivity} variant="danger">
{__("Delete")}
</DropdownItem>
</ActionDropdown>
)}
{!isSnapshotMode && activity.canDelete && (
<ActionDropdown>
<DropdownItem onClick={deleteActivity} variant="danger">
{__("Delete")}
</DropdownItem>
</ActionDropdown>
)}
</div>
<div className="mb-6">
@@ -468,7 +472,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
{...register("name")}
error={formState.errors.name?.message}
required
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
<div>
@@ -483,7 +487,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
onValueChange={field.onChange}
value={field.value}
className="w-full"
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
>
<RoleOptions />
</Select>
@@ -502,7 +506,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
{...register("purpose")}
placeholder={__("Describe the purpose of processing")}
rows={3}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
</div>
@@ -510,14 +514,14 @@ export default function ProcessingActivityDetailsPage(props: Props) {
label={__("Data Subject Category")}
{...register("dataSubjectCategory")}
placeholder={__("e.g., employees, customers, prospects")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
<Field
label={__("Personal Data Category")}
{...register("personalDataCategory")}
placeholder={__("e.g., contact details, financial data")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
<div>
@@ -536,7 +540,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
onValueChange={field.onChange}
value={field.value}
className="w-full"
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
>
<SpecialOrCriminalDataOptions />
</Select>
@@ -553,7 +557,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
label={__("Consent Evidence Link")}
{...register("consentEvidenceLink")}
placeholder={__("Link to consent evidence if applicable")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
<div>
@@ -568,7 +572,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
onValueChange={field.onChange}
value={field.value}
className="w-full"
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
>
<LawfulBasisOptions />
</Select>
@@ -589,7 +593,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
id="lastReviewDate"
type="date"
{...register("lastReviewDate")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
</div>
@@ -601,7 +605,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
id="nextReviewDate"
type="date"
{...register("nextReviewDate")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
</div>
@@ -610,7 +614,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
control={control}
name="dataProtectionOfficerId"
label={__("Data Protection Officer")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
</div>
@@ -619,14 +623,14 @@ export default function ProcessingActivityDetailsPage(props: Props) {
label={__("Recipients")}
{...register("recipients")}
placeholder={__("Who receives the data")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
<Field
label={__("Location")}
{...register("location")}
placeholder={__("Where is the data processed")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
<Controller
@@ -639,7 +643,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
<Checkbox
checked={field.value ?? false}
onChange={field.onChange}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
<span>
{__("Data is transferred internationally")}
@@ -663,7 +667,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
onValueChange={field.onChange}
value={field.value}
className="w-full"
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
>
<TransferSafeguardsOptions />
</Select>
@@ -680,7 +684,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
label={__("Retention Period")}
{...register("retentionPeriod")}
placeholder={__("How long is data retained")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
<div>
@@ -689,7 +693,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
{...register("securityMeasures")}
placeholder={__("Technical and organizational measures")}
rows={3}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
</div>
@@ -707,7 +711,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
onValueChange={field.onChange}
value={field.value}
className="w-full"
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
>
<DataProtectionImpactAssessmentOptions />
</Select>
@@ -737,7 +741,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
onValueChange={field.onChange}
value={field.value}
className="w-full"
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
>
<TransferImpactAssessmentOptions />
</Select>
@@ -761,15 +765,12 @@ export default function ProcessingActivityDetailsPage(props: Props) {
name="vendorIds"
selectedVendors={vendors}
label={__("Vendors")}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !activity.canUpdate}
/>
{!isSnapshotMode && (
<div className="flex justify-end pt-4">
{isAuthorized(
"ProcessingActivity",
"updateProcessingActivity",
) && (
{activity.canUpdate && (
<Button
type="submit"
variant="primary"
@@ -796,18 +797,14 @@ export default function ProcessingActivityDetailsPage(props: Props) {
<h2 className="text-xl font-semibold mb-6 text-center">
{__("Data Protection Impact Assessment")}
</h2>
{!isSnapshotMode &&
isAuthorized(
"ProcessingActivity",
"createDataProtectionImpactAssessment",
) && (
<Button
variant="primary"
onClick={() => setShowDpiaForm(true)}
>
{__("Create DPIA")}
</Button>
)}
{!isSnapshotMode && activity.canCreateDPIA && (
<Button
variant="primary"
onClick={() => setShowDpiaForm(true)}
>
{__("Create DPIA")}
</Button>
)}
</div>
) : (
<>
@@ -818,10 +815,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
{!isSnapshotMode &&
activity?.dataProtectionImpactAssessment?.id &&
!dpiaDeleted &&
isAuthorized(
"DataProtectionImpactAssessment",
"deleteDataProtectionImpactAssessment",
) && (
activity.dataProtectionImpactAssessment.canDelete && (
<Button variant="danger" onClick={deleteDPIA}>
{__("Delete DPIA")}
</Button>
@@ -840,7 +834,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Describe the processing activity and its purpose",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
/>
</div>
@@ -855,7 +849,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Explain why the processing is necessary and proportionate",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
/>
</div>
@@ -870,7 +864,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Describe the potential risks to data subjects",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
/>
</div>
@@ -885,7 +879,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Describe measures to mitigate the identified risks",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
/>
</div>
@@ -903,7 +897,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
onValueChange={field.onChange}
value={field.value}
className="w-full"
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateDPIA}
>
<Option value="LOW">{__("Low")}</Option>
<Option value="MEDIUM">{__("Medium")}</Option>
@@ -927,14 +921,8 @@ export default function ProcessingActivityDetailsPage(props: Props) {
)}
{(activity?.dataProtectionImpactAssessment?.id &&
!dpiaDeleted
? isAuthorized(
"DataProtectionImpactAssessment",
"updateDataProtectionImpactAssessment",
)
: isAuthorized(
"ProcessingActivity",
"createDataProtectionImpactAssessment",
)) && (
? activity.dataProtectionImpactAssessment.canUpdate
: activity.canCreateDPIA) && (
<Button
type="submit"
variant="primary"
@@ -966,18 +954,14 @@ export default function ProcessingActivityDetailsPage(props: Props) {
<h2 className="text-xl font-semibold mb-6 text-center">
{__("Transfer Impact Assessment")}
</h2>
{!isSnapshotMode &&
isAuthorized(
"ProcessingActivity",
"createTransferImpactAssessment",
) && (
<Button
variant="primary"
onClick={() => setShowTiaForm(true)}
>
{__("Create TIA")}
</Button>
)}
{!isSnapshotMode && activity.canCreateTIA && (
<Button
variant="primary"
onClick={() => setShowTiaForm(true)}
>
{__("Create TIA")}
</Button>
)}
</div>
) : (
<>
@@ -988,10 +972,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
{!isSnapshotMode &&
activity?.transferImpactAssessment?.id &&
!tiaDeleted &&
isAuthorized(
"TransferImpactAssessment",
"deleteTransferImpactAssessment",
) && (
activity.transferImpactAssessment.canDelete && (
<Button variant="danger" onClick={deleteTIA}>
{__("Delete TIA")}
</Button>
@@ -1010,7 +991,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Describe the data subjects involved in the transfer",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
/>
</div>
@@ -1025,7 +1006,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Describe the legal mechanism for the transfer (e.g., SCCs, BCRs, adequacy decision)",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
/>
</div>
@@ -1038,7 +1019,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Describe the nature and details of the data transfer",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
/>
</div>
@@ -1053,7 +1034,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Assess the risks related to the local laws of the destination country",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
/>
</div>
@@ -1068,7 +1049,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
"Describe any supplementary measures taken to ensure adequate protection",
)}
rows={4}
disabled={isSnapshotMode}
disabled={isSnapshotMode || !canCreateOrUpdateTIA}
/>
</div>
@@ -1085,14 +1066,8 @@ export default function ProcessingActivityDetailsPage(props: Props) {
</Button>
)}
{(activity?.transferImpactAssessment?.id && !tiaDeleted
? isAuthorized(
"TransferImpactAssessment",
"updateTransferImpactAssessment",
)
: isAuthorized(
"ProcessingActivity",
"createTransferImpactAssessment",
)) && (
? activity.transferImpactAssessment.canUpdate
: activity.canCreateTIA) && (
<Button
type="submit"
variant="primary"

View File

@@ -34,8 +34,6 @@ import {
} from "/hooks/graph/RiskGraph";
import type { RiskGraphNodeQuery } from "/__generated__/core/RiskGraphNodeQuery.graphql";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<RiskGraphNodeQuery>;
@@ -49,18 +47,16 @@ export default function RiskDetailPage(props: Props) {
const organizationId = useOrganizationId();
const navigate = useNavigate();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!riskId) {
throw new Error("Cannot load risk detail page without riskId parameter");
}
const { __ } = useTranslate();
const data = usePreloadedQuery<RiskGraphNodeQuery>(
const { node: risk } = usePreloadedQuery<RiskGraphNodeQuery>(
riskNodeQuery,
props.queryRef,
);
const risk = data.node;
validateSnapshotConsistency(risk, snapshotId);
const [deleteRisk] = useDeleteRiskMutation();
@@ -136,7 +132,7 @@ export default function RiskDetailPage(props: Props) {
/>
{!isSnapshotMode && (
<div className="flex gap-2">
{isAuthorized("Risk", "updateRisk") && (
{risk.canUpdate && (
<FormRiskDialog
trigger={
<Button icon={IconPencil} variant="secondary">
@@ -146,7 +142,7 @@ export default function RiskDetailPage(props: Props) {
risk={{ id: riskId, ...risk }}
/>
)}
{isAuthorized("Risk", "deleteRisk") && (
{risk.canDelete && (
<ActionDropdown variant="secondary">
<DropdownItem
variant="danger"

View File

@@ -29,8 +29,6 @@ import type { RiskGraphListQuery } from "/__generated__/core/RiskGraphListQuery.
import type { RiskGraphFragment$data } from "/__generated__/core/RiskGraphFragment.graphql";
import { useParams } from "react-router";
import { SnapshotBanner } from "/components/SnapshotBanner";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = {
queryRef: PreloadedQuery<RiskGraphListQuery>;
@@ -41,9 +39,13 @@ export default function RisksPage(props: Props) {
const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const { connectionId, risks, ...pagination } = useRisksQuery(props.queryRef);
const {
data: { canCreateRisk },
connectionId,
risks,
...pagination
} = useRisksQuery(props.queryRef);
const refetch = ({
order,
@@ -73,7 +75,7 @@ export default function RisksPage(props: Props) {
const hasAnyAction =
!isSnapshotMode &&
(isAuthorized("Risk", "updateRisk") || isAuthorized("Risk", "deleteRisk"));
risks.some(({ canDelete, canUpdate }) => canUpdate || canDelete);
return (
<div className="space-y-6">
@@ -84,7 +86,7 @@ export default function RisksPage(props: Props) {
"Risks are potential threats to your organization. Manage them by identifying, assessing, and implementing mitigation measures.",
)}
>
{!isSnapshotMode && isAuthorized("Organization", "createRisk") && (
{!isSnapshotMode && canCreateRisk && (
<FormRiskDialog
connection={connectionId}
onSuccess={() => {
@@ -153,7 +155,6 @@ function RiskRow(props: RowProps) {
const isSnapshotMode = Boolean(snapshotId);
const [deleteRisk] = useDeleteRiskMutation();
const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const onDelete = () => {
confirm(
() =>
@@ -206,7 +207,7 @@ function RiskRow(props: RowProps) {
{props.hasAnyAction && (
<Td noLink className="text-end">
<ActionDropdown>
{isAuthorized("Risk", "updateRisk") && (
{risk.canUpdate && (
<DropdownItem
icon={IconPencil}
onClick={() => formDialogRef.current?.open()}
@@ -215,7 +216,7 @@ function RiskRow(props: RowProps) {
</DropdownItem>
)}
{isAuthorized("Risk", "deleteRisk") && (
{risk.canDelete && (
<DropdownItem
variant="danger"
icon={IconTrashCan}