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

View File

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

View File

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

View File

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

View File

@@ -51,6 +51,7 @@ type Props<Params> = {
connectionId: string;
disabled?: boolean;
variant?: "card" | "table";
readOnly?: boolean;
params: Params;
@@ -102,17 +103,19 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
{variant === "card" && (
<div className="flex justify-between">
<div className="text-lg font-semibold">{__("Obligations")}</div>
<LinkedObligationDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedObligations={props.obligations}
onLink={onAttach}
onUnlink={onDetach}
>
<Button variant="tertiary" icon={IconPlusLarge}>
{__("Link obligation")}
</Button>
</LinkedObligationDialog>
{!props.readOnly && (
<LinkedObligationDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedObligations={props.obligations}
onLink={onAttach}
onUnlink={onDetach}
>
<Button variant="tertiary" icon={IconPlusLarge}>
{__("Link obligation")}
</Button>
</LinkedObligationDialog>
)}
</div>
)}
<Table className={clsx(variant === "card" && "bg-invert")}>
@@ -122,21 +125,21 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
<Th>{__("Source")}</Th>
<Th>{__("Status")}</Th>
<Th>{__("Owner")}</Th>
<Th></Th>
{!props.readOnly && <Th></Th>}
</Tr>
</Thead>
<Tbody>
{obligations.length === 0 && (
<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")}
</Td>
</Tr>
)}
{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
connectionId={props.connectionId}
disabled={props.disabled}
@@ -168,6 +171,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
function ObligationRow(props: {
obligation: LinkedObligationsCardFragment$key & { id: string };
onClick: (obligationId: string) => void;
readOnly?: boolean;
}) {
const { __ } = useTranslate();
const obligation = useFragment(linkedObligationFragment, props.obligation);
@@ -199,15 +203,17 @@ function ObligationRow(props: {
<Td>
{obligation.owner?.fullName || __("Unassigned")}
</Td>
<Td noLink width={50} className="text-end">
<Button
variant="secondary"
icon={IconTrashCan}
onClick={onDetach}
>
{__("Unlink")}
</Button>
</Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end">
<Button
variant="secondary"
icon={IconTrashCan}
onClick={onDetach}
>
{__("Unlink")}
</Button>
</Td>
)}
</Tr>
);
}

View File

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

View File

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

View File

@@ -48,6 +48,7 @@ type Props<Params> = {
onAttach: Mutation<Params>;
// Mutation to detach a risk (will receive {riskId, ...params})
onDetach: Mutation<Params>;
readOnly?: boolean;
};
/**
@@ -88,29 +89,31 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
<Th>{__("Name")}</Th>
<Th>{__("Inherent Risk")}</Th>
<Th>{__("Residual Risk")}</Th>
<Th></Th>
{!props.readOnly && <Th></Th>}
</Tr>
</Thead>
<Tbody>
{props.risks.length === 0 && (
<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")}
</Td>
</Tr>
)}
{props.risks.map((risk) => (
<RiskRow key={risk.id} risk={risk} onClick={onDetach} />
<RiskRow key={risk.id} risk={risk} onClick={onDetach} readOnly={props.readOnly} />
))}
<LinkedRisksDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedRisks={props.risks}
onLink={onAttach}
onUnlink={onDetach}
>
<TrButton colspan={4}>{__("Link risk")}</TrButton>
</LinkedRisksDialog>
{!props.readOnly && (
<LinkedRisksDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedRisks={props.risks}
onLink={onAttach}
onUnlink={onDetach}
>
<TrButton colspan={4}>{__("Link risk")}</TrButton>
</LinkedRisksDialog>
)}
</Tbody>
</Table>
</div>
@@ -120,6 +123,7 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
function RiskRow(props: {
risk: LinkedRisksCardFragment$key & { id: string };
onClick: (riskId: string) => void;
readOnly?: boolean;
}) {
const risk = useFragment(linkedRiskFragment, props.risk);
const organizationId = useOrganizationId();
@@ -134,15 +138,17 @@ function RiskRow(props: {
<Td>
<RiskBadge level={risk.residualRiskScore} />
</Td>
<Td noLink width={50} className="text-end">
<Button
variant="secondary"
onClick={() => props.onClick(risk.id)}
icon={IconTrashCan}
>
{__("Unlink")}
</Button>
</Td>
{!props.readOnly && (
<Td noLink width={50} className="text-end">
<Button
variant="secondary"
onClick={() => props.onClick(risk.id)}
icon={IconTrashCan}
>
{__("Unlink")}
</Button>
</Td>
)}
</Tr>
);
}

View File

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