Add auditor role

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-12-10 14:41:54 +01:00
parent 9e5a775f66
commit f77676c88c
45 changed files with 1256 additions and 796 deletions

View File

@@ -55,6 +55,7 @@ type Props<Params> = {
onAttach: Mutation<Params>; onAttach: Mutation<Params>;
onDetach: Mutation<Params>; onDetach: Mutation<Params>;
variant?: "card" | "table"; variant?: "card" | "table";
readOnly?: boolean;
}; };
export function LinkedAuditsCard<Params>(props: Props<Params>) { export function LinkedAuditsCard<Params>(props: Props<Params>) {
@@ -97,6 +98,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Audits")}</div> <div className="text-lg font-semibold">{__("Audits")}</div>
{!props.readOnly && (
<LinkedAuditsDialog <LinkedAuditsDialog
disabled={props.disabled} disabled={props.disabled}
linkedAudits={props.audits} linkedAudits={props.audits}
@@ -107,6 +109,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
{__("Link audit")} {__("Link audit")}
</Button> </Button>
</LinkedAuditsDialog> </LinkedAuditsDialog>
)}
</div> </div>
)} )}
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
@@ -114,13 +117,13 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{__("Name")}</Th>
<Th>{__("State")}</Th> <Th>{__("State")}</Th>
<Th></Th> {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{audits.length === 0 && ( {audits.length === 0 && (
<Tr> <Tr>
<Td colSpan={3} className="text-center text-txt-secondary"> <Td colSpan={props.readOnly ? 2 : 3} className="text-center text-txt-secondary">
{__("No audits linked")} {__("No audits linked")}
</Td> </Td>
</Tr> </Tr>
@@ -130,9 +133,10 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
key={audit.id} key={audit.id}
audit={audit} audit={audit}
onClick={onDetach} onClick={onDetach}
readOnly={props.readOnly}
/> />
))} ))}
{variant === "table" && ( {variant === "table" && !props.readOnly && (
<LinkedAuditsDialog <LinkedAuditsDialog
disabled={props.disabled} disabled={props.disabled}
linkedAudits={props.audits} linkedAudits={props.audits}
@@ -163,6 +167,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
function AuditRow(props: { function AuditRow(props: {
audit: LinkedAuditsCardFragment$key & { id: string }; audit: LinkedAuditsCardFragment$key & { id: string };
onClick: (auditId: string) => void; onClick: (auditId: string) => void;
readOnly?: boolean;
}) { }) {
const audit = useFragment(linkedAuditFragment, props.audit); const audit = useFragment(linkedAuditFragment, props.audit);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
@@ -187,6 +192,7 @@ function AuditRow(props: {
{audit.state.replace(/_/g, " ")} {audit.state.replace(/_/g, " ")}
</Badge> </Badge>
</Td> </Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<Button <Button
variant="secondary" variant="secondary"
@@ -196,6 +202,7 @@ function AuditRow(props: {
{__("Unlink")} {__("Unlink")}
</Button> </Button>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -54,6 +54,7 @@ type Props<Params> = {
onAttach?: Mutation<Params>; onAttach?: Mutation<Params>;
// Allow sorting in the table // Allow sorting in the table
refetch: ComponentProps<typeof SortableTable>["refetch"]; refetch: ComponentProps<typeof SortableTable>["refetch"];
readOnly?: boolean;
}; };
/** /**
@@ -96,13 +97,13 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
<Tr> <Tr>
<SortableTh field="SECTION_TITLE">{__("Reference")}</SortableTh> <SortableTh field="SECTION_TITLE">{__("Reference")}</SortableTh>
<Th>{__("Name")}</Th> <Th>{__("Name")}</Th>
<Th></Th> {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{controls.length === 0 && ( {controls.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="text-center text-txt-secondary"> <Td colSpan={props.readOnly ? 2 : 3} className="text-center text-txt-secondary">
{__("No controls linked")} {__("No controls linked")}
</Td> </Td>
</Tr> </Tr>
@@ -113,8 +114,10 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
control={control} control={control}
onClick={onDetach} onClick={onDetach}
onAttach={onAttach} onAttach={onAttach}
readOnly={props.readOnly}
/> />
))} ))}
{!props.readOnly && (
<LinkedControlsDialog <LinkedControlsDialog
connectionId={props.connectionId} connectionId={props.connectionId}
disabled={props.disabled} disabled={props.disabled}
@@ -124,6 +127,7 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
> >
<TrButton colspan={3}>{__("Link control")}</TrButton> <TrButton colspan={3}>{__("Link control")}</TrButton>
</LinkedControlsDialog> </LinkedControlsDialog>
)}
</Tbody> </Tbody>
</SortableTable> </SortableTable>
); );
@@ -133,6 +137,7 @@ function ControlRow(props: {
control: LinkedControlsCardFragment$key & { id: string }; control: LinkedControlsCardFragment$key & { id: string };
onClick: (controlId: string) => void; onClick: (controlId: string) => void;
onAttach?: (controlId: string) => void; onAttach?: (controlId: string) => void;
readOnly?: boolean;
}) { }) {
const control = useFragment(linkedControlFragment, props.control); const control = useFragment(linkedControlFragment, props.control);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
@@ -149,6 +154,7 @@ function ControlRow(props: {
</span> </span>
</Td> </Td>
<Td>{control.name}</Td> <Td>{control.name}</Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<Button <Button
variant="secondary" variant="secondary"
@@ -158,6 +164,7 @@ function ControlRow(props: {
{__("Unlink")} {__("Unlink")}
</Button> </Button>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -64,6 +64,7 @@ type Props<Params> = {
// Mutation to detach a document (will receive {documentId, ...params}) // Mutation to detach a document (will receive {documentId, ...params})
onDetach: Mutation<Params>; onDetach: Mutation<Params>;
variant?: "card" | "table"; variant?: "card" | "table";
readOnly?: boolean;
}; };
/** /**
@@ -109,6 +110,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Documents")}</div> <div className="text-lg font-semibold">{__("Documents")}</div>
{!props.readOnly && (
<LinkedDocumentDialog <LinkedDocumentDialog
connectionId={props.connectionId} connectionId={props.connectionId}
disabled={props.disabled} disabled={props.disabled}
@@ -120,6 +122,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
{__("Link document")} {__("Link document")}
</Button> </Button>
</LinkedDocumentDialog> </LinkedDocumentDialog>
)}
</div> </div>
)} )}
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
@@ -128,13 +131,13 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
<Th>{__("Name")}</Th> <Th>{__("Name")}</Th>
<Th>{__("Type")}</Th> <Th>{__("Type")}</Th>
<Th>{__("State")}</Th> <Th>{__("State")}</Th>
<Th></Th> {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{documents.length === 0 && ( {documents.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="text-center text-txt-secondary"> <Td colSpan={props.readOnly ? 3 : 4} className="text-center text-txt-secondary">
{__("No documents linked")} {__("No documents linked")}
</Td> </Td>
</Tr> </Tr>
@@ -144,9 +147,10 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
key={document.id} key={document.id}
document={document} document={document}
onClick={onDetach} onClick={onDetach}
readOnly={props.readOnly}
/> />
))} ))}
{variant === "table" && ( {variant === "table" && !props.readOnly && (
<LinkedDocumentDialog <LinkedDocumentDialog
connectionId={props.connectionId} connectionId={props.connectionId}
disabled={props.disabled} disabled={props.disabled}
@@ -178,6 +182,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
function DocumentRow(props: { function DocumentRow(props: {
document: LinkedDocumentsCardFragment$key & { id: string }; document: LinkedDocumentsCardFragment$key & { id: string };
onClick: (documentId: string) => void; onClick: (documentId: string) => void;
readOnly?: boolean;
}) { }) {
const document = useFragment(linkedDocumentFragment, props.document); const document = useFragment(linkedDocumentFragment, props.document);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
@@ -203,6 +208,7 @@ function DocumentRow(props: {
<Td> <Td>
<DocumentVersionBadge state={document.versions.edges[0].node.status} /> <DocumentVersionBadge state={document.versions.edges[0].node.status} />
</Td> </Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<Button <Button
variant="secondary" variant="secondary"
@@ -212,6 +218,7 @@ function DocumentRow(props: {
{__("Unlink")} {__("Unlink")}
</Button> </Button>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -55,6 +55,7 @@ type Props<Params> = {
// Mutation to detach a measure (will receive {measureId, ...params}) // Mutation to detach a measure (will receive {measureId, ...params})
onDetach: Mutation<Params>; onDetach: Mutation<Params>;
variant?: "card" | "table"; variant?: "card" | "table";
readOnly?: boolean;
}; };
/** /**
@@ -102,6 +103,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Measures")}</div> <div className="text-lg font-semibold">{__("Measures")}</div>
{!props.readOnly && (
<LinkedMeasureDialog <LinkedMeasureDialog
connectionId={props.connectionId} connectionId={props.connectionId}
disabled={props.disabled} disabled={props.disabled}
@@ -113,6 +115,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
{__("Link measure")} {__("Link measure")}
</Button> </Button>
</LinkedMeasureDialog> </LinkedMeasureDialog>
)}
</div> </div>
)} )}
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
@@ -120,21 +123,21 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{__("Name")}</Th>
<Th>{__("State")}</Th> <Th>{__("State")}</Th>
<Th></Th> {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{measures.length === 0 && ( {measures.length === 0 && (
<Tr> <Tr>
<Td colSpan={3} className="text-center text-txt-secondary"> <Td colSpan={props.readOnly ? 2 : 3} className="text-center text-txt-secondary">
{__("No measures linked")} {__("No measures linked")}
</Td> </Td>
</Tr> </Tr>
)} )}
{measures.map((measure) => ( {measures.map((measure) => (
<MeasureRow key={measure.id} measure={measure} onClick={onDetach} /> <MeasureRow key={measure.id} measure={measure} onClick={onDetach} readOnly={props.readOnly} />
))} ))}
{variant === "table" && ( {variant === "table" && !props.readOnly && (
<LinkedMeasureDialog <LinkedMeasureDialog
connectionId={props.connectionId} connectionId={props.connectionId}
disabled={props.disabled} disabled={props.disabled}
@@ -166,6 +169,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
function MeasureRow(props: { function MeasureRow(props: {
measure: LinkedMeasuresCardFragment$key & { id: string }; measure: LinkedMeasuresCardFragment$key & { id: string };
onClick: (measureId: string) => void; onClick: (measureId: string) => void;
readOnly?: boolean;
}) { }) {
const measure = useFragment(linkedMeasureFragment, props.measure); const measure = useFragment(linkedMeasureFragment, props.measure);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
@@ -177,6 +181,7 @@ function MeasureRow(props: {
<Td> <Td>
<MeasureBadge state={measure.state} /> <MeasureBadge state={measure.state} />
</Td> </Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<Button <Button
variant="secondary" variant="secondary"
@@ -186,6 +191,7 @@ function MeasureRow(props: {
{__("Unlink")} {__("Unlink")}
</Button> </Button>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -51,6 +51,7 @@ type Props<Params> = {
connectionId: string; connectionId: string;
disabled?: boolean; disabled?: boolean;
variant?: "card" | "table"; variant?: "card" | "table";
readOnly?: boolean;
params: Params; params: Params;
@@ -102,6 +103,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Obligations")}</div> <div className="text-lg font-semibold">{__("Obligations")}</div>
{!props.readOnly && (
<LinkedObligationDialog <LinkedObligationDialog
connectionId={props.connectionId} connectionId={props.connectionId}
disabled={props.disabled} disabled={props.disabled}
@@ -113,6 +115,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
{__("Link obligation")} {__("Link obligation")}
</Button> </Button>
</LinkedObligationDialog> </LinkedObligationDialog>
)}
</div> </div>
)} )}
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
@@ -122,21 +125,21 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
<Th>{__("Source")}</Th> <Th>{__("Source")}</Th>
<Th>{__("Status")}</Th> <Th>{__("Status")}</Th>
<Th>{__("Owner")}</Th> <Th>{__("Owner")}</Th>
<Th></Th> {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{obligations.length === 0 && ( {obligations.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="text-center text-txt-secondary"> <Td colSpan={props.readOnly ? 4 : 5} className="text-center text-txt-secondary">
{__("No obligations linked")} {__("No obligations linked")}
</Td> </Td>
</Tr> </Tr>
)} )}
{obligations.map((obligation) => ( {obligations.map((obligation) => (
<ObligationRow key={obligation.id} obligation={obligation} onClick={onDetach} /> <ObligationRow key={obligation.id} obligation={obligation} onClick={onDetach} readOnly={props.readOnly} />
))} ))}
{variant === "table" && ( {variant === "table" && !props.readOnly && (
<LinkedObligationDialog <LinkedObligationDialog
connectionId={props.connectionId} connectionId={props.connectionId}
disabled={props.disabled} disabled={props.disabled}
@@ -168,6 +171,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
function ObligationRow(props: { function ObligationRow(props: {
obligation: LinkedObligationsCardFragment$key & { id: string }; obligation: LinkedObligationsCardFragment$key & { id: string };
onClick: (obligationId: string) => void; onClick: (obligationId: string) => void;
readOnly?: boolean;
}) { }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const obligation = useFragment(linkedObligationFragment, props.obligation); const obligation = useFragment(linkedObligationFragment, props.obligation);
@@ -199,6 +203,7 @@ function ObligationRow(props: {
<Td> <Td>
{obligation.owner?.fullName || __("Unassigned")} {obligation.owner?.fullName || __("Unassigned")}
</Td> </Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<Button <Button
variant="secondary" variant="secondary"
@@ -208,6 +213,7 @@ function ObligationRow(props: {
{__("Unlink")} {__("Unlink")}
</Button> </Button>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -45,7 +45,7 @@ const inviteMutation = graphql`
const schema = z.object({ const schema = z.object({
email: z.string().email(), email: z.string().email(),
fullName: z.string(), fullName: z.string(),
role: z.enum(["OWNER", "ADMIN", "FULL", "VIEWER", "EMPLOYEE"]).default("VIEWER"), role: z.enum(["OWNER", "ADMIN", "FULL", "VIEWER", "AUDITOR", "EMPLOYEE"]).default("VIEWER"),
createPeople: z.boolean().default(false), createPeople: z.boolean().default(false),
}); });
@@ -126,6 +126,7 @@ function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) {
{assignableRoles.includes("OWNER") && <Option value="OWNER">{__("Owner")}</Option>} {assignableRoles.includes("OWNER") && <Option value="OWNER">{__("Owner")}</Option>}
{assignableRoles.includes("ADMIN") && <Option value="ADMIN">{__("Admin")}</Option>} {assignableRoles.includes("ADMIN") && <Option value="ADMIN">{__("Admin")}</Option>}
{assignableRoles.includes("VIEWER") && <Option value="VIEWER">{__("Viewer")}</Option>} {assignableRoles.includes("VIEWER") && <Option value="VIEWER">{__("Viewer")}</Option>}
{assignableRoles.includes("AUDITOR") && <Option value="AUDITOR">{__("Auditor")}</Option>}
{assignableRoles.includes("EMPLOYEE") && <Option value="EMPLOYEE">{__("Employee")}</Option>} {assignableRoles.includes("EMPLOYEE") && <Option value="EMPLOYEE">{__("Employee")}</Option>}
</Select> </Select>
<div className="mt-2 text-sm text-txt-tertiary"> <div className="mt-2 text-sm text-txt-tertiary">
@@ -138,6 +139,9 @@ function InviteUserDialogContent({ children, connectionId, onRefetch }: Props) {
{field.value === "VIEWER" && ( {field.value === "VIEWER" && (
<p>{__("Read-only access")}</p> <p>{__("Read-only access")}</p>
)} )}
{field.value === "AUDITOR" && (
<p>{__("Read-only access without settings, tasks and meetings")}</p>
)}
{field.value === "EMPLOYEE" && ( {field.value === "EMPLOYEE" && (
<p>{__("Access to employee page")}</p> <p>{__("Access to employee page")}</p>
)} )}

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<4dda94f89f726842762d68c28d8180b6>> * @generated SignedSource<<31b0b8ac2c59ccf3addd4508d56bd166>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,7 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type InviteUserInput = { export type InviteUserInput = {
createPeople: boolean; createPeople: boolean;
email: string; email: string;

View File

@@ -48,6 +48,7 @@ type Props<Params> = {
onAttach: Mutation<Params>; onAttach: Mutation<Params>;
// Mutation to detach a risk (will receive {riskId, ...params}) // Mutation to detach a risk (will receive {riskId, ...params})
onDetach: Mutation<Params>; onDetach: Mutation<Params>;
readOnly?: boolean;
}; };
/** /**
@@ -88,20 +89,21 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
<Th>{__("Name")}</Th> <Th>{__("Name")}</Th>
<Th>{__("Inherent Risk")}</Th> <Th>{__("Inherent Risk")}</Th>
<Th>{__("Residual Risk")}</Th> <Th>{__("Residual Risk")}</Th>
<Th></Th> {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{props.risks.length === 0 && ( {props.risks.length === 0 && (
<Tr> <Tr>
<Td colSpan={4} className="text-center text-txt-secondary"> <Td colSpan={props.readOnly ? 3 : 4} className="text-center text-txt-secondary">
{__("No risks linked")} {__("No risks linked")}
</Td> </Td>
</Tr> </Tr>
)} )}
{props.risks.map((risk) => ( {props.risks.map((risk) => (
<RiskRow key={risk.id} risk={risk} onClick={onDetach} /> <RiskRow key={risk.id} risk={risk} onClick={onDetach} readOnly={props.readOnly} />
))} ))}
{!props.readOnly && (
<LinkedRisksDialog <LinkedRisksDialog
connectionId={props.connectionId} connectionId={props.connectionId}
disabled={props.disabled} disabled={props.disabled}
@@ -111,6 +113,7 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
> >
<TrButton colspan={4}>{__("Link risk")}</TrButton> <TrButton colspan={4}>{__("Link risk")}</TrButton>
</LinkedRisksDialog> </LinkedRisksDialog>
)}
</Tbody> </Tbody>
</Table> </Table>
</div> </div>
@@ -120,6 +123,7 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
function RiskRow(props: { function RiskRow(props: {
risk: LinkedRisksCardFragment$key & { id: string }; risk: LinkedRisksCardFragment$key & { id: string };
onClick: (riskId: string) => void; onClick: (riskId: string) => void;
readOnly?: boolean;
}) { }) {
const risk = useFragment(linkedRiskFragment, props.risk); const risk = useFragment(linkedRiskFragment, props.risk);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
@@ -134,6 +138,7 @@ function RiskRow(props: {
<Td> <Td>
<RiskBadge level={risk.residualRiskScore} /> <RiskBadge level={risk.residualRiskScore} />
</Td> </Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<Button <Button
variant="secondary" variant="secondary"
@@ -143,6 +148,7 @@ function RiskRow(props: {
{__("Unlink")} {__("Unlink")}
</Button> </Button>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -50,6 +50,7 @@ type Props<Params> = {
onAttach: Mutation<Params>; onAttach: Mutation<Params>;
onDetach: Mutation<Params>; onDetach: Mutation<Params>;
variant?: "card" | "table"; variant?: "card" | "table";
readOnly?: boolean;
}; };
export function LinkedSnapshotsCard<Params>(props: Props<Params>) { export function LinkedSnapshotsCard<Params>(props: Props<Params>) {
@@ -87,11 +88,15 @@ export function LinkedSnapshotsCard<Params>(props: Props<Params>) {
const Wrapper = variant === "card" ? Card : "div"; const Wrapper = variant === "card" ? Card : "div";
const colSpanTable = props.readOnly ? 4 : 5;
const colSpanCard = props.readOnly ? 3 : 4;
return ( return (
<Wrapper padded className="space-y-[10px]"> <Wrapper padded className="space-y-[10px]">
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Snapshots")}</div> <div className="text-lg font-semibold">{__("Snapshots")}</div>
{!props.readOnly && (
<LinkedSnapshotsDialog <LinkedSnapshotsDialog
disabled={props.disabled} disabled={props.disabled}
linkedSnapshots={props.snapshots} linkedSnapshots={props.snapshots}
@@ -102,6 +107,7 @@ export function LinkedSnapshotsCard<Params>(props: Props<Params>) {
{__("Link snapshot")} {__("Link snapshot")}
</Button> </Button>
</LinkedSnapshotsDialog> </LinkedSnapshotsDialog>
)}
</div> </div>
)} )}
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
@@ -111,13 +117,13 @@ export function LinkedSnapshotsCard<Params>(props: Props<Params>) {
<Th>{__("Type")}</Th> <Th>{__("Type")}</Th>
{variant === "table" && <Th>{__("Description")}</Th>} {variant === "table" && <Th>{__("Description")}</Th>}
<Th>{__("Created")}</Th> <Th>{__("Created")}</Th>
<Th></Th> {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{snapshots.length === 0 && ( {snapshots.length === 0 && (
<Tr> <Tr>
<Td colSpan={variant === "table" ? 5 : 3} className="text-center text-txt-secondary"> <Td colSpan={variant === "table" ? colSpanTable : colSpanCard} className="text-center text-txt-secondary">
{__("No snapshots linked")} {__("No snapshots linked")}
</Td> </Td>
</Tr> </Tr>
@@ -128,16 +134,17 @@ export function LinkedSnapshotsCard<Params>(props: Props<Params>) {
snapshot={snapshot} snapshot={snapshot}
onClick={onDetach} onClick={onDetach}
variant={variant} variant={variant}
readOnly={props.readOnly}
/> />
))} ))}
{variant === "table" && ( {variant === "table" && !props.readOnly && (
<LinkedSnapshotsDialog <LinkedSnapshotsDialog
disabled={props.disabled} disabled={props.disabled}
linkedSnapshots={props.snapshots} linkedSnapshots={props.snapshots}
onLink={onAttach} onLink={onAttach}
onUnlink={onDetach} onUnlink={onDetach}
> >
<TrButton colspan={variant === "table" ? 5 : 3} icon={IconPlusLarge}> <TrButton colspan={colSpanTable} icon={IconPlusLarge}>
{__("Link snapshot")} {__("Link snapshot")}
</TrButton> </TrButton>
</LinkedSnapshotsDialog> </LinkedSnapshotsDialog>
@@ -162,6 +169,7 @@ function SnapshotRow(props: {
snapshot: LinkedSnapshotsCardFragment$key & { id: string }; snapshot: LinkedSnapshotsCardFragment$key & { id: string };
onClick: (snapshotId: string) => void; onClick: (snapshotId: string) => void;
variant: "card" | "table"; variant: "card" | "table";
readOnly?: boolean;
}) { }) {
const snapshot = useFragment(linkedSnapshotFragment, props.snapshot); const snapshot = useFragment(linkedSnapshotFragment, props.snapshot);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
@@ -186,6 +194,7 @@ function SnapshotRow(props: {
<Td className="text-txt-tertiary"> <Td className="text-txt-tertiary">
{formatDate(snapshot.createdAt)} {formatDate(snapshot.createdAt)}
</Td> </Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<Button <Button
variant="secondary" variant="secondary"
@@ -195,6 +204,7 @@ function SnapshotRow(props: {
{__("Unlink")} {__("Unlink")}
</Button> </Button>
</Td> </Td>
)}
</Tr> </Tr>
); );
} }

View File

@@ -52,14 +52,10 @@ export const measureNodeQuery = graphql`
risksInfos: risks(first: 0) { risksInfos: risks(first: 0) {
totalCount totalCount
} }
tasksInfos: tasks(first: 0) {
totalCount
}
controlsInfos: controls(first: 0) { controlsInfos: controls(first: 0) {
totalCount totalCount
} }
...MeasureRisksTabFragment ...MeasureRisksTabFragment
...MeasureTasksTabFragment
...MeasureControlsTabFragment ...MeasureControlsTabFragment
...MeasureFormDialogMeasureFragment ...MeasureFormDialogMeasureFragment
...MeasureEvidencesTabFragment ...MeasureEvidencesTabFragment

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<02c0b1d637edafea060ddb9ee3b3f130>> * @generated SignedSource<<ba6c0f7696cba9d0f9e6e092f6c0f263>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -30,10 +30,7 @@ export type MeasureGraphNodeQuery$data = {
readonly totalCount: number; readonly totalCount: number;
}; };
readonly state?: MeasureState; readonly state?: MeasureState;
readonly tasksInfos?: { readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment" | "MeasureEvidencesTabFragment" | "MeasureFormDialogMeasureFragment" | "MeasureRisksTabFragment">;
readonly totalCount: number;
};
readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment" | "MeasureEvidencesTabFragment" | "MeasureFormDialogMeasureFragment" | "MeasureRisksTabFragment" | "MeasureTasksTabFragment">;
}; };
}; };
export type MeasureGraphNodeQuery = { export type MeasureGraphNodeQuery = {
@@ -128,16 +125,6 @@ v10 = {
"storageKey": "risks(first:0)" "storageKey": "risks(first:0)"
}, },
v11 = { v11 = {
"alias": "tasksInfos",
"args": (v7/*: any*/),
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "tasks",
"plural": false,
"selections": (v8/*: any*/),
"storageKey": "tasks(first:0)"
},
v12 = {
"alias": "controlsInfos", "alias": "controlsInfos",
"args": (v7/*: any*/), "args": (v7/*: any*/),
"concreteType": "ControlConnection", "concreteType": "ControlConnection",
@@ -147,55 +134,42 @@ v12 = {
"selections": (v8/*: any*/), "selections": (v8/*: any*/),
"storageKey": "controls(first:0)" "storageKey": "controls(first:0)"
}, },
v13 = { v12 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "__typename", "name": "__typename",
"storageKey": null "storageKey": null
}, },
v14 = [ v13 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 100 "value": 100
} }
], ],
v15 = { v14 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "cursor", "name": "cursor",
"storageKey": null "storageKey": null
}, },
v16 = { v15 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "endCursor", "name": "endCursor",
"storageKey": null "storageKey": null
}, },
v17 = { v16 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "hasNextPage", "name": "hasNextPage",
"storageKey": null "storageKey": null
}, },
v18 = { v17 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
(v16/*: any*/),
(v17/*: any*/)
],
"storageKey": null
},
v19 = {
"kind": "ClientExtension", "kind": "ClientExtension",
"selections": [ "selections": [
{ {
@@ -207,14 +181,14 @@ v19 = {
} }
] ]
}, },
v20 = [ v18 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
"value": 20 "value": 20
} }
], ],
v21 = { v19 = {
"alias": null, "alias": null,
"args": null, "args": null,
"concreteType": "PageInfo", "concreteType": "PageInfo",
@@ -222,8 +196,8 @@ v21 = {
"name": "pageInfo", "name": "pageInfo",
"plural": false, "plural": false,
"selections": [ "selections": [
(v15/*: any*/),
(v16/*: any*/), (v16/*: any*/),
(v17/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -241,7 +215,7 @@ v21 = {
], ],
"storageKey": null "storageKey": null
}, },
v22 = [ v20 = [
{ {
"kind": "Literal", "kind": "Literal",
"name": "first", "name": "first",
@@ -274,17 +248,11 @@ return {
(v9/*: any*/), (v9/*: any*/),
(v10/*: any*/), (v10/*: any*/),
(v11/*: any*/), (v11/*: any*/),
(v12/*: any*/),
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
"name": "MeasureRisksTabFragment" "name": "MeasureRisksTabFragment"
}, },
{
"args": null,
"kind": "FragmentSpread",
"name": "MeasureTasksTabFragment"
},
{ {
"args": null, "args": null,
"kind": "FragmentSpread", "kind": "FragmentSpread",
@@ -325,7 +293,7 @@ return {
"name": "node", "name": "node",
"plural": false, "plural": false,
"selections": [ "selections": [
(v13/*: any*/), (v12/*: any*/),
(v2/*: any*/), (v2/*: any*/),
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
@@ -337,10 +305,9 @@ return {
(v9/*: any*/), (v9/*: any*/),
(v10/*: any*/), (v10/*: any*/),
(v11/*: any*/), (v11/*: any*/),
(v12/*: any*/),
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v13/*: any*/),
"concreteType": "RiskConnection", "concreteType": "RiskConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "risks", "name": "risks",
@@ -378,22 +345,34 @@ return {
"name": "residualRiskScore", "name": "residualRiskScore",
"storageKey": null "storageKey": null
}, },
(v13/*: any*/) (v12/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v15/*: any*/) (v14/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v18/*: any*/), {
(v19/*: any*/) "alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
(v15/*: any*/),
(v16/*: any*/)
],
"storageKey": null
},
(v17/*: any*/)
], ],
"storageKey": "risks(first:100)" "storageKey": "risks(first:100)"
}, },
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v13/*: any*/),
"filters": null, "filters": null,
"handle": "connection", "handle": "connection",
"key": "Measure__risks", "key": "Measure__risks",
@@ -402,102 +381,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v14/*: any*/), "args": (v18/*: any*/),
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "tasks",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TaskEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Task",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v5/*: any*/),
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField",
"name": "measure",
"plural": false,
"selections": [
(v2/*: any*/)
],
"storageKey": null
},
(v13/*: any*/)
],
"storageKey": null
},
(v15/*: any*/)
],
"storageKey": null
},
(v18/*: any*/),
(v19/*: any*/)
],
"storageKey": "tasks(first:100)"
},
{
"alias": null,
"args": (v14/*: any*/),
"filters": null,
"handle": "connection",
"key": "Measure__tasks",
"kind": "LinkedHandle",
"name": "tasks"
},
{
"alias": null,
"args": (v20/*: any*/),
"concreteType": "ControlConnection", "concreteType": "ControlConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "controls", "name": "controls",
@@ -541,22 +425,22 @@ return {
], ],
"storageKey": null "storageKey": null
}, },
(v13/*: any*/) (v12/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v15/*: any*/) (v14/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v21/*: any*/), (v19/*: any*/),
(v19/*: any*/) (v17/*: any*/)
], ],
"storageKey": "controls(first:20)" "storageKey": "controls(first:20)"
}, },
{ {
"alias": null, "alias": null,
"args": (v20/*: any*/), "args": (v18/*: any*/),
"filters": [ "filters": [
"orderBy", "orderBy",
"filter" "filter"
@@ -568,7 +452,7 @@ return {
}, },
{ {
"alias": null, "alias": null,
"args": (v22/*: any*/), "args": (v20/*: any*/),
"concreteType": "EvidenceConnection", "concreteType": "EvidenceConnection",
"kind": "LinkedField", "kind": "LinkedField",
"name": "evidences", "name": "evidences",
@@ -638,22 +522,22 @@ return {
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
}, },
(v13/*: any*/) (v12/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v15/*: any*/) (v14/*: any*/)
], ],
"storageKey": null "storageKey": null
}, },
(v21/*: any*/), (v19/*: any*/),
(v19/*: any*/) (v17/*: any*/)
], ],
"storageKey": "evidences(first:50)" "storageKey": "evidences(first:50)"
}, },
{ {
"alias": null, "alias": null,
"args": (v22/*: any*/), "args": (v20/*: any*/),
"filters": [ "filters": [
"orderBy" "orderBy"
], ],
@@ -672,16 +556,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "d20815cee9ca8dec2e09fcefdd54a68a", "cacheID": "fb510405c5934dbaf802c61c3ee8a547",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "MeasureGraphNodeQuery", "name": "MeasureGraphNodeQuery",
"operationKind": "query", "operationKind": "query",
"text": "query MeasureGraphNodeQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n name\n description\n state\n category\n evidencesInfos: evidences(first: 0) {\n totalCount\n }\n risksInfos: risks(first: 0) {\n totalCount\n }\n tasksInfos: tasks(first: 0) {\n totalCount\n }\n controlsInfos: controls(first: 0) {\n totalCount\n }\n ...MeasureRisksTabFragment\n ...MeasureTasksTabFragment\n ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n\nfragment LinkedRisksCardFragment on Risk {\n id\n name\n inherentRiskScore\n residualRiskScore\n}\n\nfragment MeasureControlsTabFragment on Measure {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n type\n createdAt\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasureRisksTabFragment on Measure {\n id\n risks(first: 100) {\n edges {\n node {\n id\n ...LinkedRisksCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment MeasureTasksTabFragment on Measure {\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n" "text": "query MeasureGraphNodeQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n name\n description\n state\n category\n evidencesInfos: evidences(first: 0) {\n totalCount\n }\n risksInfos: risks(first: 0) {\n totalCount\n }\n controlsInfos: controls(first: 0) {\n totalCount\n }\n ...MeasureRisksTabFragment\n ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n\nfragment LinkedRisksCardFragment on Risk {\n id\n name\n inherentRiskScore\n residualRiskScore\n}\n\nfragment MeasureControlsTabFragment on Measure {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n type\n createdAt\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasureRisksTabFragment on Measure {\n id\n risks(first: 100) {\n edges {\n node {\n id\n ...LinkedRisksCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "34931b0c662961228537121bf18bab4e"; (node as any).hash = "fea0fb9de2bebb764995341906e0de58";
export default node; export default node;

View File

@@ -461,7 +461,7 @@ export default function DocumentDetailPage(props: Props) {
]} ]}
/> />
<div className="flex gap-2"> <div className="flex gap-2">
{isDraft && ( {isDraft && isAuthorized("Document", "publishDocumentVersion") && (
<Button <Button
onClick={handlePublish} onClick={handlePublish}
icon={IconCheckmark1} icon={IconCheckmark1}
@@ -499,8 +499,7 @@ export default function DocumentDetailPage(props: Props) {
{isDraft ? __("Edit draft document") : __("Create new draft")} {isDraft ? __("Edit draft document") : __("Create new draft")}
</DropdownItem> </DropdownItem>
)} )}
{isDraft && versions.length > 1 && ( {isDraft && versions.length > 1 && isAuthorized("Document", "deleteDraftDocumentVersion") && (
isAuthorized("Document", "deleteDocument") && (
<DropdownItem <DropdownItem
onClick={handleDeleteDraft} onClick={handleDeleteDraft}
icon={IconTrashCan} icon={IconTrashCan}
@@ -508,7 +507,6 @@ export default function DocumentDetailPage(props: Props) {
> >
{__("Delete draft document")} {__("Delete draft document")}
</DropdownItem> </DropdownItem>
)
)} )}
<DropdownItem <DropdownItem
onClick={() => pdfDownloadDialogRef.current?.open()} onClick={() => pdfDownloadDialogRef.current?.open()}
@@ -567,11 +565,13 @@ export default function DocumentDetailPage(props: Props) {
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{document.title}</span> <span>{document.title}</span>
{isAuthorized("Document", "updateDocument") && (
<Button <Button
variant="quaternary" variant="quaternary"
icon={IconPencil} icon={IconPencil}
onClick={() => setIsEditingTitle(true)} onClick={() => setIsEditingTitle(true)}
/> />
)}
</div> </div>
) )
} }
@@ -616,7 +616,7 @@ export default function DocumentDetailPage(props: Props) {
/> />
</EditablePropertyContent> </EditablePropertyContent>
) : ( ) : (
<ReadOnlyPropertyContent onEdit={() => setIsEditingOwner(true)}> <ReadOnlyPropertyContent onEdit={() => setIsEditingOwner(true)} canEdit={isAuthorized("Document", "updateDocument")}>
<Badge variant="highlight" size="md" className="gap-2"> <Badge variant="highlight" size="md" className="gap-2">
<Avatar name={currentVersion.owner?.fullName ?? ""} /> <Avatar name={currentVersion.owner?.fullName ?? ""} />
{currentVersion.owner?.fullName} {currentVersion.owner?.fullName}
@@ -643,7 +643,7 @@ export default function DocumentDetailPage(props: Props) {
</ControlledField> </ControlledField>
</EditablePropertyContent> </EditablePropertyContent>
) : ( ) : (
<ReadOnlyPropertyContent onEdit={() => setIsEditingType(true)}> <ReadOnlyPropertyContent onEdit={() => setIsEditingType(true)} canEdit={isAuthorized("Document", "updateDocument")}>
<div className="text-sm text-txt-secondary"> <div className="text-sm text-txt-secondary">
{getDocumentTypeLabel(__, document.documentType)} {getDocumentTypeLabel(__, document.documentType)}
</div> </div>
@@ -671,6 +671,7 @@ export default function DocumentDetailPage(props: Props) {
) : ( ) : (
<ReadOnlyPropertyContent <ReadOnlyPropertyContent
onEdit={() => setIsEditingClassification(true)} onEdit={() => setIsEditingClassification(true)}
canEdit={isAuthorized("Document", "updateDocument")}
> >
<div className="text-sm text-txt-secondary"> <div className="text-sm text-txt-secondary">
{getDocumentClassificationLabel(__, currentVersion.classification)} {getDocumentClassificationLabel(__, currentVersion.classification)}
@@ -739,14 +740,16 @@ function EditablePropertyContent({
function ReadOnlyPropertyContent({ function ReadOnlyPropertyContent({
children, children,
onEdit, onEdit,
canEdit = true,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
onEdit: () => void; onEdit: () => void;
canEdit?: boolean;
}) { }) {
return ( return (
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
{children} {children}
<Button variant="quaternary" icon={IconPencil} onClick={onEdit} /> {canEdit && <Button variant="quaternary" icon={IconPencil} onClick={onEdit} />}
</div> </div>
); );
} }

View File

@@ -178,6 +178,22 @@ export default function FrameworkControlPage({ queryRef }: Props) {
const [attachSnapshot, isAttachingSnapshot] = useMutation(attachSnapshotMutation); const [attachSnapshot, isAttachingSnapshot] = useMutation(attachSnapshotMutation);
const [deleteControl] = useMutation(deleteControlMutation); const [deleteControl] = useMutation(deleteControlMutation);
const canLinkMeasure = isAuthorized("Control", "createControlMeasureMapping");
const canUnlinkMeasure = isAuthorized("Control", "deleteControlMeasureMapping");
const measuresReadOnly = !canLinkMeasure && !canUnlinkMeasure;
const canLinkDocument = isAuthorized("Control", "createControlDocumentMapping");
const canUnlinkDocument = isAuthorized("Control", "deleteControlDocumentMapping");
const documentsReadOnly = !canLinkDocument && !canUnlinkDocument;
const canLinkAudit = isAuthorized("Control", "createControlAuditMapping");
const canUnlinkAudit = isAuthorized("Control", "deleteControlAuditMapping");
const auditsReadOnly = !canLinkAudit && !canUnlinkAudit;
const canLinkSnapshot = isAuthorized("Control", "createControlSnapshotMapping");
const canUnlinkSnapshot = isAuthorized("Control", "deleteControlSnapshotMapping");
const snapshotsReadOnly = !canLinkSnapshot && !canUnlinkSnapshot;
const withErrorHandling = <T extends MutationParameters>( const withErrorHandling = <T extends MutationParameters>(
mutationFn: (config: UseMutationConfig<T>) => void, mutationFn: (config: UseMutationConfig<T>) => void,
errorMessage: string errorMessage: string
@@ -285,6 +301,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
onAttach={withErrorHandling(attachMeasure, __("Failed to link measure"))} onAttach={withErrorHandling(attachMeasure, __("Failed to link measure"))}
onDetach={withErrorHandling(detachMeasure, __("Failed to unlink measure"))} onDetach={withErrorHandling(detachMeasure, __("Failed to unlink measure"))}
disabled={isAttachingMeasure || isDetachingMeasure} disabled={isAttachingMeasure || isDetachingMeasure}
readOnly={measuresReadOnly}
/> />
</div> </div>
<div className="mb-4"> <div className="mb-4">
@@ -296,6 +313,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
onAttach={withErrorHandling(attachDocument, __("Failed to link document"))} onAttach={withErrorHandling(attachDocument, __("Failed to link document"))}
onDetach={withErrorHandling(detachDocument, __("Failed to unlink document"))} onDetach={withErrorHandling(detachDocument, __("Failed to unlink document"))}
disabled={isAttachingDocument || isDetachingDocument} disabled={isAttachingDocument || isDetachingDocument}
readOnly={documentsReadOnly}
/> />
</div> </div>
<div className="mb-4"> <div className="mb-4">
@@ -307,6 +325,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
onAttach={withErrorHandling(attachAudit, __("Failed to link audit"))} onAttach={withErrorHandling(attachAudit, __("Failed to link audit"))}
onDetach={withErrorHandling(detachAudit, __("Failed to unlink audit"))} onDetach={withErrorHandling(detachAudit, __("Failed to unlink audit"))}
disabled={isAttachingAudit || isDetachingAudit} disabled={isAttachingAudit || isDetachingAudit}
readOnly={auditsReadOnly}
/> />
</div> </div>
<div className="mb-4"> <div className="mb-4">
@@ -318,6 +337,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
onAttach={withErrorHandling(attachSnapshot, __("Failed to link snapshot"))} onAttach={withErrorHandling(attachSnapshot, __("Failed to link snapshot"))}
onDetach={withErrorHandling(detachSnapshot, __("Failed to unlink snapshot"))} onDetach={withErrorHandling(detachSnapshot, __("Failed to unlink snapshot"))}
disabled={isAttachingSnapshot || isDetachingSnapshot} disabled={isAttachingSnapshot || isDetachingSnapshot}
readOnly={snapshotsReadOnly}
/> />
</div> </div>
</div> </div>

View File

@@ -26,10 +26,13 @@ import { MeasureBadge } from "@probo/ui/src/Molecules/Badge/MeasureBadge";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { import {
ConnectionHandler, ConnectionHandler,
graphql,
type PreloadedQuery, type PreloadedQuery,
useLazyLoadQuery,
usePreloadedQuery, usePreloadedQuery,
} from "react-relay"; } from "react-relay";
import type { MeasureGraphNodeQuery } from "/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql"; import type { MeasureGraphNodeQuery } from "/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql";
import type { MeasureDetailPageTasksCountQuery } from "./__generated__/MeasureDetailPageTasksCountQuery.graphql";
import { import {
MeasureConnectionKey, MeasureConnectionKey,
measureNodeQuery, measureNodeQuery,
@@ -43,9 +46,30 @@ import {
sprintf, sprintf,
} from "@probo/helpers"; } from "@probo/helpers";
import MeasureFormDialog from "./dialog/MeasureFormDialog"; import MeasureFormDialog from "./dialog/MeasureFormDialog";
import { use } from "react"; import { Suspense, use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext"; import { PermissionsContext } from "/providers/PermissionsContext";
const tasksCountQuery = graphql`
query MeasureDetailPageTasksCountQuery($measureId: ID!) {
node(id: $measureId) {
... on Measure {
tasks(first: 0) {
totalCount
}
}
}
}
`;
function TasksCountBadge({ measureId }: { measureId: string }) {
const data = useLazyLoadQuery<MeasureDetailPageTasksCountQuery>(
tasksCountQuery,
{ measureId }
);
const count = data.node?.tasks?.totalCount ?? 0;
return <TabBadge>{count}</TabBadge>;
}
type Props = { type Props = {
queryRef: PreloadedQuery<MeasureGraphNodeQuery>; queryRef: PreloadedQuery<MeasureGraphNodeQuery>;
}; };
@@ -67,7 +91,7 @@ export default function MeasureDetailPage(props: Props) {
); );
} }
const tasksCount = measure.tasksInfos?.totalCount ?? 0; const canViewTasks = isAuthorized("Measure", "listTasks");
const evidencesCount = measure.evidencesInfos?.totalCount ?? 0; const evidencesCount = measure.evidencesInfos?.totalCount ?? 0;
const controlsCount = measure.controlsInfos?.totalCount ?? 0; const controlsCount = measure.controlsInfos?.totalCount ?? 0;
const risksCount = measure.risksInfos?.totalCount ?? 0; const risksCount = measure.risksInfos?.totalCount ?? 0;
@@ -160,13 +184,13 @@ export default function MeasureDetailPage(props: Props) {
</Select> </Select>
</> </>
)} )}
<ActionDropdown variant="secondary">
{isAuthorized("Measure", "deleteMeasure") && ( {isAuthorized("Measure", "deleteMeasure") && (
<ActionDropdown variant="secondary">
<DropdownItem variant="danger" icon={IconTrashCan} onClick={onDelete}> <DropdownItem variant="danger" icon={IconTrashCan} onClick={onDelete}>
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
)}
</ActionDropdown> </ActionDropdown>
)}
</PageHeader> </PageHeader>
<Tabs> <Tabs>
@@ -177,13 +201,17 @@ export default function MeasureDetailPage(props: Props) {
{__("Evidences")} {__("Evidences")}
<TabBadge>{evidencesCount}</TabBadge> <TabBadge>{evidencesCount}</TabBadge>
</TabLink> </TabLink>
{canViewTasks && (
<TabLink <TabLink
to={`/organizations/${organizationId}/measures/${measureId}/tasks`} to={`/organizations/${organizationId}/measures/${measureId}/tasks`}
> >
<IconCheckmark1 size={20} /> <IconCheckmark1 size={20} />
{__("Tasks")} {__("Tasks")}
<TabBadge>{tasksCount}</TabBadge> <Suspense fallback={<TabBadge>-</TabBadge>}>
<TasksCountBadge measureId={measureId} />
</Suspense>
</TabLink> </TabLink>
)}
<TabLink <TabLink
to={`/organizations/${organizationId}/measures/${measureId}/controls`} to={`/organizations/${organizationId}/measures/${measureId}/controls`}
> >

View File

@@ -0,0 +1,143 @@
/**
* @generated SignedSource<<c14e7d1b8a5a8d23dbe52637610f13f7>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MeasureDetailPageTasksCountQuery$variables = {
measureId: string;
};
export type MeasureDetailPageTasksCountQuery$data = {
readonly node: {
readonly tasks?: {
readonly totalCount: number;
};
};
};
export type MeasureDetailPageTasksCountQuery = {
response: MeasureDetailPageTasksCountQuery$data;
variables: MeasureDetailPageTasksCountQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "measureId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "measureId"
}
],
v2 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 0
}
],
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "tasks",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "tasks(first:0)"
}
],
"type": "Measure",
"abstractKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "MeasureDetailPageTasksCountQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/)
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "MeasureDetailPageTasksCountQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "36a1d5cf32d07ed5d76e5e64e7f19005",
"id": null,
"metadata": {},
"name": "MeasureDetailPageTasksCountQuery",
"operationKind": "query",
"text": "query MeasureDetailPageTasksCountQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n tasks(first: 0) {\n totalCount\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "f0a8dde53529b525f3fe9aa35dee9e8a";
export default node;

View File

@@ -3,6 +3,8 @@ import { useOutletContext } from "react-router";
import { LinkedControlsCard } from "/components/controls/LinkedControlsCard"; import { LinkedControlsCard } from "/components/controls/LinkedControlsCard";
import type { MeasureControlsTabFragment$key } from "./__generated__/MeasureControlsTabFragment.graphql"; import type { MeasureControlsTabFragment$key } from "./__generated__/MeasureControlsTabFragment.graphql";
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
export const controlsFragment = graphql` export const controlsFragment = graphql`
fragment MeasureControlsTabFragment on Measure fragment MeasureControlsTabFragment on Measure
@@ -69,6 +71,11 @@ export default function MeasureControlsTab() {
const [data, refetch] = useRefetchableFragment(controlsFragment, measure); const [data, refetch] = useRefetchableFragment(controlsFragment, measure);
const connectionId = data.controls.__id; const connectionId = data.controls.__id;
const controls = data.controls?.edges?.map((edge) => edge.node) ?? []; const controls = data.controls?.edges?.map((edge) => edge.node) ?? [];
const { isAuthorized } = use(PermissionsContext);
const canLinkControl = isAuthorized("Control", "createControlMeasureMapping");
const canUnlinkControl = isAuthorized("Control", "deleteControlMeasureMapping");
const readOnly = !canLinkControl && !canUnlinkControl;
const incrementOptions = { const incrementOptions = {
id: data.id, id: data.id,
@@ -99,6 +106,7 @@ export default function MeasureControlsTab() {
params={{ measureId: data.id }} params={{ measureId: data.id }}
connectionId={connectionId} connectionId={connectionId}
refetch={refetch} refetch={refetch}
readOnly={readOnly}
/> />
); );
} }

View File

@@ -29,10 +29,11 @@ import { fileSize, fileType, sprintf, formatDate } from "@probo/helpers";
import { EvidencePreviewDialog } from "../dialog/EvidencePreviewDialog"; import { EvidencePreviewDialog } from "../dialog/EvidencePreviewDialog";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
import { CreateEvidenceDialog } from "../dialog/CreateEvidenceDialog"; import { CreateEvidenceDialog } from "../dialog/CreateEvidenceDialog";
import { useState } from "react"; import { use, useState } from "react";
import { EvidenceDownloadDialog } from "../dialog/EvidenceDownloadDialog"; import { EvidenceDownloadDialog } from "../dialog/EvidenceDownloadDialog";
import { updateStoreCounter } from "/hooks/useMutationWithIncrement"; import { updateStoreCounter } from "/hooks/useMutationWithIncrement";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { PermissionsContext } from "/providers/PermissionsContext";
export const evidencesFragment = graphql` export const evidencesFragment = graphql`
fragment MeasureEvidencesTabFragment on Measure fragment MeasureEvidencesTabFragment on Measure
@@ -107,6 +108,10 @@ export default function MeasureEvidencesTab() {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const canAddEvidence = isAuthorized("Measure", "uploadMeasureEvidence");
const canDeleteEvidence = isAuthorized("Evidence", "deleteEvidence");
usePageTitle(measure.name + " - " + __("Evidences")); usePageTitle(measure.name + " - " + __("Evidences"));
@@ -131,10 +136,11 @@ export default function MeasureEvidencesTab() {
organizationId={organizationId} organizationId={organizationId}
connectionId={connectionId} connectionId={connectionId}
hideActions={isSnapshotMode} hideActions={isSnapshotMode}
canDelete={canDeleteEvidence}
snapshotId={snapshotId} snapshotId={snapshotId}
/> />
))} ))}
{!isSnapshotMode && ( {!isSnapshotMode && canAddEvidence && (
<TrButton <TrButton
colspan={5} colspan={5}
onClick={() => dialogRef.current?.open()} onClick={() => dialogRef.current?.open()}
@@ -158,7 +164,7 @@ export default function MeasureEvidencesTab() {
filename={evidence.file?.fileName || ""} filename={evidence.file?.fileName || ""}
/> />
)} )}
{!isSnapshotMode && ( {!isSnapshotMode && canAddEvidence && (
<CreateEvidenceDialog <CreateEvidenceDialog
ref={dialogRef} ref={dialogRef}
measureId={measure.id} measureId={measure.id}
@@ -175,6 +181,7 @@ function EvidenceRow(props: {
organizationId: string; organizationId: string;
connectionId: string; connectionId: string;
hideActions?: boolean; hideActions?: boolean;
canDelete?: boolean;
snapshotId?: string; snapshotId?: string;
}) { }) {
const evidence = useFragment(evidenceFragment, props.evidenceKey); const evidence = useFragment(evidenceFragment, props.evidenceKey);
@@ -249,6 +256,7 @@ function EvidenceRow(props: {
<IconArrowInbox size={16} /> <IconArrowInbox size={16} />
{__("Download")} {__("Download")}
</DropdownItem> </DropdownItem>
{props.canDelete && (
<DropdownItem <DropdownItem
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
@@ -257,6 +265,7 @@ function EvidenceRow(props: {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
)}
</ActionDropdown> </ActionDropdown>
</div> </div>
)} )}

View File

@@ -3,6 +3,8 @@ import type { MeasureRisksTabFragment$key } from "./__generated__/MeasureRisksTa
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { LinkedRisksCard } from "/components/risks/LinkedRisksCard"; import { LinkedRisksCard } from "/components/risks/LinkedRisksCard";
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
export const risksFragment = graphql` export const risksFragment = graphql`
fragment MeasureRisksTabFragment on Measure { fragment MeasureRisksTabFragment on Measure {
@@ -53,6 +55,11 @@ export default function MeasureRisksTab() {
const data = useFragment(risksFragment, measure); const data = useFragment(risksFragment, measure);
const connectionId = data.risks.__id; const connectionId = data.risks.__id;
const risks = data.risks?.edges?.map((edge) => edge.node) ?? []; const risks = data.risks?.edges?.map((edge) => edge.node) ?? [];
const { isAuthorized } = use(PermissionsContext);
const canLinkRisk = isAuthorized("Risk", "createRiskMeasureMapping");
const canUnlinkRisk = isAuthorized("Risk", "deleteRiskMeasureMapping");
const readOnly = !canLinkRisk && !canUnlinkRisk;
const incrementOptions = { const incrementOptions = {
id: data.id, id: data.id,
@@ -82,6 +89,7 @@ export default function MeasureRisksTab() {
onDetach={detachRisk} onDetach={detachRisk}
params={{ measureId: data.id }} params={{ measureId: data.id }}
connectionId={connectionId} connectionId={connectionId}
readOnly={readOnly}
/> />
); );
} }

View File

@@ -1,14 +1,17 @@
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { MeasureTasksTabFragment$key } from "./__generated__/MeasureTasksTabFragment.graphql"; import type { MeasureTasksTabQuery } from "./__generated__/MeasureTasksTabQuery.graphql";
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { useFragment } from "react-relay"; import { useLazyLoadQuery } from "react-relay";
import TasksCard from "/components/tasks/TasksCard"; import TasksCard from "/components/tasks/TasksCard";
import { Button, IconPlusLarge } from "@probo/ui"; import { Button, IconPlusLarge } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import TaskFormDialog from "/components/tasks/TaskFormDialog"; import TaskFormDialog from "/components/tasks/TaskFormDialog";
export const tasksFragment = graphql` const tasksQuery = graphql`
fragment MeasureTasksTabFragment on Measure { query MeasureTasksTabQuery($measureId: ID!) {
node(id: $measureId) {
... on Measure {
id
tasks(first: 100) @connection(key: "Measure__tasks") { tasks(first: 100) @connection(key: "Measure__tasks") {
__id __id
edges { edges {
@@ -26,16 +29,24 @@ export const tasksFragment = graphql`
} }
} }
} }
}
}
`; `;
export default function MeasureTasksTab() { export default function MeasureTasksTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { measure } = useOutletContext<{ const { measure } = useOutletContext<{
measure: MeasureTasksTabFragment$key & { id: string }; measure: { id: string };
}>(); }>();
const data = useFragment(tasksFragment, measure); const data = useLazyLoadQuery<MeasureTasksTabQuery>(tasksQuery, {
const connectionId = data.tasks.__id; measureId: measure.id,
const tasks = data.tasks?.edges?.map((edge) => edge.node) ?? []; });
const node = data.node;
if (!node || !node.tasks) {
return null;
}
const connectionId = node.tasks.__id;
const tasks = node.tasks.edges?.map((edge) => edge.node) ?? [];
return ( return (
<div className="relative"> <div className="relative">

View File

@@ -1,201 +0,0 @@
/**
* @generated SignedSource<<cfddd118e2b6db135ad87fe157c89151>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type TaskState = "DONE" | "TODO";
import { FragmentRefs } from "relay-runtime";
export type MeasureTasksTabFragment$data = {
readonly tasks: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly assignedTo: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly name: string;
readonly state: TaskState;
readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment">;
};
}>;
};
readonly " $fragmentType": "MeasureTasksTabFragment";
};
export type MeasureTasksTabFragment$key = {
readonly " $data"?: MeasureTasksTabFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"MeasureTasksTabFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"tasks"
]
}
]
},
"name": "MeasureTasksTabFragment",
"selections": [
{
"alias": "tasks",
"args": null,
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "__Measure__tasks_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TaskEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Task",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "TaskFormDialogFragment"
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": null
}
],
"type": "Measure",
"abstractKey": null
};
})();
(node as any).hash = "40d3211a52860f68da7c89ba9123af61";
export default node;

View File

@@ -0,0 +1,370 @@
/**
* @generated SignedSource<<f2e7ca902ae7ee08f260f27a0fefa043>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type TaskState = "DONE" | "TODO";
export type MeasureTasksTabQuery$variables = {
measureId: string;
};
export type MeasureTasksTabQuery$data = {
readonly node: {
readonly id?: string;
readonly tasks?: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly assignedTo: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly description: string | null | undefined;
readonly id: string;
readonly name: string;
readonly state: TaskState;
readonly " $fragmentSpreads": FragmentRefs<"TaskFormDialogFragment">;
};
}>;
};
};
};
export type MeasureTasksTabQuery = {
response: MeasureTasksTabQuery$data;
variables: MeasureTasksTabQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "measureId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "measureId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
},
v10 = {
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
},
v11 = [
{
"kind": "Literal",
"name": "first",
"value": 100
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "MeasureTasksTabQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
{
"alias": "tasks",
"args": null,
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "__Measure__tasks_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TaskEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Task",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "TaskFormDialogFragment"
},
(v6/*: any*/),
(v7/*: any*/)
],
"storageKey": null
},
(v8/*: any*/)
],
"storageKey": null
},
(v9/*: any*/),
(v10/*: any*/)
],
"storageKey": null
}
],
"type": "Measure",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "MeasureTasksTabQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v11/*: any*/),
"concreteType": "TaskConnection",
"kind": "LinkedField",
"name": "tasks",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TaskEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Task",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deadline",
"storageKey": null
},
(v6/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField",
"name": "measure",
"plural": false,
"selections": [
(v2/*: any*/)
],
"storageKey": null
},
(v7/*: any*/)
],
"storageKey": null
},
(v8/*: any*/)
],
"storageKey": null
},
(v9/*: any*/),
(v10/*: any*/)
],
"storageKey": "tasks(first:100)"
},
{
"alias": null,
"args": (v11/*: any*/),
"filters": null,
"handle": "connection",
"key": "Measure__tasks",
"kind": "LinkedHandle",
"name": "tasks"
}
],
"type": "Measure",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "bc71d01128d96026a1e630b96e649409",
"id": null,
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"node",
"tasks"
]
}
]
},
"name": "MeasureTasksTabQuery",
"operationKind": "query",
"text": "query MeasureTasksTabQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n"
}
};
})();
(node as any).hash = "12445eb032af3bc00fea9edce6f427e2";
export default node;

View File

@@ -1,6 +1,6 @@
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { Button, Card, IconCheckmark1 } from "@probo/ui"; import { Button, Card, IconCheckmark1 } from "@probo/ui";
import type { PropsWithChildren } from "react"; import { use, type PropsWithChildren } from "react";
import z from "zod"; import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { ControlledField } from "/components/form/ControlledField"; import { ControlledField } from "/components/form/ControlledField";
@@ -11,6 +11,7 @@ import { useOutletContext } from "react-router";
import { updatePeopleMutation } from "/hooks/graph/PeopleGraph"; import { updatePeopleMutation } from "/hooks/graph/PeopleGraph";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql"; import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql";
import { PermissionsContext } from "/providers/PermissionsContext";
const schema = z.object({ const schema = z.object({
kind: z.enum(peopleRoles), kind: z.enum(peopleRoles),
@@ -21,6 +22,8 @@ export default function PeopleRoleTab() {
people: PeopleGraphNodeQuery$data["node"]; people: PeopleGraphNodeQuery$data["node"];
}>(); }>();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const canUpdatePeople = isAuthorized("People", "updatePeople");
const { control, formState, handleSubmit, reset } = useFormWithSchema( const { control, formState, handleSubmit, reset } = useFormWithSchema(
schema, schema,
{ {
@@ -65,6 +68,7 @@ export default function PeopleRoleTab() {
name="kind" name="kind"
type="select" type="select"
label={__("Role")} label={__("Role")}
disabled={!canUpdatePeople}
> >
{getRoles(__).map((role) => ( {getRoles(__).map((role) => (
<Option key={role.value} value={role.value}> <Option key={role.value} value={role.value}>
@@ -93,6 +97,7 @@ export default function PeopleRoleTab() {
</ul> </ul>
</div> </div>
</Card> </Card>
{canUpdatePeople && (
<div className="flex justify-end"> <div className="flex justify-end">
{formState.isDirty && ( {formState.isDirty && (
<Button type="submit" disabled={isMutating}> <Button type="submit" disabled={isMutating}>
@@ -100,6 +105,7 @@ export default function PeopleRoleTab() {
</Button> </Button>
)} )}
</div> </div>
)}
</form> </form>
); );
} }

View File

@@ -3,6 +3,8 @@ import { useOutletContext } from "react-router";
import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard"; import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard";
import type { RiskDocumentsTabFragment$key } from "./__generated__/RiskDocumentsTabFragment.graphql"; import type { RiskDocumentsTabFragment$key } from "./__generated__/RiskDocumentsTabFragment.graphql";
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
export const documentsFragment = graphql` export const documentsFragment = graphql`
fragment RiskDocumentsTabFragment on Risk { fragment RiskDocumentsTabFragment on Risk {
@@ -53,6 +55,11 @@ export default function RiskDocumentsTab() {
const data = useFragment(documentsFragment, risk); const data = useFragment(documentsFragment, risk);
const connectionId = data.documents.__id; const connectionId = data.documents.__id;
const documents = data.documents?.edges?.map((edge) => edge.node) ?? []; const documents = data.documents?.edges?.map((edge) => edge.node) ?? [];
const { isAuthorized } = use(PermissionsContext);
const canLinkDocument = isAuthorized("Risk", "createRiskDocumentMapping");
const canUnlinkDocument = isAuthorized("Risk", "deleteRiskDocumentMapping");
const readOnly = !canLinkDocument && !canUnlinkDocument;
const incrementOptions = { const incrementOptions = {
id: data.id, id: data.id,
@@ -82,6 +89,7 @@ export default function RiskDocumentsTab() {
onDetach={detachDocument} onDetach={detachDocument}
params={{ riskId: data.id }} params={{ riskId: data.id }}
connectionId={connectionId} connectionId={connectionId}
readOnly={readOnly}
/> />
); );
} }

View File

@@ -3,6 +3,8 @@ import type { RiskMeasuresTabFragment$key } from "./__generated__/RiskMeasuresTa
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard"; import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard";
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
export const measuresFragment = graphql` export const measuresFragment = graphql`
fragment RiskMeasuresTabFragment on Risk { fragment RiskMeasuresTabFragment on Risk {
@@ -53,6 +55,12 @@ export default function RiskMeasuresTab() {
const data = useFragment(measuresFragment, risk); const data = useFragment(measuresFragment, risk);
const connectionId = data.measures.__id; const connectionId = data.measures.__id;
const measures = data.measures?.edges?.map((edge) => edge.node) ?? []; const measures = data.measures?.edges?.map((edge) => edge.node) ?? [];
const { isAuthorized } = use(PermissionsContext);
const canLinkMeasure = isAuthorized("Risk", "createRiskMeasureMapping");
const canUnlinkMeasure = isAuthorized("Risk", "deleteRiskMeasureMapping");
const readOnly = !canLinkMeasure && !canUnlinkMeasure;
const incrementOptions = { const incrementOptions = {
id: data.id, id: data.id,
node: "measures(first:0)", node: "measures(first:0)",
@@ -81,6 +89,7 @@ export default function RiskMeasuresTab() {
onDetach={detachMeasure} onDetach={detachMeasure}
params={{ riskId: data.id }} params={{ riskId: data.id }}
connectionId={connectionId} connectionId={connectionId}
readOnly={readOnly}
/> />
); );
} }

View File

@@ -3,6 +3,8 @@ import type { RiskObligationsTabFragment$key } from "./__generated__/RiskObligat
import { useOutletContext } from "react-router"; import { useOutletContext } from "react-router";
import { LinkedObligationsCard } from "/components/obligations/LinkedObligationsCard"; import { LinkedObligationsCard } from "/components/obligations/LinkedObligationsCard";
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement"; import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
export const obligationsFragment = graphql` export const obligationsFragment = graphql`
fragment RiskObligationsTabFragment on Risk { fragment RiskObligationsTabFragment on Risk {
@@ -53,6 +55,12 @@ export default function RiskObligationsTab() {
const data = useFragment(obligationsFragment, risk); const data = useFragment(obligationsFragment, risk);
const connectionId = data.obligations.__id; const connectionId = data.obligations.__id;
const obligations = data.obligations?.edges?.map((edge) => edge.node) ?? []; const obligations = data.obligations?.edges?.map((edge) => edge.node) ?? [];
const { isAuthorized } = use(PermissionsContext);
const canLinkObligation = isAuthorized("Risk", "createRiskObligationMapping");
const canUnlinkObligation = isAuthorized("Risk", "deleteRiskObligationMapping");
const readOnly = !canLinkObligation && !canUnlinkObligation;
const incrementOptions = { const incrementOptions = {
id: data.id, id: data.id,
node: "obligations(first:0)", node: "obligations(first:0)",
@@ -82,6 +90,7 @@ export default function RiskObligationsTab() {
params={{ riskId: data.id }} params={{ riskId: data.id }}
connectionId={connectionId} connectionId={connectionId}
variant="table" variant="table"
readOnly={readOnly}
/> />
); );
} }

View File

@@ -535,6 +535,7 @@ function MembershipRowContent(props: {
{availableRoles.includes("OWNER") && <Option value="OWNER">{__("Owner")}</Option>} {availableRoles.includes("OWNER") && <Option value="OWNER">{__("Owner")}</Option>}
{availableRoles.includes("ADMIN") && <Option value="ADMIN">{__("Admin")}</Option>} {availableRoles.includes("ADMIN") && <Option value="ADMIN">{__("Admin")}</Option>}
{availableRoles.includes("VIEWER") && <Option value="VIEWER">{__("Viewer")}</Option>} {availableRoles.includes("VIEWER") && <Option value="VIEWER">{__("Viewer")}</Option>}
{availableRoles.includes("AUDITOR") && <Option value="AUDITOR">{__("Auditor")}</Option>}
{availableRoles.includes("EMPLOYEE") && <Option value="EMPLOYEE">{__("Employee")}</Option>} {availableRoles.includes("EMPLOYEE") && <Option value="EMPLOYEE">{__("Employee")}</Option>}
</Select> </Select>
</Field> </Field>
@@ -549,6 +550,9 @@ function MembershipRowContent(props: {
{selectedRole === "VIEWER" && ( {selectedRole === "VIEWER" && (
<p>{__("Read-only access")}</p> <p>{__("Read-only access")}</p>
)} )}
{selectedRole === "AUDITOR" && (
<p>{__("Read-only access without settings, tasks and meetings")}</p>
)}
{selectedRole === "EMPLOYEE" && ( {selectedRole === "EMPLOYEE" && (
<p>{__("Access to employee page")}</p> <p>{__("Access to employee page")}</p>
)} )}

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<0bd95d20e79294c530610625c86e88d7>> * @generated SignedSource<<dd7d5d34935f87fcfb658ca6cfc5e794>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -10,7 +10,7 @@
import { ReaderFragment } from 'relay-runtime'; import { ReaderFragment } from 'relay-runtime';
export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING"; export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING";
export type MembershipRole = "ADMIN" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type MembersSettingsTabInvitationsFragment$data = { export type MembersSettingsTabInvitationsFragment$data = {
readonly id: string; readonly id: string;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<8fcd99714c4bf7dba138fcb0de398a3a>> * @generated SignedSource<<c3bf3676d3f8688315fc342efcf54547>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,7 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ReaderFragment } from 'relay-runtime'; import { ReaderFragment } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type UserAuthMethod = "PASSWORD" | "SAML"; export type UserAuthMethod = "PASSWORD" | "SAML";
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type MembersSettingsTabMembershipsFragment$data = { export type MembersSettingsTabMembershipsFragment$data = {

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<9aaf763355340403cfd0c9666b61be19>> * @generated SignedSource<<e40f8c235af2bfed3a33bdf0e5a3c24b>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -9,7 +9,7 @@
// @ts-nocheck // @ts-nocheck
import { ConcreteRequest } from 'relay-runtime'; import { ConcreteRequest } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "EMPLOYEE" | "OWNER" | "VIEWER"; export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type UpdateMembershipInput = { export type UpdateMembershipInput = {
memberId: string; memberId: string;
organizationId: string; organizationId: string;

View File

@@ -91,11 +91,13 @@ export default function VendorDetailPage(props: Props) {
</div> </div>
{!isSnapshotMode && ( {!isSnapshotMode && (
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{isAuthorized("Vendor", "assessVendor") && (
<ImportAssessmentDialog vendorId={vendor.id!}> <ImportAssessmentDialog vendorId={vendor.id!}>
<Button icon={IconPageTextLine} variant="secondary"> <Button icon={IconPageTextLine} variant="secondary">
{__("Assessment From Website")} {__("Assessment From Website")}
</Button> </Button>
</ImportAssessmentDialog> </ImportAssessmentDialog>
)}
{isAuthorized("Vendor", "deleteVendor") && ( {isAuthorized("Vendor", "deleteVendor") && (
<ActionDropdown variant="secondary"> <ActionDropdown variant="secondary">
<DropdownItem <DropdownItem

View File

@@ -11,14 +11,15 @@ import {
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { useVendorForm } from "/hooks/forms/useVendorForm"; import { useVendorForm } from "/hooks/forms/useVendorForm";
import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVendorFormFragment.graphql"; import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVendorFormFragment.graphql";
import { useOutletContext } from "react-router"; import { useOutletContext, useParams } from "react-router";
import { import {
certificationCategoryLabel, certificationCategoryLabel,
certifications, certifications,
objectEntries, objectEntries,
} from "@probo/helpers"; } from "@probo/helpers";
import { useRef, useState } from "react"; import { use, useRef, useState } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { PermissionsContext } from "/providers/PermissionsContext";
/** /**
* Vendor certifications tab * Vendor certifications tab
@@ -29,9 +30,13 @@ export default function VendorCertificationsTab() {
}>(); }>();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { control, handleSubmit } = useVendorForm(vendor); const { control, handleSubmit } = useVendorForm(vendor);
const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const canUpdateVendor = isAuthorized("Vendor", "updateVendor");
return ( return (
<form className="space-y-4" onSubmit={handleSubmit}> <form className="space-y-4" onSubmit={!isSnapshotMode && canUpdateVendor ? handleSubmit : undefined}>
<Card padded> <Card padded>
<Controller <Controller
control={control} control={control}
@@ -40,13 +45,16 @@ export default function VendorCertificationsTab() {
<Certifications <Certifications
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value ?? []} value={field.value ?? []}
readOnly={isSnapshotMode || !canUpdateVendor}
/> />
)} )}
/> />
</Card> </Card>
{!isSnapshotMode && canUpdateVendor && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button type="submit">{__("Update vendor")}</Button> <Button type="submit">{__("Update vendor")}</Button>
</div> </div>
)}
</form> </form>
); );
} }
@@ -54,6 +62,7 @@ export default function VendorCertificationsTab() {
type CertificationsProps = { type CertificationsProps = {
value: string[]; value: string[];
onValueChange: (value: string[]) => void; onValueChange: (value: string[]) => void;
readOnly?: boolean;
}; };
/** /**
@@ -94,6 +103,9 @@ function Certifications(props: CertificationsProps) {
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{certifications.map((certification) => ( {certifications.map((certification) => (
<Badge asChild size="md" key={certification}> <Badge asChild size="md" key={certification}>
{props.readOnly ? (
<span>{certification}</span>
) : (
<button <button
onClick={() => removeCertificate(certification)} onClick={() => removeCertificate(certification)}
type="button" type="button"
@@ -108,17 +120,20 @@ function Certifications(props: CertificationsProps) {
<IconCrossLargeX size={12} /> <IconCrossLargeX size={12} />
</div> </div>
</button> </button>
)}
</Badge> </Badge>
))} ))}
</div> </div>
</div> </div>
))} ))}
{!props.readOnly && (
<CertificationInput <CertificationInput
certifications={categorizedCertifications.filter( certifications={categorizedCertifications.filter(
(c) => !props.value.includes(c) (c) => !props.value.includes(c)
)} )}
onAdd={addCertificate} onAdd={addCertificate}
/> />
)}
</div> </div>
); );
} }

View File

@@ -20,6 +20,8 @@ import type { VendorComplianceTabFragment_report$key } from "./__generated__/Ven
import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { sprintf, fileSize, formatDate } from "@probo/helpers"; import { sprintf, fileSize, formatDate } from "@probo/helpers";
import { SortableTable, SortableTh } from "/components/SortableTable"; import { SortableTable, SortableTh } from "/components/SortableTable";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
export const complianceReportsFragment = graphql` export const complianceReportsFragment = graphql`
fragment VendorComplianceTabFragment on Vendor fragment VendorComplianceTabFragment on Vendor
@@ -103,6 +105,9 @@ export default function VendorComplianceTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const { isAuthorized } = use(PermissionsContext);
const canUploadReport = isAuthorized("Vendor", "uploadVendorComplianceReport");
const canDeleteReport = isAuthorized("VendorComplianceReport", "deleteVendorComplianceReport");
usePageTitle(vendor.name + " - " + __("Compliance reports")); usePageTitle(vendor.name + " - " + __("Compliance reports"));
@@ -130,7 +135,7 @@ export default function VendorComplianceTab() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{!isSnapshotMode && ( {!isSnapshotMode && canUploadReport && (
<Dropzone <Dropzone
description={__("Only PDF files up to 10MB are allowed")} description={__("Only PDF files up to 10MB are allowed")}
isUploading={isMutating} isUploading={isMutating}
@@ -148,7 +153,7 @@ export default function VendorComplianceTab() {
<SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh> <SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh>
<Th>{__("Valid until")}</Th> <Th>{__("Valid until")}</Th>
<Th>{__("File size")}</Th> <Th>{__("File size")}</Th>
{!isSnapshotMode && <Th>{__("Actions")}</Th>} {!isSnapshotMode && canDeleteReport && <Th>{__("Actions")}</Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -158,6 +163,7 @@ export default function VendorComplianceTab() {
reportKey={report} reportKey={report}
connectionId={connectionId} connectionId={connectionId}
isSnapshotMode={isSnapshotMode} isSnapshotMode={isSnapshotMode}
canDelete={canDeleteReport}
/> />
))} ))}
</Tbody> </Tbody>
@@ -170,6 +176,7 @@ type ReportRowProps = {
reportKey: VendorComplianceTabFragment_report$key; reportKey: VendorComplianceTabFragment_report$key;
connectionId: string; connectionId: string;
isSnapshotMode: boolean; isSnapshotMode: boolean;
canDelete?: boolean;
}; };
function ReportRow(props: ReportRowProps) { function ReportRow(props: ReportRowProps) {
@@ -212,7 +219,7 @@ function ReportRow(props: ReportRowProps) {
<Td>{formatDate(report.reportDate)}</Td> <Td>{formatDate(report.reportDate)}</Td>
<Td>{formatDate(report.validUntil)}</Td> <Td>{formatDate(report.validUntil)}</Td>
<Td>{fileSize(__, report.file?.size)}</Td> <Td>{fileSize(__, report.file?.size)}</Td>
{!props.isSnapshotMode && ( {!props.isSnapshotMode && props.canDelete && (
<Td width={50} className="text-end"> <Td width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
<DropdownItem <DropdownItem

View File

@@ -25,7 +25,8 @@ import { sprintf } from "@probo/helpers";
import { SortableTable, SortableTh } from "/components/SortableTable"; import { SortableTable, SortableTh } from "/components/SortableTable";
import { CreateContactDialog } from "../dialogs/CreateContactDialog"; import { CreateContactDialog } from "../dialogs/CreateContactDialog";
import { EditContactDialog } from "../dialogs/EditContactDialog"; import { EditContactDialog } from "../dialogs/EditContactDialog";
import { useState } from "react"; import { use, useState } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
export const vendorContactsFragment = graphql` export const vendorContactsFragment = graphql`
fragment VendorContactsTabFragment on Vendor fragment VendorContactsTabFragment on Vendor
@@ -98,6 +99,11 @@ export default function VendorContactsTab() {
phone?: string | null; phone?: string | null;
role?: string | null; role?: string | null;
} | null>(null); } | null>(null);
const { isAuthorized } = use(PermissionsContext);
const canCreateContact = isAuthorized("Vendor", "createVendorContact");
const canUpdateContact = isAuthorized("VendorContact", "updateVendorContact");
const canDeleteContact = isAuthorized("VendorContact", "deleteVendorContact");
const hasAnyAction = canUpdateContact || canDeleteContact;
usePageTitle(vendor.name + " - " + __("Contacts")); usePageTitle(vendor.name + " - " + __("Contacts"));
@@ -107,7 +113,7 @@ export default function VendorContactsTab() {
title={__("Contacts")} title={__("Contacts")}
description={__("Manage vendor contacts and their information.")} description={__("Manage vendor contacts and their information.")}
> >
{!isSnapshotMode && ( {!isSnapshotMode && canCreateContact && (
<CreateContactDialog <CreateContactDialog
vendorId={vendor.id} vendorId={vendor.id}
connectionId={connectionId} connectionId={connectionId}
@@ -124,7 +130,7 @@ export default function VendorContactsTab() {
<SortableTh field="EMAIL">{__("Email")}</SortableTh> <SortableTh field="EMAIL">{__("Email")}</SortableTh>
<Th>{__("Phone")}</Th> <Th>{__("Phone")}</Th>
<Th>{__("Role")}</Th> <Th>{__("Role")}</Th>
{!isSnapshotMode && <Th>{__("Actions")}</Th>} {!isSnapshotMode && hasAnyAction && <Th>{__("Actions")}</Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -135,12 +141,14 @@ export default function VendorContactsTab() {
connectionId={connectionId} connectionId={connectionId}
onEdit={setEditingContact} onEdit={setEditingContact}
isSnapshotMode={isSnapshotMode} isSnapshotMode={isSnapshotMode}
canUpdate={canUpdateContact}
canDelete={canDeleteContact}
/> />
))} ))}
</Tbody> </Tbody>
</SortableTable> </SortableTable>
{editingContact && !isSnapshotMode && ( {editingContact && !isSnapshotMode && canUpdateContact && (
<EditContactDialog <EditContactDialog
contactId={editingContact.id} contactId={editingContact.id}
contact={editingContact} contact={editingContact}
@@ -162,6 +170,8 @@ type ContactRowProps = {
role?: string | null; role?: string | null;
}) => void; }) => void;
isSnapshotMode: boolean; isSnapshotMode: boolean;
canUpdate?: boolean;
canDelete?: boolean;
}; };
function ContactRow(props: ContactRowProps) { function ContactRow(props: ContactRowProps) {
@@ -175,6 +185,7 @@ function ContactRow(props: ContactRowProps) {
successMessage: __("Contact deleted successfully"), successMessage: __("Contact deleted successfully"),
errorMessage: __("Failed to delete contact"), errorMessage: __("Failed to delete contact"),
}); });
const hasAnyAction = props.canUpdate || props.canDelete;
const handleDelete = () => { const handleDelete = () => {
confirm( confirm(
@@ -226,9 +237,10 @@ function ContactRow(props: ContactRowProps) {
)} )}
</Td> </Td>
<Td>{contact.role || __("—")}</Td> <Td>{contact.role || __("—")}</Td>
{!props.isSnapshotMode && ( {!props.isSnapshotMode && hasAnyAction && (
<Td width={50} className="text-end"> <Td width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
{props.canUpdate && (
<DropdownItem <DropdownItem
icon={IconPencil} icon={IconPencil}
onClick={() => props.onEdit({ onClick={() => props.onEdit({
@@ -241,6 +253,8 @@ function ContactRow(props: ContactRowProps) {
> >
{__("Edit")} {__("Edit")}
</DropdownItem> </DropdownItem>
)}
{props.canDelete && (
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
onClick={handleDelete} onClick={handleDelete}
@@ -248,6 +262,7 @@ function ContactRow(props: ContactRowProps) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
)}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -59,6 +59,13 @@ export default function VendorOverviewTab() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext); const { isAuthorized } = use(PermissionsContext);
const canUpdateVendor = isAuthorized("Vendor", "updateVendor");
const canUploadBAA = isAuthorized("Vendor", "uploadVendorBusinessAssociateAgreement");
const canUpdateBAA = isAuthorized("Vendor", "updateVendorBusinessAssociateAgreement");
const canDeleteBAA = isAuthorized("Vendor", "deleteVendorBusinessAssociateAgreement");
const canUploadDPA = isAuthorized("Vendor", "uploadVendorDataPrivacyAgreement");
const canUpdateDPA = isAuthorized("Vendor", "updateVendorDataPrivacyAgreement");
const canDeleteDPA = isAuthorized("Vendor", "deleteVendorDataPrivacyAgreement");
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") },
@@ -128,8 +135,10 @@ export default function VendorOverviewTab() {
usePageTitle(vendor.name + " - " + __("Overview")); usePageTitle(vendor.name + " - " + __("Overview"));
const isFormDisabled = isSubmitting || isSnapshotMode || !canUpdateVendor;
return ( return (
<form onSubmit={isSnapshotMode ? undefined : handleSubmit} className="space-y-12"> <form onSubmit={isSnapshotMode || !canUpdateVendor ? undefined : handleSubmit} className="space-y-12">
{/* Vendor Details */} {/* Vendor Details */}
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-base font-medium">{__("Vendor details")}</h2> <h2 className="text-base font-medium">{__("Vendor details")}</h2>
@@ -139,14 +148,14 @@ export default function VendorOverviewTab() {
label={__("Name")} label={__("Name")}
type="text" type="text"
error={errors.name?.message} error={errors.name?.message}
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
/> />
<Field <Field
{...register("description")} {...register("description")}
label={__("Description")} label={__("Description")}
type="textarea" type="textarea"
error={errors.description?.message} error={errors.description?.message}
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
/> />
<ControlledField <ControlledField
control={control} control={control}
@@ -155,7 +164,7 @@ export default function VendorOverviewTab() {
label={__("Category")} label={__("Category")}
placeholder={__("Select a category")} placeholder={__("Select a category")}
error={errors.category?.message} error={errors.category?.message}
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
> >
{vendorCategories.map((category) => ( {vendorCategories.map((category) => (
<Option key={category.value} value={category.value}> <Option key={category.value} value={category.value}>
@@ -168,21 +177,21 @@ export default function VendorOverviewTab() {
label={__("Legal name")} label={__("Legal name")}
type="text" type="text"
error={errors.legalName?.message} error={errors.legalName?.message}
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
/> />
<Field <Field
{...register("headquarterAddress")} {...register("headquarterAddress")}
label={__("Headquarter address")} label={__("Headquarter address")}
type="textarea" type="textarea"
error={errors.headquarterAddress?.message} error={errors.headquarterAddress?.message}
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
/> />
<Field <Field
{...register("websiteUrl")} {...register("websiteUrl")}
label={__("Website URL")} label={__("Website URL")}
type="text" type="text"
error={errors.websiteUrl?.message} error={errors.websiteUrl?.message}
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
/> />
</Card> </Card>
</div> </div>
@@ -193,7 +202,7 @@ export default function VendorOverviewTab() {
<CountriesField <CountriesField
control={control} control={control}
name="countries" name="countries"
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
/> />
</Card> </Card>
</div> </div>
@@ -208,7 +217,7 @@ export default function VendorOverviewTab() {
name="businessOwnerId" name="businessOwnerId"
label={__("Business owner")} label={__("Business owner")}
error={errors.businessOwnerId?.message} error={errors.businessOwnerId?.message}
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
optional={true} optional={true}
/> />
<PeopleSelectField <PeopleSelectField
@@ -217,7 +226,7 @@ export default function VendorOverviewTab() {
name="securityOwnerId" name="securityOwnerId"
label={__("Security owner")} label={__("Security owner")}
error={errors.securityOwnerId?.message} error={errors.securityOwnerId?.message}
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
optional={true} optional={true}
/> />
</Card> </Card>
@@ -246,7 +255,7 @@ export default function VendorOverviewTab() {
type="text" type="text"
placeholder="https://..." placeholder="https://..."
variant="ghost" variant="ghost"
disabled={isSubmitting || isSnapshotMode} disabled={isFormDisabled}
/> />
</div> </div>
))} ))}
@@ -287,8 +296,7 @@ export default function VendorOverviewTab() {
> >
{__("Download PDF")} {__("Download PDF")}
</Button> </Button>
{!isSnapshotMode && ( {!isSnapshotMode && canUpdateBAA && (
<>
<EditBusinessAssociateAgreementDialog <EditBusinessAssociateAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
agreement={{ agreement={{
@@ -299,6 +307,8 @@ export default function VendorOverviewTab() {
> >
<Button variant="quaternary" icon={IconPencil} /> <Button variant="quaternary" icon={IconPencil} />
</EditBusinessAssociateAgreementDialog> </EditBusinessAssociateAgreementDialog>
)}
{!isSnapshotMode && canDeleteBAA && (
<DeleteBusinessAssociateAgreementDialog <DeleteBusinessAssociateAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
fileName={businessAssociateAgreement.fileName} fileName={businessAssociateAgreement.fileName}
@@ -306,11 +316,10 @@ export default function VendorOverviewTab() {
> >
<Button variant="quaternary" icon={IconTrashCan} /> <Button variant="quaternary" icon={IconTrashCan} />
</DeleteBusinessAssociateAgreementDialog> </DeleteBusinessAssociateAgreementDialog>
</>
)} )}
</> </>
) : ( ) : (
!isSnapshotMode && ( !isSnapshotMode && canUploadBAA && (
<UploadBusinessAssociateAgreementDialog <UploadBusinessAssociateAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
onSuccess={() => window.location.reload()} onSuccess={() => window.location.reload()}
@@ -354,8 +363,7 @@ export default function VendorOverviewTab() {
> >
{__("Download PDF")} {__("Download PDF")}
</Button> </Button>
{!isSnapshotMode && ( {!isSnapshotMode && canUpdateDPA && (
<>
<EditDataPrivacyAgreementDialog <EditDataPrivacyAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
agreement={{ agreement={{
@@ -366,6 +374,8 @@ export default function VendorOverviewTab() {
> >
<Button variant="quaternary" icon={IconPencil} /> <Button variant="quaternary" icon={IconPencil} />
</EditDataPrivacyAgreementDialog> </EditDataPrivacyAgreementDialog>
)}
{!isSnapshotMode && canDeleteDPA && (
<DeleteDataPrivacyAgreementDialog <DeleteDataPrivacyAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
fileName={dataPrivacyAgreement.fileName} fileName={dataPrivacyAgreement.fileName}
@@ -373,11 +383,10 @@ export default function VendorOverviewTab() {
> >
<Button variant="quaternary" icon={IconTrashCan} /> <Button variant="quaternary" icon={IconTrashCan} />
</DeleteDataPrivacyAgreementDialog> </DeleteDataPrivacyAgreementDialog>
</>
)} )}
</> </>
) : ( ) : (
!isSnapshotMode && ( !isSnapshotMode && canUploadDPA && (
<UploadDataPrivacyAgreementDialog <UploadDataPrivacyAgreementDialog
vendorId={vendor.id} vendorId={vendor.id}
onSuccess={() => window.location.reload()} onSuccess={() => window.location.reload()}

View File

@@ -21,7 +21,8 @@ import type { VendorRiskAssessmentTabFragment_assessment$key } from "./__generat
import { SortableTable, SortableTh } from "/components/SortableTable"; import { SortableTable, SortableTh } from "/components/SortableTable";
import { CreateRiskAssessmentDialog } from "../dialogs/CreateRiskAssessmentDialog"; import { CreateRiskAssessmentDialog } from "../dialogs/CreateRiskAssessmentDialog";
import clsx from "clsx"; import clsx from "clsx";
import { useState } from "react"; import { use, useState } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
const riskAssessmentsFragment = graphql` const riskAssessmentsFragment = graphql`
fragment VendorRiskAssessmentTabFragment on Vendor fragment VendorRiskAssessmentTabFragment on Vendor
@@ -81,6 +82,8 @@ export default function VendorRiskAssessmentTab() {
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const isSnapshotMode = Boolean(snapshotId); const isSnapshotMode = Boolean(snapshotId);
const [expanded, setExpanded] = useState<string | null>(null); const [expanded, setExpanded] = useState<string | null>(null);
const { isAuthorized } = use(PermissionsContext);
const canCreateRiskAssessment = isAuthorized("Vendor", "createVendorRiskAssessment");
usePageTitle(vendor.name + " - " + __("Risk Assessments")); usePageTitle(vendor.name + " - " + __("Risk Assessments"));
@@ -88,7 +91,7 @@ export default function VendorRiskAssessmentTab() {
return ( return (
<div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2"> <div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2">
{__("No risk assessments found")} {__("No risk assessments found")}
{!isSnapshotMode && ( {!isSnapshotMode && canCreateRiskAssessment && (
<CreateRiskAssessmentDialog <CreateRiskAssessmentDialog
vendorId={vendor.id} vendorId={vendor.id}
connection={data.riskAssessments.__id} connection={data.riskAssessments.__id}
@@ -116,7 +119,7 @@ export default function VendorRiskAssessmentTab() {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{!isSnapshotMode && ( {!isSnapshotMode && canCreateRiskAssessment && (
<CreateRiskAssessmentDialog <CreateRiskAssessmentDialog
vendorId={vendor.id} vendorId={vendor.id}
connection={data.riskAssessments.__id} connection={data.riskAssessments.__id}

View File

@@ -25,7 +25,8 @@ import { sprintf } from "@probo/helpers";
import { SortableTable, SortableTh } from "/components/SortableTable"; import { SortableTable, SortableTh } from "/components/SortableTable";
import { CreateServiceDialog } from "../dialogs/CreateServiceDialog"; import { CreateServiceDialog } from "../dialogs/CreateServiceDialog";
import { EditServiceDialog } from "../dialogs/EditServiceDialog"; import { EditServiceDialog } from "../dialogs/EditServiceDialog";
import { useState } from "react"; import { use, useState } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
export const vendorServicesFragment = graphql` export const vendorServicesFragment = graphql`
fragment VendorServicesTabFragment on Vendor fragment VendorServicesTabFragment on Vendor
@@ -94,6 +95,11 @@ export default function VendorServicesTab() {
name: string; name: string;
description?: string | null; description?: string | null;
} | null>(null); } | null>(null);
const { isAuthorized } = use(PermissionsContext);
const canCreateService = isAuthorized("Vendor", "createVendorService");
const canUpdateService = isAuthorized("VendorService", "updateVendorService");
const canDeleteService = isAuthorized("VendorService", "deleteVendorService");
const hasAnyAction = canUpdateService || canDeleteService;
usePageTitle(vendor.name + " - " + __("Services")); usePageTitle(vendor.name + " - " + __("Services"));
@@ -103,7 +109,7 @@ export default function VendorServicesTab() {
title={__("Services")} title={__("Services")}
description={__("Manage services provided by this vendor.")} description={__("Manage services provided by this vendor.")}
> >
{!isSnapshotMode && ( {!isSnapshotMode && canCreateService && (
<CreateServiceDialog <CreateServiceDialog
vendorId={vendor.id} vendorId={vendor.id}
connectionId={connectionId} connectionId={connectionId}
@@ -118,7 +124,7 @@ export default function VendorServicesTab() {
<Tr> <Tr>
<SortableTh field="NAME">{__("Name")}</SortableTh> <SortableTh field="NAME">{__("Name")}</SortableTh>
<Th>{__("Description")}</Th> <Th>{__("Description")}</Th>
{!isSnapshotMode && <Th>{__("Actions")}</Th>} {!isSnapshotMode && hasAnyAction && <Th>{__("Actions")}</Th>}
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -129,12 +135,14 @@ export default function VendorServicesTab() {
connectionId={connectionId} connectionId={connectionId}
onEdit={setEditingService} onEdit={setEditingService}
isSnapshotMode={isSnapshotMode} isSnapshotMode={isSnapshotMode}
canUpdate={canUpdateService}
canDelete={canDeleteService}
/> />
))} ))}
</Tbody> </Tbody>
</SortableTable> </SortableTable>
{editingService && !isSnapshotMode && ( {editingService && !isSnapshotMode && canUpdateService && (
<EditServiceDialog <EditServiceDialog
serviceId={editingService.id} serviceId={editingService.id}
service={editingService} service={editingService}
@@ -154,6 +162,8 @@ type ServiceRowProps = {
description?: string | null; description?: string | null;
}) => void; }) => void;
isSnapshotMode: boolean; isSnapshotMode: boolean;
canUpdate?: boolean;
canDelete?: boolean;
}; };
function ServiceRow(props: ServiceRowProps) { function ServiceRow(props: ServiceRowProps) {
@@ -167,6 +177,7 @@ function ServiceRow(props: ServiceRowProps) {
successMessage: __("Service deleted successfully"), successMessage: __("Service deleted successfully"),
errorMessage: __("Failed to delete service"), errorMessage: __("Failed to delete service"),
}); });
const hasAnyAction = props.canUpdate || props.canDelete;
const handleDelete = () => { const handleDelete = () => {
confirm( confirm(
@@ -194,9 +205,10 @@ function ServiceRow(props: ServiceRowProps) {
<Tr> <Tr>
<Td>{service.name}</Td> <Td>{service.name}</Td>
<Td>{service.description || __("—")}</Td> <Td>{service.description || __("—")}</Td>
{!props.isSnapshotMode && ( {!props.isSnapshotMode && hasAnyAction && (
<Td width={50} className="text-end"> <Td width={50} className="text-end">
<ActionDropdown> <ActionDropdown>
{props.canUpdate && (
<DropdownItem <DropdownItem
icon={IconPencil} icon={IconPencil}
onClick={() => props.onEdit({ onClick={() => props.onEdit({
@@ -207,6 +219,8 @@ function ServiceRow(props: ServiceRowProps) {
> >
{__("Edit")} {__("Edit")}
</DropdownItem> </DropdownItem>
)}
{props.canDelete && (
<DropdownItem <DropdownItem
icon={IconTrashCan} icon={IconTrashCan}
onClick={handleDelete} onClick={handleDelete}
@@ -214,6 +228,7 @@ function ServiceRow(props: ServiceRowProps) {
> >
{__("Delete")} {__("Delete")}
</DropdownItem> </DropdownItem>
)}
</ActionDropdown> </ActionDropdown>
</Td> </Td>
)} )}

View File

@@ -165,10 +165,14 @@ const routes = [
path: "", path: "",
Component: () => { Component: () => {
const { role } = use(PermissionsContext); const { role } = use(PermissionsContext);
if (role === Role.EMPLOYEE) { switch (role) {
case Role.EMPLOYEE:
return <Navigate to="employee" />; return <Navigate to="employee" />;
} case Role.AUDITOR:
return <Navigate to="measures" />;
default:
return <Navigate to="tasks" />; return <Navigate to="tasks" />;
}
}, },
}, },
{ {

View File

@@ -2,6 +2,7 @@ export const Role = {
OWNER: "OWNER", OWNER: "OWNER",
ADMIN: "ADMIN", ADMIN: "ADMIN",
VIEWER: "VIEWER", VIEWER: "VIEWER",
AUDITOR: "AUDITOR",
EMPLOYEE: "EMPLOYEE", EMPLOYEE: "EMPLOYEE",
} as const } as const
@@ -9,11 +10,11 @@ export type Role = (typeof Role)[keyof typeof Role];
export function getAssignableRoles(currentRole: Role): Role[] { export function getAssignableRoles(currentRole: Role): Role[] {
if (currentRole === Role.OWNER) { if (currentRole === Role.OWNER) {
return [Role.OWNER, Role.ADMIN, Role.VIEWER, Role.EMPLOYEE]; return [Role.OWNER, Role.ADMIN, Role.VIEWER, Role.AUDITOR, Role.EMPLOYEE];
} }
if (currentRole === Role.ADMIN) { if (currentRole === Role.ADMIN) {
return [Role.ADMIN, Role.VIEWER, Role.EMPLOYEE]; return [Role.ADMIN, Role.VIEWER, Role.AUDITOR, Role.EMPLOYEE];
} }
return []; return [];

View File

@@ -31,6 +31,7 @@ const (
RoleAdmin Role = "ADMIN" RoleAdmin Role = "ADMIN"
RoleEmployee Role = "EMPLOYEE" RoleEmployee Role = "EMPLOYEE"
RoleViewer Role = "VIEWER" RoleViewer Role = "VIEWER"
RoleAuditor Role = "AUDITOR"
RoleFull Role = "FULL" RoleFull Role = "FULL"
) )
@@ -257,33 +258,32 @@ const (
) )
var ( var (
AllRoles = []Role{RoleOwner, RoleAdmin, RoleEmployee, RoleViewer, RoleFull} AllRoles = []Role{RoleOwner, RoleAdmin, RoleEmployee, RoleViewer, RoleAuditor, RoleFull}
NonEmployeeRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleFull}
EditRoles = []Role{RoleOwner, RoleAdmin, RoleFull} EditRoles = []Role{RoleOwner, RoleAdmin, RoleFull}
CoreRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleFull}
NonEmployeeRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleAuditor, RoleFull}
InternalRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleEmployee, RoleFull}
) )
var Permissions = map[uint16]map[Action][]Role{ var Permissions = map[uint16]map[Action][]Role{
coredata.OrganizationEntityType: { coredata.OrganizationEntityType: {
ActionGet: AllRoles, ActionGet: AllRoles,
ActionListSignableDocuments: AllRoles,
ActionGetLogoUrl: AllRoles, ActionGetLogoUrl: AllRoles,
ActionListSignableDocuments: InternalRoles,
ActionListDocuments: NonEmployeeRoles, ActionListDocuments: NonEmployeeRoles,
ActionGetHorizontalLogoUrl: NonEmployeeRoles, ActionGetHorizontalLogoUrl: NonEmployeeRoles,
ActionMemberships: NonEmployeeRoles,
ActionPeoples: NonEmployeeRoles, ActionPeoples: NonEmployeeRoles,
ActionTotalCount: NonEmployeeRoles, ActionTotalCount: NonEmployeeRoles,
ActionListMembers: NonEmployeeRoles,
ActionListInvitations: NonEmployeeRoles,
ActionListSlackConnections: NonEmployeeRoles,
ActionListFrameworks: NonEmployeeRoles, ActionListFrameworks: NonEmployeeRoles,
ActionListControls: NonEmployeeRoles, ActionListControls: NonEmployeeRoles,
ActionListVendors: NonEmployeeRoles, ActionListVendors: NonEmployeeRoles,
ActionListPeople: NonEmployeeRoles, ActionListPeople: NonEmployeeRoles,
ActionListMeetings: NonEmployeeRoles,
ActionListMeasures: NonEmployeeRoles, ActionListMeasures: NonEmployeeRoles,
ActionListRisks: NonEmployeeRoles, ActionListRisks: NonEmployeeRoles,
ActionListTasks: NonEmployeeRoles,
ActionListAssets: NonEmployeeRoles, ActionListAssets: NonEmployeeRoles,
ActionListData: NonEmployeeRoles, ActionListData: NonEmployeeRoles,
ActionListAudits: NonEmployeeRoles, ActionListAudits: NonEmployeeRoles,
@@ -292,13 +292,20 @@ var Permissions = map[uint16]map[Action][]Role{
ActionListContinualImprovements: NonEmployeeRoles, ActionListContinualImprovements: NonEmployeeRoles,
ActionListProcessingActivities: NonEmployeeRoles, ActionListProcessingActivities: NonEmployeeRoles,
ActionListSnapshots: NonEmployeeRoles, ActionListSnapshots: NonEmployeeRoles,
ActionListTrustCenterFiles: NonEmployeeRoles,
ActionGetTrustCenter: NonEmployeeRoles,
ActionGetCustomDomain: NonEmployeeRoles,
ActionListSAMLConfigurations: NonEmployeeRoles,
ActionConfirmEmail: NonEmployeeRoles, ActionConfirmEmail: NonEmployeeRoles,
ActionAcceptInvitation: NonEmployeeRoles, ActionAcceptInvitation: NonEmployeeRoles,
ActionListTrustCenterFiles: CoreRoles,
ActionGetTrustCenter: CoreRoles,
ActionMemberships: CoreRoles,
ActionListMembers: CoreRoles,
ActionListInvitations: CoreRoles,
ActionListSlackConnections: CoreRoles,
ActionGetCustomDomain: CoreRoles,
ActionListSAMLConfigurations: CoreRoles,
ActionListMeetings: CoreRoles,
ActionListTasks: CoreRoles,
ActionUpdateOrganization: EditRoles, ActionUpdateOrganization: EditRoles,
ActionDeleteOrganizationHorizontalLogo: EditRoles, ActionDeleteOrganizationHorizontalLogo: EditRoles,
ActionCreateTrustCenter: EditRoles, ActionCreateTrustCenter: EditRoles,
@@ -336,11 +343,11 @@ var Permissions = map[uint16]map[Action][]Role{
ActionDeleteOrganization: {RoleOwner}, ActionDeleteOrganization: {RoleOwner},
}, },
coredata.TrustCenterEntityType: { coredata.TrustCenterEntityType: {
ActionGet: NonEmployeeRoles, ActionGet: CoreRoles,
ActionGetNdaFileUrl: NonEmployeeRoles, ActionGetNdaFileUrl: CoreRoles,
ActionGetOrganization: NonEmployeeRoles, ActionGetOrganization: CoreRoles,
ActionListAccesses: NonEmployeeRoles, ActionListAccesses: CoreRoles,
ActionListReferences: NonEmployeeRoles, ActionListReferences: CoreRoles,
ActionUpdateTrustCenter: EditRoles, ActionUpdateTrustCenter: EditRoles,
ActionUploadTrustCenterNDA: EditRoles, ActionUploadTrustCenterNDA: EditRoles,
@@ -349,26 +356,26 @@ var Permissions = map[uint16]map[Action][]Role{
ActionCreateTrustCenterReference: EditRoles, ActionCreateTrustCenterReference: EditRoles,
}, },
coredata.TrustCenterAccessEntityType: { coredata.TrustCenterAccessEntityType: {
ActionGet: NonEmployeeRoles, ActionGet: CoreRoles,
ActionActiveCount: NonEmployeeRoles, ActionActiveCount: CoreRoles,
ActionPendingRequestCount: NonEmployeeRoles, ActionPendingRequestCount: CoreRoles,
ActionAvailableDocumentAccesses: NonEmployeeRoles, ActionAvailableDocumentAccesses: CoreRoles,
ActionGetTrustCenterFile: NonEmployeeRoles, ActionGetTrustCenterFile: CoreRoles,
ActionGetReport: NonEmployeeRoles, ActionGetReport: CoreRoles,
ActionUpdateTrustCenterAccess: EditRoles, ActionUpdateTrustCenterAccess: EditRoles,
ActionDeleteTrustCenterAccess: EditRoles, ActionDeleteTrustCenterAccess: EditRoles,
}, },
coredata.TrustCenterReferenceEntityType: { coredata.TrustCenterReferenceEntityType: {
ActionGet: NonEmployeeRoles, ActionGet: CoreRoles,
ActionGetLogoUrl: NonEmployeeRoles, ActionGetLogoUrl: CoreRoles,
ActionUpdateTrustCenterReference: EditRoles, ActionUpdateTrustCenterReference: EditRoles,
ActionDeleteTrustCenterReference: EditRoles, ActionDeleteTrustCenterReference: EditRoles,
}, },
coredata.TrustCenterFileEntityType: { coredata.TrustCenterFileEntityType: {
ActionGet: NonEmployeeRoles, ActionGet: CoreRoles,
ActionGetFileUrl: NonEmployeeRoles, ActionGetFileUrl: CoreRoles,
ActionUpdateTrustCenterFile: EditRoles, ActionUpdateTrustCenterFile: EditRoles,
ActionGetTrustCenterFile: EditRoles, ActionGetTrustCenterFile: EditRoles,
@@ -487,9 +494,10 @@ var Permissions = map[uint16]map[Action][]Role{
ActionDeleteControlSnapshotMapping: EditRoles, ActionDeleteControlSnapshotMapping: EditRoles,
}, },
coredata.MeasureEntityType: { coredata.MeasureEntityType: {
ActionListTasks: CoreRoles,
ActionGet: NonEmployeeRoles, ActionGet: NonEmployeeRoles,
ActionListEvidences: NonEmployeeRoles, ActionListEvidences: NonEmployeeRoles,
ActionListTasks: NonEmployeeRoles,
ActionListRisks: NonEmployeeRoles, ActionListRisks: NonEmployeeRoles,
ActionListControls: NonEmployeeRoles, ActionListControls: NonEmployeeRoles,
ActionTotalCount: NonEmployeeRoles, ActionTotalCount: NonEmployeeRoles,
@@ -499,11 +507,11 @@ var Permissions = map[uint16]map[Action][]Role{
ActionUploadMeasureEvidence: EditRoles, ActionUploadMeasureEvidence: EditRoles,
}, },
coredata.TaskEntityType: { coredata.TaskEntityType: {
ActionGet: NonEmployeeRoles, ActionGet: CoreRoles,
ActionGetAssignedTo: NonEmployeeRoles, ActionGetAssignedTo: CoreRoles,
ActionGetOrganization: NonEmployeeRoles, ActionGetOrganization: CoreRoles,
ActionGetMeasure: NonEmployeeRoles, ActionGetMeasure: CoreRoles,
ActionListEvidences: NonEmployeeRoles, ActionListEvidences: CoreRoles,
ActionUpdateTask: EditRoles, ActionUpdateTask: EditRoles,
ActionDeleteTask: EditRoles, ActionDeleteTask: EditRoles,
@@ -513,7 +521,7 @@ var Permissions = map[uint16]map[Action][]Role{
coredata.EvidenceEntityType: { coredata.EvidenceEntityType: {
ActionGet: NonEmployeeRoles, ActionGet: NonEmployeeRoles,
ActionGetFile: NonEmployeeRoles, ActionGetFile: NonEmployeeRoles,
ActionGetTask: NonEmployeeRoles, ActionGetTask: CoreRoles,
ActionGetMeasure: NonEmployeeRoles, ActionGetMeasure: NonEmployeeRoles,
ActionDeleteEvidence: EditRoles, ActionDeleteEvidence: EditRoles,
@@ -521,7 +529,8 @@ var Permissions = map[uint16]map[Action][]Role{
coredata.DocumentEntityType: { coredata.DocumentEntityType: {
ActionListSignableDocumentVersion: AllRoles, ActionListSignableDocumentVersion: AllRoles,
ActionGetSigned: AllRoles, ActionGetSigned: AllRoles,
ActionGetSignableDocument: AllRoles,
ActionGetSignableDocument: InternalRoles,
ActionGet: NonEmployeeRoles, ActionGet: NonEmployeeRoles,
ActionGetOwner: NonEmployeeRoles, ActionGetOwner: NonEmployeeRoles,
@@ -689,14 +698,14 @@ var Permissions = map[uint16]map[Action][]Role{
ActionDownloadUrl: NonEmployeeRoles, ActionDownloadUrl: NonEmployeeRoles,
}, },
coredata.TrustCenterDocumentAccessEntityType: { coredata.TrustCenterDocumentAccessEntityType: {
ActionGet: NonEmployeeRoles, ActionGet: CoreRoles,
ActionReport: NonEmployeeRoles, ActionReport: CoreRoles,
ActionTrustCenterFile: NonEmployeeRoles, ActionTrustCenterFile: CoreRoles,
}, },
coredata.MeetingEntityType: { coredata.MeetingEntityType: {
ActionGet: NonEmployeeRoles, ActionGet: CoreRoles,
ActionGetOrganization: NonEmployeeRoles, ActionGetOrganization: CoreRoles,
ActionTotalCount: NonEmployeeRoles, ActionTotalCount: CoreRoles,
ActionUpdateMeeting: EditRoles, ActionUpdateMeeting: EditRoles,
ActionDeleteMeeting: EditRoles, ActionDeleteMeeting: EditRoles,

View File

@@ -220,7 +220,7 @@ var entityRegistry = map[uint16]EntityInfo{
Table: "trust_center_references", Table: "trust_center_references",
}, },
TrustCenterDocumentAccessEntityType: { TrustCenterDocumentAccessEntityType: {
Model: "", Model: "TrustCenterDocumentAccess",
Table: "trust_center_document_accesses", Table: "trust_center_document_accesses",
}, },
CustomDomainEntityType: { CustomDomainEntityType: {

View File

@@ -26,6 +26,7 @@ const (
MembershipRoleAdmin MembershipRole = "ADMIN" MembershipRoleAdmin MembershipRole = "ADMIN"
MembershipRoleEmployee MembershipRole = "EMPLOYEE" MembershipRoleEmployee MembershipRole = "EMPLOYEE"
MembershipRoleViewer MembershipRole = "VIEWER" MembershipRoleViewer MembershipRole = "VIEWER"
MembershipRoleAuditor MembershipRole = "AUDITOR"
) )
func (r MembershipRole) String() string { func (r MembershipRole) String() string {
@@ -52,6 +53,8 @@ func (r *MembershipRole) Scan(value any) error {
*r = MembershipRoleEmployee *r = MembershipRoleEmployee
case "VIEWER": case "VIEWER":
*r = MembershipRoleViewer *r = MembershipRoleViewer
case "AUDITOR":
*r = MembershipRoleAuditor
default: default:
return fmt.Errorf("invalid MembershipRole value: %q", s) return fmt.Errorf("invalid MembershipRole value: %q", s)
} }

View File

@@ -0,0 +1 @@
ALTER TYPE authz_role ADD VALUE 'AUDITOR';

View File

@@ -38,6 +38,7 @@ enum Role {
OWNER OWNER
ADMIN ADMIN
VIEWER VIEWER
AUDITOR
FULL FULL
} }
@@ -98,6 +99,7 @@ enum MembershipRole
EMPLOYEE EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee") @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee")
VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer") VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer")
AUDITOR @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor")
} }
enum APIRole @goModel(model: "go.probo.inc/probo/pkg/coredata.APIRole") { enum APIRole @goModel(model: "go.probo.inc/probo/pkg/coredata.APIRole") {

View File

@@ -9592,6 +9592,7 @@ enum Role {
OWNER OWNER
ADMIN ADMIN
VIEWER VIEWER
AUDITOR
FULL FULL
} }
@@ -9652,6 +9653,7 @@ enum MembershipRole
EMPLOYEE EMPLOYEE
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee") @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee")
VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer") VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer")
AUDITOR @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor")
} }
enum APIRole @goModel(model: "go.probo.inc/probo/pkg/coredata.APIRole") { enum APIRole @goModel(model: "go.probo.inc/probo/pkg/coredata.APIRole") {
@@ -87915,12 +87917,14 @@ var (
"ADMIN": coredata.MembershipRoleAdmin, "ADMIN": coredata.MembershipRoleAdmin,
"EMPLOYEE": coredata.MembershipRoleEmployee, "EMPLOYEE": coredata.MembershipRoleEmployee,
"VIEWER": coredata.MembershipRoleViewer, "VIEWER": coredata.MembershipRoleViewer,
"AUDITOR": coredata.MembershipRoleAuditor,
} }
marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole = map[coredata.MembershipRole]string{ marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole = map[coredata.MembershipRole]string{
coredata.MembershipRoleOwner: "OWNER", coredata.MembershipRoleOwner: "OWNER",
coredata.MembershipRoleAdmin: "ADMIN", coredata.MembershipRoleAdmin: "ADMIN",
coredata.MembershipRoleEmployee: "EMPLOYEE", coredata.MembershipRoleEmployee: "EMPLOYEE",
coredata.MembershipRoleViewer: "VIEWER", coredata.MembershipRoleViewer: "VIEWER",
coredata.MembershipRoleAuditor: "AUDITOR",
} }
) )

View File

@@ -2540,6 +2540,7 @@ const (
RoleOwner Role = "OWNER" RoleOwner Role = "OWNER"
RoleAdmin Role = "ADMIN" RoleAdmin Role = "ADMIN"
RoleViewer Role = "VIEWER" RoleViewer Role = "VIEWER"
RoleAuditor Role = "AUDITOR"
RoleFull Role = "FULL" RoleFull Role = "FULL"
) )
@@ -2547,12 +2548,13 @@ var AllRole = []Role{
RoleOwner, RoleOwner,
RoleAdmin, RoleAdmin,
RoleViewer, RoleViewer,
RoleAuditor,
RoleFull, RoleFull,
} }
func (e Role) IsValid() bool { func (e Role) IsValid() bool {
switch e { switch e {
case RoleOwner, RoleAdmin, RoleViewer, RoleFull: case RoleOwner, RoleAdmin, RoleViewer, RoleAuditor, RoleFull:
return true return true
} }
return false return false