Fix permissions handling with react context

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-02 17:11:02 +04:00
parent accc8292b5
commit e65097f248
57 changed files with 609 additions and 840 deletions

View File

@@ -6,7 +6,8 @@ import type { CustomDomainManagerDeleteMutation } from "./__generated__/CustomDo
import { CreateCustomDomainDialog } from "./CreateCustomDomainDialog"; import { CreateCustomDomainDialog } from "./CreateCustomDomainDialog";
import { DeleteCustomDomainDialog } from "./DeleteCustomDomainDialog"; import { DeleteCustomDomainDialog } from "./DeleteCustomDomainDialog";
import { DomainDetailsDialog } from "./DomainDetailsDialog"; import { DomainDetailsDialog } from "./DomainDetailsDialog";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const deleteCustomDomainMutation = graphql` const deleteCustomDomainMutation = graphql`
mutation CustomDomainManagerDeleteMutation($input: DeleteCustomDomainInput!) { mutation CustomDomainManagerDeleteMutation($input: DeleteCustomDomainInput!) {
@@ -45,7 +46,7 @@ export function CustomDomainManager({
customDomain, customDomain,
}: CustomDomainManagerProps) { }: CustomDomainManagerProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const [deleteCustomDomain] = const [deleteCustomDomain] =
useMutationWithToasts<CustomDomainManagerDeleteMutation>( useMutationWithToasts<CustomDomainManagerDeleteMutation>(
deleteCustomDomainMutation, deleteCustomDomainMutation,
@@ -107,11 +108,11 @@ export function CustomDomainManager({
)} )}
</p> </p>
<div className="flex justify-center"> <div className="flex justify-center">
<Authorized entity="Organization" action="createCustomDomain"> {isAuthorized("Organization", "createCustomDomain") && (
<CreateCustomDomainDialog organizationId={organizationId}> <CreateCustomDomainDialog organizationId={organizationId}>
<Button icon={IconPlusLarge}>{__("Add Domain")}</Button> <Button icon={IconPlusLarge}>{__("Add Domain")}</Button>
</CreateCustomDomainDialog> </CreateCustomDomainDialog>
</Authorized> )}
</div> </div>
</div> </div>
</Card> </Card>
@@ -139,14 +140,14 @@ export function CustomDomainManager({
<Button variant="secondary">{__("View Details")}</Button> <Button variant="secondary">{__("View Details")}</Button>
</DomainDetailsDialog> </DomainDetailsDialog>
<Authorized entity="CustomDomain" action="deleteCustomDomain"> {isAuthorized("CustomDomain", "deleteCustomDomain") && (
<DeleteCustomDomainDialog <DeleteCustomDomainDialog
domainName={domain.domain} domainName={domain.domain}
onConfirm={handleDeleteDomain} onConfirm={handleDeleteDomain}
> >
<Button variant="danger">{__("Delete")}</Button> <Button variant="danger">{__("Delete")}</Button>
</DeleteCustomDomainDialog> </DeleteCustomDomainDialog>
</Authorized> )}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -17,8 +17,9 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { z } from "zod"; import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { Suspense } from "react"; import { Suspense, use } from "react";
import { getAssignableRoles } from "/permissions"; import { getAssignableRoles } from "@probo/helpers";
import { PermissionsContext } from "/providers/PermissionsContext";
const inviteMutation = graphql` const inviteMutation = graphql`
mutation InviteUserDialogMutation( mutation InviteUserDialogMutation(
@@ -56,7 +57,8 @@ type Props = PropsWithChildren & {
function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) { function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const assignableRoles = getAssignableRoles(organizationId); const { role: currentUserRole } = use(PermissionsContext);
const assignableRoles = getAssignableRoles(currentUserRole);
const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, { const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, {
successMessage: __("Invitation sent successfully"), successMessage: __("Invitation sent successfully"),
errorMessage: __("Failed to send invitation"), errorMessage: __("Failed to send invitation"),

View File

@@ -2,7 +2,8 @@ import { Badge, Button, Card } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers"; import { sprintf } from "@probo/helpers";
import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql"; import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
organizationId: string; organizationId: string;
@@ -11,7 +12,7 @@ type Props = {
export function SlackConnections({ organizationId, slackConnections: connectedSlackConnections }: Props) { export function SlackConnections({ organizationId, slackConnections: connectedSlackConnections }: Props) {
const { __, dateTimeFormat } = useTranslate(); const { __, dateTimeFormat } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const slackConnectionDefinitions = [ const slackConnectionDefinitions = [
{ {
id: "SLACK", id: "SLACK",
@@ -77,11 +78,11 @@ export function SlackConnections({ organizationId, slackConnections: connectedSl
</Badge> </Badge>
</div> </div>
) : ( ) : (
<Authorized entity="TrustCenter" action="updateTrustCenter"> isAuthorized("TrustCenter", "updateTrustCenter") && (
<Button variant="secondary" asChild> <Button variant="secondary" asChild>
<a href={getUrl(slackConnection.id)}>{__("Connect")}</a> <a href={getUrl(slackConnection.id)}>{__("Connect")}</a>
</Button> </Button>
</Authorized> )
)} )}
</Card> </Card>
))} ))}

View File

@@ -1,3 +1,6 @@
import { promisifyMutation } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { import {
ActionDropdown, ActionDropdown,
Avatar, Avatar,
@@ -15,21 +18,17 @@ import {
useConfirm, useConfirm,
useDialogRef, useDialogRef,
} from "@probo/ui"; } from "@probo/ui";
import { Fragment } from "react"; import { Fragment, use } from "react";
import { graphql, useMutation, useRelayEnvironment } from "react-relay"; import { graphql, useMutation, useRelayEnvironment } from "react-relay";
import { useTranslate } from "@probo/i18n"; import { Link, useLocation, useParams } from "react-router";
import { usePageTitle } from "@probo/hooks"; import type { TaskFormDialogFragment$key } from "./__generated__/TaskFormDialogFragment.graphql";
import type { ItemOf } from "/types";
import TaskFormDialog, { import TaskFormDialog, {
taskUpdateMutation, taskUpdateMutation,
} from "/components/tasks/TaskFormDialog"; } from "/components/tasks/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"; import { updateStoreCounter } from "/hooks/useMutationWithIncrement";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { PermissionsContext } from "/providers/PermissionsContext";
import type { ItemOf } from "/types";
type Props = { type Props = {
tasks: ({ tasks: ({
@@ -51,7 +50,6 @@ type Props = {
export default function TasksCard({ tasks, connectionId }: Props) { export default function TasksCard({ tasks, connectionId }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId();
const hash = useLocation().hash.replace("#", ""); const hash = useLocation().hash.replace("#", "");
const hashes = [ const hashes = [
@@ -70,8 +68,10 @@ export default function TasksCard({ tasks, connectionId }: Props) {
usePageTitle(__("Tasks")); usePageTitle(__("Tasks"));
const hasAnyAction = isAuthorized(organizationId, "Task", "updateTask") || const { isAuthorized } = use(PermissionsContext);
isAuthorized(organizationId, "Task", "deleteTask");
const hasAnyAction = isAuthorized("Task", "updateTask") ||
isAuthorized("Task", "deleteTask");
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -151,6 +151,7 @@ function TaskRow(props: TaskRowProps) {
const confirm = useConfirm(); const confirm = useConfirm();
const [deleteTask] = useMutation(deleteMutation); const [deleteTask] = useMutation(deleteMutation);
const params = useParams<{ measureId?: string }>(); const params = useParams<{ measureId?: string }>();
const { isAuthorized } = use(PermissionsContext);
const relayEnv = useRelayEnvironment(); const relayEnv = useRelayEnvironment();
const [updateTask, isUpdating] = useMutation(taskUpdateMutation); const [updateTask, isUpdating] = useMutation(taskUpdateMutation);
@@ -232,15 +233,15 @@ function TaskRow(props: TaskRowProps) {
)} )}
{props.hasAnyAction && ( {props.hasAnyAction && (
<ActionDropdown> <ActionDropdown>
<Authorized entity="Task" action="updateTask"> {isAuthorized("Task", "updateTask") && (
<DropdownItem <DropdownItem
icon={IconPencil} icon={IconPencil}
onClick={() => dialogRef.current?.open()} onClick={() => dialogRef.current?.open()}
> >
{__("Edit")} {__("Edit")}
</DropdownItem> </DropdownItem>
</Authorized> )}
<Authorized entity="Task" action="deleteTask"> {isAuthorized("Task", "deleteTask") && (
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -248,7 +249,7 @@ function TaskRow(props: TaskRowProps) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
)} )}
</div> </div>

View File

@@ -14,11 +14,11 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react"; import { useMemo, useState, useCallback, useEffect, use } from "react";
import { sprintf, getAuditStateVariant, getAuditStateLabel, formatDate, getTrustCenterVisibilityOptions } from "@probo/helpers"; import { sprintf, getAuditStateVariant, getAuditStateLabel, formatDate, getTrustCenterVisibilityOptions } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import type { TrustCenterAuditsCardFragment$key } from "./__generated__/TrustCenterAuditsCardFragment.graphql"; import type { TrustCenterAuditsCardFragment$key } from "./__generated__/TrustCenterAuditsCardFragment.graphql";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
const trustCenterAuditFragment = graphql` const trustCenterAuditFragment = graphql`
fragment TrustCenterAuditsCardFragment on Audit { fragment TrustCenterAuditsCardFragment on Audit {
@@ -124,8 +124,8 @@ function AuditRow(props: {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null); const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const { isAuthorized } = use(PermissionsContext);
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false; const canUpdate = organizationId ? isAuthorized("TrustCenter", "updateTrustCenter") : false;
const handleValueChange = useCallback((value: string | {}) => { const handleValueChange = useCallback((value: string | {}) => {
const stringValue = typeof value === 'string' ? value : ''; const stringValue = typeof value === 'string' ? value : '';

View File

@@ -17,10 +17,10 @@ import {
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import type { TrustCenterDocumentsCardFragment$key } from "./__generated__/TrustCenterDocumentsCardFragment.graphql"; import type { TrustCenterDocumentsCardFragment$key } from "./__generated__/TrustCenterDocumentsCardFragment.graphql";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react"; import { useMemo, useState, useCallback, useEffect, use } from "react";
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers"; import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
const trustCenterDocumentFragment = graphql` const trustCenterDocumentFragment = graphql`
fragment TrustCenterDocumentsCardFragment on Document { fragment TrustCenterDocumentsCardFragment on Document {
@@ -129,8 +129,8 @@ function DocumentRow(props: {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null); const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const { isAuthorized } = use(PermissionsContext);
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false; const canUpdate = isAuthorized("TrustCenter", "updateTrustCenter");
const handleValueChange = useCallback((value: string | {}) => { const handleValueChange = useCallback((value: string | {}) => {
const stringValue = typeof value === 'string' ? value : ''; const stringValue = typeof value === 'string' ? value : '';

View File

@@ -18,12 +18,10 @@ import {
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import type { TrustCenterFilesCardFragment$key, TrustCenterFilesCardFragment$data } from "./__generated__/TrustCenterFilesCardFragment.graphql"; import type { TrustCenterFilesCardFragment$key, TrustCenterFilesCardFragment$data } from "./__generated__/TrustCenterFilesCardFragment.graphql";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react"; import { useMemo, useState, useCallback, useEffect, use } from "react";
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers"; import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
import { formatDate } from "@probo/helpers"; import { formatDate } from "@probo/helpers";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { isAuthorized } from "/permissions";
import { useParams } from "react-router";
const trustCenterFileFragment = graphql` const trustCenterFileFragment = graphql`
fragment TrustCenterFilesCardFragment on TrustCenterFile { fragment TrustCenterFilesCardFragment on TrustCenterFile {
@@ -150,9 +148,8 @@ function FileRow(props: {
const file = props.file; const file = props.file;
const { __ } = useTranslate(); const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null); const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const { organizationId } = useParams(); const { isAuthorized } = use(PermissionsContext);
const canUpdate = isAuthorized("TrustCenter", "updateTrustCenter");
const canUpdate = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false;
const handleValueChange = useCallback((value: string | {}) => { const handleValueChange = useCallback((value: string | {}) => {
const stringValue = typeof value === 'string' ? value : ''; const stringValue = typeof value === 'string' ? value : '';
@@ -207,7 +204,7 @@ function FileRow(props: {
onClick={() => window.open(file.fileUrl, '_blank', 'noopener,noreferrer')} onClick={() => window.open(file.fileUrl, '_blank', 'noopener,noreferrer')}
title={__("Download")} title={__("Download")}
/> />
<Authorized entity="TrustCenterFile" action="updateTrustCenterFile"> {isAuthorized("TrustCenterFile", "updateTrustCenterFile") && (
<Button <Button
variant="secondary" variant="secondary"
icon={IconPencil} icon={IconPencil}
@@ -215,8 +212,8 @@ function FileRow(props: {
disabled={props.disabled} disabled={props.disabled}
title={__("Edit")} title={__("Edit")}
/> />
</Authorized> )}
<Authorized entity="TrustCenterFile" action="deleteTrustCenterFile"> {isAuthorized("TrustCenterFile", "deleteTrustCenterFile") && (
<Button <Button
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -224,7 +221,7 @@ function FileRow(props: {
disabled={props.disabled} disabled={props.disabled}
title={__("Delete")} title={__("Delete")}
/> />
</Authorized> )}
</div> </div>
</Td> </Td>
</Tr> </Tr>

View File

@@ -14,14 +14,14 @@ import {
IconPencil, IconPencil,
IconArrowLink, IconArrowLink,
} from "@probo/ui"; } from "@probo/ui";
import { type ReactNode, useRef, useState } from "react"; import { type ReactNode, useRef, useState, use } from "react";
import { import {
useTrustCenterReferences, useTrustCenterReferences,
useUpdateTrustCenterReferenceRankMutation, useUpdateTrustCenterReferenceRankMutation,
} from "/hooks/graph/TrustCenterReferenceGraph"; } from "/hooks/graph/TrustCenterReferenceGraph";
import { TrustCenterReferenceDialog, type TrustCenterReferenceDialogRef } from "./TrustCenterReferenceDialog"; import { TrustCenterReferenceDialog, type TrustCenterReferenceDialogRef } from "./TrustCenterReferenceDialog";
import { DeleteTrustCenterReferenceDialog } from "./DeleteTrustCenterReferenceDialog"; import { DeleteTrustCenterReferenceDialog } from "./DeleteTrustCenterReferenceDialog";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
trustCenterId: string; trustCenterId: string;
@@ -41,6 +41,7 @@ type Reference = {
export function TrustCenterReferencesSection({ trustCenterId }: Props) { export function TrustCenterReferencesSection({ trustCenterId }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const dialogRef = useRef<TrustCenterReferenceDialogRef>(null); const dialogRef = useRef<TrustCenterReferenceDialogRef>(null);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null); const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null); const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
@@ -112,14 +113,14 @@ export function TrustCenterReferencesSection({ trustCenterId }: Props) {
{__("Showcase your customers and partners on your trust center")} {__("Showcase your customers and partners on your trust center")}
</p> </p>
</div> </div>
<Authorized entity="TrustCenter" action="createTrustCenterReference"> {isAuthorized("TrustCenter", "createTrustCenterReference") && (
<Button <Button
icon={IconPlusLarge} icon={IconPlusLarge}
onClick={handleCreate} onClick={handleCreate}
> >
{__("Add Reference")} {__("Add Reference")}
</Button> </Button>
</Authorized> )}
</div> </div>
<Table> <Table>
@@ -190,6 +191,7 @@ function ReferenceRow({
onDrop, onDrop,
}: ReferenceRowProps) { }: ReferenceRowProps) {
const [isMouseDown, setIsMouseDown] = useState(false); const [isMouseDown, setIsMouseDown] = useState(false);
const { isAuthorized } = use(PermissionsContext);
const className = [ const className = [
isDragging && "opacity-50 cursor-grabbing", isDragging && "opacity-50 cursor-grabbing",
@@ -231,14 +233,14 @@ function ReferenceRow({
icon={IconArrowLink} icon={IconArrowLink}
onClick={onVisitWebsite} onClick={onVisitWebsite}
/> />
<Authorized entity="TrustCenterReference" action="updateTrustCenterReference"> {isAuthorized("TrustCenterReference", "updateTrustCenterReference") && (
<Button <Button
variant="secondary" variant="secondary"
icon={IconPencil} icon={IconPencil}
onClick={onEdit} onClick={onEdit}
/> />
</Authorized> )}
<Authorized entity="TrustCenterReference" action="deleteTrustCenterReference"> {isAuthorized("TrustCenterReference", "deleteTrustCenterReference") && (
<DeleteTrustCenterReferenceDialog <DeleteTrustCenterReferenceDialog
referenceId={reference.id} referenceId={reference.id}
referenceName={reference.name} referenceName={reference.name}
@@ -249,7 +251,7 @@ function ReferenceRow({
icon={IconTrashCan} icon={IconTrashCan}
/> />
</DeleteTrustCenterReferenceDialog> </DeleteTrustCenterReferenceDialog>
</Authorized> )}
</div> </div>
</Td> </Td>
</Tr> </Tr>

View File

@@ -14,11 +14,11 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useMemo, useState, useEffect } from "react"; import { useMemo, useState, use } from "react";
import { sprintf } from "@probo/helpers"; import { sprintf } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { isAuthorized } from "/permissions/permissions";
import type { TrustCenterVendorsCardFragment$key } from "./__generated__/TrustCenterVendorsCardFragment.graphql"; import type { TrustCenterVendorsCardFragment$key } from "./__generated__/TrustCenterVendorsCardFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
const trustCenterVendorFragment = graphql` const trustCenterVendorFragment = graphql`
fragment TrustCenterVendorsCardFragment on Vendor { fragment TrustCenterVendorsCardFragment on Vendor {
@@ -49,44 +49,14 @@ type Props<Params> = {
export function TrustCenterVendorsCard<Params>(props: Props<Params>) { export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId();
const [limit, setLimit] = useState<number | null>(100); const [limit, setLimit] = useState<number | null>(100);
const [canUpdate, setCanUpdate] = useState<boolean>(false); const { isAuthorized } = use(PermissionsContext);
const canUpdate = isAuthorized("Vendor", "updateVendor");
const vendors = useMemo(() => { const vendors = useMemo(() => {
return limit ? props.vendors.slice(0, limit) : props.vendors; return limit ? props.vendors.slice(0, limit) : props.vendors;
}, [props.vendors, limit]); }, [props.vendors, limit]);
const showMoreButton = limit !== null && props.vendors.length > limit; const showMoreButton = limit !== null && props.vendors.length > limit;
useEffect(() => {
if (!organizationId) {
setCanUpdate(false);
return;
}
try {
const authorized = isAuthorized(organizationId, "Vendor", "updateVendor");
setCanUpdate(authorized);
} catch (promise) {
if (promise instanceof Promise) {
promise
.then(() => {
try {
const authorized = isAuthorized(organizationId, "Vendor", "updateVendor");
setCanUpdate(authorized);
} catch {
setCanUpdate(false);
}
})
.catch(() => {
setCanUpdate(false);
});
} else {
setCanUpdate(false);
}
}
}, [organizationId]);
const onToggleVisibility = (vendorId: string, showOnTrustCenter: boolean) => { const onToggleVisibility = (vendorId: string, showOnTrustCenter: boolean) => {
props.onToggleVisibility({ props.onToggleVisibility({
variables: { variables: {

View File

@@ -39,15 +39,14 @@ import {
UserDropdown as UserDropdownRoot, UserDropdown as UserDropdownRoot,
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
import { Suspense, useEffect, useState } from "react"; import { Suspense, use, useEffect, useState } from "react";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { useLazyLoadQuery } from "react-relay"; import { useLazyLoadQuery } from "react-relay";
import { Link, Navigate, Outlet, useParams } from "react-router"; import { Link, Navigate, Outlet, useParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql"; import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql";
import { PageError } from "/components/PageError"; import { PageError } from "/components/PageError";
import { Authorized } from "/permissions"; import { PermissionsContext, PermissionsProvider } from "/providers/PermissionsProvider";
import { PermissionsProvider } from "/providers/PermissionsProvider";
import { buildEndpoint } from "/providers/RelayProviders"; import { buildEndpoint } from "/providers/RelayProviders";
const MainLayoutQuery = graphql` const MainLayoutQuery = graphql`
@@ -98,6 +97,7 @@ function MainLayoutContent({
prefix: string; prefix: string;
}) { }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
organizationId, organizationId,
}); });
@@ -116,132 +116,132 @@ function MainLayoutContent({
} }
sidebar={ sidebar={
<ul className="space-y-[2px]"> <ul className="space-y-[2px]">
<Authorized entity="Organization" action="listMeetings"> {isAuthorized("Organization", "listMeetings") && (
<SidebarItem <SidebarItem
label={__("Meetings")} label={__("Meetings")}
icon={IconCalendar1} icon={IconCalendar1}
to={`${prefix}/meetings`} to={`${prefix}/meetings`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listTasks"> {isAuthorized("Organization", "listTasks") && (
<SidebarItem <SidebarItem
label={__("Tasks")} label={__("Tasks")}
icon={IconInboxEmpty} icon={IconInboxEmpty}
to={`${prefix}/tasks`} to={`${prefix}/tasks`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listMeasures"> {isAuthorized("Organization", "listMeasures") && (
<SidebarItem <SidebarItem
label={__("Measures")} label={__("Measures")}
icon={IconTodo} icon={IconTodo}
to={`${prefix}/measures`} to={`${prefix}/measures`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listRisks"> {isAuthorized("Organization", "listRisks") && (
<SidebarItem <SidebarItem
label={__("Risks")} label={__("Risks")}
icon={IconFire3} icon={IconFire3}
to={`${prefix}/risks`} to={`${prefix}/risks`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listFrameworks"> {isAuthorized("Organization", "listFrameworks") && (
<SidebarItem <SidebarItem
label={__("Frameworks")} label={__("Frameworks")}
icon={IconBank} icon={IconBank}
to={`${prefix}/frameworks`} to={`${prefix}/frameworks`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listPeople"> {isAuthorized("Organization", "listPeople") && (
<SidebarItem <SidebarItem
label={__("People")} label={__("People")}
icon={IconGroup1} icon={IconGroup1}
to={`${prefix}/people`} to={`${prefix}/people`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listVendors"> {isAuthorized("Organization", "listVendors") && (
<SidebarItem <SidebarItem
label={__("Vendors")} label={__("Vendors")}
icon={IconStore} icon={IconStore}
to={`${prefix}/vendors`} to={`${prefix}/vendors`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listDocuments"> {isAuthorized("Organization", "listDocuments") && (
<SidebarItem <SidebarItem
label={__("Documents")} label={__("Documents")}
icon={IconPageTextLine} icon={IconPageTextLine}
to={`${prefix}/documents`} to={`${prefix}/documents`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listAssets"> {isAuthorized("Organization", "listAssets") && (
<SidebarItem <SidebarItem
label={__("Assets")} label={__("Assets")}
icon={IconBox} icon={IconBox}
to={`${prefix}/assets`} to={`${prefix}/assets`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listData"> {isAuthorized("Organization", "listData") && (
<SidebarItem <SidebarItem
label={__("Data")} label={__("Data")}
icon={IconListStack} icon={IconListStack}
to={`${prefix}/data`} to={`${prefix}/data`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listAudits"> {isAuthorized("Organization", "listAudits") && (
<SidebarItem <SidebarItem
label={__("Audits")} label={__("Audits")}
icon={IconMedal} icon={IconMedal}
to={`${prefix}/audits`} to={`${prefix}/audits`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listNonconformities"> {isAuthorized("Organization", "listNonconformities") && (
<SidebarItem <SidebarItem
label={__("Nonconformities")} label={__("Nonconformities")}
icon={IconCrossLargeX} icon={IconCrossLargeX}
to={`${prefix}/nonconformities`} to={`${prefix}/nonconformities`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listObligations"> {isAuthorized("Organization", "listObligations") && (
<SidebarItem <SidebarItem
label={__("Obligations")} label={__("Obligations")}
icon={IconBook} icon={IconBook}
to={`${prefix}/obligations`} to={`${prefix}/obligations`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listContinualImprovements"> {isAuthorized("Organization", "listContinualImprovements") && (
<SidebarItem <SidebarItem
label={__("Continual Improvements")} label={__("Continual Improvements")}
icon={IconRotateCw} icon={IconRotateCw}
to={`${prefix}/continual-improvements`} to={`${prefix}/continual-improvements`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listProcessingActivities"> {isAuthorized("Organization", "listProcessingActivities") && (
<SidebarItem <SidebarItem
label={__("Processing Activities")} label={__("Processing Activities")}
icon={IconCircleProgress} icon={IconCircleProgress}
to={`${prefix}/processing-activities`} to={`${prefix}/processing-activities`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listSnapshots"> {isAuthorized("Organization", "listSnapshots") && (
<SidebarItem <SidebarItem
label={__("Snapshots")} label={__("Snapshots")}
icon={IconClock} icon={IconClock}
to={`${prefix}/snapshots`} to={`${prefix}/snapshots`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="getTrustCenter"> {isAuthorized("Organization", "getTrustCenter") && (
<SidebarItem <SidebarItem
label={__("Trust Center")} label={__("Trust Center")}
icon={IconShield} icon={IconShield}
to={`${prefix}/trust-center`} to={`${prefix}/trust-center`}
/> />
</Authorized> )}
<Authorized entity="Organization" action="listMembers"> {isAuthorized("Organization", "listMembers") && (
<SidebarItem <SidebarItem
label={__("Settings")} label={__("Settings")}
icon={IconSettingsGear2} icon={IconSettingsGear2}
to={`${prefix}/settings`} to={`${prefix}/settings`}
/> />
</Authorized> )}
</ul> </ul>
} }
> >
@@ -255,6 +255,7 @@ function MainLayoutContent({
function UserDropdown({ organizationId }: { organizationId: string }) { function UserDropdown({ organizationId }: { organizationId: string }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const { isAuthorized } = use(PermissionsContext);
const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { const user = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, {
organizationId, organizationId,
}).viewer.user; }).viewer.user;
@@ -290,13 +291,13 @@ function UserDropdown({ organizationId }: { organizationId: string }) {
return ( return (
<UserDropdownRoot fullName={user.fullName} email={user.email}> <UserDropdownRoot fullName={user.fullName} email={user.email}>
<Authorized entity="Organization" action="deleteOrganization"> {isAuthorized("Organization", "deleteOrganization") && (
<UserDropdownItem <UserDropdownItem
to="/api-keys" to="/api-keys"
icon={IconKey} icon={IconKey}
label={__("API Keys")} label={__("API Keys")}
/> />
</Authorized> )}
<UserDropdownItem <UserDropdownItem
to="mailto:support@getprobo.com" to="mailto:support@getprobo.com"
icon={IconCircleQuestionmark} icon={IconCircleQuestionmark}

View File

@@ -29,7 +29,8 @@ import { VendorsMultiSelectField } from "/components/form/VendorsMultiSelectFiel
import type { AssetGraphNodeQuery } from "/hooks/graph/__generated__/AssetGraphNodeQuery.graphql"; import type { AssetGraphNodeQuery } from "/hooks/graph/__generated__/AssetGraphNodeQuery.graphql";
import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateAssetSchema = z.object({ const updateAssetSchema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
@@ -51,7 +52,7 @@ export default function AssetDetailsPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!assetEntry || !assetEntry.id) { if (!assetEntry || !assetEntry.id) {
return <div>{__("Asset not found")}</div>; return <div>{__("Asset not found")}</div>;
} }
@@ -116,7 +117,7 @@ export default function AssetDetailsPage(props: Props) {
</Badge> </Badge>
</div> </div>
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="Asset" action="deleteAsset"> isAuthorized("Asset", "deleteAsset") && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
variant="danger" variant="danger"
@@ -126,7 +127,7 @@ export default function AssetDetailsPage(props: Props) {
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )
)} )}
</div> </div>
@@ -182,11 +183,11 @@ export default function AssetDetailsPage(props: Props) {
<div className="flex justify-end"> <div className="flex justify-end">
{formState.isDirty && !isSnapshotMode && ( {formState.isDirty && !isSnapshotMode && (
<Authorized entity="Asset" action="updateAsset"> isAuthorized("Asset", "updateAsset") && (
<Button type="submit" disabled={formState.isSubmitting}> <Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")} {formState.isSubmitting ? __("Updating...") : __("Update")}
</Button> </Button>
</Authorized> )
)} )}
</div> </div>
</form> </form>

View File

@@ -23,7 +23,7 @@ import {
} from "/hooks/graph/AssetGraph"; } from "/hooks/graph/AssetGraph";
import type { AssetGraphListQuery } from "/hooks/graph/__generated__/AssetGraphListQuery.graphql"; import type { AssetGraphListQuery } from "/hooks/graph/__generated__/AssetGraphListQuery.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { PermissionsContext } from "/providers/PermissionsProvider"; import { PermissionsContext } from "/providers/PermissionsContext";
const paginatedAssetsFragment = graphql` const paginatedAssetsFragment = graphql`
fragment AssetsPageFragment on Organization fragment AssetsPageFragment on Organization

View File

@@ -34,7 +34,8 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
import z from "zod"; import z from "zod";
import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers"; import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers";
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql"; import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateAuditSchema = z.object({ const updateAuditSchema = z.object({
name: z.string().nullable().optional(), name: z.string().nullable().optional(),
@@ -52,7 +53,7 @@ export default function AuditDetailsPage(props: Props) {
const auditEntry = audit.node; const auditEntry = audit.node;
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
if (!auditEntry || !auditEntry.id || !auditEntry.framework) { if (!auditEntry || !auditEntry.id || !auditEntry.framework) {
return <div>{__("Audit not found")}</div>; return <div>{__("Audit not found")}</div>;
} }
@@ -146,7 +147,7 @@ export default function AuditDetailsPage(props: Props) {
</Badge> </Badge>
</div> </div>
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<Authorized entity="Audit" action="deleteAudit"> {isAuthorized("Audit", "deleteAudit") && (
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -154,7 +155,7 @@ export default function AuditDetailsPage(props: Props) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</div> </div>
@@ -187,11 +188,11 @@ export default function AuditDetailsPage(props: Props) {
<div className="flex justify-end"> <div className="flex justify-end">
{formState.isDirty && ( {formState.isDirty && (
<Authorized entity="Audit" action="updateAudit"> isAuthorized("Audit", "updateAudit") && (
<Button type="submit" disabled={formState.isSubmitting}> <Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")} {formState.isSubmitting ? __("Updating...") : __("Update")}
</Button> </Button>
</Authorized> )
)} )}
</div> </div>
</form> </form>

View File

@@ -31,8 +31,8 @@ import type {
AuditsPageFragment$key, AuditsPageFragment$key,
} from "./__generated__/AuditsPageFragment.graphql"; } from "./__generated__/AuditsPageFragment.graphql";
import { SortableTable } from "/components/SortableTable"; import { SortableTable } from "/components/SortableTable";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { isAuthorized } from "/permissions"; import { use } from "react";
const paginatedAuditsFragment = graphql` const paginatedAuditsFragment = graphql`
fragment AuditsPageFragment on Organization fragment AuditsPageFragment on Organization
@@ -83,6 +83,7 @@ type Props = {
export default function AuditsPage(props: Props) { export default function AuditsPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const data = usePreloadedQuery(auditsQuery, props.queryRef); const data = usePreloadedQuery(auditsQuery, props.queryRef);
const pagination = usePaginationFragment( const pagination = usePaginationFragment(
@@ -94,8 +95,8 @@ export default function AuditsPage(props: Props) {
usePageTitle(__("Audits")); usePageTitle(__("Audits"));
const hasAnyAction = isAuthorized(organizationId, "Audit", "updateAudit") || const hasAnyAction = isAuthorized("Audit", "updateAudit") ||
isAuthorized(organizationId, "Audit", "deleteAudit"); isAuthorized("Audit", "deleteAudit");
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -105,14 +106,14 @@ export default function AuditsPage(props: Props) {
"Manage your organization's compliance audits and their progress." "Manage your organization's compliance audits and their progress."
)} )}
> >
<Authorized entity="Organization" action="createAudit"> {isAuthorized("Organization", "createAudit") && (
<CreateAuditDialog <CreateAuditDialog
connection={connectionId} connection={connectionId}
organizationId={organizationId} organizationId={organizationId}
> >
<Button icon={IconPlusLarge}>{__("Add audit")}</Button> <Button icon={IconPlusLarge}>{__("Add audit")}</Button>
</CreateAuditDialog> </CreateAuditDialog>
</Authorized> )}
</PageHeader> </PageHeader>
<SortableTable {...pagination}> <SortableTable {...pagination}>
<Thead> <Thead>
@@ -153,6 +154,7 @@ function AuditRow({
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const deleteAudit = useDeleteAudit(entry, connectionId); const deleteAudit = useDeleteAudit(entry, connectionId);
const { isAuthorized } = use(PermissionsContext);
return ( return (
<Tr to={`/organizations/${organizationId}/audits/${entry.id}`}> <Tr to={`/organizations/${organizationId}/audits/${entry.id}`}>
@@ -177,7 +179,7 @@ function AuditRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Audit" action="deleteAudit"> {isAuthorized("Audit", "deleteAudit") && (
<DropdownItem <DropdownItem
onClick={deleteAudit} onClick={deleteAudit}
variant="danger" variant="danger"
@@ -185,7 +187,7 @@ function AuditRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -35,7 +35,8 @@ import z from "zod";
import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency } from "@probo/helpers"; import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import type { ContinualImprovementGraphNodeQuery } from "/hooks/graph/__generated__/ContinualImprovementGraphNodeQuery.graphql"; import type { ContinualImprovementGraphNodeQuery } from "/hooks/graph/__generated__/ContinualImprovementGraphNodeQuery.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateImprovementSchema = z.object({ const updateImprovementSchema = z.object({
referenceId: z.string().min(1, "Reference ID is required"), referenceId: z.string().min(1, "Reference ID is required"),
@@ -59,7 +60,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!improvement) { if (!improvement) {
return <div>{__("Continual improvement entry not found")}</div>; return <div>{__("Continual improvement entry not found")}</div>;
} }
@@ -150,13 +151,13 @@ export default function ContinualImprovementDetailsPage(props: Props) {
]} ]}
/> />
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="ContinualImprovement" action="deleteContinualImprovement"> isAuthorized("ContinualImprovement", "deleteContinualImprovement") && (
<ActionDropdown> <ActionDropdown>
<DropdownItem onClick={deleteImprovement} variant="danger"> <DropdownItem onClick={deleteImprovement} variant="danger">
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )
)} )}
</div> </div>
@@ -289,7 +290,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
{!isSnapshotMode && ( {!isSnapshotMode && (
<div className="flex justify-end pt-4"> <div className="flex justify-end pt-4">
<Authorized entity="ContinualImprovement" action="updateContinualImprovement"> {isAuthorized("ContinualImprovement", "updateContinualImprovement") && (
<Button <Button
type="submit" type="submit"
variant="primary" variant="primary"
@@ -297,7 +298,7 @@ export default function ContinualImprovementDetailsPage(props: Props) {
> >
{formState.isSubmitting ? __("Saving...") : __("Save Changes")} {formState.isSubmitting ? __("Saving...") : __("Save Changes")}
</Button> </Button>
</Authorized> )}
</div> </div>
)} )}
</form> </form>

View File

@@ -37,8 +37,8 @@ import type {
ContinualImprovementsPageFragment$key, ContinualImprovementsPageFragment$key,
ContinualImprovementsPageFragment$data, ContinualImprovementsPageFragment$data,
} from "./__generated__/ContinualImprovementsPageFragment.graphql"; } from "./__generated__/ContinualImprovementsPageFragment.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
interface ContinualImprovementsPageProps { interface ContinualImprovementsPageProps {
queryRef: PreloadedQuery<ContinualImprovementsPageQuery>; queryRef: PreloadedQuery<ContinualImprovementsPageQuery>;
@@ -93,6 +93,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Continual Improvements")); usePageTitle(__("Continual Improvements"));
@@ -130,8 +131,8 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
const improvements = data?.continualImprovements?.edges?.map((edge) => edge.node) ?? []; const improvements = data?.continualImprovements?.edges?.map((edge) => edge.node) ?? [];
const hasAnyAction = !isSnapshotMode && ( const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "ContinualImprovement", "updateContinualImprovement") || isAuthorized("ContinualImprovement", "updateContinualImprovement") ||
isAuthorized(organizationId, "ContinualImprovement", "deleteContinualImprovement") isAuthorized("ContinualImprovement", "deleteContinualImprovement")
); );
return ( return (
@@ -141,7 +142,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
)} )}
<PageHeader title={__("Continual Improvements")} description={__("Manage your continual improvements.")}> <PageHeader title={__("Continual Improvements")} description={__("Manage your continual improvements.")}>
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="Organization" action="createContinualImprovement"> isAuthorized("Organization", "createContinualImprovement") && (
<CreateContinualImprovementDialog <CreateContinualImprovementDialog
organizationId={organizationId} organizationId={organizationId}
connectionId={connectionId} connectionId={connectionId}
@@ -150,7 +151,7 @@ export default function ContinualImprovementsPage({ queryRef }: ContinualImprove
{__("Add continual improvement")} {__("Add continual improvement")}
</Button> </Button>
</CreateContinualImprovementDialog> </CreateContinualImprovementDialog>
</Authorized> )
)} )}
</PageHeader> </PageHeader>
@@ -225,6 +226,7 @@ function ImprovementRow({
const [deleteImprovement] = useMutation(deleteContinualImprovementMutation); const [deleteImprovement] = useMutation(deleteContinualImprovementMutation);
const confirm = useConfirm(); const confirm = useConfirm();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => { const handleDelete = () => {
@@ -282,7 +284,7 @@ function ImprovementRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="ContinualImprovement" action="deleteContinualImprovement"> {isAuthorized("ContinualImprovement", "deleteContinualImprovement") && (
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
variant="danger" variant="danger"
@@ -290,7 +292,7 @@ function ImprovementRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -35,8 +35,8 @@ import type {
import type { DataListQuery } from "./__generated__/DataListQuery.graphql"; import type { DataListQuery } from "./__generated__/DataListQuery.graphql";
import { SortableTable } from "/components/SortableTable"; import { SortableTable } from "/components/SortableTable";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions"; import { use } from "react";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
const paginatedDataFragment = graphql` const paginatedDataFragment = graphql`
fragment DataPageFragment on Organization fragment DataPageFragment on Organization
@@ -93,6 +93,7 @@ export default function DataPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const queryData = usePreloadedQuery<DatumGraphListQuery>( const queryData = usePreloadedQuery<DatumGraphListQuery>(
dataQuery, dataQuery,
@@ -121,8 +122,9 @@ export default function DataPage(props: Props) {
usePageTitle(__("Data")); usePageTitle(__("Data"));
const hasAnyAction = !isSnapshotMode && ( isAuthorized(organizationId, "Datum", "updateDatum") || const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Datum", "deleteDatum") isAuthorized("Datum", "updateDatum") ||
isAuthorized("Datum", "deleteDatum")
); );
return ( return (
@@ -137,7 +139,7 @@ export default function DataPage(props: Props) {
)} )}
> >
{!snapshotId && ( {!snapshotId && (
<Authorized entity="Organization" action="createDatum"> isAuthorized("Organization", "createDatum") && (
<CreateDatumDialog <CreateDatumDialog
connection={connectionId} connection={connectionId}
organizationId={organizationId} organizationId={organizationId}
@@ -145,7 +147,7 @@ export default function DataPage(props: Props) {
> >
<Button icon={IconPlusLarge}>{__("Add data")}</Button> <Button icon={IconPlusLarge}>{__("Add data")}</Button>
</CreateDatumDialog> </CreateDatumDialog>
</Authorized> )
)} )}
</PageHeader> </PageHeader>
<SortableTable <SortableTable
@@ -186,7 +188,7 @@ function DataRow({
const { __ } = useTranslate(); const { __ } = useTranslate();
const deleteDatum = useDeleteDatum(entry, connectionId); const deleteDatum = useDeleteDatum(entry, connectionId);
const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? []; const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? [];
const { isAuthorized } = use(PermissionsContext);
const detailUrl = snapshotId const detailUrl = snapshotId
? `/organizations/${organizationId}/snapshots/${snapshotId}/data/${entry.id}` ? `/organizations/${organizationId}/snapshots/${snapshotId}/data/${entry.id}`
: `/organizations/${organizationId}/data/${entry.id}`; : `/organizations/${organizationId}/data/${entry.id}`;
@@ -228,7 +230,7 @@ function DataRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Datum" action="deleteDatum"> {isAuthorized("Datum", "deleteDatum") && (
<DropdownItem <DropdownItem
onClick={deleteDatum} onClick={deleteDatum}
variant="danger" variant="danger"
@@ -236,7 +238,7 @@ function DataRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -29,7 +29,8 @@ import z from "zod";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { validateSnapshotConsistency } from "@probo/helpers"; import { validateSnapshotConsistency } from "@probo/helpers";
import type { DatumGraphNodeQuery } from "/hooks/graph/__generated__/DatumGraphNodeQuery.graphql"; import type { DatumGraphNodeQuery } from "/hooks/graph/__generated__/DatumGraphNodeQuery.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateDatumSchema = z.object({ const updateDatumSchema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
@@ -57,6 +58,7 @@ export default function DatumDetailsPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const deleteDatum = useDeleteDatum( const deleteDatum = useDeleteDatum(
datumEntry, datumEntry,
@@ -125,7 +127,7 @@ export default function DatumDetailsPage(props: Props) {
<Badge variant="info">{datumEntry?.dataClassification}</Badge> <Badge variant="info">{datumEntry?.dataClassification}</Badge>
</div> </div>
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="Datum" action="deleteDatum"> isAuthorized("Datum", "deleteDatum") && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
variant="danger" variant="danger"
@@ -135,7 +137,7 @@ export default function DatumDetailsPage(props: Props) {
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )
)} )}
</div> </div>
@@ -180,11 +182,11 @@ export default function DatumDetailsPage(props: Props) {
{!isSnapshotMode && ( {!isSnapshotMode && (
<div className="flex justify-end"> <div className="flex justify-end">
{formState.isDirty && ( {formState.isDirty && (
<Authorized entity="Datum" action="updateDatum"> isAuthorized("Datum", "updateDatum") && (
<Button type="submit" disabled={formState.isSubmitting}> <Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")} {formState.isSubmitting ? __("Updating...") : __("Update")}
</Button> </Button>
</Authorized> )
)} )}
</div> </div>
)} )}

View File

@@ -67,7 +67,7 @@ import {
PdfDownloadDialog, PdfDownloadDialog,
type PdfDownloadDialogRef, type PdfDownloadDialogRef,
} from "/components/documents/PdfDownloadDialog"; } from "/components/documents/PdfDownloadDialog";
import { useRef, useState } from "react"; import { use, useRef, useState } from "react";
import type { NodeOf } from "/types.ts"; import type { NodeOf } from "/types.ts";
import clsx from "clsx"; import clsx from "clsx";
import { PeopleSelectField } from "/components/form/PeopleSelectField"; import { PeopleSelectField } from "/components/form/PeopleSelectField";
@@ -76,7 +76,7 @@ import { DocumentTypeOptions } from "/components/form/DocumentTypeOptions";
import { DocumentClassificationOptions } from "/components/form/DocumentClassificationOptions"; import { DocumentClassificationOptions } from "/components/form/DocumentClassificationOptions";
import { z } from "zod"; import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
queryRef: PreloadedQuery<DocumentGraphNodeQuery>; queryRef: PreloadedQuery<DocumentGraphNodeQuery>;
@@ -132,37 +132,6 @@ const documentFragment = graphql`
} }
`; `;
graphql`
fragment DocumentDetailPageRowFragment on Document {
id
title
description
documentType
classification
updatedAt
owner {
id
fullName
}
versions(first: 1) {
edges {
node {
id
status
signatures(first: 100) {
edges {
node {
id
state
}
}
}
}
}
}
}
`;
const publishDocumentVersionMutation = graphql` const publishDocumentVersionMutation = graphql`
mutation DocumentDetailPagePublishMutation( mutation DocumentDetailPagePublishMutation(
$input: PublishDocumentVersionInput! $input: PublishDocumentVersionInput!
@@ -170,7 +139,31 @@ const publishDocumentVersionMutation = graphql`
publishDocumentVersion(input: $input) { publishDocumentVersion(input: $input) {
document { document {
id id
...DocumentDetailPageRowFragment title
description
documentType
classification
updatedAt
owner {
id
fullName
}
versions(first: 1) {
edges {
node {
id
status
signatures(first: 100) {
edges {
node {
id
state
}
}
}
}
}
}
} }
} }
} }
@@ -230,6 +223,7 @@ export default function DocumentDetailPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthorized } = use(PermissionsContext);
const [isEditingTitle, setIsEditingTitle] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false);
const [isEditingOwner, setIsEditingOwner] = useState(false); const [isEditingOwner, setIsEditingOwner] = useState(false);
@@ -522,16 +516,16 @@ export default function DocumentDetailPage(props: Props) {
</Dropdown> </Dropdown>
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<Authorized entity="Document" action="updateDocument"> {isAuthorized("Document", "updateDocument") && (
<DropdownItem <DropdownItem
onClick={() => updateDialogRef.current?.open()} onClick={() => updateDialogRef.current?.open()}
icon={IconPencil} icon={IconPencil}
> >
{isDraft ? __("Edit draft document") : __("Create new draft")} {isDraft ? __("Edit draft document") : __("Create new draft")}
</DropdownItem> </DropdownItem>
</Authorized> )}
{isDraft && versions.length > 1 && ( {isDraft && versions.length > 1 && (
<Authorized entity="Document" action="deleteDocument"> isAuthorized("Document", "deleteDocument") && (
<DropdownItem <DropdownItem
onClick={handleDeleteDraft} onClick={handleDeleteDraft}
icon={IconTrashCan} icon={IconTrashCan}
@@ -539,7 +533,7 @@ export default function DocumentDetailPage(props: Props) {
> >
{__("Delete draft document")} {__("Delete draft document")}
</DropdownItem> </DropdownItem>
</Authorized> )
)} )}
<DropdownItem <DropdownItem
onClick={() => pdfDownloadDialogRef.current?.open()} onClick={() => pdfDownloadDialogRef.current?.open()}
@@ -548,7 +542,7 @@ export default function DocumentDetailPage(props: Props) {
> >
{__("Download PDF")} {__("Download PDF")}
</DropdownItem> </DropdownItem>
<Authorized entity="Document" action="deleteDocument"> {isAuthorized("Document", "deleteDocument") && (
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -557,7 +551,7 @@ export default function DocumentDetailPage(props: Props) {
> >
{__("Delete document")} {__("Delete document")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</div> </div>
</div> </div>

View File

@@ -29,7 +29,7 @@ import {
useLazyLoadQuery, useLazyLoadQuery,
type PreloadedQuery, type PreloadedQuery,
} from "react-relay"; } from "react-relay";
import { useRef } from "react"; import { use, useRef } from "react";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { DocumentGraphListQuery } from "/hooks/graph/__generated__/DocumentGraphListQuery.graphql"; import type { DocumentGraphListQuery } from "/hooks/graph/__generated__/DocumentGraphListQuery.graphql";
import { import {
@@ -57,8 +57,7 @@ import {
type BulkExportDialogRef, type BulkExportDialogRef,
} from "/components/documents/BulkExportDialog"; } from "/components/documents/BulkExportDialog";
import type { DocumentsPageUserEmailQuery } from "./__generated__/DocumentsPageUserEmailQuery.graphql"; import type { DocumentsPageUserEmailQuery } from "./__generated__/DocumentsPageUserEmailQuery.graphql";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsProvider.tsx";
import { isAuthorized } from "/permissions";
const documentsFragment = graphql` const documentsFragment = graphql`
fragment DocumentsPageListFragment on Organization fragment DocumentsPageListFragment on Organization
@@ -107,6 +106,7 @@ const UserEmailQuery = graphql`
export default function DocumentsPage(props: Props) { export default function DocumentsPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const organization = usePreloadedQuery( const organization = usePreloadedQuery(
documentsQuery, documentsQuery,
@@ -137,8 +137,8 @@ export default function DocumentsPage(props: Props) {
usePageTitle(__("Documents")); usePageTitle(__("Documents"));
const hasAnyAction = isAuthorized(organization.id, "Document", "updateDocument") || const hasAnyAction = isAuthorized("Document", "updateDocument") ||
isAuthorized(organization.id, "Document", "deleteDocument"); isAuthorized("Document", "deleteDocument");
const handleSendSigningNotifications = () => { const handleSendSigningNotifications = () => {
sendSigningNotifications({ sendSigningNotifications({
@@ -198,21 +198,21 @@ export default function DocumentsPage(props: Props) {
description={__("Manage your organization's documents")} description={__("Manage your organization's documents")}
> >
<div className="flex gap-2"> <div className="flex gap-2">
<Authorized entity="Document" action="sendSigningNotifications"> {isAuthorized("Document", "sendSigningNotifications") && (
<Button <Button
icon={IconBell2} icon={IconBell2}
variant="secondary" variant="secondary"
onClick={handleSendSigningNotifications} onClick={handleSendSigningNotifications}
> >
{__("Send signing notifications")} {__("Send signing notifications")}
</Button> </Button>
</Authorized> )}
<Authorized entity="Organization" action="createDocument"> {isAuthorized("Organization", "createDocument") && (
<CreateDocumentDialog <CreateDocumentDialog
connection={connectionId} connection={connectionId}
trigger={<Button icon={IconPlusLarge}>{__("New document")}</Button>} trigger={<Button icon={IconPlusLarge}>{__("New document")}</Button>}
/> />
</Authorized> )}
</div> </div>
</PageHeader> </PageHeader>
{documents.length > 0 ? ( {documents.length > 0 ? (
@@ -258,7 +258,7 @@ export default function DocumentsPage(props: Props) {
</button> </button>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Authorized entity="Document" action="updateDocument"> {isAuthorized("Document", "updateDocument") && (
<PublishDocumentsDialog <PublishDocumentsDialog
documentIds={selection} documentIds={selection}
onSave={clear} onSave={clear}
@@ -270,8 +270,8 @@ export default function DocumentsPage(props: Props) {
{__("Publish")} {__("Publish")}
</Button> </Button>
</PublishDocumentsDialog> </PublishDocumentsDialog>
</Authorized> )}
<Authorized entity="Document" action="bulkRequestSignatures"> {isAuthorized("Document", "bulkRequestSignatures") && (
<SignatureDocumentsDialog <SignatureDocumentsDialog
documentIds={selection} documentIds={selection}
onSave={clear} onSave={clear}
@@ -284,7 +284,7 @@ export default function DocumentsPage(props: Props) {
{__("Request signature")} {__("Request signature")}
</Button> </Button>
</SignatureDocumentsDialog> </SignatureDocumentsDialog>
</Authorized> )}
<BulkExportDialog <BulkExportDialog
ref={bulkExportDialogRef} ref={bulkExportDialogRef}
onExport={handleBulkExport} onExport={handleBulkExport}
@@ -300,7 +300,7 @@ export default function DocumentsPage(props: Props) {
{__("Export")} {__("Export")}
</Button> </Button>
</BulkExportDialog> </BulkExportDialog>
<Authorized entity="Document" action="deleteDocument"> {isAuthorized("Document", "deleteDocument") && (
<Button <Button
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -309,7 +309,7 @@ export default function DocumentsPage(props: Props) {
> >
{__("Delete")} {__("Delete")}
</Button> </Button>
</Authorized> )}
</div> </div>
</div> </div>
</Th> </Th>
@@ -392,6 +392,7 @@ function DocumentRow({
onCheck: () => void; onCheck: () => void;
hasAnyAction: boolean; hasAnyAction: boolean;
}) { }) {
const { isAuthorized } = use(PermissionsContext);
const document = useFragment<DocumentsPageRowFragment$key>( const document = useFragment<DocumentsPageRowFragment$key>(
rowFragment, rowFragment,
documentKey documentKey
@@ -465,7 +466,7 @@ function DocumentRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end w-18"> <Td noLink width={50} className="text-end w-18">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Document" action="deleteDocument"> {isAuthorized("Document", "deleteDocument") && (
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -473,7 +474,7 @@ function DocumentRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -8,7 +8,7 @@ import {
Spinner, Spinner,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { Suspense, useState, useEffect, useRef } from "react"; import { Suspense, useState, useEffect, useRef, use } from "react";
import type { ItemOf, NodeOf } from "/types"; import type { ItemOf, NodeOf } from "/types";
import { graphql, useFragment, useRefetchableFragment } from "react-relay"; import { graphql, useFragment, useRefetchableFragment } from "react-relay";
import { usePeople } from "/hooks/graph/PeopleGraph.ts"; import { usePeople } from "/hooks/graph/PeopleGraph.ts";
@@ -20,7 +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_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 { DocumentSignaturesTab_version$key } from "/pages/organizations/documents/tabs/__generated__/DocumentSignaturesTab_version.graphql.ts";
import type { DocumentSignaturesTabRefetchQuery } from "./__generated__/DocumentSignaturesTabRefetchQuery.graphql"; import type { DocumentSignaturesTabRefetchQuery } from "./__generated__/DocumentSignaturesTabRefetchQuery.graphql";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>; type Version = NodeOf<DocumentDetailPageDocumentFragment$data["versions"]>;
@@ -246,6 +246,7 @@ function SignatureItem(props: {
}) { }) {
const signature = useFragment(signatureFragment, props.signature); const signature = useFragment(signatureFragment, props.signature);
const { __, dateTimeFormat } = useTranslate(); const { __, dateTimeFormat } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const [requestSignature, isSendingRequest] = useMutationWithToasts( const [requestSignature, isSendingRequest] = useMutationWithToasts(
requestSignatureMutation, requestSignatureMutation,
{ {
@@ -275,26 +276,26 @@ function SignatureItem(props: {
</div> </div>
</div> </div>
{props.signable && ( {props.signable && (
<Authorized entity="Document" action="requestSignature"> isAuthorized("Document", "requestSignature") && (
<Button <Button
variant="secondary" variant="secondary"
className="ml-auto" className="ml-auto"
disabled={isSendingRequest} disabled={isSendingRequest}
onClick={() => { onClick={() => {
requestSignature({ requestSignature({
variables: { variables: {
input: { input: {
documentVersionId: props.versionId, documentVersionId: props.versionId,
signatoryId: props.people.id, signatoryId: props.people.id,
},
connections: [props.connectionId],
}, },
connections: [props.connectionId], });
}, }}
}); >
}} {__("Request signature")}
> </Button>
{__("Request signature")} )
</Button>
</Authorized>
)} )}
</div> </div>
); );
@@ -331,25 +332,25 @@ function SignatureItem(props: {
{__("Signed")} {__("Signed")}
</Badge> </Badge>
) : ( ) : (
<Authorized entity="DocumentVersionSignature" action="cancelSignatureRequest"> isAuthorized("DocumentVersionSignature", "cancelSignatureRequest") && (
<Button <Button
variant="danger" variant="danger"
className="ml-auto" className="ml-auto"
disabled={isCancellingSignature} disabled={isCancellingSignature}
onClick={() => { onClick={() => {
cancelSignature({ cancelSignature({
variables: { variables: {
input: { input: {
documentVersionSignatureId: signature.id, documentVersionSignatureId: signature.id,
},
connections: [props.connectionId],
}, },
connections: [props.connectionId], });
}, }}
}); >
}} {__("Cancel request")}
> </Button>
{__("Cancel request")} )
</Button>
</Authorized>
)} )}
</div> </div>
); );

View File

@@ -27,7 +27,8 @@ import { promisifyMutation } from "@probo/helpers";
import type { FrameworkGraphControlNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql"; import type { FrameworkGraphControlNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql";
import { frameworkControlNodeQuery } from "/hooks/graph/FrameworkGraph"; import { frameworkControlNodeQuery } from "/hooks/graph/FrameworkGraph";
import type { FrameworkDetailPageFragment$data } from "./__generated__/FrameworkDetailPageFragment.graphql"; import type { FrameworkDetailPageFragment$data } from "./__generated__/FrameworkDetailPageFragment.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const attachMeasureMutation = graphql` const attachMeasureMutation = graphql`
mutation FrameworkControlPageAttachMutation( mutation FrameworkControlPageAttachMutation(
@@ -166,7 +167,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const confirm = useConfirm(); const confirm = useConfirm();
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthorized } = use(PermissionsContext);
const [detachMeasure, isDetachingMeasure] = useMutation(detachMeasureMutation); const [detachMeasure, isDetachingMeasure] = useMutation(detachMeasureMutation);
const [attachMeasure, isAttachingMeasure] = useMutation(attachMeasureMutation); const [attachMeasure, isAttachingMeasure] = useMutation(attachMeasureMutation);
const [detachDocument, isDetachingDocument] = useMutation(detachDocumentMutation); const [detachDocument, isDetachingDocument] = useMutation(detachDocumentMutation);
@@ -236,7 +237,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Authorized entity="Control" action="updateControl"> {isAuthorized("Control", "updateControl") && (
<FrameworkControlDialog <FrameworkControlDialog
frameworkId={framework.id} frameworkId={framework.id}
connectionId={connectionId} connectionId={connectionId}
@@ -246,8 +247,8 @@ export default function FrameworkControlPage({ queryRef }: Props) {
{__("Edit control")} {__("Edit control")}
</Button> </Button>
</FrameworkControlDialog> </FrameworkControlDialog>
</Authorized> )}
<Authorized entity="Control" action="deleteControl"> {isAuthorized("Control", "deleteControl") && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
@@ -257,7 +258,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )}
</div> </div>
</div> </div>

View File

@@ -31,7 +31,8 @@ import type { FrameworkDetailPageExportFrameworkMutation } from "./__generated__
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog"; import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const frameworkDetailFragment = graphql` const frameworkDetailFragment = graphql`
fragment FrameworkDetailPageFragment on Framework { fragment FrameworkDetailPageFragment on Framework {
@@ -103,7 +104,7 @@ export default function FrameworkDetailPage(props: Props) {
framework, framework,
ConnectionHandler.getConnectionID(organizationId, connectionListKey)! ConnectionHandler.getConnectionID(organizationId, connectionListKey)!
); );
const { isAuthorized } = use(PermissionsContext);
const [generateFrameworkStateOfApplicability] = const [generateFrameworkStateOfApplicability] =
useMutationWithToasts<FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation>( useMutationWithToasts<FrameworkDetailPageGenerateFrameworkStateOfApplicabilityMutation>(
generateFrameworkStateOfApplicabilityMutation, generateFrameworkStateOfApplicabilityMutation,
@@ -150,7 +151,7 @@ export default function FrameworkDetailPage(props: Props) {
</> </>
} }
> >
<Authorized entity="Framework" action="updateFramework"> {isAuthorized("Framework", "updateFramework") && (
<FrameworkFormDialog <FrameworkFormDialog
organizationId={organizationId} organizationId={organizationId}
framework={framework} framework={framework}
@@ -159,7 +160,7 @@ export default function FrameworkDetailPage(props: Props) {
{__("Edit")} {__("Edit")}
</Button> </Button>
</FrameworkFormDialog> </FrameworkFormDialog>
</Authorized> )}
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
variant="primary" variant="primary"
@@ -191,11 +192,11 @@ export default function FrameworkDetailPage(props: Props) {
> >
{__("Export Framework")} {__("Export Framework")}
</DropdownItem> </DropdownItem>
<Authorized entity="Framework" action="deleteFramework"> {isAuthorized("Framework", "deleteFramework") && (
<DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}> <DropdownItem icon={IconTrashCan} variant="danger" onClick={onDelete}>
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</PageHeader> </PageHeader>
<div className="text-lg font-semibold"> <div className="text-lg font-semibold">
@@ -216,7 +217,7 @@ export default function FrameworkDetailPage(props: Props) {
active={selectedControl?.id === control.id} active={selectedControl?.id === control.id}
/> />
))} ))}
<Authorized entity="Organization" action="createControl"> {isAuthorized("Organization", "createControl") && (
<FrameworkControlDialog <FrameworkControlDialog
frameworkId={framework.id} frameworkId={framework.id}
connectionId={connectionId} connectionId={connectionId}
@@ -226,7 +227,7 @@ export default function FrameworkDetailPage(props: Props) {
{__("Add new control")} {__("Add new control")}
</button> </button>
</FrameworkControlDialog> </FrameworkControlDialog>
</Authorized> )}
</div> </div>
<Outlet context={{ framework }} /> <Outlet context={{ framework }} />
</div> </div>

View File

@@ -26,11 +26,10 @@ import {
import { Link } from "react-router"; import { Link } from "react-router";
import type { FrameworksPageCardFragment$key } from "./__generated__/FrameworksPageCardFragment.graphql"; import type { FrameworksPageCardFragment$key } from "./__generated__/FrameworksPageCardFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { useState, type ChangeEventHandler } from "react"; import { useState, type ChangeEventHandler, use } from "react";
import { FrameworkLogo } from "/components/FrameworkLogo"; import { FrameworkLogo } from "/components/FrameworkLogo";
import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { isAuthorized } from "/permissions";
type Props = { type Props = {
queryRef: PreloadedQuery<FrameworkGraphListQuery>; queryRef: PreloadedQuery<FrameworkGraphListQuery>;
@@ -54,6 +53,7 @@ const importFrameworkMutation = graphql`
export default function FrameworksPage(props: Props) { export default function FrameworksPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Frameworks")); usePageTitle(__("Frameworks"));
const data = usePreloadedQuery(frameworksQuery, props.queryRef); const data = usePreloadedQuery(frameworksQuery, props.queryRef);
const connectionId = data.organization.frameworks!.__id; const connectionId = data.organization.frameworks!.__id;
@@ -120,8 +120,8 @@ export default function FrameworksPage(props: Props) {
const isLoading = isUploading || isImporting; const isLoading = isUploading || isImporting;
const hasAnyAction = isAuthorized(data.organization.id!, "Framework", "updateFramework") || const hasAnyAction = isAuthorized("Framework", "updateFramework") ||
isAuthorized(data.organization.id!, "Framework", "deleteFramework"); isAuthorized("Framework", "deleteFramework");
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -134,20 +134,22 @@ export default function FrameworksPage(props: Props) {
title={__("Frameworks")} title={__("Frameworks")}
description={__("Manage your compliance frameworks")} description={__("Manage your compliance frameworks")}
> >
<Authorized entity="Organization" action="createFramework"> {isAuthorized("Organization", "createFramework") && (
<FileButton <>
variant="secondary" <FileButton
icon={IconFolderUpload} variant="secondary"
onChange={handleUpload} icon={IconFolderUpload}
disabled={isLoading} onChange={handleUpload}
> disabled={isLoading}
{__("Import")} >
</FileButton> {__("Import")}
<FrameworkSelector </FileButton>
onSelect={importNamedFramework} <FrameworkSelector
disabled={isLoading} onSelect={importNamedFramework}
/> disabled={isLoading}
</Authorized> />
</>
)}
</PageHeader> </PageHeader>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -182,6 +184,7 @@ type FrameworkCardProps = {
function FrameworkCard(props: FrameworkCardProps) { function FrameworkCard(props: FrameworkCardProps) {
const framework = useFragment(frameworkCardFragment, props.framework); const framework = useFragment(frameworkCardFragment, props.framework);
const { isAuthorized } = use(PermissionsContext);
const deleteFramework = useDeleteFrameworkMutation( const deleteFramework = useDeleteFrameworkMutation(
framework, framework,
props.connectionId props.connectionId
@@ -200,7 +203,7 @@ function FrameworkCard(props: FrameworkCardProps) {
<FrameworkLogo {...framework} /> <FrameworkLogo {...framework} />
{props.hasAnyAction && ( {props.hasAnyAction && (
<ActionDropdown className="z-10 relative"> <ActionDropdown className="z-10 relative">
<Authorized entity="Framework" action="updateFramework"> {isAuthorized("Framework", "updateFramework") && (
<DropdownItem <DropdownItem
icon={IconPencil} icon={IconPencil}
onClick={() => { onClick={() => {
@@ -209,8 +212,8 @@ function FrameworkCard(props: FrameworkCardProps) {
> >
{__("Edit")} {__("Edit")}
</DropdownItem> </DropdownItem>
</Authorized> )}
<Authorized entity="Framework" action="deleteFramework"> {isAuthorized("Framework", "deleteFramework") && (
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
onClick={() => deleteFramework()} onClick={() => deleteFramework()}
@@ -218,7 +221,7 @@ function FrameworkCard(props: FrameworkCardProps) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
)} )}
</div> </div>

View File

@@ -43,7 +43,8 @@ import {
sprintf, sprintf,
} from "@probo/helpers"; } from "@probo/helpers";
import MeasureFormDialog from "./dialog/MeasureFormDialog"; import MeasureFormDialog from "./dialog/MeasureFormDialog";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
queryRef: PreloadedQuery<MeasureGraphNodeQuery>; queryRef: PreloadedQuery<MeasureGraphNodeQuery>;
@@ -59,7 +60,7 @@ export default function MeasureDetailPage(props: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const confirm = useConfirm(); const confirm = useConfirm();
const [updateMeasure, isUpdating] = useUpdateMeasure(); const [updateMeasure, isUpdating] = useUpdateMeasure();
const { isAuthorized } = use(PermissionsContext);
if (!measureId) { if (!measureId) {
throw new Error( throw new Error(
"Cannot load measure detail page without measureId parameter", "Cannot load measure detail page without measureId parameter",
@@ -136,33 +137,35 @@ export default function MeasureDetailPage(props: Props) {
/> />
<PageHeader title={measure.name} description={measure.description}> <PageHeader title={measure.name} description={measure.description}>
<Authorized entity="Measure" action="updateMeasure"> {isAuthorized("Measure", "updateMeasure") && (
<MeasureFormDialog measure={measure}> <>
<Button variant="secondary" icon={IconPencil}> <MeasureFormDialog measure={measure}>
{__("Edit")} <Button variant="secondary" icon={IconPencil}>
</Button> {__("Edit")}
</MeasureFormDialog> </Button>
<Select </MeasureFormDialog>
disabled={isUpdating} <Select
onValueChange={onStateChange} disabled={isUpdating}
name="state" onValueChange={onStateChange}
placeholder={__("Select state")} name="state"
className="rounded-full" placeholder={__("Select state")}
value={measure.state} className="rounded-full"
> value={measure.state}
{measureStates.map((state) => ( >
<Option key={state} value={state}> {measureStates.map((state) => (
{getMeasureStateLabel(__, state)} <Option key={state} value={state}>
</Option> {getMeasureStateLabel(__, state)}
))} </Option>
</Select> ))}
</Authorized> </Select>
</>
)}
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<Authorized entity="Measure" action="deleteMeasure"> {isAuthorized("Measure", "deleteMeasure") && (
<DropdownItem variant="danger" icon={IconTrashCan} onClick={onDelete}> <DropdownItem variant="danger" icon={IconTrashCan} onClick={onDelete}>
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</PageHeader> </PageHeader>

View File

@@ -40,7 +40,7 @@ import type {
MeasuresPageFragment$key, MeasuresPageFragment$key,
} from "./__generated__/MeasuresPageFragment.graphql"; } from "./__generated__/MeasuresPageFragment.graphql";
import { groupBy, objectKeys, slugify, sprintf } from "@probo/helpers"; import { groupBy, objectKeys, slugify, sprintf } from "@probo/helpers";
import { useMemo, useRef, useState, type ChangeEventHandler } from "react"; import { useMemo, useRef, useState, type ChangeEventHandler, use } from "react";
import type { NodeOf } from "/types"; import type { NodeOf } from "/types";
import type { MeasuresPageImportMutation } from "./__generated__/MeasuresPageImportMutation.graphql"; import type { MeasuresPageImportMutation } from "./__generated__/MeasuresPageImportMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
@@ -48,8 +48,7 @@ import { useOrganizationId } from "/hooks/useOrganizationId";
import { Link, useParams } from "react-router"; import { Link, useParams } from "react-router";
import MeasureFormDialog from "./dialog/MeasureFormDialog"; import MeasureFormDialog from "./dialog/MeasureFormDialog";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { isAuthorized } from "/permissions";
type Props = { type Props = {
queryRef: PreloadedQuery<MeasureGraphListQuery>; queryRef: PreloadedQuery<MeasureGraphListQuery>;
@@ -92,6 +91,7 @@ const importMeasuresMutation = graphql`
export default function MeasuresPage(props: Props) { export default function MeasuresPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const organization = usePreloadedQuery( const organization = usePreloadedQuery(
measuresQuery, measuresQuery,
props.queryRef props.queryRef
@@ -115,8 +115,8 @@ export default function MeasuresPage(props: Props) {
const importFileRef = useRef<HTMLInputElement>(null); const importFileRef = useRef<HTMLInputElement>(null);
usePageTitle(__("Measures")); usePageTitle(__("Measures"));
const hasAnyAction = isAuthorized(organization.id, "Measure", "updateMeasure") || const hasAnyAction = isAuthorized("Measure", "updateMeasure") ||
isAuthorized(organization.id, "Measure", "deleteMeasure"); isAuthorized("Measure", "deleteMeasure");
const handleImport: ChangeEventHandler<HTMLInputElement> = (event) => { const handleImport: ChangeEventHandler<HTMLInputElement> = (event) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
@@ -148,21 +148,23 @@ export default function MeasuresPage(props: Props) {
"Measures are actions taken to reduce the risk. Add them to track their implementation status." "Measures are actions taken to reduce the risk. Add them to track their implementation status."
)} )}
> >
<Authorized entity="Organization" action="createMeasure"> {isAuthorized("Organization", "createMeasure") && (
<FileButton <>
ref={importFileRef} <FileButton
variant="secondary" ref={importFileRef}
icon={IconFolderUpload} variant="secondary"
onChange={handleImport} icon={IconFolderUpload}
> onChange={handleImport}
{__("Import")} >
</FileButton> {__("Import")}
<MeasureFormDialog connection={connectionId}> </FileButton>
<Button variant="primary" icon={IconPlusLarge}> <MeasureFormDialog connection={connectionId}>
{__("New measure")} <Button variant="primary" icon={IconPlusLarge}>
</Button> {__("New measure")}
</MeasureFormDialog> </Button>
</Authorized> </MeasureFormDialog>
</>
)}
</PageHeader> </PageHeader>
<MeasureImplementation measures={measures} className="my-10" /> <MeasureImplementation measures={measures} className="my-10" />
{objectKeys(measuresPerCategory) {objectKeys(measuresPerCategory)
@@ -269,6 +271,7 @@ function MeasureRow(props: MeasureRowProps) {
const [deleteMeasure, isDeleting] = useDeleteMeasureMutation(); const [deleteMeasure, isDeleting] = useDeleteMeasureMutation();
const confirm = useConfirm(); const confirm = useConfirm();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const onDelete = () => { const onDelete = () => {
confirm( confirm(
@@ -306,15 +309,15 @@ function MeasureRow(props: MeasureRowProps) {
{props.hasAnyAction && ( {props.hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Measure" action="updateMeasure"> {isAuthorized("Measure", "updateMeasure") && (
<DropdownItem <DropdownItem
icon={IconPencil} icon={IconPencil}
onClick={() => dialogRef.current?.open()} onClick={() => dialogRef.current?.open()}
> >
{__("Edit")} {__("Edit")}
</DropdownItem> </DropdownItem>
</Authorized> )}
<Authorized entity="Measure" action="deleteMeasure"> {isAuthorized("Measure", "deleteMeasure") && (
<DropdownItem <DropdownItem
onClick={onDelete} onClick={onDelete}
disabled={isDeleting} disabled={isDeleting}
@@ -323,7 +326,7 @@ function MeasureRow(props: MeasureRowProps) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -21,12 +21,12 @@ import {
UpdateMeetingMinutesDialog, UpdateMeetingMinutesDialog,
type UpdateMeetingMinutesDialogRef, type UpdateMeetingMinutesDialogRef,
} from "./dialogs/UpdateMeetingMinutesDialog"; } from "./dialogs/UpdateMeetingMinutesDialog";
import { useRef, useState, useEffect } from "react"; import { useRef, useState, useEffect, use } from "react";
import { import {
meetingNodeQuery, meetingNodeQuery,
useDeleteMeetingMutation, useDeleteMeetingMutation,
} from "/hooks/graph/MeetingGraph"; } from "/hooks/graph/MeetingGraph";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
const meetingFragment = graphql` const meetingFragment = graphql`
fragment MeetingDetailPageMeetingFragment on Meeting { fragment MeetingDetailPageMeetingFragment on Meeting {
@@ -54,6 +54,7 @@ export default function MeetingDetailPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthorized } = use(PermissionsContext);
if (!meeting) { if (!meeting) {
return <div>{__("Meeting not found")}</div>; return <div>{__("Meeting not found")}</div>;
@@ -74,14 +75,14 @@ export default function MeetingDetailPage(props: Props) {
} }
try { try {
const updateAuth = isAuthorized(organizationId, "Meeting", "updateMeeting"); const updateAuth = isAuthorized("Meeting", "updateMeeting");
setCanUpdate(updateAuth); setCanUpdate(updateAuth);
} catch (promise) { } catch (promise) {
if (promise instanceof Promise) { if (promise instanceof Promise) {
promise promise
.then(() => { .then(() => {
try { try {
const updateAuth = isAuthorized(organizationId, "Meeting", "updateMeeting"); const updateAuth = isAuthorized("Meeting", "updateMeeting");
setCanUpdate(updateAuth); setCanUpdate(updateAuth);
} catch { } catch {
setCanUpdate(false); setCanUpdate(false);
@@ -96,14 +97,14 @@ export default function MeetingDetailPage(props: Props) {
} }
try { try {
const deleteAuth = isAuthorized(organizationId, "Meeting", "deleteMeeting"); const deleteAuth = isAuthorized("Meeting", "deleteMeeting");
setCanDelete(deleteAuth); setCanDelete(deleteAuth);
} catch (promise) { } catch (promise) {
if (promise instanceof Promise) { if (promise instanceof Promise) {
promise promise
.then(() => { .then(() => {
try { try {
const deleteAuth = isAuthorized(organizationId, "Meeting", "deleteMeeting"); const deleteAuth = isAuthorized("Meeting", "deleteMeeting");
setCanDelete(deleteAuth); setCanDelete(deleteAuth);
} catch { } catch {
setCanDelete(false); setCanDelete(false);

View File

@@ -42,7 +42,8 @@ import { Link } from "react-router";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { MeetingsPage_UpdateSummaryMutation } from "./__generated__/MeetingsPage_UpdateSummaryMutation.graphql"; import type { MeetingsPage_UpdateSummaryMutation } from "./__generated__/MeetingsPage_UpdateSummaryMutation.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const meetingsFragment = graphql` const meetingsFragment = graphql`
fragment MeetingsPageListFragment on Organization fragment MeetingsPageListFragment on Organization
@@ -85,7 +86,7 @@ type Props = {
export default function MeetingsPage(props: Props) { export default function MeetingsPage(props: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const organization = usePreloadedQuery( const organization = usePreloadedQuery(
meetingsQuery, meetingsQuery,
props.queryRef props.queryRef
@@ -218,7 +219,7 @@ export default function MeetingsPage(props: Props) {
<h3 className="text-sm font-semibold text-txt-secondary"> <h3 className="text-sm font-semibold text-txt-secondary">
{__("Summary")} {__("Summary")}
</h3> </h3>
<Authorized entity="Meeting" action="updateMeeting"> {isAuthorized("Meeting", "updateMeeting") && (
<Button <Button
variant="quaternary" variant="quaternary"
icon={IconPencil} icon={IconPencil}
@@ -226,7 +227,7 @@ export default function MeetingsPage(props: Props) {
> >
{__("Edit")} {__("Edit")}
</Button> </Button>
</Authorized> )}
</div> </div>
<div className="w-full"> <div className="w-full">
{displayedSummary ? ( {displayedSummary ? (
@@ -248,11 +249,11 @@ export default function MeetingsPage(props: Props) {
"Track and manage your organization's meetings and their minutes." "Track and manage your organization's meetings and their minutes."
)} )}
> >
<Authorized entity="Organization" action="createMeeting"> {isAuthorized("Organization", "createMeeting") && (
<CreateMeetingDialog connectionId={connectionId}> <CreateMeetingDialog connectionId={connectionId}>
<Button icon={IconPlusLarge}>{__("Add meeting")}</Button> <Button icon={IconPlusLarge}>{__("Add meeting")}</Button>
</CreateMeetingDialog> </CreateMeetingDialog>
</Authorized> )}
</PageHeader> </PageHeader>
{meetingNodes.length > 0 ? ( {meetingNodes.length > 0 ? (
<SortableTable {...pagination}> <SortableTable {...pagination}>
@@ -320,7 +321,7 @@ function MeetingRow({
const { __ } = useTranslate(); const { __ } = useTranslate();
const [deleteMeeting] = useDeleteMeetingMutation(); const [deleteMeeting] = useDeleteMeetingMutation();
const confirm = useConfirm(); const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => { const handleDelete = () => {
confirm( confirm(
() => () =>
@@ -370,7 +371,7 @@ function MeetingRow({
</span> </span>
)} )}
</Td> </Td>
<Authorized entity="Meeting" action="deleteMeeting"> {isAuthorized("Meeting", "deleteMeeting") && (
<Td noLink width={50} className="text-end w-18"> <Td noLink width={50} className="text-end w-18">
<ActionDropdown> <ActionDropdown>
<DropdownItem <DropdownItem
@@ -382,7 +383,7 @@ function MeetingRow({
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Td> </Td>
</Authorized> )}
</Tr> </Tr>
); );
} }

View File

@@ -31,13 +31,13 @@ import { deleteNonconformityMutation, NonconformitiesConnectionKey } from "../..
import { sprintf, promisifyMutation, getStatusVariant, getStatusLabel, formatDate } from "@probo/helpers"; import { sprintf, promisifyMutation, getStatusVariant, getStatusLabel, formatDate } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import type { NonconformitiesPageQuery } from "./__generated__/NonconformitiesPageQuery.graphql"; import type { NonconformitiesPageQuery } from "./__generated__/NonconformitiesPageQuery.graphql";
import type { import type {
NonconformitiesPageFragment$key, NonconformitiesPageFragment$key,
NonconformitiesPageFragment$data, NonconformitiesPageFragment$data,
} from "./__generated__/NonconformitiesPageFragment.graphql"; } from "./__generated__/NonconformitiesPageFragment.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Nonconformity = NonconformitiesPageFragment$data['nonconformities']['edges'][number]['node']; type Nonconformity = NonconformitiesPageFragment$data['nonconformities']['edges'][number]['node'];
@@ -103,6 +103,7 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Nonconformities")); usePageTitle(__("Nonconformities"));
@@ -132,8 +133,8 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
const nonconformities: Nonconformity[] = nonconformitiesData?.nonconformities?.edges?.map((edge) => edge.node) ?? []; const nonconformities: Nonconformity[] = nonconformitiesData?.nonconformities?.edges?.map((edge) => edge.node) ?? [];
const hasAnyAction = !isSnapshotMode && ( const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Nonconformity", "updateNonconformity") || isAuthorized("Nonconformity", "updateNonconformity") ||
isAuthorized(organizationId, "Nonconformity", "deleteNonconformity") isAuthorized("Nonconformity", "deleteNonconformity")
); );
return ( return (
@@ -148,11 +149,11 @@ export default function NonconformitiesPage({ queryRef }: NonconformitiesPagePro
)} )}
> >
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="Organization" action="createNonconformity"> isAuthorized("Organization", "createNonconformity") && (
<CreateNonconformityDialog organizationId={organizationId} connection={connectionId}> <CreateNonconformityDialog organizationId={organizationId} connection={connectionId}>
<Button icon={IconPlusLarge}>{__("Add nonconformity")}</Button> <Button icon={IconPlusLarge}>{__("Add nonconformity")}</Button>
</CreateNonconformityDialog> </CreateNonconformityDialog>
</Authorized> )
)} )}
</PageHeader> </PageHeader>
@@ -231,7 +232,7 @@ function NonconformityRow({
const { __ } = useTranslate(); const { __ } = useTranslate();
const confirm = useConfirm(); const confirm = useConfirm();
const [deleteNonconformity] = useMutation(deleteNonconformityMutation); const [deleteNonconformity] = useMutation(deleteNonconformityMutation);
const { isAuthorized } = use(PermissionsContext);
const nonconformityDetailUrl = isSnapshotMode const nonconformityDetailUrl = isSnapshotMode
? `/organizations/${organizationId}/snapshots/${snapshotId}/nonconformities/${nonconformity.id}` ? `/organizations/${organizationId}/snapshots/${snapshotId}/nonconformities/${nonconformity.id}`
@@ -298,7 +299,7 @@ function NonconformityRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Nonconformity" action="deleteNonconformity"> {isAuthorized("Nonconformity", "deleteNonconformity") && (
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
variant="danger" variant="danger"
@@ -306,7 +307,7 @@ function NonconformityRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -34,7 +34,8 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
import z from "zod"; import z from "zod";
import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency, getStatusOptions, formatError, type GraphQLError } from "@probo/helpers"; import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency, getStatusOptions, formatError, type GraphQLError } from "@probo/helpers";
import type { NonconformityGraphNodeQuery } from "/hooks/graph/__generated__/NonconformityGraphNodeQuery.graphql"; import type { NonconformityGraphNodeQuery } from "/hooks/graph/__generated__/NonconformityGraphNodeQuery.graphql";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const updateNonconformitySchema = z.object({ const updateNonconformitySchema = z.object({
referenceId: z.string().min(1, "Reference ID is required"), referenceId: z.string().min(1, "Reference ID is required"),
@@ -63,6 +64,7 @@ export default function NonconformityDetailsPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
validateSnapshotConsistency(nonconformity, snapshotId); validateSnapshotConsistency(nonconformity, snapshotId);
@@ -162,7 +164,7 @@ export default function NonconformityDetailsPage(props: Props) {
</div> </div>
{!isSnapshotMode && ( {!isSnapshotMode && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<Authorized entity="Nonconformity" action="deleteNonconformity"> {isAuthorized("Nonconformity", "deleteNonconformity") && (
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -170,7 +172,7 @@ export default function NonconformityDetailsPage(props: Props) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
)} )}
</div> </div>
@@ -279,11 +281,11 @@ export default function NonconformityDetailsPage(props: Props) {
<div className="flex justify-end"> <div className="flex justify-end">
{formState.isDirty && !isSnapshotMode && ( {formState.isDirty && !isSnapshotMode && (
<Authorized entity="Nonconformity" action="updateNonconformity"> isAuthorized("Nonconformity", "updateNonconformity") && (
<Button type="submit" disabled={formState.isSubmitting}> <Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")} {formState.isSubmitting ? __("Updating...") : __("Update")}
</Button> </Button>
</Authorized> )
)} )}
</div> </div>
</form> </form>

View File

@@ -35,7 +35,8 @@ import z from "zod";
import { getObligationStatusVariant, getObligationStatusLabel, formatDatetime, getObligationStatusOptions, validateSnapshotConsistency } from "@probo/helpers"; import { getObligationStatusVariant, getObligationStatusLabel, formatDatetime, getObligationStatusOptions, validateSnapshotConsistency } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import type { ObligationGraphNodeQuery } from "/hooks/graph/__generated__/ObligationGraphNodeQuery.graphql"; import type { ObligationGraphNodeQuery } from "/hooks/graph/__generated__/ObligationGraphNodeQuery.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateObligationSchema = z.object({ const updateObligationSchema = z.object({
area: z.string().optional(), area: z.string().optional(),
@@ -61,6 +62,7 @@ export default function ObligationDetailsPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!obligation) { if (!obligation) {
return <div>{__("Obligation not found")}</div>; return <div>{__("Obligation not found")}</div>;
@@ -157,13 +159,13 @@ export default function ObligationDetailsPage(props: Props) {
</div> </div>
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="Obligation" action="deleteObligation"> isAuthorized("Obligation", "deleteObligation") && (
<ActionDropdown> <ActionDropdown>
<DropdownItem icon={IconTrashCan} onClick={deleteObligation}> <DropdownItem icon={IconTrashCan} onClick={deleteObligation}>
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )
)} )}
</div> </div>
@@ -301,14 +303,14 @@ export default function ObligationDetailsPage(props: Props) {
{!isSnapshotMode && ( {!isSnapshotMode && (
<div className="flex justify-end"> <div className="flex justify-end">
<Authorized entity="Obligation" action="updateObligation"> {isAuthorized("Obligation", "updateObligation") && (
<Button <Button
type="submit" type="submit"
disabled={formState.isSubmitting} disabled={formState.isSubmitting}
> >
{formState.isSubmitting ? __("Saving...") : __("Save Changes")} {formState.isSubmitting ? __("Saving...") : __("Save Changes")}
</Button> </Button>
</Authorized> )}
</div> </div>
)} )}
</form> </form>

View File

@@ -31,12 +31,12 @@ import { deleteObligationMutation } from "../../../hooks/graph/ObligationGraph";
import { promisifyMutation, getObligationStatusVariant, getObligationStatusLabel, formatDate } from "@probo/helpers"; import { promisifyMutation, getObligationStatusVariant, getObligationStatusLabel, formatDate } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import type { ObligationsPageQuery } from "./__generated__/ObligationsPageQuery.graphql"; import type { ObligationsPageQuery } from "./__generated__/ObligationsPageQuery.graphql";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import type { import type {
ObligationsPageFragment$key, ObligationsPageFragment$key,
ObligationsPageFragment$data, ObligationsPageFragment$data,
} from "./__generated__/ObligationsPageFragment.graphql"; } from "./__generated__/ObligationsPageFragment.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Obligation = ObligationsPageFragment$data['obligations']['edges'][number]['node']; type Obligation = ObligationsPageFragment$data['obligations']['edges'][number]['node'];
@@ -95,6 +95,7 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Obligations")); usePageTitle(__("Obligations"));
@@ -120,8 +121,8 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
const obligations: Obligation[] = obligationsData?.obligations?.edges?.map((edge) => edge.node) ?? []; const obligations: Obligation[] = obligationsData?.obligations?.edges?.map((edge) => edge.node) ?? [];
const hasAnyAction = !isSnapshotMode && ( const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Obligation", "updateObligation") || isAuthorized("Obligation", "updateObligation") ||
isAuthorized(organizationId, "Obligation", "deleteObligation") isAuthorized("Obligation", "deleteObligation")
); );
return ( return (
@@ -136,11 +137,11 @@ export default function ObligationsPage({ queryRef }: ObligationsPageProps) {
)} )}
> >
{!snapshotId && ( {!snapshotId && (
<Authorized entity="Organization" action="createObligation"> isAuthorized("Organization", "createObligation") && (
<CreateObligationDialog organizationId={organizationId} connection={connectionId}> <CreateObligationDialog organizationId={organizationId} connection={connectionId}>
<Button icon={IconPlusLarge}>{__("Add obligation")}</Button> <Button icon={IconPlusLarge}>{__("Add obligation")}</Button>
</CreateObligationDialog> </CreateObligationDialog>
</Authorized> )
)} )}
</PageHeader> </PageHeader>
@@ -214,7 +215,7 @@ function ObligationRow({
const [deleteObligation] = useMutation(deleteObligationMutation); const [deleteObligation] = useMutation(deleteObligationMutation);
const confirm = useConfirm(); const confirm = useConfirm();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => { const handleDelete = () => {
confirm( confirm(
@@ -261,7 +262,7 @@ function ObligationRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Obligation" action="deleteObligation"> {isAuthorized("Obligation", "deleteObligation") && (
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
variant="danger" variant="danger"
@@ -269,7 +270,7 @@ function ObligationRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -21,7 +21,8 @@ import {
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { Outlet } from "react-router"; import { Outlet } from "react-router";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
queryRef: PreloadedQuery<PeopleGraphNodeQuery>; queryRef: PreloadedQuery<PeopleGraphNodeQuery>;
@@ -32,6 +33,7 @@ export default function PeopleDetailPage(props: Props) {
const people = data.node; const people = data.node;
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const deletePeople = useDeletePeople( const deletePeople = useDeletePeople(
people, people,
ConnectionHandler.getConnectionID(organizationId, PeopleConnectionKey) ConnectionHandler.getConnectionID(organizationId, PeopleConnectionKey)
@@ -55,7 +57,7 @@ export default function PeopleDetailPage(props: Props) {
<Avatar name={people.fullName ?? ""} size="xl" /> <Avatar name={people.fullName ?? ""} size="xl" />
<div className="text-2xl">{people.fullName}</div> <div className="text-2xl">{people.fullName}</div>
</div> </div>
<Authorized entity="People" action="deletePeople"> {isAuthorized("People", "deletePeople") && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
variant="danger" variant="danger"
@@ -65,7 +67,7 @@ export default function PeopleDetailPage(props: Props) {
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )}
</div> </div>
<Tabs> <Tabs>

View File

@@ -23,8 +23,8 @@ import { usePageTitle } from "@probo/hooks";
import { getRole } from "@probo/helpers"; import { getRole } from "@probo/helpers";
import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog"; import { CreatePeopleDialog } from "./dialogs/CreatePeopleDialog";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { isAuthorized } from "/permissions"; import { use } from "react";
type People = NodeOf<PeopleGraphPaginatedFragment$data["peoples"]>; type People = NodeOf<PeopleGraphPaginatedFragment$data["peoples"]>;
@@ -42,14 +42,14 @@ export default function PeopleListPage({
queryRef: PreloadedQuery<PeopleGraphPaginatedQuery>; queryRef: PreloadedQuery<PeopleGraphPaginatedQuery>;
}) { }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const { isAuthorized } = use(PermissionsContext);
const { people, refetch, connectionId, hasNext, loadNext, isLoadingNext } = const { people, refetch, connectionId, hasNext, loadNext, isLoadingNext } =
usePeopleQuery(queryRef); usePeopleQuery(queryRef);
usePageTitle(__("Members")); usePageTitle(__("Members"));
const hasAnyAction = isAuthorized(organizationId, "People", "updatePeople") || const hasAnyAction = isAuthorized("People", "updatePeople") ||
isAuthorized(organizationId, "People", "deletePeople"); isAuthorized("People", "deletePeople");
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -59,11 +59,11 @@ export default function PeopleListPage({
"Keep track of your company's workforce and their progress towards completing tasks assigned to them." "Keep track of your company's workforce and their progress towards completing tasks assigned to them."
)} )}
> >
<Authorized entity="Organization" action="createPeople"> {isAuthorized("Organization", "createPeople") && (
<CreatePeopleDialog connectionId={connectionId}> <CreatePeopleDialog connectionId={connectionId}>
<Button icon={IconPlusLarge}>{__("Add member")}</Button> <Button icon={IconPlusLarge}>{__("Add member")}</Button>
</CreatePeopleDialog> </CreatePeopleDialog>
</Authorized> )}
</PageHeader> </PageHeader>
<SortableTable <SortableTable
refetch={refetch} refetch={refetch}
@@ -107,6 +107,7 @@ function PeopleRow({
const { __ } = useTranslate(); const { __ } = useTranslate();
const deletePeople = useDeletePeople(people, connectionId); const deletePeople = useDeletePeople(people, connectionId);
const contractEnded = isContractEnded(people); const contractEnded = isContractEnded(people);
const { isAuthorized } = use(PermissionsContext);
return ( return (
<Tr <Tr
@@ -129,7 +130,7 @@ function PeopleRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="People" action="deletePeople"> {isAuthorized("People", "deletePeople") && (
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
variant="danger" variant="danger"
@@ -137,7 +138,7 @@ function PeopleRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -9,7 +9,8 @@ import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/Peopl
import { updatePeopleMutation } from "/hooks/graph/PeopleGraph"; import { updatePeopleMutation } from "/hooks/graph/PeopleGraph";
import { Button, Card, Field, Input } from "@probo/ui"; import { Button, Card, Field, Input } from "@probo/ui";
import { EmailsField } from "/components/form/EmailsField"; import { EmailsField } from "/components/form/EmailsField";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const schema = z.object({ const schema = z.object({
fullName: z.string().min(1), fullName: z.string().min(1),
@@ -49,7 +50,7 @@ export default function PeopleProfileTab() {
errorMessage: __("Failed to update member"), errorMessage: __("Failed to update member"),
} }
); );
const { isAuthorized } = use(PermissionsContext);
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
const input = { const input = {
id: people.id!, id: people.id!,
@@ -95,11 +96,11 @@ export default function PeopleProfileTab() {
</Card> </Card>
<div className="flex justify-end"> <div className="flex justify-end">
{formState.isDirty && ( {formState.isDirty && (
<Authorized entity="People" action="updatePeople"> isAuthorized("People", "updatePeople") && (
<Button type="submit" disabled={isMutating}> <Button type="submit" disabled={isMutating}>
{__("Update")} {__("Update")}
</Button> </Button>
</Authorized> )
)} )}
</div> </div>
</form> </form>

View File

@@ -32,14 +32,14 @@ import { CreateProcessingActivityDialog } from "./dialogs/CreateProcessingActivi
import { deleteProcessingActivityMutation, ProcessingActivitiesConnectionKey } from "../../../hooks/graph/ProcessingActivityGraph"; import { deleteProcessingActivityMutation, ProcessingActivitiesConnectionKey } from "../../../hooks/graph/ProcessingActivityGraph";
import { sprintf, promisifyMutation } from "@probo/helpers"; import { sprintf, promisifyMutation } from "@probo/helpers";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions";
import { isAuthorized } from "/permissions";
import type { NodeOf } from "/types"; import type { NodeOf } from "/types";
import type { ProcessingActivitiesPageQuery } from "./__generated__/ProcessingActivitiesPageQuery.graphql"; import type { ProcessingActivitiesPageQuery } from "./__generated__/ProcessingActivitiesPageQuery.graphql";
import type { import type {
ProcessingActivitiesPageFragment$key, ProcessingActivitiesPageFragment$key,
ProcessingActivitiesPageFragment$data, ProcessingActivitiesPageFragment$data,
} from "./__generated__/ProcessingActivitiesPageFragment.graphql"; } from "./__generated__/ProcessingActivitiesPageFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
interface ProcessingActivitiesPageProps { interface ProcessingActivitiesPageProps {
queryRef: PreloadedQuery<ProcessingActivitiesPageQuery>; queryRef: PreloadedQuery<ProcessingActivitiesPageQuery>;
@@ -91,7 +91,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Processing Activities")); usePageTitle(__("Processing Activities"));
@@ -129,8 +129,8 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
const activities = data?.processingActivities?.edges?.map((edge) => edge.node) ?? []; const activities = data?.processingActivities?.edges?.map((edge) => edge.node) ?? [];
const hasAnyAction = !isSnapshotMode && ( const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "ProcessingActivity", "updateProcessingActivity") || isAuthorized("ProcessingActivity", "updateProcessingActivity") ||
isAuthorized(organizationId, "ProcessingActivity", "deleteProcessingActivity") isAuthorized("ProcessingActivity", "deleteProcessingActivity")
); );
return ( return (
@@ -140,7 +140,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
)} )}
<PageHeader title={__("Processing Activities")} description={__("Manage your processing activities under GDPR")}> <PageHeader title={__("Processing Activities")} description={__("Manage your processing activities under GDPR")}>
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="Organization" action="createProcessingActivity"> isAuthorized("Organization", "createProcessingActivity") && (
<CreateProcessingActivityDialog <CreateProcessingActivityDialog
organizationId={organizationId} organizationId={organizationId}
connectionId={connectionId} connectionId={connectionId}
@@ -149,7 +149,7 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
{__("Add processing activity")} {__("Add processing activity")}
</Button> </Button>
</CreateProcessingActivityDialog> </CreateProcessingActivityDialog>
</Authorized> )
)} )}
</PageHeader> </PageHeader>
@@ -222,6 +222,7 @@ function ActivityRow({
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const [deleteActivity] = useMutation(deleteProcessingActivityMutation); const [deleteActivity] = useMutation(deleteProcessingActivityMutation);
const confirm = useConfirm(); const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const handleDelete = () => { const handleDelete = () => {
confirm( confirm(
@@ -270,7 +271,7 @@ function ActivityRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="ProcessingActivity" action="deleteProcessingActivity"> {isAuthorized("ProcessingActivity", "deleteProcessingActivity") && (
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
variant="danger" variant="danger"
@@ -278,7 +279,7 @@ function ActivityRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -41,7 +41,8 @@ import {
} from "../../../components/form/ProcessingActivityEnumOptions"; } from "../../../components/form/ProcessingActivityEnumOptions";
import type { ProcessingActivityGraphNodeQuery } from "/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql"; import type { ProcessingActivityGraphNodeQuery } from "/hooks/graph/__generated__/ProcessingActivityGraphNodeQuery.graphql";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const updateProcessingActivitySchema = z.object({ const updateProcessingActivitySchema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
@@ -74,6 +75,7 @@ export default function ProcessingActivityDetailsPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!activity) { if (!activity) {
return <div>{__("Processing activity not found")}</div>; return <div>{__("Processing activity not found")}</div>;
@@ -171,13 +173,13 @@ export default function ProcessingActivityDetailsPage(props: Props) {
]} ]}
/> />
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="ProcessingActivity" action="deleteProcessingActivity"> isAuthorized("ProcessingActivity", "deleteProcessingActivity") && (
<ActionDropdown> <ActionDropdown>
<DropdownItem onClick={deleteActivity} variant="danger"> <DropdownItem onClick={deleteActivity} variant="danger">
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )
)} )}
</div> </div>

View File

@@ -30,7 +30,8 @@ import {
} from "/hooks/graph/RiskGraph"; } from "/hooks/graph/RiskGraph";
import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql"; import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
queryRef: PreloadedQuery<RiskGraphNodeQuery>; queryRef: PreloadedQuery<RiskGraphNodeQuery>;
@@ -41,6 +42,7 @@ export default function RiskDetailPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const navigate = useNavigate(); const navigate = useNavigate();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!riskId) { if (!riskId) {
throw new Error("Cannot load risk detail page without riskId parameter"); throw new Error("Cannot load risk detail page without riskId parameter");
@@ -121,7 +123,7 @@ export default function RiskDetailPage(props: Props) {
/> />
{!isSnapshotMode && ( {!isSnapshotMode && (
<div className="flex gap-2"> <div className="flex gap-2">
<Authorized entity="Risk" action="updateRisk"> {isAuthorized("Risk", "updateRisk") && (
<FormRiskDialog <FormRiskDialog
trigger={ trigger={
<Button icon={IconPencil} variant="secondary"> <Button icon={IconPencil} variant="secondary">
@@ -130,8 +132,8 @@ export default function RiskDetailPage(props: Props) {
} }
risk={{ id: riskId, ...risk }} risk={{ id: riskId, ...risk }}
/> />
</Authorized> )}
<Authorized entity="Risk" action="deleteRisk"> {isAuthorized("Risk", "deleteRisk") && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
variant="danger" variant="danger"
@@ -141,7 +143,7 @@ export default function RiskDetailPage(props: Props) {
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )}
</div> </div>
)} )}
</div> </div>

View File

@@ -29,8 +29,8 @@ import type { RiskGraphListQuery } from "/hooks/graph/__generated__/RiskGraphLis
import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql"; import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions"; import { use } from "react";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
queryRef: PreloadedQuery<RiskGraphListQuery>; queryRef: PreloadedQuery<RiskGraphListQuery>;
@@ -41,6 +41,7 @@ export default function RisksPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const { connectionId, risks, ...pagination } = useRisksQuery(props.queryRef); const { connectionId, risks, ...pagination } = useRisksQuery(props.queryRef);
@@ -57,8 +58,8 @@ export default function RisksPage(props: Props) {
usePageTitle(__("Risks")); usePageTitle(__("Risks"));
const hasAnyAction = !isSnapshotMode && ( const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Risk", "updateRisk") || isAuthorized("Risk", "updateRisk") ||
isAuthorized(organizationId, "Risk", "deleteRisk") isAuthorized("Risk", "deleteRisk")
); );
return ( return (
@@ -71,7 +72,7 @@ export default function RisksPage(props: Props) {
)} )}
> >
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="Organization" action="createRisk"> isAuthorized("Organization", "createRisk") && (
<FormRiskDialog <FormRiskDialog
connection={connectionId} connection={connectionId}
onSuccess={() => { onSuccess={() => {
@@ -79,7 +80,7 @@ export default function RisksPage(props: Props) {
}} }}
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>} trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
/> />
</Authorized> )
)} )}
</PageHeader> </PageHeader>
@@ -143,6 +144,7 @@ function RiskRow(props: RowProps) {
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const [deleteRisk] = useDeleteRiskMutation(); const [deleteRisk] = useDeleteRiskMutation();
const confirm = useConfirm(); const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const onDelete = () => { const onDelete = () => {
confirm( confirm(
() => () =>
@@ -194,16 +196,16 @@ function RiskRow(props: RowProps) {
{props.hasAnyAction && ( {props.hasAnyAction && (
<Td noLink className="text-end"> <Td noLink className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Risk" action="updateRisk"> {isAuthorized("Risk", "updateRisk") && (
<DropdownItem <DropdownItem
icon={IconPencil} icon={IconPencil}
onClick={() => formDialogRef.current?.open()} onClick={() => formDialogRef.current?.open()}
> >
{__("Edit")} {__("Edit")}
</DropdownItem> </DropdownItem>
</Authorized> )}
<Authorized entity="Risk" action="deleteRisk"> {isAuthorized("Risk", "deleteRisk") && (
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -211,7 +213,7 @@ function RiskRow(props: RowProps) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, type ChangeEventHandler } from "react"; import { useState, useRef, useEffect, type ChangeEventHandler, use } from "react";
import { useOutletContext, useNavigate } from "react-router"; import { useOutletContext, useNavigate } from "react-router";
import { useFragment, graphql } from "react-relay"; import { useFragment, graphql } from "react-relay";
import { import {
@@ -23,7 +23,7 @@ import { z } from "zod";
import type { GeneralSettingsTabFragment$key } from "./__generated__/GeneralSettingsTabFragment.graphql"; import type { GeneralSettingsTabFragment$key } from "./__generated__/GeneralSettingsTabFragment.graphql";
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog"; import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph"; import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
const generalSettingsTabFragment = graphql` const generalSettingsTabFragment = graphql`
fragment GeneralSettingsTabFragment on Organization { fragment GeneralSettingsTabFragment on Organization {
@@ -90,9 +90,10 @@ export default function GeneralSettingsTab() {
const { organization: organizationKey } = useOutletContext<OutletContext>(); const { organization: organizationKey } = useOutletContext<OutletContext>();
const organization = useFragment(generalSettingsTabFragment, organizationKey); const organization = useFragment(generalSettingsTabFragment, organizationKey);
const deleteDialogRef = useDialogRef(); const deleteDialogRef = useDialogRef();
const { isAuthorized } = use(PermissionsContext);
const canUpdate = isAuthorized(organization.id, "Organization", "updateOrganization"); const canUpdate = isAuthorized("Organization", "updateOrganization");
const canDelete = isAuthorized(organization.id, "Organization", "deleteOrganization"); const canDelete = isAuthorized("Organization", "deleteOrganization");
const [logoPreview, setLogoPreview] = useState<string | null>(null); const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [horizontalLogoPreview, setHorizontalLogoPreview] = useState< const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<

View File

@@ -1,4 +1,4 @@
import { useState, Suspense } from "react"; import { useState, Suspense, use } from "react";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { usePaginationFragment, graphql } from "react-relay"; import { usePaginationFragment, graphql } from "react-relay";
import { import {
@@ -29,11 +29,9 @@ import { useTranslate } from "@probo/i18n";
import { SortableTable, SortableTh } from "/components/SortableTable"; import { SortableTable, SortableTh } from "/components/SortableTable";
import { InviteUserDialog } from "/components/organizations/InviteUserDialog"; import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { sprintf } from "@probo/helpers"; import { getAssignableRoles, sprintf } from "@probo/helpers";
import clsx from "clsx"; import clsx from "clsx";
import type { NodeOf } from "/types"; import type { NodeOf } from "/types";
import { Authorized } from "/permissions";
import { getAssignableRoles, getUserRole } from "/permissions";
import type { import type {
MembersSettingsTabMembershipsFragment$data, MembersSettingsTabMembershipsFragment$data,
MembersSettingsTabMembershipsFragment$key MembersSettingsTabMembershipsFragment$key
@@ -42,6 +40,7 @@ import type {
MembersSettingsTabInvitationsFragment$data, MembersSettingsTabInvitationsFragment$data,
MembersSettingsTabInvitationsFragment$key MembersSettingsTabInvitationsFragment$key
} from "./__generated__/MembersSettingsTabInvitationsFragment.graphql"; } from "./__generated__/MembersSettingsTabInvitationsFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
const paginatedMembershipsFragment = graphql` const paginatedMembershipsFragment = graphql`
fragment MembersSettingsTabMembershipsFragment on Organization fragment MembersSettingsTabMembershipsFragment on Organization
@@ -153,7 +152,7 @@ type OutletContext = {
export default function MembersSettingsTab() { export default function MembersSettingsTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { organization: organizationKey } = useOutletContext<OutletContext>(); const { organization: organizationKey } = useOutletContext<OutletContext>();
const { isAuthorized } = use(PermissionsContext);
const membershipsPagination = usePaginationFragment( const membershipsPagination = usePaginationFragment(
paginatedMembershipsFragment, paginatedMembershipsFragment,
organizationKey as MembersSettingsTabMembershipsFragment$key organizationKey as MembersSettingsTabMembershipsFragment$key
@@ -180,14 +179,14 @@ export default function MembersSettingsTab() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-base font-medium">{__("Workspace members")}</h2> <h2 className="text-base font-medium">{__("Workspace members")}</h2>
<Authorized entity="Organization" action="inviteUser"> {isAuthorized("Organization", "inviteUser") && (
<InviteUserDialog <InviteUserDialog
connectionId={invitationsPagination.data.invitations?.__id} connectionId={invitationsPagination.data.invitations?.__id}
onRefetch={refetchInvitations} onRefetch={refetchInvitations}
> >
<Button variant="secondary">{__("Invite member")}</Button> <Button variant="secondary">{__("Invite member")}</Button>
</InviteUserDialog> </InviteUserDialog>
</Authorized> )}
</div> </div>
<Tabs> <Tabs>
@@ -314,6 +313,7 @@ function InvitationRow(props: {
}) { }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const confirm = useConfirm(); const confirm = useConfirm();
const { isAuthorized } = use(PermissionsContext);
const [deleteInvitation, isDeleting] = useMutationWithToasts( const [deleteInvitation, isDeleting] = useMutationWithToasts(
deleteInvitationMutation, deleteInvitationMutation,
{ {
@@ -376,7 +376,7 @@ function InvitationRow(props: {
{isDeleting ? ( {isDeleting ? (
<Spinner size={16} /> <Spinner size={16} />
) : ( ) : (
<Authorized entity="Invitation" action="deleteInvitation"> isAuthorized("Invitation", "deleteInvitation") && (
<Button <Button
variant="danger" variant="danger"
onClick={onDelete} onClick={onDelete}
@@ -384,7 +384,7 @@ function InvitationRow(props: {
icon={IconTrashCan} icon={IconTrashCan}
aria-label={__("Delete invitation")} aria-label={__("Delete invitation")}
/> />
</Authorized> )
)} )}
</div> </div>
</Td> </Td>
@@ -399,9 +399,9 @@ function MembershipRowContent(props: {
onRefetch: () => void; onRefetch: () => void;
}) { }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const availableRoles = getAssignableRoles(props.organizationId); const { role: currentUserRole } = use(PermissionsContext);
const currentUserRole = getUserRole(props.organizationId); const availableRoles = getAssignableRoles(currentUserRole);
const { isAuthorized } = use(PermissionsContext);
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, { const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
successMessage: __("Member removed successfully"), successMessage: __("Member removed successfully"),
errorMessage: __("Failed to remove member"), errorMessage: __("Failed to remove member"),
@@ -494,31 +494,27 @@ function MembershipRowContent(props: {
className="flex gap-2 justify-end" className="flex gap-2 justify-end"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<Authorized entity="Organization" action="updateMembership"> {isAuthorized("Organization", "updateMembership") && canEditThisRole && (
{canEditThisRole && ( <Button
<Button variant="secondary"
variant="secondary" onClick={handleEditClick}
onClick={handleEditClick} disabled={isUpdating}
disabled={isUpdating} icon={IconPencil}
icon={IconPencil} aria-label={__("Edit role")}
aria-label={__("Edit role")} />
/> )}
)}
</Authorized>
{isRemoving ? ( {isRemoving ? (
<Spinner size={16} /> <Spinner size={16} />
) : ( ) : (
<Authorized entity="Organization" action="removeMember"> isAuthorized("Organization", "removeMember") && canEditThisRole && (
{canEditThisRole && ( <Button
<Button variant="danger"
variant="danger" onClick={onRemove}
onClick={onRemove} disabled={isRemoving}
disabled={isRemoving} icon={IconTrashCan}
icon={IconTrashCan} aria-label={__("Remove member")}
aria-label={__("Remove member")} />
/> )
)}
</Authorized>
)} )}
</div> </div>
</Td> </Td>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, use } from "react";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { useFragment, graphql } from "react-relay"; import { useFragment, graphql } from "react-relay";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
@@ -37,7 +37,7 @@ import {
useVerifyDomainMutation, useVerifyDomainMutation,
} from "/hooks/graph/SAMLConfigurationGraph"; } from "/hooks/graph/SAMLConfigurationGraph";
import type { SAMLSettingsTabFragment$key } from "./__generated__/SAMLSettingsTabFragment.graphql"; import type { SAMLSettingsTabFragment$key } from "./__generated__/SAMLSettingsTabFragment.graphql";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
const samlSettingsTabFragment = graphql` const samlSettingsTabFragment = graphql`
fragment SAMLSettingsTabFragment on Organization { fragment SAMLSettingsTabFragment on Organization {
@@ -97,6 +97,7 @@ type SetupStep = "initiate" | "verify" | "configure";
export default function SAMLSettingsTab() { export default function SAMLSettingsTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { organization: organizationKey } = useOutletContext<OutletContext>(); const { organization: organizationKey } = useOutletContext<OutletContext>();
const { isAuthorized } = use(PermissionsContext);
const organization = useFragment(samlSettingsTabFragment, organizationKey); const organization = useFragment(samlSettingsTabFragment, organizationKey);
const configs = organization.samlConfigurations; const configs = organization.samlConfigurations;
@@ -372,11 +373,11 @@ export default function SAMLSettingsTab() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-base font-medium">{__("SAML Single Sign-On")}</h2> <h2 className="text-base font-medium">{__("SAML Single Sign-On")}</h2>
<Authorized entity="Organization" action="createSAMLConfiguration"> {isAuthorized("Organization", "createSAMLConfiguration") && (
<Button onClick={() => handleOpenModal()}> <Button onClick={() => handleOpenModal()}>
{__("Add Configuration")} {__("Add Configuration")}
</Button> </Button>
</Authorized> )}
</div> </div>
{configs.length === 0 ? ( {configs.length === 0 ? (
@@ -388,11 +389,11 @@ export default function SAMLSettingsTab() {
<p className="text-gray-600 mb-6"> <p className="text-gray-600 mb-6">
{__("Set up SAML 2.0 single sign-on for your organization by adding a configuration for each email domain.")} {__("Set up SAML 2.0 single sign-on for your organization by adding a configuration for each email domain.")}
</p> </p>
<Authorized entity="Organization" action="createSAMLConfiguration"> {isAuthorized("Organization", "createSAMLConfiguration") && (
<Button onClick={() => handleOpenModal()}> <Button onClick={() => handleOpenModal()}>
{__("Add Your First Configuration")} {__("Add Your First Configuration")}
</Button> </Button>
</Authorized> )}
</div> </div>
</Card> </Card>
) : ( ) : (
@@ -457,7 +458,7 @@ export default function SAMLSettingsTab() {
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
{config.domainVerified ? ( {config.domainVerified ? (
<> <>
<Authorized entity="SAMLConfiguration" action="updateSAMLConfiguration"> {isAuthorized("SAMLConfiguration", "updateSAMLConfiguration") && (
<Button <Button
variant={config.enabled ? "danger" : "primary"} variant={config.enabled ? "danger" : "primary"}
onClick={() => handleToggleEnabled(config)} onClick={() => handleToggleEnabled(config)}
@@ -465,34 +466,34 @@ export default function SAMLSettingsTab() {
> >
{config.enabled ? __("Disable") : __("Enable")} {config.enabled ? __("Disable") : __("Enable")}
</Button> </Button>
</Authorized> )}
<Authorized entity="SAMLConfiguration" action="updateSAMLConfiguration"> {isAuthorized("SAMLConfiguration", "updateSAMLConfiguration") && (
<Button <Button
variant="secondary" variant="secondary"
onClick={() => handleOpenModal(config)} onClick={() => handleOpenModal(config)}
> >
{__("Edit")} {__("Edit")}
</Button> </Button>
</Authorized> )}
</> </>
) : ( ) : (
<> <>
<Authorized entity="Organization" action="verifyDomain"> {isAuthorized("Organization", "verifyDomain") && (
<Button <Button
variant="primary" variant="primary"
onClick={() => handleOpenModal(config)} onClick={() => handleOpenModal(config)}
> >
{__("Verify Domain")} {__("Verify Domain")}
</Button> </Button>
</Authorized> )}
<Authorized entity="Organization" action="deleteOrganization"> {isAuthorized("Organization", "deleteOrganization") && (
<Button <Button
variant="danger" variant="danger"
onClick={() => handleDelete(config)} onClick={() => handleDelete(config)}
> >
{__("Delete")} {__("Delete")}
</Button> </Button>
</Authorized> )}
</> </>
)} )}
</div> </div>

View File

@@ -34,8 +34,8 @@ import type { NodeOf } from "/types";
import SnapshotFormDialog from "./dialog/SnapshotFormDialog"; import SnapshotFormDialog from "./dialog/SnapshotFormDialog";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { isAuthorized } from "/permissions"; import { use } from "react";
type Props = { type Props = {
queryRef: PreloadedQuery<SnapshotGraphListQuery>; queryRef: PreloadedQuery<SnapshotGraphListQuery>;
@@ -71,9 +71,10 @@ export default function SnapshotsPage(props: Props) {
); );
const connectionId = data.snapshots.__id; const connectionId = data.snapshots.__id;
const snapshots = data.snapshots.edges.map((edge) => edge.node); const snapshots = data.snapshots.edges.map((edge) => edge.node);
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Snapshots")); usePageTitle(__("Snapshots"));
const hasAnyAction = isAuthorized(organizationId, "Snapshot", "deleteSnapshot"); const hasAnyAction = isAuthorized("Snapshot", "deleteSnapshot");
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -83,13 +84,13 @@ export default function SnapshotsPage(props: Props) {
"Snapshots capture point-in-time views of your organization's compliance state. Create snapshots to track progress over time." "Snapshots capture point-in-time views of your organization's compliance state. Create snapshots to track progress over time."
)} )}
> >
<Authorized entity="Organization" action="createSnapshot"> {isAuthorized("Organization", "createSnapshot") && (
<SnapshotFormDialog connection={connectionId}> <SnapshotFormDialog connection={connectionId}>
<Button variant="primary" icon={IconPlusLarge}> <Button variant="primary" icon={IconPlusLarge}>
{__("New snapshot")} {__("New snapshot")}
</Button> </Button>
</SnapshotFormDialog> </SnapshotFormDialog>
</Authorized> )}
</PageHeader> </PageHeader>
{snapshots.length > 0 ? ( {snapshots.length > 0 ? (
@@ -139,7 +140,7 @@ type SnapshotRowProps = {
function SnapshotRow(props: SnapshotRowProps) { function SnapshotRow(props: SnapshotRowProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const deleteSnapshot = useDeleteSnapshot(props.snapshot, props.connectionId); const deleteSnapshot = useDeleteSnapshot(props.snapshot, props.connectionId);
const { isAuthorized } = use(PermissionsContext);
const typePath = getSnapshotTypeUrlPath(props.snapshot.type); const typePath = getSnapshotTypeUrlPath(props.snapshot.type);
return ( return (
@@ -159,7 +160,7 @@ function SnapshotRow(props: SnapshotRowProps) {
{props.hasAnyAction && ( {props.hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Snapshot" action="deleteSnapshot"> {isAuthorized("Snapshot", "deleteSnapshot") && (
<DropdownItem <DropdownItem
onClick={deleteSnapshot} onClick={deleteSnapshot}
variant="danger" variant="danger"
@@ -167,7 +168,7 @@ function SnapshotRow(props: SnapshotRowProps) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -12,7 +12,8 @@ import { tasksQuery } from "/hooks/graph/TaskGraph";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import TasksCard from "/components/tasks/TasksCard"; import TasksCard from "/components/tasks/TasksCard";
import TaskFormDialog from "/components/tasks/TaskFormDialog"; import TaskFormDialog from "/components/tasks/TaskFormDialog";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const tasksFragment = graphql` const tasksFragment = graphql`
fragment TasksPageFragment on Organization fragment TasksPageFragment on Organization
@@ -66,7 +67,7 @@ export default function TasksPage({ queryRef }: Props) {
); );
const tasks = data.tasks?.edges.map((edge) => edge.node); const tasks = data.tasks?.edges.map((edge) => edge.node);
const connectionId = data.tasks.__id; const connectionId = data.tasks.__id;
const { isAuthorized } = use(PermissionsContext);
usePageTitle(__("Tasks")); usePageTitle(__("Tasks"));
return ( return (
@@ -77,11 +78,11 @@ export default function TasksPage({ queryRef }: Props) {
"Track your assigned compliance tasks and keep progress on track." "Track your assigned compliance tasks and keep progress on track."
)} )}
> >
<Authorized entity="Organization" action="createTask"> {isAuthorized("Organization", "createTask") && (
<TaskFormDialog connection={connectionId}> <TaskFormDialog connection={connectionId}>
<Button icon={IconPlusLarge}>{__("New task")}</Button> <Button icon={IconPlusLarge}>{__("New task")}</Button>
</TaskFormDialog> </TaskFormDialog>
</Authorized> )}
</PageHeader> </PageHeader>
<TasksCard connectionId={connectionId} tasks={tasks ?? []} /> <TasksCard connectionId={connectionId} tasks={tasks ?? []} />
</div> </div>

View File

@@ -24,7 +24,7 @@ import {
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { formatDate } from "@probo/helpers"; import { formatDate } from "@probo/helpers";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef, use } from "react";
import { useQueryLoader, usePreloadedQuery } from 'react-relay'; import { useQueryLoader, usePreloadedQuery } from 'react-relay';
import z from "zod"; import z from "zod";
import { import {
@@ -36,7 +36,7 @@ import {
} from "/hooks/graph/TrustCenterAccessGraph"; } from "/hooks/graph/TrustCenterAccessGraph";
import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
type ContextType = { type ContextType = {
organization: { organization: {
@@ -130,7 +130,7 @@ function DocumentAccessesLoader({
export default function TrustCenterAccessTab() { export default function TrustCenterAccessTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { organization } = useOutletContext<ContextType>(); const { organization } = useOutletContext<ContextType>();
const { isAuthorized } = use(PermissionsContext);
const inviteSchema = z.object({ const inviteSchema = z.object({
name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")), name: z.string().min(1, __("Name is required")).min(2, __("Name must be at least 2 characters long")),
email: z.string().min(1, __("Email is required")).email(__("Please enter a valid email address")), email: z.string().min(1, __("Email is required")).email(__("Please enter a valid email address")),
@@ -423,14 +423,14 @@ export default function TrustCenterAccessTab() {
</p> </p>
</div> </div>
{organization.trustCenter?.id && ( {organization.trustCenter?.id && (
<Authorized entity="TrustCenter" action="createTrustCenterAccess"> {isAuthorized("TrustCenter", "createTrustCenterAccess") && (
<Button icon={IconPlusLarge} onClick={() => { <Button icon={IconPlusLarge} onClick={() => {
inviteForm.reset(); inviteForm.reset();
dialogRef.current?.open(); dialogRef.current?.open();
}}> }}>
{__("Add Access")} {__("Add Access")}
</Button> </Button>
</Authorized> )}
)} )}
</div> </div>
@@ -515,22 +515,22 @@ export default function TrustCenterAccessTab() {
className="flex gap-2 justify-end" className="flex gap-2 justify-end"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<Authorized entity="TrustCenterAccess" action="updateTrustCenterAccess"> {isAuthorized("TrustCenterAccess", "updateTrustCenterAccess") && (
<Button <Button
variant="secondary" variant="secondary"
onClick={() => handleEditAccess(access)} onClick={() => handleEditAccess(access)}
disabled={isUpdating} disabled={isUpdating}
icon={IconPencil} icon={IconPencil}
/> />
</Authorized> )}
<Authorized entity="TrustCenterAccess" action="deleteTrustCenterAccess"> {isAuthorized("TrustCenterAccess", "deleteTrustCenterAccess") && (
<Button <Button
variant="danger" variant="danger"
onClick={() => handleDelete(access.id)} onClick={() => handleDelete(access.id)}
disabled={isDeleting} disabled={isDeleting}
icon={IconTrashCan} icon={IconTrashCan}
/> />
</Authorized> )}
</div> </div>
</Td> </Td>
</Tr> </Tr>

View File

@@ -13,7 +13,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { useState, useCallback } from "react"; import { useState, useCallback, use } from "react";
import z from "zod"; import z from "zod";
import { getTrustCenterVisibilityOptions } from "@probo/helpers"; import { getTrustCenterVisibilityOptions } from "@probo/helpers";
import { import {
@@ -23,8 +23,8 @@ import {
} from "/hooks/graph/TrustCenterFileGraph"; } from "/hooks/graph/TrustCenterFileGraph";
import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { TrustCenterFilesCard } from "/components/trustCenter/TrustCenterFilesCard"; import { TrustCenterFilesCard } from "/components/trustCenter/TrustCenterFilesCard";
import { Authorized } from "/permissions";
import type { TrustCenterFilesCardFragment$key } from "/components/trustCenter/__generated__/TrustCenterFilesCardFragment.graphql"; import type { TrustCenterFilesCardFragment$key } from "/components/trustCenter/__generated__/TrustCenterFilesCardFragment.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
type ContextType = { type ContextType = {
organization: { organization: {
@@ -41,7 +41,7 @@ type ContextType = {
export default function TrustCenterFilesTab() { export default function TrustCenterFilesTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { organization } = useOutletContext<ContextType>(); const { organization } = useOutletContext<ContextType>();
const { isAuthorized } = use(PermissionsContext);
const createSchema = z.object({ const createSchema = z.object({
name: z.string().min(1, __("Name is required")), name: z.string().min(1, __("Name is required")),
category: z.string().min(1, __("Category is required")), category: z.string().min(1, __("Category is required")),
@@ -201,11 +201,11 @@ export default function TrustCenterFilesTab() {
{__("Upload and manage files for your trust center")} {__("Upload and manage files for your trust center")}
</p> </p>
</div> </div>
<Authorized entity="Organization" action="createTrustCenterFile"> {isAuthorized("Organization", "createTrustCenterFile") && (
<Button icon={IconPlusLarge} onClick={() => createDialogRef.current?.open()}> <Button icon={IconPlusLarge} onClick={() => createDialogRef.current?.open()}>
{__("Add File")} {__("Add File")}
</Button> </Button>
</Authorized> )}
</div> </div>
{(isUpdating || isDeleting) && ( {(isUpdating || isDeleting) && (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">

View File

@@ -11,10 +11,9 @@ import {
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { useUpdateTrustCenterMutation, useUploadTrustCenterNDAMutation, useDeleteTrustCenterNDAMutation } from "/hooks/graph/TrustCenterGraph"; import { useUpdateTrustCenterMutation, useUploadTrustCenterNDAMutation, useDeleteTrustCenterNDAMutation } from "/hooks/graph/TrustCenterGraph";
import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql"; import type { TrustCenterGraphQuery$data } from "/hooks/graph/__generated__/TrustCenterGraphQuery.graphql";
import { useState } from "react"; import { use, useState } from "react";
import { SlackConnections } from "../../../components/organizations/SlackConnection"; import { SlackConnections } from "../../../components/organizations/SlackConnection";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
import { useParams } from "react-router";
type ContextType = { type ContextType = {
organization: TrustCenterGraphQuery$data["organization"]; organization: TrustCenterGraphQuery$data["organization"];
@@ -24,14 +23,14 @@ export default function TrustCenterOverviewTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const { organization } = useOutletContext<ContextType>(); const { organization } = useOutletContext<ContextType>();
const { organizationId } = useParams(); const { isAuthorized } = use(PermissionsContext);
const [updateTrustCenter, isUpdating] = useUpdateTrustCenterMutation(); const [updateTrustCenter, isUpdating] = useUpdateTrustCenterMutation();
const [uploadNDA, isUploadingNDA] = useUploadTrustCenterNDAMutation(); const [uploadNDA, isUploadingNDA] = useUploadTrustCenterNDAMutation();
const [deleteNDA, isDeletingNDA] = useDeleteTrustCenterNDAMutation(); const [deleteNDA, isDeletingNDA] = useDeleteTrustCenterNDAMutation();
const [isActive, setIsActive] = useState(organization.trustCenter?.active || false); const [isActive, setIsActive] = useState(organization.trustCenter?.active || false);
const canUpdateTrustCenter = organizationId ? isAuthorized(organizationId, "TrustCenter", "updateTrustCenter") : false; const canUpdateTrustCenter = isAuthorized("TrustCenter", "updateTrustCenter");
const handleToggleActive = async (active: boolean) => { const handleToggleActive = async (active: boolean) => {
if (!organization.trustCenter?.id) { if (!organization.trustCenter?.id) {

View File

@@ -29,7 +29,8 @@ import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog";
import { complianceReportsFragment } from "./tabs/VendorComplianceTab"; import { complianceReportsFragment } from "./tabs/VendorComplianceTab";
import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql"; import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions"; import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
type Props = { type Props = {
queryRef: PreloadedQuery<VendorGraphNodeQuery>; queryRef: PreloadedQuery<VendorGraphNodeQuery>;
@@ -42,6 +43,7 @@ export default function VendorDetailPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
if (!vendor) { if (!vendor) {
return <div>{__("Vendor not found")}</div>; return <div>{__("Vendor not found")}</div>;
@@ -98,7 +100,7 @@ export default function VendorDetailPage(props: Props) {
{__("Assessment From Website")} {__("Assessment From Website")}
</Button> </Button>
</ImportAssessmentDialog> </ImportAssessmentDialog>
<Authorized entity="Vendor" action="deleteVendor"> {isAuthorized("Vendor", "deleteVendor") && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem
variant="danger" variant="danger"
@@ -108,7 +110,7 @@ export default function VendorDetailPage(props: Props) {
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
</Authorized> )}
</div> </div>
)} )}
</div> </div>

View File

@@ -37,8 +37,8 @@ import type {
} from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql"; } from "/hooks/graph/__generated__/VendorGraphPaginatedFragment.graphql";
import { SortableTable, SortableTh } from "/components/SortableTable"; import { SortableTable, SortableTh } from "/components/SortableTable";
import { SnapshotBanner } from "/components/SnapshotBanner"; import { SnapshotBanner } from "/components/SnapshotBanner";
import { Authorized } from "/permissions"; import { use } from "react";
import { isAuthorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>; type Vendor = NodeOf<VendorGraphPaginatedFragment$data["vendors"]>;
@@ -51,6 +51,7 @@ export default function VendorsPage(props: Props) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const data = usePreloadedQuery(vendorsQuery, props.queryRef); const data = usePreloadedQuery(vendorsQuery, props.queryRef);
const pagination = usePaginationFragment( const pagination = usePaginationFragment(
@@ -64,8 +65,8 @@ export default function VendorsPage(props: Props) {
usePageTitle(__("Vendors")); usePageTitle(__("Vendors"));
const hasAnyAction = !isSnapshotMode && ( const hasAnyAction = !isSnapshotMode && (
isAuthorized(organizationId, "Vendor", "updateVendor") || isAuthorized("Vendor", "updateVendor") ||
isAuthorized(organizationId, "Vendor", "deleteVendor") isAuthorized("Vendor", "deleteVendor")
); );
return ( return (
@@ -78,14 +79,14 @@ export default function VendorsPage(props: Props) {
)} )}
> >
{!isSnapshotMode && ( {!isSnapshotMode && (
<Authorized entity="Organization" action="createVendor"> isAuthorized("Organization", "createVendor") && (
<CreateVendorDialog <CreateVendorDialog
connection={connectionId} connection={connectionId}
organizationId={organizationId} organizationId={organizationId}
> >
<Button icon={IconPlusLarge}>{__("Add vendor")}</Button> <Button icon={IconPlusLarge}>{__("Add vendor")}</Button>
</CreateVendorDialog> </CreateVendorDialog>
</Authorized> )
)} )}
</PageHeader> </PageHeader>
<SortableTable {...pagination}> <SortableTable {...pagination}>
@@ -129,7 +130,7 @@ function VendorRow({
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { __ } = useTranslate(); const { __ } = useTranslate();
const latestAssessment = vendor.riskAssessments?.edges[0]?.node; const latestAssessment = vendor.riskAssessments?.edges[0]?.node;
const { isAuthorized } = use(PermissionsContext);
const deleteVendor = useDeleteVendor(vendor, connectionId); const deleteVendor = useDeleteVendor(vendor, connectionId);
const vendorUrl = isSnapshotMode && snapshotId const vendorUrl = isSnapshotMode && snapshotId
@@ -159,7 +160,7 @@ function VendorRow({
{hasAnyAction && ( {hasAnyAction && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<Authorized entity="Vendor" action="deleteVendor"> {isAuthorized("Vendor", "deleteVendor") && (
<DropdownItem <DropdownItem
onClick={deleteVendor} onClick={deleteVendor}
variant="danger" variant="danger"
@@ -167,7 +168,7 @@ function VendorRow({
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
</Authorized> )}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -6,7 +6,7 @@ import { PeopleSelectField } from "/components/form/PeopleSelectField";
import { ControlledField } from "/components/form/ControlledField"; import { ControlledField } from "/components/form/ControlledField";
import { CountriesField } from "/components/form/CountriesField"; import { CountriesField } from "/components/form/CountriesField";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { useMemo } from "react"; import { use, useMemo } from "react";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { downloadFile, formatDate } from "@probo/helpers"; import { downloadFile, formatDate } from "@probo/helpers";
import { useFragment, graphql } from "react-relay"; import { useFragment, graphql } from "react-relay";
@@ -20,7 +20,7 @@ import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVe
import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "./__generated__/VendorOverviewTabBusinessAssociateAgreementFragment.graphql"; import type { VendorOverviewTabBusinessAssociateAgreementFragment$key } from "./__generated__/VendorOverviewTabBusinessAssociateAgreementFragment.graphql";
import type { VendorOverviewTabDataPrivacyAgreementFragment$key } from "./__generated__/VendorOverviewTabDataPrivacyAgreementFragment.graphql"; import type { VendorOverviewTabDataPrivacyAgreementFragment$key } from "./__generated__/VendorOverviewTabDataPrivacyAgreementFragment.graphql";
import type { VendorCategory } from "@probo/vendors"; import type { VendorCategory } from "@probo/vendors";
import { Authorized } from "/permissions"; import { PermissionsContext } from "/providers/PermissionsContext";
const vendorBusinessAssociateAgreementFragment = graphql` const vendorBusinessAssociateAgreementFragment = graphql`
fragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor { fragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {
@@ -58,7 +58,7 @@ export default function VendorOverviewTab() {
}>(); }>();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const vendorCategories: { value: VendorCategory; label: string }[] = [ const vendorCategories: { value: VendorCategory; label: string }[] = [
{ value: "ANALYTICS", label: __("Analytics") }, { value: "ANALYTICS", label: __("Analytics") },
{ value: "CLOUD_MONITORING", label: __("Cloud Monitoring") }, { value: "CLOUD_MONITORING", label: __("Cloud Monitoring") },
@@ -396,11 +396,11 @@ export default function VendorOverviewTab() {
{/* Submit */} {/* Submit */}
{!isSnapshotMode && ( {!isSnapshotMode && (
<div className="flex justify-end"> <div className="flex justify-end">
<Authorized entity="Vendor" action="updateVendor"> {isAuthorized("Vendor", "updateVendor") && (
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
{__("Update vendor")} {__("Update vendor")}
</Button> </Button>
</Authorized> )}
</div> </div>
)} )}
</form> </form>

View File

@@ -1,48 +0,0 @@
import { type ReactNode } from "react";
import { useParams } from "react-router";
import { usePermissions } from "./permissions";
type Props = {
entity: string;
action: string;
children: ReactNode;
fallback?: ReactNode;
};
/**
* Conditionally render children based on authorization check
* Automatically fetches permissions if not cached
*
* @param entity - The entity name (e.g., "Document", "Organization", "Vendor")
* @param action - The action/field name (e.g., "get", "createDocument", "updateVendor")
*
* @example
* <Authorized entity="Organization" action="createDocument">
* <CreateButton />
* </Authorized>
*
* @example
* <Authorized entity="Document" action="get">
* <DocumentViewer />
* </Authorized>
*
* @example
* <Authorized entity="Vendor" action="updateVendor">
* <EditButton />
* </Authorized>
*/
export function Authorized({
entity,
action,
children,
fallback = null,
}: Props) {
const { organizationId } = useParams();
const { loading, error, isAuthorized } = usePermissions(organizationId || "");
if (!organizationId || loading || error || !isAuthorized(entity, action)) {
return fallback;
}
return children;
}

View File

@@ -1,3 +0,0 @@
export { isAuthorized, getUserRole, getAssignableRoles } from "./permissions";
export { Authorized } from "./Authorized";
export type { EntityPermissions } from "./permissions";

View File

@@ -1,211 +0,0 @@
import { useEffect, useState } from 'react';
// Authorization system that checks permissions from the backend
// Permissions are fetched per organization from /authz/:organizationId/permissions
// Format: { permissions: { "Document": { "node": true, "updateDocument": true }, "Organization": { "createDocument": true } }, role: "ADMIN" }
export type EntityPermissions = Record<string, Record<string, boolean>>;
type PermissionsResponse = {
permissions: EntityPermissions;
role: string;
};
type PermissionsCache = {
[organizationId: string]: {
permissions: EntityPermissions;
role: string;
};
};
const cache: PermissionsCache = {};
const pendingRequests: Map<string, Promise<PermissionsResponse>> = new Map();
/**
* Fetch permissions for the current user's role in the organization
*/
function fetchPermissions(organizationId: string): Promise<PermissionsResponse> {
if (cache[organizationId]) {
return Promise.resolve(cache[organizationId]);
}
const pending = pendingRequests.get(organizationId);
if (pending) {
return pending;
}
const promise = fetch(`/authz/${encodeURIComponent(organizationId)}/permissions`, {
credentials: 'include',
})
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch permissions: ${response.statusText}`);
}
return response.json();
})
.then((data: PermissionsResponse) => {
if (!data || typeof data.permissions !== 'object' || !data.role) {
throw new Error('Invalid permissions response structure');
}
cache[organizationId] = {
permissions: data.permissions,
role: data.role,
};
pendingRequests.delete(organizationId);
return data;
})
.catch((error) => {
pendingRequests.delete(organizationId);
throw error;
});
pendingRequests.set(organizationId, promise);
return promise;
}
/**
* React hook to fetch and manage permissions for an organization
*
* @param organizationId - The organization ID
* @returns Object with loading, error, permissions, role, and helper functions
*
* @example
* const { loading, error, permissions, role, isAuthorized, getAssignableRoles } = usePermissions(orgId);
*
* if (loading) return <Spinner />;
* if (error) return <ErrorMessage />;
* if (isAuthorized("Document", "updateDocument")) { ... }
*/
export function usePermissions(organizationId: string) {
const [state, setState] = useState<{
loading: boolean;
error: Error | null;
permissions: EntityPermissions | null;
role: string | null;
}>({
loading: true,
error: null,
permissions: null,
role: null,
});
useEffect(() => {
setState({ loading: true, error: null, permissions: null, role: null });
fetchPermissions(organizationId)
.then((data) => {
setState({
loading: false,
error: null,
permissions: data.permissions,
role: data.role,
});
})
.catch((error) => {
setState({
loading: false,
error,
permissions: null,
role: null,
});
});
}, [organizationId]);
const checkAuthorized = (entity: string, action: string): boolean => {
if (!state.permissions) return false;
const entityPermissions = state.permissions[entity];
if (!entityPermissions) return false;
return entityPermissions[action] === true;
};
const getAssignableRolesList = (): string[] => {
if (!state.role) return [];
if (state.role === "OWNER" || state.role === "FULL") {
return ["OWNER", "ADMIN", "VIEWER"];
}
if (state.role === "ADMIN") {
return ["ADMIN", "VIEWER"];
}
return [];
};
return {
loading: state.loading,
error: state.error,
permissions: state.permissions,
role: state.role,
isAuthorized: checkAuthorized,
getAssignableRoles: getAssignableRolesList,
};
}
/**
* Check if the user has permission for an entity and action (synchronous)
* Returns false if permissions are not loaded yet
*
* @param organizationId - The organization ID
* @param entity - The entity name (e.g., "Document", "Organization", "Vendor")
* @param action - The action/field name (e.g., "node", "updateDocument", "createDocument")
* @returns true if the user has permission, false otherwise
*
* @example
* isAuthorized(orgId, "Document", "get")
* isAuthorized(orgId, "Document", "updateDocument")
* isAuthorized(orgId, "Organization", "createDocument")
*/
export function isAuthorized(
organizationId: string,
entity: string,
action: string
): boolean {
const cached = cache[organizationId];
if (!cached) return false;
const entityPermissions = cached.permissions[entity];
if (!entityPermissions) return false;
return entityPermissions[action] === true;
}
/**
* Get the current user's role in the organization
* Returns empty string if not loaded yet
*
* @param organizationId - The organization ID
* @returns The user's role (e.g., "OWNER", "ADMIN", "VIEWER", "FULL")
*
* @example
* getUserRole(orgId) // Returns "ADMIN"
*/
export function getUserRole(organizationId: string): string {
const cached = cache[organizationId];
return cached?.role || "";
}
/**
* Get available roles that the current user can assign
* Based on the rule: OWNER and FULL can assign any role, ADMIN can assign ADMIN and VIEWER but not OWNER
*
* @param organizationId - The organization ID
* @returns Array of roles that can be assigned
*/
export function getAssignableRoles(organizationId: string): string[] {
const currentRole = getUserRole(organizationId);
if (!currentRole) return [];
if (currentRole === "OWNER" || currentRole === "FULL") {
return ["OWNER", "ADMIN", "VIEWER"];
}
if (currentRole === "ADMIN") {
return ["ADMIN", "VIEWER"];
}
return [];
}

View File

@@ -0,0 +1,17 @@
import { createContext } from "react";
import { Role } from "@probo/helpers";
export type PermissionsResponse = {
permissions: Record<string, Record<string, boolean>>;
role: Role;
};
type PermissionsContextType = {
isAuthorized: (entity: string, action: string) => boolean;
} & PermissionsResponse;
export const PermissionsContext = createContext<PermissionsContextType>({
permissions: {},
role: Role.VIEWER,
isAuthorized: () => false,
});

View File

@@ -1,27 +1,7 @@
import { useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query";
import { createContext, type PropsWithChildren } from "react"; import { type PropsWithChildren } from "react";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { PermissionsContext, type PermissionsResponse } from "./PermissionsContext";
enum Role {
OWNER = "OWNER",
ADMIN = "ADMIN",
VIEWER = "VIEWER",
}
type PermissionsResponse = {
permissions: Record<string, Record<string, boolean>>;
role: Role;
};
type PermissionsContextType = {
isAuthorized: (entity: string, action: string) => boolean;
} & PermissionsResponse;
export const PermissionsContext = createContext<PermissionsContextType>({
permissions: {},
role: Role.VIEWER,
isAuthorized: () => false,
});
export function PermissionsProvider(props: PropsWithChildren) { export function PermissionsProvider(props: PropsWithChildren) {
const { children } = props; const { children } = props;

View File

@@ -65,3 +65,4 @@ export { fileType, fileSize } from "./file";
export { formatDatetime, formatDate } from "./date"; export { formatDatetime, formatDate } from "./date";
export { getLogoUrl, getTrustCenterUrl } from "./trustCenter"; export { getLogoUrl, getTrustCenterUrl } from "./trustCenter";
export { formatError, type GraphQLError } from "./error"; export { formatError, type GraphQLError } from "./error";
export { Role, getAssignableRoles } from "./roles";

View File

@@ -0,0 +1,19 @@
export enum Role {
OWNER = "OWNER",
ADMIN = "ADMIN",
VIEWER = "VIEWER",
}
export function getAssignableRoles(currentRole: Role): string[] {
if (!currentRole) return [];
if (currentRole === "OWNER") {
return ["OWNER", "ADMIN", "VIEWER"];
}
if (currentRole === "ADMIN") {
return ["ADMIN", "VIEWER"];
}
return [];
}