diff --git a/apps/console/src/components/PageError.tsx b/apps/console/src/components/PageError.tsx index a27b8d42a..0e9ff3356 100644 --- a/apps/console/src/components/PageError.tsx +++ b/apps/console/src/components/PageError.tsx @@ -68,6 +68,20 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) { ); } + if (error && error.toString().includes("FORBIDDEN")) { + return ( +
+

+ + {__("Page not found")} +

+

+ {__("The page you are looking for does not exist")} +

+
+ ); + } + if (error && error.toString().includes("UNAUTHORIZED")) { return (
diff --git a/apps/console/src/components/customDomains/CustomDomainManager.tsx b/apps/console/src/components/customDomains/CustomDomainManager.tsx index 6b7b47a12..6a324d598 100644 --- a/apps/console/src/components/customDomains/CustomDomainManager.tsx +++ b/apps/console/src/components/customDomains/CustomDomainManager.tsx @@ -6,6 +6,7 @@ import type { CustomDomainManagerDeleteMutation } from "./__generated__/CustomDo import { CreateCustomDomainDialog } from "./CreateCustomDomainDialog"; import { DeleteCustomDomainDialog } from "./DeleteCustomDomainDialog"; import { DomainDetailsDialog } from "./DomainDetailsDialog"; +import { Authorized } from "/permissions"; const deleteCustomDomainMutation = graphql` mutation CustomDomainManagerDeleteMutation($input: DeleteCustomDomainInput!) { @@ -106,9 +107,11 @@ export function CustomDomainManager({ )}

- - - + + + + +
@@ -136,12 +139,14 @@ export function CustomDomainManager({ - - - + + + + + diff --git a/apps/console/src/components/organizations/InviteUserDialog.tsx b/apps/console/src/components/organizations/InviteUserDialog.tsx index 914fbcfdf..826e1ea7f 100644 --- a/apps/console/src/components/organizations/InviteUserDialog.tsx +++ b/apps/console/src/components/organizations/InviteUserDialog.tsx @@ -5,6 +5,8 @@ import { DialogFooter, Field, Checkbox, + Select, + Option, useDialogRef, } from "@probo/ui"; import type { PropsWithChildren } from "react"; @@ -15,6 +17,8 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { z } from "zod"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { Controller } from "react-hook-form"; +import { Suspense } from "react"; +import { getAssignableRoles } from "/permissions"; const inviteMutation = graphql` mutation InviteUserDialogMutation( @@ -40,6 +44,7 @@ const inviteMutation = graphql` const schema = z.object({ email: z.string().email(), fullName: z.string(), + role: z.enum(["OWNER", "ADMIN", "FULL", "VIEWER"]).default("VIEWER"), createPeople: z.boolean().default(false), }); @@ -48,16 +53,17 @@ type Props = PropsWithChildren & { onRefetch: () => void; }; -export function InviteUserDialog({ children, connectionId, onRefetch }: Props) { +function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); + const assignableRoles = getAssignableRoles(organizationId); const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, { successMessage: __("Invitation sent successfully"), errorMessage: __("Failed to send invitation"), }); const { register, handleSubmit, formState, reset, control } = useFormWithSchema( schema, - { defaultValues: { createPeople: false } }, + { defaultValues: { role: "VIEWER", createPeople: false } }, ); const dialogRef = useDialogRef(); @@ -69,6 +75,7 @@ export function InviteUserDialog({ children, connectionId, onRefetch }: Props) { organizationId, email: data.email, fullName: data.fullName, + role: data.role, createPeople: data.createPeople, }, connections: connectionId ? [connectionId] : ["SettingsPageInvitations_invitations"], @@ -107,6 +114,32 @@ export function InviteUserDialog({ children, connectionId, onRefetch }: Props) { {...register("fullName")} error={formState.errors.fullName?.message} /> + + ( + <> + +
+ {field.value === "OWNER" && ( +

{__("Full access to everything")}

+ )} + {field.value === "ADMIN" && ( +

{__("Full access except organization setup and API keys")}

+ )} + {field.value === "VIEWER" && ( +

{__("Read-only access")}

+ )} +
+ + )} + /> +
); } + +export function InviteUserDialog(props: Props) { + return ( + + + + ); +} diff --git a/apps/console/src/components/organizations/SlackConnection.tsx b/apps/console/src/components/organizations/SlackConnection.tsx index a7105d1f3..12c68651b 100644 --- a/apps/console/src/components/organizations/SlackConnection.tsx +++ b/apps/console/src/components/organizations/SlackConnection.tsx @@ -2,6 +2,7 @@ import { Badge, Button, Card } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { sprintf } from "@probo/helpers"; import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql"; +import { Authorized } from "/permissions"; type Props = { organizationId: string; @@ -76,9 +77,11 @@ export function SlackConnections({ organizationId, slackConnections: connectedSl
) : ( - + + + )} ))} diff --git a/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts b/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts index 0736cb93b..46bd0a77c 100644 --- a/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts +++ b/apps/console/src/components/organizations/__generated__/InviteUserDialogMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<617ea2708c8402c706671dc1a63b1316>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,12 +9,13 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type Role = "ADMIN" | "MEMBER" | "OWNER" | "VIEWER"; +export type MembershipRole = "ADMIN" | "OWNER" | "VIEWER"; export type InviteUserInput = { createPeople: boolean; email: string; fullName: string; organizationId: string; + role: MembershipRole; }; export type InviteUserDialogMutation$variables = { connections: ReadonlyArray; @@ -30,7 +31,7 @@ export type InviteUserDialogMutation$data = { readonly expiresAt: any; readonly fullName: string; readonly id: string; - readonly role: Role; + readonly role: MembershipRole; }; }; }; diff --git a/apps/console/src/components/tasks/TasksCard.tsx b/apps/console/src/components/tasks/TasksCard.tsx index 1acb3df69..d176a987c 100644 --- a/apps/console/src/components/tasks/TasksCard.tsx +++ b/apps/console/src/components/tasks/TasksCard.tsx @@ -26,6 +26,8 @@ import TaskFormDialog, { import { useOrganizationId } from "/hooks/useOrganizationId"; import { Link, useLocation, useParams } from "react-router"; import { promisifyMutation } from "@probo/helpers"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; import type { TaskFormDialogFragment$key } from "./__generated__/TaskFormDialogFragment.graphql"; import { updateStoreCounter } from "/hooks/useMutationWithIncrement"; @@ -49,6 +51,7 @@ type Props = { export default function TasksCard({ tasks, connectionId }: Props) { const { __ } = useTranslate(); + const organizationId = useOrganizationId(); const hash = useLocation().hash.replace("#", ""); const hashes = [ @@ -67,6 +70,9 @@ export default function TasksCard({ tasks, connectionId }: Props) { usePageTitle(__("Tasks")); + const hasAnyAction = isAuthorized(organizationId, "Task", "updateTask") || + isAuthorized(organizationId, "Task", "deleteTask"); + return (
{tasks?.length === 0 ? ( @@ -100,6 +106,7 @@ export default function TasksCard({ tasks, connectionId }: Props) { key={task.id} task={task} connectionId={connectionId} + hasAnyAction={hasAnyAction} /> ))} @@ -110,6 +117,7 @@ export default function TasksCard({ tasks, connectionId }: Props) { key={task.id} task={task} connectionId={connectionId} + hasAnyAction={hasAnyAction} /> ))}
@@ -122,6 +130,7 @@ export default function TasksCard({ tasks, connectionId }: Props) { type TaskRowProps = { task: ItemOf & TaskFormDialogFragment$key; connectionId: string; + hasAnyAction: boolean; }; const deleteMutation = graphql` @@ -221,21 +230,27 @@ function TaskRow(props: TaskRowProps) { )} - - dialogRef.current?.open()} - > - {__("Edit")} - - - {__("Delete")} - - + {props.hasAnyAction && ( + + + dialogRef.current?.open()} + > + {__("Edit")} + + + + + {__("Delete")} + + + + )}
diff --git a/apps/console/src/components/trustCenter/TrustCenterAuditsCard.tsx b/apps/console/src/components/trustCenter/TrustCenterAuditsCard.tsx index 23ac375fc..9e9a84723 100644 --- a/apps/console/src/components/trustCenter/TrustCenterAuditsCard.tsx +++ b/apps/console/src/components/trustCenter/TrustCenterAuditsCard.tsx @@ -18,6 +18,7 @@ import { useMemo, useState, useCallback, useEffect } from "react"; import { sprintf, getAuditStateVariant, getAuditStateLabel, formatDate, getTrustCenterVisibilityOptions } from "@probo/helpers"; import { useOrganizationId } from "/hooks/useOrganizationId"; import type { TrustCenterAuditsCardFragment$key } from "./__generated__/TrustCenterAuditsCardFragment.graphql"; +import { isAuthorized } from "/permissions"; const trustCenterAuditFragment = graphql` fragment TrustCenterAuditsCardFragment on Audit { @@ -124,6 +125,8 @@ function AuditRow(props: { const { __ } = useTranslate(); const [optimisticValue, setOptimisticValue] = useState(null); + const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false; + const handleValueChange = useCallback((value: string | {}) => { const stringValue = typeof value === 'string' ? value : ''; const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC"; @@ -164,7 +167,7 @@ function AuditRow(props: { type="select" value={currentValue} onValueChange={handleValueChange} - disabled={props.disabled} + disabled={props.disabled || !canUpdate} className="w-[105px]" > {visibilityOptions.map((option) => ( diff --git a/apps/console/src/components/trustCenter/TrustCenterDocumentsCard.tsx b/apps/console/src/components/trustCenter/TrustCenterDocumentsCard.tsx index 0fa01b883..1deedbbef 100644 --- a/apps/console/src/components/trustCenter/TrustCenterDocumentsCard.tsx +++ b/apps/console/src/components/trustCenter/TrustCenterDocumentsCard.tsx @@ -20,6 +20,7 @@ import { useFragment } from "react-relay"; import { useMemo, useState, useCallback, useEffect } from "react"; import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers"; import { useOrganizationId } from "/hooks/useOrganizationId"; +import { isAuthorized } from "/permissions"; const trustCenterDocumentFragment = graphql` fragment TrustCenterDocumentsCardFragment on Document { @@ -129,6 +130,8 @@ function DocumentRow(props: { const { __ } = useTranslate(); const [optimisticValue, setOptimisticValue] = useState(null); + const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false; + const handleValueChange = useCallback((value: string | {}) => { const stringValue = typeof value === 'string' ? value : ''; const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC"; @@ -164,7 +167,7 @@ function DocumentRow(props: { type="select" value={currentValue} onValueChange={handleValueChange} - disabled={props.disabled} + disabled={props.disabled || !canUpdate} className="w-[105px]" > {visibilityOptions.map((option) => ( diff --git a/apps/console/src/components/trustCenter/TrustCenterFilesCard.tsx b/apps/console/src/components/trustCenter/TrustCenterFilesCard.tsx index 532671aa1..a6992e0c9 100644 --- a/apps/console/src/components/trustCenter/TrustCenterFilesCard.tsx +++ b/apps/console/src/components/trustCenter/TrustCenterFilesCard.tsx @@ -21,6 +21,9 @@ import { useFragment } from "react-relay"; import { useMemo, useState, useCallback, useEffect } from "react"; import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers"; import { formatDate } from "@probo/helpers"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; +import { useParams } from "react-router"; const trustCenterFileFragment = graphql` fragment TrustCenterFilesCardFragment on TrustCenterFile { @@ -147,6 +150,9 @@ function FileRow(props: { const file = props.file; const { __ } = useTranslate(); const [optimisticValue, setOptimisticValue] = useState(null); + const { organizationId } = useParams(); + + const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false; const handleValueChange = useCallback((value: string | {}) => { const stringValue = typeof value === 'string' ? value : ''; @@ -179,7 +185,7 @@ function FileRow(props: { type="select" value={currentValue} onValueChange={handleValueChange} - disabled={props.disabled} + disabled={props.disabled || !canUpdate} className="w-[105px]" > {visibilityOptions.map((option) => ( @@ -201,20 +207,24 @@ function FileRow(props: { onClick={() => window.open(file.fileUrl, '_blank', 'noopener,noreferrer')} title={__("Download")} /> - + + + @@ -228,21 +231,25 @@ function ReferenceRow({ icon={IconArrowLink} onClick={onVisitWebsite} /> - - + {canUpdate && } {vendors.length === 0 && ( - @@ -91,6 +124,7 @@ export function TrustCenterVendorsCard(props: Props) { vendor={vendor} onToggleVisibility={onToggleVisibility} disabled={props.disabled} + canUpdate={canUpdate} /> ))} @@ -113,6 +147,7 @@ function VendorRow(props: { vendor: TrustCenterVendorsCardFragment$key; onToggleVisibility: (vendorId: string, showOnTrustCenter: boolean) => void; disabled?: boolean; + canUpdate: boolean; }) { const vendor = useFragment(trustCenterVendorFragment, props.vendor); const organizationId = useOrganizationId(); @@ -135,16 +170,18 @@ function VendorRow(props: { {vendor.showOnTrustCenter ? __("Visible") : __("None")} - + {props.canUpdate && ( + + )} ); } diff --git a/apps/console/src/layouts/MainLayout.tsx b/apps/console/src/layouts/MainLayout.tsx index 93ca4f5a7..87d4824cd 100644 --- a/apps/console/src/layouts/MainLayout.tsx +++ b/apps/console/src/layouts/MainLayout.tsx @@ -47,6 +47,7 @@ import { useToast } from "@probo/ui"; import { ErrorBoundary } from "react-error-boundary"; import { PageError } from "/components/PageError"; import { buildEndpoint } from "/providers/RelayProviders"; +import { Authorized } from "/permissions"; const MainLayoutQuery = graphql` query MainLayoutQuery($organizationId: ID!) { @@ -72,7 +73,6 @@ const MainLayoutQuery = graphql` */ export function MainLayout() { const { organizationId } = useParams(); - const { __ } = useTranslate(); const prefix = `/organizations/${organizationId}`; @@ -80,14 +80,31 @@ export function MainLayout() { return ; } + return ( + }> + + + ); +} + +function MainLayoutContent({ + organizationId, + prefix, +}: { + organizationId: string; + prefix: string; +}) { + const { __ } = useTranslate(); + const data = useLazyLoadQuery(MainLayoutQuery, { + organizationId, + }); + return (
- }> - - +
}> @@ -96,96 +113,132 @@ export function MainLayout() { } sidebar={
    - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
} > @@ -234,11 +287,13 @@ function UserDropdown({ organizationId }: { organizationId: string }) { return ( - + + + (MainLayoutQuery, { - organizationId, - }); - return ; -} function OrganizationSelector({ currentOrganization, diff --git a/apps/console/src/pages/APIKeysPage.tsx b/apps/console/src/pages/APIKeysPage.tsx index b99ff45ec..ec167c189 100644 --- a/apps/console/src/pages/APIKeysPage.tsx +++ b/apps/console/src/pages/APIKeysPage.tsx @@ -120,7 +120,7 @@ export default function APIKeysPage() { try { const [apiKeysResponse, organizationsResponse] = await Promise.all([ fetch('/connect/api-keys', { credentials: 'include' }), - fetch('/connect/organizations', { credentials: 'include' }), + fetch('/connect/organizations?role=OWNER', { credentials: 'include' }), ]); if (apiKeysResponse.status === 401 || organizationsResponse.status === 401) { diff --git a/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx b/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx index 4c48fb208..777c1ff5b 100644 --- a/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx +++ b/apps/console/src/pages/organizations/assets/AssetDetailsPage.tsx @@ -28,6 +28,7 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema"; import z from "zod"; import { getAssetTypeVariant, validateSnapshotConsistency } from "@probo/helpers"; import { SnapshotBanner } from "/components/SnapshotBanner"; +import { Authorized } from "/permissions"; const updateAssetSchema = z.object({ name: z.string().min(1, "Name is required"), @@ -114,15 +115,17 @@ export default function AssetDetailsPage(props: Props) { {!isSnapshotMode && ( - - - {__("Delete")} - - + + + + {__("Delete")} + + + )} @@ -178,9 +181,11 @@ export default function AssetDetailsPage(props: Props) {
{formState.isDirty && !isSnapshotMode && ( - + + + )}
diff --git a/apps/console/src/pages/organizations/assets/AssetsPage.tsx b/apps/console/src/pages/organizations/assets/AssetsPage.tsx index 28e3dbe13..56fd9a8b9 100644 --- a/apps/console/src/pages/organizations/assets/AssetsPage.tsx +++ b/apps/console/src/pages/organizations/assets/AssetsPage.tsx @@ -35,6 +35,8 @@ import type { } from "./__generated__/AssetsPageFragment.graphql"; import { SortableTable } from "/components/SortableTable"; import { SnapshotBanner } from "/components/SnapshotBanner"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; const paginatedAssetsFragment = graphql` fragment AssetsPageFragment on Organization @@ -106,6 +108,11 @@ export default function AssetsPage(props: Props) { usePageTitle(__("Assets")); + const hasAnyAction = !isSnapshotMode && ( + isAuthorized(organizationId, "Asset", "updateAsset") || + isAuthorized(organizationId, "Asset", "deleteAsset") + ); + return (
{snapshotId && } @@ -116,12 +123,14 @@ export default function AssetsPage(props: Props) { )} > {!isSnapshotMode && ( - - - + + + + + )} @@ -132,7 +141,7 @@ export default function AssetsPage(props: Props) {
- + {hasAnyAction && } @@ -141,6 +150,7 @@ export default function AssetsPage(props: Props) { key={entry.id} entry={entry} connectionId={connectionId} + hasAnyAction={hasAnyAction} /> ))} @@ -152,9 +162,11 @@ export default function AssetsPage(props: Props) { function AssetRow({ entry, connectionId, + hasAnyAction, }: { entry: AssetEntry; connectionId: string; + hasAnyAction: boolean; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -204,19 +216,21 @@ function AssetRow({ {__("None")} )} - + {hasAnyAction && ( + + )} ); } diff --git a/apps/console/src/pages/organizations/audits/AuditDetailsPage.tsx b/apps/console/src/pages/organizations/audits/AuditDetailsPage.tsx index e93c79fb7..a0f1db377 100644 --- a/apps/console/src/pages/organizations/audits/AuditDetailsPage.tsx +++ b/apps/console/src/pages/organizations/audits/AuditDetailsPage.tsx @@ -34,6 +34,7 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema"; import z from "zod"; import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers"; import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql"; +import { Authorized } from "/permissions"; const updateAuditSchema = z.object({ name: z.string().nullable().optional(), @@ -145,13 +146,15 @@ export default function AuditDetailsPage(props: Props) { - - {__("Delete")} - + + + {__("Delete")} + + @@ -184,9 +187,11 @@ export default function AuditDetailsPage(props: Props) {
{formState.isDirty && ( - + + + )}
diff --git a/apps/console/src/pages/organizations/audits/AuditsPage.tsx b/apps/console/src/pages/organizations/audits/AuditsPage.tsx index 113d75549..a58bc0582 100644 --- a/apps/console/src/pages/organizations/audits/AuditsPage.tsx +++ b/apps/console/src/pages/organizations/audits/AuditsPage.tsx @@ -31,6 +31,8 @@ import type { AuditsPageFragment$key, } from "./__generated__/AuditsPageFragment.graphql"; import { SortableTable } from "/components/SortableTable"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; const paginatedAuditsFragment = graphql` fragment AuditsPageFragment on Organization @@ -92,6 +94,9 @@ export default function AuditsPage(props: Props) { usePageTitle(__("Audits")); + const hasAnyAction = isAuthorized(organizationId, "Audit", "updateAudit") || + isAuthorized(organizationId, "Audit", "deleteAudit"); + return (
- - - + + + + +
@@ -116,7 +123,7 @@ export default function AuditsPage(props: Props) { - + {hasAnyAction && } @@ -125,6 +132,7 @@ export default function AuditsPage(props: Props) { key={entry.id} entry={entry} connectionId={connectionId} + hasAnyAction={hasAnyAction} /> ))} @@ -136,9 +144,11 @@ export default function AuditsPage(props: Props) { function AuditRow({ entry, connectionId, + hasAnyAction, }: { entry: AuditEntry; connectionId: string; + hasAnyAction: boolean; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -164,17 +174,21 @@ function AuditRow({ {__("Not uploaded")} )} - + {hasAnyAction && ( + + )} ); } diff --git a/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx b/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx index 0a6b95e52..ae0b33cd5 100644 --- a/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx +++ b/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx @@ -35,6 +35,7 @@ import z from "zod"; import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency } from "@probo/helpers"; import { SnapshotBanner } from "/components/SnapshotBanner"; import type { ContinualImprovementGraphNodeQuery } from "/hooks/graph/__generated__/ContinualImprovementGraphNodeQuery.graphql"; +import { Authorized } from "/permissions"; const updateImprovementSchema = z.object({ referenceId: z.string().min(1, "Reference ID is required"), @@ -149,11 +150,13 @@ export default function ContinualImprovementDetailsPage(props: Props) { ]} /> {!isSnapshotMode && ( - - - {__("Delete")} - - + + + + {__("Delete")} + + + )} @@ -286,13 +289,15 @@ export default function ContinualImprovementDetailsPage(props: Props) { {!isSnapshotMode && (
- + + +
)} diff --git a/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementsPage.tsx b/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementsPage.tsx index 383b06a8f..c40ccec5c 100644 --- a/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementsPage.tsx +++ b/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementsPage.tsx @@ -37,6 +37,8 @@ import type { ContinualImprovementsPageFragment$key, ContinualImprovementsPageFragment$data, } from "./__generated__/ContinualImprovementsPageFragment.graphql"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; interface ContinualImprovementsPageProps { queryRef: PreloadedQuery; @@ -127,6 +129,11 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove ); const improvements = data?.continualImprovements?.edges?.map((edge) => edge.node) ?? []; + const hasAnyAction = !isSnapshotMode && ( + isAuthorized(organizationId, "ContinualImprovement", "updateContinualImprovement") || + isAuthorized(organizationId, "ContinualImprovement", "deleteContinualImprovement") + ); + return (
{isSnapshotMode && snapshotId && ( @@ -134,14 +141,16 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove )} {!isSnapshotMode && ( - - - + + + + + )} @@ -156,7 +165,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
- {!isSnapshotMode && } + {hasAnyAction && } @@ -166,6 +175,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove improvement={improvement} connectionId={connectionId} snapshotId={snapshotId} + hasAnyAction={hasAnyAction} /> ))} @@ -203,10 +213,12 @@ function ImprovementRow({ improvement, connectionId, snapshotId, + hasAnyAction, }: { improvement: NodeOf>; connectionId: string; snapshotId?: string; + hasAnyAction: boolean; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -267,16 +279,18 @@ function ImprovementRow({ {__("No target date")} )} - {!isSnapshotMode && ( + {hasAnyAction && ( )} diff --git a/apps/console/src/pages/organizations/data/DataPage.tsx b/apps/console/src/pages/organizations/data/DataPage.tsx index 4dc6329bc..3f388890a 100644 --- a/apps/console/src/pages/organizations/data/DataPage.tsx +++ b/apps/console/src/pages/organizations/data/DataPage.tsx @@ -35,6 +35,8 @@ import type { import type { DataListQuery } from "./__generated__/DataListQuery.graphql"; import { SortableTable } from "/components/SortableTable"; import { SnapshotBanner } from "/components/SnapshotBanner"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; const paginatedDataFragment = graphql` fragment DataPageFragment on Organization @@ -119,6 +121,10 @@ export default function DataPage(props: Props) { usePageTitle(__("Data")); + const hasAnyAction = !isSnapshotMode && ( isAuthorized(organizationId, "Datum", "updateDatum") || + isAuthorized(organizationId, "Datum", "deleteDatum") + ); + return (
{isSnapshotMode && snapshotId && ( @@ -131,13 +137,15 @@ export default function DataPage(props: Props) { )} > {!snapshotId && ( - pagination.refetch({ snapshotId })} - > - - + + pagination.refetch({ snapshotId })} + > + + + )} {__("Classification")}
- + {hasAnyAction && } {dataEntries.map((entry) => ( - + ))} @@ -167,10 +175,12 @@ function DataRow({ entry, connectionId, snapshotId, + hasAnyAction, }: { entry: DataEntry; connectionId: string; snapshotId?: string; + hasAnyAction: boolean; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -215,18 +225,21 @@ function DataRow({ {__("None")} )} - + + )} ); } diff --git a/apps/console/src/pages/organizations/data/DatumDetailsPage.tsx b/apps/console/src/pages/organizations/data/DatumDetailsPage.tsx index caddc7ec0..4f79b902d 100644 --- a/apps/console/src/pages/organizations/data/DatumDetailsPage.tsx +++ b/apps/console/src/pages/organizations/data/DatumDetailsPage.tsx @@ -29,6 +29,7 @@ import z from "zod"; import { SnapshotBanner } from "/components/SnapshotBanner"; import { validateSnapshotConsistency } from "@probo/helpers"; import type { DatumGraphNodeQuery } from "/hooks/graph/__generated__/DatumGraphNodeQuery.graphql"; +import { Authorized } from "/permissions"; const updateDatumSchema = z.object({ name: z.string().min(1, "Name is required"), @@ -124,15 +125,17 @@ export default function DatumDetailsPage(props: Props) { {datumEntry?.dataClassification} {!isSnapshotMode && ( - - - {__("Delete")} - - + + + + {__("Delete")} + + + )} @@ -177,9 +180,11 @@ export default function DatumDetailsPage(props: Props) { {!isSnapshotMode && (
{formState.isDirty && ( - + + + )}
)} diff --git a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx index 0b8461536..89ceeb3a0 100644 --- a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx @@ -76,6 +76,7 @@ import { DocumentTypeOptions } from "/components/form/DocumentTypeOptions"; import { DocumentClassificationOptions } from "/components/form/DocumentClassificationOptions"; import { z } from "zod"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { Authorized } from "/permissions"; type Props = { queryRef: PreloadedQuery; @@ -521,20 +522,24 @@ export default function DocumentDetailPage(props: Props) { - updateDialogRef.current?.open()} - icon={IconPencil} - > - {isDraft ? __("Edit draft document") : __("Create new draft")} - - {isDraft && versions.length > 1 && ( + updateDialogRef.current?.open()} + icon={IconPencil} > - {__("Delete draft document")} + {isDraft ? __("Edit draft document") : __("Create new draft")} + + {isDraft && versions.length > 1 && ( + + + {__("Delete draft document")} + + )} pdfDownloadDialogRef.current?.open()} @@ -543,14 +548,16 @@ export default function DocumentDetailPage(props: Props) { > {__("Download PDF")} - - {__("Delete document")} - + + + {__("Delete document")} + + diff --git a/apps/console/src/pages/organizations/documents/DocumentsPage.tsx b/apps/console/src/pages/organizations/documents/DocumentsPage.tsx index eb80afb1f..529b90d3e 100644 --- a/apps/console/src/pages/organizations/documents/DocumentsPage.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentsPage.tsx @@ -57,6 +57,8 @@ import { type BulkExportDialogRef, } from "/components/documents/BulkExportDialog"; import type { DocumentsPageUserEmailQuery } from "./__generated__/DocumentsPageUserEmailQuery.graphql"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; const documentsFragment = graphql` fragment DocumentsPageListFragment on Organization @@ -135,6 +137,9 @@ export default function DocumentsPage(props: Props) { usePageTitle(__("Documents")); + const hasAnyAction = isAuthorized(organization.id, "Document", "updateDocument") || + isAuthorized(organization.id, "Document", "deleteDocument"); + const handleSendSigningNotifications = () => { sendSigningNotifications({ variables: { @@ -193,6 +198,7 @@ export default function DocumentsPage(props: Props) { description={__("Manage your organization's documents")} >
+ - {__("New document")}} - /> + + + {__("New document")}} + /> +
{documents.length > 0 ? ( @@ -232,7 +241,7 @@ export default function DocumentsPage(props: Props) {
- + {hasAnyAction && } ) : ( @@ -249,29 +258,33 @@ export default function DocumentsPage(props: Props) {
- - - - - + + + + - {__("Request signature")} - - + + + - + + +
@@ -310,6 +325,7 @@ export default function DocumentsPage(props: Props) { document={document} organizationId={organization.id} connectionId={connectionId} + hasAnyAction={hasAnyAction} /> ))} @@ -367,12 +383,14 @@ function DocumentRow({ organizationId, checked, onCheck, + hasAnyAction, }: { document: DocumentsPageRowFragment$key; organizationId: string; connectionId: string; checked: boolean; onCheck: () => void; + hasAnyAction: boolean; }) { const document = useFragment( rowFragment, @@ -444,17 +462,21 @@ function DocumentRow({ - + {hasAnyAction && ( + + )} ); } diff --git a/apps/console/src/pages/organizations/documents/tabs/DocumentSignaturesTab.tsx b/apps/console/src/pages/organizations/documents/tabs/DocumentSignaturesTab.tsx index ba1861e94..2bcab9a26 100644 --- a/apps/console/src/pages/organizations/documents/tabs/DocumentSignaturesTab.tsx +++ b/apps/console/src/pages/organizations/documents/tabs/DocumentSignaturesTab.tsx @@ -20,6 +20,7 @@ import { useOutletContext } from "react-router"; import type { DocumentSignaturesTab_signature$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_signature.graphql.ts"; import type { DocumentSignaturesTab_version$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts"; import type { DocumentSignaturesTabRefetchQuery } from "./__generated__/DocumentSignaturesTabRefetchQuery.graphql"; +import { Authorized } from "/permissions"; type Version = NodeOf; @@ -274,6 +275,7 @@ function SignatureItem(props: { {props.signable && ( + + )} ); @@ -328,6 +331,7 @@ function SignatureItem(props: { {__("Signed")} ) : ( + + )} ); diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx index 3f919092a..a3bf28fe7 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx @@ -27,6 +27,7 @@ import { promisifyMutation } from "@probo/helpers"; import type { FrameworkGraphControlNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql"; import { frameworkControlNodeQuery } from "/hooks/graph/FrameworkGraph"; import type { FrameworkDetailPageFragment$data } from "./__generated__/FrameworkDetailPageFragment.graphql"; +import { Authorized } from "/permissions"; const attachMeasureMutation = graphql` mutation FrameworkControlPageAttachMutation( @@ -235,24 +236,28 @@ export default function FrameworkControlPage({ queryRef }: Props) {
- - - - - + - {__("Delete")} - - + + + + + + + {__("Delete")} + + +
diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx index 7c9b23e7c..c334b3057 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx @@ -31,6 +31,7 @@ import type { FrameworkDetailPageExportFrameworkMutation } from "./__generated__ import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { Authorized } from "/permissions"; const frameworkDetailFragment = graphql` fragment FrameworkDetailPageFragment on Framework { @@ -149,14 +150,16 @@ export default function FrameworkDetailPage(props: Props) { } > - - - + + + + + {__("Export Framework")} - - {__("Delete")} - + + + {__("Delete")} + +
@@ -211,15 +216,17 @@ export default function FrameworkDetailPage(props: Props) { active={selectedControl?.id === control.id} /> ))} - - - + + + + +
diff --git a/apps/console/src/pages/organizations/frameworks/FrameworksPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworksPage.tsx index c71ff7cde..c6e13dc9b 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworksPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworksPage.tsx @@ -29,6 +29,8 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useState, type ChangeEventHandler } from "react"; import { FrameworkLogo } from "/components/FrameworkLogo"; import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; type Props = { queryRef: PreloadedQuery; @@ -118,6 +120,9 @@ export default function FrameworksPage(props: Props) { const isLoading = isUploading || isImporting; + const hasAnyAction = isAuthorized(data.organization.id!, "Framework", "updateFramework") || + isAuthorized(data.organization.id!, "Framework", "deleteFramework"); + return (
- - {__("Import")} - - + + + {__("Import")} + + +
@@ -150,6 +157,7 @@ export default function FrameworksPage(props: Props) { connectionId={connectionId} key={framework.id} framework={framework} + hasAnyAction={hasAnyAction} /> ))}
@@ -169,6 +177,7 @@ type FrameworkCardProps = { organizationId: string; connectionId: string; framework: FrameworksPageCardFragment$key; + hasAnyAction: boolean; }; function FrameworkCard(props: FrameworkCardProps) { @@ -189,23 +198,29 @@ function FrameworkCard(props: FrameworkCardProps) { />
- - { - dialogRef.current?.open(); - }} - > - {__("Edit")} - - deleteFramework()} - variant="danger" - > - {__("Delete")} - - + {props.hasAnyAction && ( + + + { + dialogRef.current?.open(); + }} + > + {__("Edit")} + + + + deleteFramework()} + variant="danger" + > + {__("Delete")} + + + + )}

; @@ -135,29 +136,33 @@ export default function MeasureDetailPage(props: Props) { /> - - - - + + + + + + - - {__("Delete")} - + + + {__("Delete")} + + diff --git a/apps/console/src/pages/organizations/measures/MeasuresPage.tsx b/apps/console/src/pages/organizations/measures/MeasuresPage.tsx index 67eac2912..601742c32 100644 --- a/apps/console/src/pages/organizations/measures/MeasuresPage.tsx +++ b/apps/console/src/pages/organizations/measures/MeasuresPage.tsx @@ -48,6 +48,8 @@ import { useOrganizationId } from "/hooks/useOrganizationId"; import { Link, useParams } from "react-router"; import MeasureFormDialog from "./dialog/MeasureFormDialog"; import { usePageTitle } from "@probo/hooks"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; type Props = { queryRef: PreloadedQuery; @@ -113,6 +115,9 @@ export default function MeasuresPage(props: Props) { const importFileRef = useRef(null); usePageTitle(__("Measures")); + const hasAnyAction = isAuthorized(organization.id, "Measure", "updateMeasure") || + isAuthorized(organization.id, "Measure", "deleteMeasure"); + const handleImport: ChangeEventHandler = (event) => { const file = event.target.files?.[0]; if (!file) { @@ -143,19 +148,21 @@ export default function MeasuresPage(props: Props) { "Measures are actions taken to reduce the risk. Add them to track their implementation status." )} > - - {__("Import")} - - - - + + + {__("Import")} + + + + + {objectKeys(measuresPerCategory) @@ -166,6 +173,7 @@ export default function MeasuresPage(props: Props) { category={category} measures={measuresPerCategory[category]} connectionId={connectionId} + hasAnyAction={hasAnyAction} /> ))}

@@ -176,6 +184,7 @@ type CategoryProps = { category: string; measures: NodeOf[]; connectionId: string; + hasAnyAction: boolean; }; function Category(props: CategoryProps) { @@ -219,7 +228,7 @@ function Category(props: CategoryProps) {
- + {props.hasAnyAction && } @@ -228,6 +237,7 @@ function Category(props: CategoryProps) { key={measure.id} measure={measure} connectionId={props.connectionId} + hasAnyAction={props.hasAnyAction} /> ))} @@ -251,6 +261,7 @@ function Category(props: CategoryProps) { type MeasureRowProps = { measure: NodeOf; connectionId: string; + hasAnyAction: boolean; }; function MeasureRow(props: MeasureRowProps) { @@ -292,24 +303,30 @@ function MeasureRow(props: MeasureRowProps) { - + {props.hasAnyAction && ( + + )} ); diff --git a/apps/console/src/pages/organizations/meetings/MeetingDetailPage.tsx b/apps/console/src/pages/organizations/meetings/MeetingDetailPage.tsx index 48345622c..7256bda80 100644 --- a/apps/console/src/pages/organizations/meetings/MeetingDetailPage.tsx +++ b/apps/console/src/pages/organizations/meetings/MeetingDetailPage.tsx @@ -21,11 +21,12 @@ import { UpdateMeetingMinutesDialog, type UpdateMeetingMinutesDialogRef, } from "./dialogs/UpdateMeetingMinutesDialog"; -import { useRef } from "react"; +import { useRef, useState, useEffect } from "react"; import { meetingNodeQuery, useDeleteMeetingMutation, } from "/hooks/graph/MeetingGraph"; +import { isAuthorized } from "/permissions"; const meetingFragment = graphql` fragment MeetingDetailPageMeetingFragment on Meeting { @@ -62,6 +63,63 @@ export default function MeetingDetailPage(props: Props) { const confirm = useConfirm(); const updateMinutesDialogRef = useRef(null); + const [canUpdate, setCanUpdate] = useState(false); + const [canDelete, setCanDelete] = useState(false); + + useEffect(() => { + if (!organizationId) { + setCanUpdate(false); + setCanDelete(false); + return; + } + + try { + const updateAuth = isAuthorized(organizationId, "Meeting", "updateMeeting"); + setCanUpdate(updateAuth); + } catch (promise) { + if (promise instanceof Promise) { + promise + .then(() => { + try { + const updateAuth = isAuthorized(organizationId, "Meeting", "updateMeeting"); + setCanUpdate(updateAuth); + } catch { + setCanUpdate(false); + } + }) + .catch(() => { + setCanUpdate(false); + }); + } else { + setCanUpdate(false); + } + } + + try { + const deleteAuth = isAuthorized(organizationId, "Meeting", "deleteMeeting"); + setCanDelete(deleteAuth); + } catch (promise) { + if (promise instanceof Promise) { + promise + .then(() => { + try { + const deleteAuth = isAuthorized(organizationId, "Meeting", "deleteMeeting"); + setCanDelete(deleteAuth); + } catch { + setCanDelete(false); + } + }) + .catch(() => { + setCanDelete(false); + }); + } else { + setCanDelete(false); + } + } + }, [organizationId]); + + const hasAnyAction = canUpdate || canDelete; + usePageTitle(meeting.name); const handleDelete = () => { @@ -105,22 +163,28 @@ export default function MeetingDetailPage(props: Props) { }, ]} /> - - updateMinutesDialogRef.current?.open()} - icon={IconPencil} - > - {__("Edit minutes")} - - - {__("Delete meeting")} - - + {hasAnyAction && ( + + {canUpdate && ( + updateMinutesDialogRef.current?.open()} + icon={IconPencil} + > + {__("Edit minutes")} + + )} + {canDelete && ( + + {__("Delete meeting")} + + )} + + )} {__("Summary")} - + + +
{displayedSummary ? ( @@ -245,9 +248,11 @@ export default function MeetingsPage(props: Props) { "Track and manage your organization's meetings and their minutes." )} > - - - + + + + + {meetingNodes.length > 0 ? ( @@ -365,17 +370,19 @@ function MeetingRow({ )} -
+ + + ); } diff --git a/apps/console/src/pages/organizations/meetings/__generated__/MeetingsPage_UpdateSummaryMutation.graphql.ts b/apps/console/src/pages/organizations/meetings/__generated__/MeetingsPage_UpdateSummaryMutation.graphql.ts index 647767bd1..63a0187d9 100644 --- a/apps/console/src/pages/organizations/meetings/__generated__/MeetingsPage_UpdateSummaryMutation.graphql.ts +++ b/apps/console/src/pages/organizations/meetings/__generated__/MeetingsPage_UpdateSummaryMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<60e0c9d7301cff5c1df299e76debb633>> + * @generated SignedSource<<1fff8c5cca1610284185c485630e84de>> * @lightSyntaxTransform * @nogrep */ @@ -19,6 +19,7 @@ export type MeetingsPage_UpdateSummaryMutation$variables = { export type MeetingsPage_UpdateSummaryMutation$data = { readonly updateOrganizationContext: { readonly context: { + readonly organizationId: string; readonly summary: string | null | undefined; }; }; @@ -59,6 +60,13 @@ v1 = [ "name": "context", "plural": false, "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "organizationId", + "storageKey": null + }, { "alias": null, "args": null, @@ -91,16 +99,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "e6bead1dde5239f3cfd2fa1440191454", + "cacheID": "cb37cdde6dc5ac655a7cefc63d9e72e7", "id": null, "metadata": {}, "name": "MeetingsPage_UpdateSummaryMutation", "operationKind": "mutation", - "text": "mutation MeetingsPage_UpdateSummaryMutation(\n $input: UpdateOrganizationContextInput!\n) {\n updateOrganizationContext(input: $input) {\n context {\n summary\n }\n }\n}\n" + "text": "mutation MeetingsPage_UpdateSummaryMutation(\n $input: UpdateOrganizationContextInput!\n) {\n updateOrganizationContext(input: $input) {\n context {\n organizationId\n summary\n }\n }\n}\n" } }; })(); -(node as any).hash = "f354a34b18a449f02508c45d0e7d9dc5"; +(node as any).hash = "8bfa5b636dbc3535dbed22e1869bc941"; export default node; diff --git a/apps/console/src/pages/organizations/nonconformities/NonconformitiesPage.tsx b/apps/console/src/pages/organizations/nonconformities/NonconformitiesPage.tsx index 2a36c5f77..8dfd216cc 100644 --- a/apps/console/src/pages/organizations/nonconformities/NonconformitiesPage.tsx +++ b/apps/console/src/pages/organizations/nonconformities/NonconformitiesPage.tsx @@ -31,6 +31,8 @@ import { deleteNonconformityMutation, NonconformitiesConnectionKey } from "../.. import { sprintf, promisifyMutation, getStatusVariant, getStatusLabel, formatDate } from "@probo/helpers"; import { SnapshotBanner } from "/components/SnapshotBanner"; import { useParams } from "react-router"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; import type { NonconformitiesPageQuery } from "./__generated__/NonconformitiesPageQuery.graphql"; import type { NonconformitiesPageFragment$key, @@ -129,6 +131,11 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro ); const nonconformities: Nonconformity[] = nonconformitiesData?.nonconformities?.edges?.map((edge) => edge.node) ?? []; + const hasAnyAction = !isSnapshotMode && ( + isAuthorized(organizationId, "Nonconformity", "updateNonconformity") || + isAuthorized(organizationId, "Nonconformity", "deleteNonconformity") + ); + return (
{isSnapshotMode && ( @@ -141,9 +148,11 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro )} > {!isSnapshotMode && ( - - - + + + + + )} @@ -169,7 +178,7 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
- {!isSnapshotMode && ()} + {hasAnyAction && } @@ -180,6 +189,7 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro connectionId={connectionId} isSnapshotMode={isSnapshotMode} snapshotId={snapshotId} + hasAnyAction={hasAnyAction} /> ))} @@ -209,11 +219,13 @@ function NonconformityRow({ connectionId, isSnapshotMode, snapshotId, + hasAnyAction, }: { nonconformity: Nonconformity; connectionId: string; isSnapshotMode: boolean; snapshotId?: string; + hasAnyAction: boolean; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -283,15 +295,18 @@ function NonconformityRow({ {__("No due date")} )} - {!isSnapshotMode && ( )} diff --git a/apps/console/src/pages/organizations/nonconformities/NonconformityDetailsPage.tsx b/apps/console/src/pages/organizations/nonconformities/NonconformityDetailsPage.tsx index 5dd7a0553..bf10a4935 100644 --- a/apps/console/src/pages/organizations/nonconformities/NonconformityDetailsPage.tsx +++ b/apps/console/src/pages/organizations/nonconformities/NonconformityDetailsPage.tsx @@ -34,6 +34,7 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema"; import z from "zod"; import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency, getStatusOptions, formatError, type GraphQLError } from "@probo/helpers"; import type { NonconformityGraphNodeQuery } from "/hooks/graph/__generated__/NonconformityGraphNodeQuery.graphql"; +import { Authorized } from "/permissions"; const updateNonconformitySchema = z.object({ referenceId: z.string().min(1, "Reference ID is required"), @@ -161,13 +162,15 @@ export default function NonconformityDetailsPage(props: Props) { {!isSnapshotMode && ( - - {__("Delete")} - + + + {__("Delete")} + + )} @@ -276,9 +279,11 @@ export default function NonconformityDetailsPage(props: Props) {
{formState.isDirty && !isSnapshotMode && ( - + + + )}
diff --git a/apps/console/src/pages/organizations/obligations/ObligationDetailsPage.tsx b/apps/console/src/pages/organizations/obligations/ObligationDetailsPage.tsx index 80fc472fc..5b272e29c 100644 --- a/apps/console/src/pages/organizations/obligations/ObligationDetailsPage.tsx +++ b/apps/console/src/pages/organizations/obligations/ObligationDetailsPage.tsx @@ -35,6 +35,7 @@ import z from "zod"; import { getObligationStatusVariant, getObligationStatusLabel, formatDatetime, getObligationStatusOptions, validateSnapshotConsistency } from "@probo/helpers"; import { SnapshotBanner } from "/components/SnapshotBanner"; import type { ObligationGraphNodeQuery } from "/hooks/graph/__generated__/ObligationGraphNodeQuery.graphql"; +import { Authorized } from "/permissions"; const updateObligationSchema = z.object({ area: z.string().optional(), @@ -156,11 +157,13 @@ export default function ObligationDetailsPage(props: Props) { {!isSnapshotMode && ( - - - {__("Delete")} - - + + + + {__("Delete")} + + + )} @@ -298,12 +301,14 @@ export default function ObligationDetailsPage(props: Props) { {!isSnapshotMode && (
- + + +
)} diff --git a/apps/console/src/pages/organizations/obligations/ObligationsPage.tsx b/apps/console/src/pages/organizations/obligations/ObligationsPage.tsx index 2e06c3a8a..c423f20db 100644 --- a/apps/console/src/pages/organizations/obligations/ObligationsPage.tsx +++ b/apps/console/src/pages/organizations/obligations/ObligationsPage.tsx @@ -31,6 +31,8 @@ import { deleteObligationMutation } from "../../../hooks/graph/ObligationGraph"; import { promisifyMutation, getObligationStatusVariant, getObligationStatusLabel, formatDate } from "@probo/helpers"; import { SnapshotBanner } from "/components/SnapshotBanner"; import type { ObligationsPageQuery } from "./__generated__/ObligationsPageQuery.graphql"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; import type { ObligationsPageFragment$key, ObligationsPageFragment$data, @@ -117,6 +119,11 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) { const connectionId = obligationsData?.obligations?.__id || ""; const obligations: Obligation[] = obligationsData?.obligations?.edges?.map((edge) => edge.node) ?? []; + const hasAnyAction = !isSnapshotMode && ( + isAuthorized(organizationId, "Obligation", "updateObligation") || + isAuthorized(organizationId, "Obligation", "deleteObligation") + ); + return (
{isSnapshotMode && snapshotId && ( @@ -129,9 +136,11 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) { )} > {!snapshotId && ( - - - + + + + + )} @@ -156,7 +165,7 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
- + {hasAnyAction && } @@ -166,6 +175,7 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) { obligation={obligation} connectionId={connectionId} snapshotId={snapshotId} + hasAnyAction={hasAnyAction} /> ))} @@ -192,10 +202,12 @@ function ObligationRow({ obligation, connectionId, snapshotId, + hasAnyAction, }: { obligation: Obligation; connectionId: string; snapshotId?: string; + hasAnyAction: boolean; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -246,19 +258,21 @@ function ObligationRow({ {__("No due date")} )} - + + )} ); } diff --git a/apps/console/src/pages/organizations/people/PeopleDetailPage.tsx b/apps/console/src/pages/organizations/people/PeopleDetailPage.tsx index 59b9a194e..1ac7b8c72 100644 --- a/apps/console/src/pages/organizations/people/PeopleDetailPage.tsx +++ b/apps/console/src/pages/organizations/people/PeopleDetailPage.tsx @@ -21,6 +21,7 @@ import { import { useTranslate } from "@probo/i18n"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { Outlet } from "react-router"; +import { Authorized } from "/permissions"; type Props = { queryRef: PreloadedQuery; @@ -54,15 +55,17 @@ export default function PeopleDetailPage(props: Props) {
{people.fullName}
- - - {__("Delete")} - - + + + + {__("Delete")} + + + diff --git a/apps/console/src/pages/organizations/people/PeopleListPage.tsx b/apps/console/src/pages/organizations/people/PeopleListPage.tsx index 78612792a..0bb59199d 100644 --- a/apps/console/src/pages/organizations/people/PeopleListPage.tsx +++ b/apps/console/src/pages/organizations/people/PeopleListPage.tsx @@ -23,6 +23,8 @@ import { usePageTitle } from "@probo/hooks"; import { getRole } from "@probo/helpers"; import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog"; import { useOrganizationId } from "/hooks/useOrganizationId"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; type People = NodeOf; @@ -40,11 +42,15 @@ export default function PeopleListPage({ queryRef: PreloadedQuery; }) { const { __ } = useTranslate(); + const organizationId = useOrganizationId(); const { people, refetch, connectionId, hasNext, loadNext, isLoadingNext } = usePeopleQuery(queryRef); usePageTitle(__("Members")); + const hasAnyAction = isAuthorized(organizationId, "People", "updatePeople") || + isAuthorized(organizationId, "People", "deletePeople"); + return (
- - - + + + + + {__("Name")} {__("Role")}
- + {hasAnyAction && } @@ -77,6 +85,7 @@ export default function PeopleListPage({ key={person.id} people={person} connectionId={connectionId} + hasAnyAction={hasAnyAction} /> ))} @@ -88,9 +97,11 @@ export default function PeopleListPage({ function PeopleRow({ people, connectionId, + hasAnyAction, }: { people: People; connectionId: string; + hasAnyAction: boolean; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -115,17 +126,21 @@ function PeopleRow({ - + {hasAnyAction && ( + + )} ); } diff --git a/apps/console/src/pages/organizations/people/tabs/PeopleProfileTab.tsx b/apps/console/src/pages/organizations/people/tabs/PeopleProfileTab.tsx index 1e25f0318..61b9b3cd1 100644 --- a/apps/console/src/pages/organizations/people/tabs/PeopleProfileTab.tsx +++ b/apps/console/src/pages/organizations/people/tabs/PeopleProfileTab.tsx @@ -9,6 +9,7 @@ import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/Peopl import { updatePeopleMutation } from "/hooks/graph/PeopleGraph"; import { Button, Card, Field, Input } from "@probo/ui"; import { EmailsField } from "/components/form/EmailsField"; +import { Authorized } from "/permissions"; const schema = z.object({ fullName: z.string().min(1), @@ -94,9 +95,11 @@ export default function PeopleProfileTab() {
{formState.isDirty && ( - + + + )}
diff --git a/apps/console/src/pages/organizations/processingActivities/ProcessingActivitiesPage.tsx b/apps/console/src/pages/organizations/processingActivities/ProcessingActivitiesPage.tsx index 769cd39d7..7b3388aa1 100644 --- a/apps/console/src/pages/organizations/processingActivities/ProcessingActivitiesPage.tsx +++ b/apps/console/src/pages/organizations/processingActivities/ProcessingActivitiesPage.tsx @@ -32,6 +32,8 @@ import { CreateProcessingActivityDialog } from "./dialogs/CreateProcessingActivi import { deleteProcessingActivityMutation, ProcessingActivitiesConnectionKey } from "../../../hooks/graph/ProcessingActivityGraph"; import { sprintf, promisifyMutation } from "@probo/helpers"; import { SnapshotBanner } from "/components/SnapshotBanner"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; import type { NodeOf } from "/types"; import type { ProcessingActivitiesPageQuery } from "./__generated__/ProcessingActivitiesPageQuery.graphql"; import type { @@ -126,6 +128,11 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit ); const activities = data?.processingActivities?.edges?.map((edge) => edge.node) ?? []; + const hasAnyAction = !isSnapshotMode && ( + isAuthorized(organizationId, "ProcessingActivity", "updateProcessingActivity") || + isAuthorized(organizationId, "ProcessingActivity", "deleteProcessingActivity") + ); + return (
{isSnapshotMode && snapshotId && ( @@ -133,14 +140,16 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit )} {!isSnapshotMode && ( - - - + + + + + )} @@ -155,7 +164,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
- {!isSnapshotMode && } + {hasAnyAction && } @@ -164,6 +173,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit key={activity.id} activity={activity} connectionId={connectionId} + hasAnyAction={hasAnyAction} /> ))} @@ -200,9 +210,11 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit function ActivityRow({ activity, connectionId, + hasAnyAction, }: { activity: NodeOf>; connectionId: string; + hasAnyAction: boolean; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -255,16 +267,18 @@ function ActivityRow({ {activity.internationalTransfers ? __("Yes") : __("No")} - {!isSnapshotMode && ( + {hasAnyAction && ( )} diff --git a/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx b/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx index 9ff1dccdd..77fe65df4 100644 --- a/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx +++ b/apps/console/src/pages/organizations/processingActivities/ProcessingActivityDetailsPage.tsx @@ -41,6 +41,7 @@ import { } from "../../../components/form/ProcessingActivityEnumOptions"; import type { ProcessingActivityGraphNodeQuery } from "/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql"; +import { Authorized } from "/permissions"; const updateProcessingActivitySchema = z.object({ name: z.string().min(1, "Name is required"), @@ -170,11 +171,13 @@ export default function ProcessingActivityDetailsPage(props: Props) { ]} /> {!isSnapshotMode && ( - - - {__("Delete")} - - + + + + {__("Delete")} + + + )} @@ -407,13 +410,15 @@ export default function ProcessingActivityDetailsPage(props: Props) { {!isSnapshotMode && (
- + + +
)} diff --git a/apps/console/src/pages/organizations/risks/RiskDetailPage.tsx b/apps/console/src/pages/organizations/risks/RiskDetailPage.tsx index 1badb4ae3..4f08c4d05 100644 --- a/apps/console/src/pages/organizations/risks/RiskDetailPage.tsx +++ b/apps/console/src/pages/organizations/risks/RiskDetailPage.tsx @@ -30,6 +30,7 @@ import { } from "/hooks/graph/RiskGraph"; import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql"; import { SnapshotBanner } from "/components/SnapshotBanner"; +import { Authorized } from "/permissions"; type Props = { queryRef: PreloadedQuery; @@ -120,23 +121,27 @@ export default function RiskDetailPage(props: Props) { /> {!isSnapshotMode && (
- - {__("Edit")} - - } - risk={{ id: riskId, ...risk }} - /> - - - {__("Delete")} - - + + + {__("Edit")} + + } + risk={{ id: riskId, ...risk }} + /> + + + + + {__("Delete")} + + +
)} diff --git a/apps/console/src/pages/organizations/risks/RisksPage.tsx b/apps/console/src/pages/organizations/risks/RisksPage.tsx index 505ef3dd4..3675cbfed 100644 --- a/apps/console/src/pages/organizations/risks/RisksPage.tsx +++ b/apps/console/src/pages/organizations/risks/RisksPage.tsx @@ -29,6 +29,8 @@ import type { RiskGraphListQuery } from "/hooks/graph/__generated__/RiskGraphLis import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql"; import { useParams } from "react-router"; import { SnapshotBanner } from "/components/SnapshotBanner"; +import { Authorized } from "/permissions"; +import { isAuthorized } from "/permissions"; type Props = { queryRef: PreloadedQuery; @@ -54,6 +56,11 @@ export default function RisksPage(props: Props) { usePageTitle(__("Risks")); + const hasAnyAction = !isSnapshotMode && ( + isAuthorized(organizationId, "Risk", "updateRisk") || + isAuthorized(organizationId, "Risk", "deleteRisk") + ); + return (
{snapshotId && } @@ -64,13 +71,15 @@ export default function RisksPage(props: Props) { )} > {!isSnapshotMode && ( - { - pagination.refetch({ snapshotId }); - }} - trigger={} - /> + + { + pagination.refetch({ snapshotId }); + }} + trigger={} + /> + )} @@ -101,7 +110,7 @@ export default function RisksPage(props: Props) { {__("Owner")} -
+ {hasAnyAction && } @@ -111,6 +120,7 @@ export default function RisksPage(props: Props) { key={risk.id} connectionId={connectionId} organizationId={organizationId} + hasAnyAction={hasAnyAction} /> ))} @@ -123,6 +133,7 @@ type RowProps = { risk: NodeOf; connectionId: string; organizationId: string; + hasAnyAction: boolean; }; function RiskRow(props: RowProps) { @@ -180,26 +191,30 @@ function RiskRow(props: RowProps) { - + + )} ); diff --git a/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx b/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx index 12d915cdb..cf0c53414 100644 --- a/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx +++ b/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx @@ -23,6 +23,7 @@ import { z } from "zod"; import type { GeneralSettingsTabFragment$key } from "./__generated__/GeneralSettingsTabFragment.graphql"; import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog"; import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph"; +import { isAuthorized } from "/permissions"; const generalSettingsTabFragment = graphql` fragment GeneralSettingsTabFragment on Organization { @@ -90,6 +91,9 @@ export default function GeneralSettingsTab() { const organization = useFragment(generalSettingsTabFragment, organizationKey); const deleteDialogRef = useDialogRef(); + const canUpdate = isAuthorized(organization.id, "Organization", "updateOrganization"); + const canDelete = isAuthorized(organization.id, "Organization", "deleteOrganization"); + const [logoPreview, setLogoPreview] = useState(null); const [horizontalLogoPreview, setHorizontalLogoPreview] = useState< string | null @@ -267,17 +271,19 @@ export default function GeneralSettingsTab() { name={organization.name} size="xl" /> - - {isUpdatingOrganization - ? __("Uploading...") - : __("Change logo")} - + {canUpdate && ( + + {isUpdatingOrganization + ? __("Uploading...") + : __("Change logo")} + + )}
@@ -301,19 +307,21 @@ export default function GeneralSettingsTab() { />
)} - - {isUpdatingOrganization - ? __("Uploading...") - : horizontalLogoPreview || organization.horizontalLogoUrl - ? __("Change horizontal logo") - : __("Upload horizontal logo")} - - {organization.horizontalLogoUrl && ( + {canUpdate && ( + + {isUpdatingOrganization + ? __("Uploading...") + : horizontalLogoPreview || organization.horizontalLogoUrl + ? __("Change horizontal logo") + : __("Upload horizontal logo")} + + )} + {canUpdate && organization.horizontalLogoUrl && ( {__("Description")}
{__("Name")} {__("Category")} {__("Visibility")}
+ {__("No vendors available")}
- - + +
{__("Amount")} {__("Owner")} {__("Vendors")}
- {!isSnapshotMode && ( - - - {__("Delete")} - - - )} - + + + + {__("Delete")} + + + +
{__("Valid From")} {__("Valid Until")} {__("Report")}
- - - {__("Delete")} - - - + + + + {__("Delete")} + + + +
{__("Priority")} {__("Owner")} {__("Target Date")}{__("Actions")}{__("Actions")}
- - {__("Delete")} - + + + {__("Delete")} + + {__("Owner")} {__("Vendors")}
- {!snapshotId && ( - - {__("Delete")} - + {hasAnyAction && ( + + + + + {__("Delete")} + + - )} -
{__("Owner")} {__("Last update")} {__("Signatures")}
{signedCount}/{signatures.length} - - - {__("Delete")} - - - + + + + {__("Delete")} + + + +
{__("Measure")} {__("State")}
- - dialogRef.current?.open()} - > - {__("Edit")} - - - {__("Delete")} - - - + + + dialogRef.current?.open()} + > + {__("Edit")} + + + + + {__("Delete")} + + + +
- - - {__("Delete")} - - - + + + {__("Delete")} + + +
{__("Audit")} {__("Owner")} {__("Due Date")}{__("Actions")}{__("Actions")}
+ {hasAnyAction && ( + - handleDeleteNonconformity(nonconformity)} - > - {__("Delete")} - + + handleDeleteNonconformity(nonconformity)} + > + {__("Delete")} + + {__("Status")} {__("Owner")} {__("Due Date")}{__("Actions")}{__("Actions")}
- {!isSnapshotMode && ( + {hasAnyAction && ( + - - {__("Delete")} - + + + {__("Delete")} + + - )} -
{__("Position")}{__("Actions")}{__("Actions")}
{getRole(__, people.kind)} {people.position} - - - {__("Delete")} - - - + + + + {__("Delete")} + + + +
{__("Lawful Basis")} {__("Location")} {__("International Transfers")}{__("Actions")}{__("Actions")}
- - {__("Delete")} - + + + {__("Delete")} + +
{risk.owner?.fullName || __("Unassigned")} - {!isSnapshotMode && ( + {props.hasAnyAction && ( + - formDialogRef.current?.open()} - > - {__("Edit")} - + + formDialogRef.current?.open()} + > + {__("Edit")} + + - - {__("Delete")} - + + + {__("Delete")} + + - )} -