@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,14 +52,10 @@ export const measureNodeQuery = graphql`
|
||||
risksInfos: risks(first: 0) {
|
||||
totalCount
|
||||
}
|
||||
tasksInfos: tasks(first: 0) {
|
||||
totalCount
|
||||
}
|
||||
controlsInfos: controls(first: 0) {
|
||||
totalCount
|
||||
}
|
||||
...MeasureRisksTabFragment
|
||||
...MeasureTasksTabFragment
|
||||
...MeasureControlsTabFragment
|
||||
...MeasureFormDialogMeasureFragment
|
||||
...MeasureEvidencesTabFragment
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<02c0b1d637edafea060ddb9ee3b3f130>>
|
||||
* @generated SignedSource<<ba6c0f7696cba9d0f9e6e092f6c0f263>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -30,10 +30,7 @@ export type MeasureGraphNodeQuery$data = {
|
||||
readonly totalCount: number;
|
||||
};
|
||||
readonly state?: MeasureState;
|
||||
readonly tasksInfos?: {
|
||||
readonly totalCount: number;
|
||||
};
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment" | "MeasureEvidencesTabFragment" | "MeasureFormDialogMeasureFragment" | "MeasureRisksTabFragment" | "MeasureTasksTabFragment">;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment" | "MeasureEvidencesTabFragment" | "MeasureFormDialogMeasureFragment" | "MeasureRisksTabFragment">;
|
||||
};
|
||||
};
|
||||
export type MeasureGraphNodeQuery = {
|
||||
@@ -128,16 +125,6 @@ v10 = {
|
||||
"storageKey": "risks(first:0)"
|
||||
},
|
||||
v11 = {
|
||||
"alias": "tasksInfos",
|
||||
"args": (v7/*: any*/),
|
||||
"concreteType": "TaskConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "tasks",
|
||||
"plural": false,
|
||||
"selections": (v8/*: any*/),
|
||||
"storageKey": "tasks(first:0)"
|
||||
},
|
||||
v12 = {
|
||||
"alias": "controlsInfos",
|
||||
"args": (v7/*: any*/),
|
||||
"concreteType": "ControlConnection",
|
||||
@@ -147,55 +134,42 @@ v12 = {
|
||||
"selections": (v8/*: any*/),
|
||||
"storageKey": "controls(first:0)"
|
||||
},
|
||||
v13 = {
|
||||
v12 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
v14 = [
|
||||
v13 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 100
|
||||
}
|
||||
],
|
||||
v15 = {
|
||||
v14 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
},
|
||||
v16 = {
|
||||
v15 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
v17 = {
|
||||
v16 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
},
|
||||
v18 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v16/*: any*/),
|
||||
(v17/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
v19 = {
|
||||
v17 = {
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
@@ -207,14 +181,14 @@ v19 = {
|
||||
}
|
||||
]
|
||||
},
|
||||
v20 = [
|
||||
v18 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 20
|
||||
}
|
||||
],
|
||||
v21 = {
|
||||
v19 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
@@ -222,8 +196,8 @@ v21 = {
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v15/*: any*/),
|
||||
(v16/*: any*/),
|
||||
(v17/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -241,7 +215,7 @@ v21 = {
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
v22 = [
|
||||
v20 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
@@ -274,17 +248,11 @@ return {
|
||||
(v9/*: any*/),
|
||||
(v10/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v12/*: any*/),
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "MeasureRisksTabFragment"
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "MeasureTasksTabFragment"
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
@@ -325,7 +293,7 @@ return {
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v13/*: any*/),
|
||||
(v12/*: any*/),
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
@@ -337,10 +305,9 @@ return {
|
||||
(v9/*: any*/),
|
||||
(v10/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v12/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v14/*: any*/),
|
||||
"args": (v13/*: any*/),
|
||||
"concreteType": "RiskConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "risks",
|
||||
@@ -378,22 +345,34 @@ return {
|
||||
"name": "residualRiskScore",
|
||||
"storageKey": null
|
||||
},
|
||||
(v13/*: any*/)
|
||||
(v12/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v15/*: any*/)
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"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)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v14/*: any*/),
|
||||
"args": (v13/*: any*/),
|
||||
"filters": null,
|
||||
"handle": "connection",
|
||||
"key": "Measure__risks",
|
||||
@@ -402,102 +381,7 @@ return {
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v14/*: 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*/),
|
||||
"args": (v18/*: any*/),
|
||||
"concreteType": "ControlConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "controls",
|
||||
@@ -541,22 +425,22 @@ return {
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v13/*: any*/)
|
||||
(v12/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v15/*: any*/)
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v21/*: any*/),
|
||||
(v19/*: any*/)
|
||||
(v19/*: any*/),
|
||||
(v17/*: any*/)
|
||||
],
|
||||
"storageKey": "controls(first:20)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v20/*: any*/),
|
||||
"args": (v18/*: any*/),
|
||||
"filters": [
|
||||
"orderBy",
|
||||
"filter"
|
||||
@@ -568,7 +452,7 @@ return {
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v22/*: any*/),
|
||||
"args": (v20/*: any*/),
|
||||
"concreteType": "EvidenceConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "evidences",
|
||||
@@ -638,22 +522,22 @@ return {
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
(v13/*: any*/)
|
||||
(v12/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v15/*: any*/)
|
||||
(v14/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v21/*: any*/),
|
||||
(v19/*: any*/)
|
||||
(v19/*: any*/),
|
||||
(v17/*: any*/)
|
||||
],
|
||||
"storageKey": "evidences(first:50)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v22/*: any*/),
|
||||
"args": (v20/*: any*/),
|
||||
"filters": [
|
||||
"orderBy"
|
||||
],
|
||||
@@ -672,16 +556,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "d20815cee9ca8dec2e09fcefdd54a68a",
|
||||
"cacheID": "fb510405c5934dbaf802c61c3ee8a547",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "MeasureGraphNodeQuery",
|
||||
"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;
|
||||
|
||||
@@ -461,7 +461,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
]}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{isDraft && (
|
||||
{isDraft && isAuthorized("Document", "publishDocumentVersion") && (
|
||||
<Button
|
||||
onClick={handlePublish}
|
||||
icon={IconCheckmark1}
|
||||
@@ -499,16 +499,14 @@ export default function DocumentDetailPage(props: Props) {
|
||||
{isDraft ? __("Edit draft document") : __("Create new draft")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{isDraft && versions.length > 1 && (
|
||||
isAuthorized("Document", "deleteDocument") && (
|
||||
<DropdownItem
|
||||
onClick={handleDeleteDraft}
|
||||
icon={IconTrashCan}
|
||||
disabled={isDeletingDraft}
|
||||
>
|
||||
{__("Delete draft document")}
|
||||
</DropdownItem>
|
||||
)
|
||||
{isDraft && versions.length > 1 && isAuthorized("Document", "deleteDraftDocumentVersion") && (
|
||||
<DropdownItem
|
||||
onClick={handleDeleteDraft}
|
||||
icon={IconTrashCan}
|
||||
disabled={isDeletingDraft}
|
||||
>
|
||||
{__("Delete draft document")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem
|
||||
onClick={() => pdfDownloadDialogRef.current?.open()}
|
||||
@@ -567,11 +565,13 @@ export default function DocumentDetailPage(props: Props) {
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{document.title}</span>
|
||||
<Button
|
||||
variant="quaternary"
|
||||
icon={IconPencil}
|
||||
onClick={() => setIsEditingTitle(true)}
|
||||
/>
|
||||
{isAuthorized("Document", "updateDocument") && (
|
||||
<Button
|
||||
variant="quaternary"
|
||||
icon={IconPencil}
|
||||
onClick={() => setIsEditingTitle(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -616,7 +616,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
/>
|
||||
</EditablePropertyContent>
|
||||
) : (
|
||||
<ReadOnlyPropertyContent onEdit={() => setIsEditingOwner(true)}>
|
||||
<ReadOnlyPropertyContent onEdit={() => setIsEditingOwner(true)} canEdit={isAuthorized("Document", "updateDocument")}>
|
||||
<Badge variant="highlight" size="md" className="gap-2">
|
||||
<Avatar name={currentVersion.owner?.fullName ?? ""} />
|
||||
{currentVersion.owner?.fullName}
|
||||
@@ -643,7 +643,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
</ControlledField>
|
||||
</EditablePropertyContent>
|
||||
) : (
|
||||
<ReadOnlyPropertyContent onEdit={() => setIsEditingType(true)}>
|
||||
<ReadOnlyPropertyContent onEdit={() => setIsEditingType(true)} canEdit={isAuthorized("Document", "updateDocument")}>
|
||||
<div className="text-sm text-txt-secondary">
|
||||
{getDocumentTypeLabel(__, document.documentType)}
|
||||
</div>
|
||||
@@ -671,6 +671,7 @@ export default function DocumentDetailPage(props: Props) {
|
||||
) : (
|
||||
<ReadOnlyPropertyContent
|
||||
onEdit={() => setIsEditingClassification(true)}
|
||||
canEdit={isAuthorized("Document", "updateDocument")}
|
||||
>
|
||||
<div className="text-sm text-txt-secondary">
|
||||
{getDocumentClassificationLabel(__, currentVersion.classification)}
|
||||
@@ -739,14 +740,16 @@ function EditablePropertyContent({
|
||||
function ReadOnlyPropertyContent({
|
||||
children,
|
||||
onEdit,
|
||||
canEdit = true,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onEdit: () => void;
|
||||
canEdit?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{children}
|
||||
<Button variant="quaternary" icon={IconPencil} onClick={onEdit} />
|
||||
{canEdit && <Button variant="quaternary" icon={IconPencil} onClick={onEdit} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,6 +178,22 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
const [attachSnapshot, isAttachingSnapshot] = useMutation(attachSnapshotMutation);
|
||||
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>(
|
||||
mutationFn: (config: UseMutationConfig<T>) => void,
|
||||
errorMessage: string
|
||||
@@ -285,6 +301,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
onAttach={withErrorHandling(attachMeasure, __("Failed to link measure"))}
|
||||
onDetach={withErrorHandling(detachMeasure, __("Failed to unlink measure"))}
|
||||
disabled={isAttachingMeasure || isDetachingMeasure}
|
||||
readOnly={measuresReadOnly}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
@@ -296,6 +313,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
onAttach={withErrorHandling(attachDocument, __("Failed to link document"))}
|
||||
onDetach={withErrorHandling(detachDocument, __("Failed to unlink document"))}
|
||||
disabled={isAttachingDocument || isDetachingDocument}
|
||||
readOnly={documentsReadOnly}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
@@ -307,6 +325,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
onAttach={withErrorHandling(attachAudit, __("Failed to link audit"))}
|
||||
onDetach={withErrorHandling(detachAudit, __("Failed to unlink audit"))}
|
||||
disabled={isAttachingAudit || isDetachingAudit}
|
||||
readOnly={auditsReadOnly}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
@@ -318,6 +337,7 @@ export default function FrameworkControlPage({ queryRef }: Props) {
|
||||
onAttach={withErrorHandling(attachSnapshot, __("Failed to link snapshot"))}
|
||||
onDetach={withErrorHandling(detachSnapshot, __("Failed to unlink snapshot"))}
|
||||
disabled={isAttachingSnapshot || isDetachingSnapshot}
|
||||
readOnly={snapshotsReadOnly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,10 +26,13 @@ import { MeasureBadge } from "@probo/ui/src/Molecules/Badge/MeasureBadge";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
ConnectionHandler,
|
||||
graphql,
|
||||
type PreloadedQuery,
|
||||
useLazyLoadQuery,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import type { MeasureGraphNodeQuery } from "/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql";
|
||||
import type { MeasureDetailPageTasksCountQuery } from "./__generated__/MeasureDetailPageTasksCountQuery.graphql";
|
||||
import {
|
||||
MeasureConnectionKey,
|
||||
measureNodeQuery,
|
||||
@@ -43,9 +46,30 @@ import {
|
||||
sprintf,
|
||||
} from "@probo/helpers";
|
||||
import MeasureFormDialog from "./dialog/MeasureFormDialog";
|
||||
import { use } from "react";
|
||||
import { Suspense, use } from "react";
|
||||
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 = {
|
||||
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 controlsCount = measure.controlsInfos?.totalCount ?? 0;
|
||||
const risksCount = measure.risksInfos?.totalCount ?? 0;
|
||||
@@ -160,13 +184,13 @@ export default function MeasureDetailPage(props: Props) {
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
<ActionDropdown variant="secondary">
|
||||
{isAuthorized("Measure", "deleteMeasure") && (
|
||||
{isAuthorized("Measure", "deleteMeasure") && (
|
||||
<ActionDropdown variant="secondary">
|
||||
<DropdownItem variant="danger" icon={IconTrashCan} onClick={onDelete}>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
</ActionDropdown>
|
||||
)}
|
||||
</PageHeader>
|
||||
|
||||
<Tabs>
|
||||
@@ -177,13 +201,17 @@ export default function MeasureDetailPage(props: Props) {
|
||||
{__("Evidences")}
|
||||
<TabBadge>{evidencesCount}</TabBadge>
|
||||
</TabLink>
|
||||
<TabLink
|
||||
to={`/organizations/${organizationId}/measures/${measureId}/tasks`}
|
||||
>
|
||||
<IconCheckmark1 size={20} />
|
||||
{__("Tasks")}
|
||||
<TabBadge>{tasksCount}</TabBadge>
|
||||
</TabLink>
|
||||
{canViewTasks && (
|
||||
<TabLink
|
||||
to={`/organizations/${organizationId}/measures/${measureId}/tasks`}
|
||||
>
|
||||
<IconCheckmark1 size={20} />
|
||||
{__("Tasks")}
|
||||
<Suspense fallback={<TabBadge>-</TabBadge>}>
|
||||
<TasksCountBadge measureId={measureId} />
|
||||
</Suspense>
|
||||
</TabLink>
|
||||
)}
|
||||
<TabLink
|
||||
to={`/organizations/${organizationId}/measures/${measureId}/controls`}
|
||||
>
|
||||
|
||||
143
apps/console/src/pages/organizations/measures/__generated__/MeasureDetailPageTasksCountQuery.graphql.ts
generated
Normal file
143
apps/console/src/pages/organizations/measures/__generated__/MeasureDetailPageTasksCountQuery.graphql.ts
generated
Normal 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;
|
||||
@@ -3,6 +3,8 @@ import { useOutletContext } from "react-router";
|
||||
import { LinkedControlsCard } from "/components/controls/LinkedControlsCard";
|
||||
import type { MeasureControlsTabFragment$key } from "./__generated__/MeasureControlsTabFragment.graphql";
|
||||
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
|
||||
import { use } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const controlsFragment = graphql`
|
||||
fragment MeasureControlsTabFragment on Measure
|
||||
@@ -69,6 +71,11 @@ export default function MeasureControlsTab() {
|
||||
const [data, refetch] = useRefetchableFragment(controlsFragment, measure);
|
||||
const connectionId = data.controls.__id;
|
||||
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 = {
|
||||
id: data.id,
|
||||
@@ -99,6 +106,7 @@ export default function MeasureControlsTab() {
|
||||
params={{ measureId: data.id }}
|
||||
connectionId={connectionId}
|
||||
refetch={refetch}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,10 +29,11 @@ import { fileSize, fileType, sprintf, formatDate } from "@probo/helpers";
|
||||
import { EvidencePreviewDialog } from "../dialog/EvidencePreviewDialog";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { CreateEvidenceDialog } from "../dialog/CreateEvidenceDialog";
|
||||
import { useState } from "react";
|
||||
import { use, useState } from "react";
|
||||
import { EvidenceDownloadDialog } from "../dialog/EvidenceDownloadDialog";
|
||||
import { updateStoreCounter } from "/hooks/useMutationWithIncrement";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const evidencesFragment = graphql`
|
||||
fragment MeasureEvidencesTabFragment on Measure
|
||||
@@ -107,6 +108,10 @@ export default function MeasureEvidencesTab() {
|
||||
const organizationId = useOrganizationId();
|
||||
const dialogRef = useDialogRef();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
|
||||
const canAddEvidence = isAuthorized("Measure", "uploadMeasureEvidence");
|
||||
const canDeleteEvidence = isAuthorized("Evidence", "deleteEvidence");
|
||||
|
||||
usePageTitle(measure.name + " - " + __("Evidences"));
|
||||
|
||||
@@ -131,10 +136,11 @@ export default function MeasureEvidencesTab() {
|
||||
organizationId={organizationId}
|
||||
connectionId={connectionId}
|
||||
hideActions={isSnapshotMode}
|
||||
canDelete={canDeleteEvidence}
|
||||
snapshotId={snapshotId}
|
||||
/>
|
||||
))}
|
||||
{!isSnapshotMode && (
|
||||
{!isSnapshotMode && canAddEvidence && (
|
||||
<TrButton
|
||||
colspan={5}
|
||||
onClick={() => dialogRef.current?.open()}
|
||||
@@ -158,7 +164,7 @@ export default function MeasureEvidencesTab() {
|
||||
filename={evidence.file?.fileName || ""}
|
||||
/>
|
||||
)}
|
||||
{!isSnapshotMode && (
|
||||
{!isSnapshotMode && canAddEvidence && (
|
||||
<CreateEvidenceDialog
|
||||
ref={dialogRef}
|
||||
measureId={measure.id}
|
||||
@@ -175,6 +181,7 @@ function EvidenceRow(props: {
|
||||
organizationId: string;
|
||||
connectionId: string;
|
||||
hideActions?: boolean;
|
||||
canDelete?: boolean;
|
||||
snapshotId?: string;
|
||||
}) {
|
||||
const evidence = useFragment(evidenceFragment, props.evidenceKey);
|
||||
@@ -249,14 +256,16 @@ function EvidenceRow(props: {
|
||||
<IconArrowInbox size={16} />
|
||||
{__("Download")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
{props.canDelete && (
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { MeasureRisksTabFragment$key } from "./__generated__/MeasureRisksTa
|
||||
import { useOutletContext } from "react-router";
|
||||
import { LinkedRisksCard } from "/components/risks/LinkedRisksCard";
|
||||
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
|
||||
import { use } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const risksFragment = graphql`
|
||||
fragment MeasureRisksTabFragment on Measure {
|
||||
@@ -53,6 +55,11 @@ export default function MeasureRisksTab() {
|
||||
const data = useFragment(risksFragment, measure);
|
||||
const connectionId = data.risks.__id;
|
||||
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 = {
|
||||
id: data.id,
|
||||
@@ -82,6 +89,7 @@ export default function MeasureRisksTab() {
|
||||
onDetach={detachRisk}
|
||||
params={{ measureId: data.id }}
|
||||
connectionId={connectionId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
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 { useFragment } from "react-relay";
|
||||
import { useLazyLoadQuery } from "react-relay";
|
||||
import TasksCard from "/components/tasks/TasksCard";
|
||||
import { Button, IconPlusLarge } from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import TaskFormDialog from "/components/tasks/TaskFormDialog";
|
||||
|
||||
export const tasksFragment = graphql`
|
||||
fragment MeasureTasksTabFragment on Measure {
|
||||
tasks(first: 100) @connection(key: "Measure__tasks") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
state
|
||||
description
|
||||
...TaskFormDialogFragment
|
||||
assignedTo {
|
||||
id
|
||||
fullName
|
||||
const tasksQuery = graphql`
|
||||
query MeasureTasksTabQuery($measureId: ID!) {
|
||||
node(id: $measureId) {
|
||||
... on Measure {
|
||||
id
|
||||
tasks(first: 100) @connection(key: "Measure__tasks") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
state
|
||||
description
|
||||
...TaskFormDialogFragment
|
||||
assignedTo {
|
||||
id
|
||||
fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,11 +36,17 @@ export const tasksFragment = graphql`
|
||||
export default function MeasureTasksTab() {
|
||||
const { __ } = useTranslate();
|
||||
const { measure } = useOutletContext<{
|
||||
measure: MeasureTasksTabFragment$key & { id: string };
|
||||
measure: { id: string };
|
||||
}>();
|
||||
const data = useFragment(tasksFragment, measure);
|
||||
const connectionId = data.tasks.__id;
|
||||
const tasks = data.tasks?.edges?.map((edge) => edge.node) ?? [];
|
||||
const data = useLazyLoadQuery<MeasureTasksTabQuery>(tasksQuery, {
|
||||
measureId: measure.id,
|
||||
});
|
||||
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 (
|
||||
<div className="relative">
|
||||
|
||||
@@ -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;
|
||||
370
apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabQuery.graphql.ts
generated
Normal file
370
apps/console/src/pages/organizations/measures/tabs/__generated__/MeasureTasksTabQuery.graphql.ts
generated
Normal 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;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Card, IconCheckmark1 } from "@probo/ui";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { use, type PropsWithChildren } from "react";
|
||||
import z from "zod";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { ControlledField } from "/components/form/ControlledField";
|
||||
@@ -11,6 +11,7 @@ import { useOutletContext } from "react-router";
|
||||
import { updatePeopleMutation } from "/hooks/graph/PeopleGraph";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
const schema = z.object({
|
||||
kind: z.enum(peopleRoles),
|
||||
@@ -21,6 +22,8 @@ export default function PeopleRoleTab() {
|
||||
people: PeopleGraphNodeQuery$data["node"];
|
||||
}>();
|
||||
const { __ } = useTranslate();
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const canUpdatePeople = isAuthorized("People", "updatePeople");
|
||||
const { control, formState, handleSubmit, reset } = useFormWithSchema(
|
||||
schema,
|
||||
{
|
||||
@@ -65,6 +68,7 @@ export default function PeopleRoleTab() {
|
||||
name="kind"
|
||||
type="select"
|
||||
label={__("Role")}
|
||||
disabled={!canUpdatePeople}
|
||||
>
|
||||
{getRoles(__).map((role) => (
|
||||
<Option key={role.value} value={role.value}>
|
||||
@@ -93,13 +97,15 @@ export default function PeopleRoleTab() {
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="flex justify-end">
|
||||
{formState.isDirty && (
|
||||
<Button type="submit" disabled={isMutating}>
|
||||
{__("Update")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{canUpdatePeople && (
|
||||
<div className="flex justify-end">
|
||||
{formState.isDirty && (
|
||||
<Button type="submit" disabled={isMutating}>
|
||||
{__("Update")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useOutletContext } from "react-router";
|
||||
import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard";
|
||||
import type { RiskDocumentsTabFragment$key } from "./__generated__/RiskDocumentsTabFragment.graphql";
|
||||
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
|
||||
import { use } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const documentsFragment = graphql`
|
||||
fragment RiskDocumentsTabFragment on Risk {
|
||||
@@ -53,6 +55,11 @@ export default function RiskDocumentsTab() {
|
||||
const data = useFragment(documentsFragment, risk);
|
||||
const connectionId = data.documents.__id;
|
||||
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 = {
|
||||
id: data.id,
|
||||
@@ -82,6 +89,7 @@ export default function RiskDocumentsTab() {
|
||||
onDetach={detachDocument}
|
||||
params={{ riskId: data.id }}
|
||||
connectionId={connectionId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { RiskMeasuresTabFragment$key } from "./__generated__/RiskMeasuresTa
|
||||
import { useOutletContext } from "react-router";
|
||||
import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard";
|
||||
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
|
||||
import { use } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const measuresFragment = graphql`
|
||||
fragment RiskMeasuresTabFragment on Risk {
|
||||
@@ -53,6 +55,12 @@ export default function RiskMeasuresTab() {
|
||||
const data = useFragment(measuresFragment, risk);
|
||||
const connectionId = data.measures.__id;
|
||||
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 = {
|
||||
id: data.id,
|
||||
node: "measures(first:0)",
|
||||
@@ -81,6 +89,7 @@ export default function RiskMeasuresTab() {
|
||||
onDetach={detachMeasure}
|
||||
params={{ riskId: data.id }}
|
||||
connectionId={connectionId}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { RiskObligationsTabFragment$key } from "./__generated__/RiskObligat
|
||||
import { useOutletContext } from "react-router";
|
||||
import { LinkedObligationsCard } from "/components/obligations/LinkedObligationsCard";
|
||||
import { useMutationWithIncrement } from "/hooks/useMutationWithIncrement";
|
||||
import { use } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const obligationsFragment = graphql`
|
||||
fragment RiskObligationsTabFragment on Risk {
|
||||
@@ -53,6 +55,12 @@ export default function RiskObligationsTab() {
|
||||
const data = useFragment(obligationsFragment, risk);
|
||||
const connectionId = data.obligations.__id;
|
||||
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 = {
|
||||
id: data.id,
|
||||
node: "obligations(first:0)",
|
||||
@@ -82,6 +90,7 @@ export default function RiskObligationsTab() {
|
||||
params={{ riskId: data.id }}
|
||||
connectionId={connectionId}
|
||||
variant="table"
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -535,6 +535,7 @@ function MembershipRowContent(props: {
|
||||
{availableRoles.includes("OWNER") && <Option value="OWNER">{__("Owner")}</Option>}
|
||||
{availableRoles.includes("ADMIN") && <Option value="ADMIN">{__("Admin")}</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>}
|
||||
</Select>
|
||||
</Field>
|
||||
@@ -549,6 +550,9 @@ function MembershipRowContent(props: {
|
||||
{selectedRole === "VIEWER" && (
|
||||
<p>{__("Read-only access")}</p>
|
||||
)}
|
||||
{selectedRole === "AUDITOR" && (
|
||||
<p>{__("Read-only access without settings, tasks and meetings")}</p>
|
||||
)}
|
||||
{selectedRole === "EMPLOYEE" && (
|
||||
<p>{__("Access to employee page")}</p>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<0bd95d20e79294c530610625c86e88d7>>
|
||||
* @generated SignedSource<<dd7d5d34935f87fcfb658ca6cfc5e794>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
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";
|
||||
export type MembersSettingsTabInvitationsFragment$data = {
|
||||
readonly id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<8fcd99714c4bf7dba138fcb0de398a3a>>
|
||||
* @generated SignedSource<<c3bf3676d3f8688315fc342efcf54547>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -9,7 +9,7 @@
|
||||
// @ts-nocheck
|
||||
|
||||
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";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type MembersSettingsTabMembershipsFragment$data = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<9aaf763355340403cfd0c9666b61be19>>
|
||||
* @generated SignedSource<<e40f8c235af2bfed3a33bdf0e5a3c24b>>
|
||||
* @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 UpdateMembershipInput = {
|
||||
memberId: string;
|
||||
organizationId: string;
|
||||
|
||||
@@ -91,11 +91,13 @@ export default function VendorDetailPage(props: Props) {
|
||||
</div>
|
||||
{!isSnapshotMode && (
|
||||
<div className="flex gap-2 items-center">
|
||||
<ImportAssessmentDialog vendorId={vendor.id!}>
|
||||
<Button icon={IconPageTextLine} variant="secondary">
|
||||
{__("Assessment From Website")}
|
||||
</Button>
|
||||
</ImportAssessmentDialog>
|
||||
{isAuthorized("Vendor", "assessVendor") && (
|
||||
<ImportAssessmentDialog vendorId={vendor.id!}>
|
||||
<Button icon={IconPageTextLine} variant="secondary">
|
||||
{__("Assessment From Website")}
|
||||
</Button>
|
||||
</ImportAssessmentDialog>
|
||||
)}
|
||||
{isAuthorized("Vendor", "deleteVendor") && (
|
||||
<ActionDropdown variant="secondary">
|
||||
<DropdownItem
|
||||
|
||||
@@ -11,14 +11,15 @@ import {
|
||||
import { Controller } from "react-hook-form";
|
||||
import { useVendorForm } from "/hooks/forms/useVendorForm";
|
||||
import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVendorFormFragment.graphql";
|
||||
import { useOutletContext } from "react-router";
|
||||
import { useOutletContext, useParams } from "react-router";
|
||||
import {
|
||||
certificationCategoryLabel,
|
||||
certifications,
|
||||
objectEntries,
|
||||
} from "@probo/helpers";
|
||||
import { useRef, useState } from "react";
|
||||
import { use, useRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
/**
|
||||
* Vendor certifications tab
|
||||
@@ -29,9 +30,13 @@ export default function VendorCertificationsTab() {
|
||||
}>();
|
||||
const { __ } = useTranslate();
|
||||
const { control, handleSubmit } = useVendorForm(vendor);
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const canUpdateVendor = isAuthorized("Vendor", "updateVendor");
|
||||
|
||||
return (
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<form className="space-y-4" onSubmit={!isSnapshotMode && canUpdateVendor ? handleSubmit : undefined}>
|
||||
<Card padded>
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -40,13 +45,16 @@ export default function VendorCertificationsTab() {
|
||||
<Certifications
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? []}
|
||||
readOnly={isSnapshotMode || !canUpdateVendor}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">{__("Update vendor")}</Button>
|
||||
</div>
|
||||
{!isSnapshotMode && canUpdateVendor && (
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">{__("Update vendor")}</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -54,6 +62,7 @@ export default function VendorCertificationsTab() {
|
||||
type CertificationsProps = {
|
||||
value: string[];
|
||||
onValueChange: (value: string[]) => void;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -94,31 +103,37 @@ function Certifications(props: CertificationsProps) {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{certifications.map((certification) => (
|
||||
<Badge asChild size="md" key={certification}>
|
||||
<button
|
||||
onClick={() => removeCertificate(certification)}
|
||||
type="button"
|
||||
className={clsx(
|
||||
"hover:bg-subtle-hover cursor-pointer",
|
||||
animateBadge.current &&
|
||||
"starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent"
|
||||
)}
|
||||
>
|
||||
{certification}
|
||||
<div className="w-0 overflow-hidden group-hover:w-4 duration-200">
|
||||
<IconCrossLargeX size={12} />
|
||||
</div>
|
||||
</button>
|
||||
{props.readOnly ? (
|
||||
<span>{certification}</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => removeCertificate(certification)}
|
||||
type="button"
|
||||
className={clsx(
|
||||
"hover:bg-subtle-hover cursor-pointer",
|
||||
animateBadge.current &&
|
||||
"starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent"
|
||||
)}
|
||||
>
|
||||
{certification}
|
||||
<div className="w-0 overflow-hidden group-hover:w-4 duration-200">
|
||||
<IconCrossLargeX size={12} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<CertificationInput
|
||||
certifications={categorizedCertifications.filter(
|
||||
(c) => !props.value.includes(c)
|
||||
)}
|
||||
onAdd={addCertificate}
|
||||
/>
|
||||
{!props.readOnly && (
|
||||
<CertificationInput
|
||||
certifications={categorizedCertifications.filter(
|
||||
(c) => !props.value.includes(c)
|
||||
)}
|
||||
onAdd={addCertificate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import type { VendorComplianceTabFragment_report$key } from "./__generated__/Ven
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { sprintf, fileSize, formatDate } from "@probo/helpers";
|
||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||
import { use } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const complianceReportsFragment = graphql`
|
||||
fragment VendorComplianceTabFragment on Vendor
|
||||
@@ -103,6 +105,9 @@ export default function VendorComplianceTab() {
|
||||
const { __ } = useTranslate();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const canUploadReport = isAuthorized("Vendor", "uploadVendorComplianceReport");
|
||||
const canDeleteReport = isAuthorized("VendorComplianceReport", "deleteVendorComplianceReport");
|
||||
|
||||
usePageTitle(vendor.name + " - " + __("Compliance reports"));
|
||||
|
||||
@@ -130,7 +135,7 @@ export default function VendorComplianceTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{!isSnapshotMode && (
|
||||
{!isSnapshotMode && canUploadReport && (
|
||||
<Dropzone
|
||||
description={__("Only PDF files up to 10MB are allowed")}
|
||||
isUploading={isMutating}
|
||||
@@ -148,7 +153,7 @@ export default function VendorComplianceTab() {
|
||||
<SortableTh field="REPORT_DATE">{__("Report date")}</SortableTh>
|
||||
<Th>{__("Valid until")}</Th>
|
||||
<Th>{__("File size")}</Th>
|
||||
{!isSnapshotMode && <Th>{__("Actions")}</Th>}
|
||||
{!isSnapshotMode && canDeleteReport && <Th>{__("Actions")}</Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -158,6 +163,7 @@ export default function VendorComplianceTab() {
|
||||
reportKey={report}
|
||||
connectionId={connectionId}
|
||||
isSnapshotMode={isSnapshotMode}
|
||||
canDelete={canDeleteReport}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
@@ -170,6 +176,7 @@ type ReportRowProps = {
|
||||
reportKey: VendorComplianceTabFragment_report$key;
|
||||
connectionId: string;
|
||||
isSnapshotMode: boolean;
|
||||
canDelete?: boolean;
|
||||
};
|
||||
|
||||
function ReportRow(props: ReportRowProps) {
|
||||
@@ -212,7 +219,7 @@ function ReportRow(props: ReportRowProps) {
|
||||
<Td>{formatDate(report.reportDate)}</Td>
|
||||
<Td>{formatDate(report.validUntil)}</Td>
|
||||
<Td>{fileSize(__, report.file?.size)}</Td>
|
||||
{!props.isSnapshotMode && (
|
||||
{!props.isSnapshotMode && props.canDelete && (
|
||||
<Td width={50} className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
|
||||
@@ -25,7 +25,8 @@ import { sprintf } from "@probo/helpers";
|
||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||
import { CreateContactDialog } from "../dialogs/CreateContactDialog";
|
||||
import { EditContactDialog } from "../dialogs/EditContactDialog";
|
||||
import { useState } from "react";
|
||||
import { use, useState } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const vendorContactsFragment = graphql`
|
||||
fragment VendorContactsTabFragment on Vendor
|
||||
@@ -98,6 +99,11 @@ export default function VendorContactsTab() {
|
||||
phone?: string | null;
|
||||
role?: string | 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"));
|
||||
|
||||
@@ -107,7 +113,7 @@ export default function VendorContactsTab() {
|
||||
title={__("Contacts")}
|
||||
description={__("Manage vendor contacts and their information.")}
|
||||
>
|
||||
{!isSnapshotMode && (
|
||||
{!isSnapshotMode && canCreateContact && (
|
||||
<CreateContactDialog
|
||||
vendorId={vendor.id}
|
||||
connectionId={connectionId}
|
||||
@@ -124,7 +130,7 @@ export default function VendorContactsTab() {
|
||||
<SortableTh field="EMAIL">{__("Email")}</SortableTh>
|
||||
<Th>{__("Phone")}</Th>
|
||||
<Th>{__("Role")}</Th>
|
||||
{!isSnapshotMode && <Th>{__("Actions")}</Th>}
|
||||
{!isSnapshotMode && hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -135,12 +141,14 @@ export default function VendorContactsTab() {
|
||||
connectionId={connectionId}
|
||||
onEdit={setEditingContact}
|
||||
isSnapshotMode={isSnapshotMode}
|
||||
canUpdate={canUpdateContact}
|
||||
canDelete={canDeleteContact}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
|
||||
{editingContact && !isSnapshotMode && (
|
||||
{editingContact && !isSnapshotMode && canUpdateContact && (
|
||||
<EditContactDialog
|
||||
contactId={editingContact.id}
|
||||
contact={editingContact}
|
||||
@@ -162,6 +170,8 @@ type ContactRowProps = {
|
||||
role?: string | null;
|
||||
}) => void;
|
||||
isSnapshotMode: boolean;
|
||||
canUpdate?: boolean;
|
||||
canDelete?: boolean;
|
||||
};
|
||||
|
||||
function ContactRow(props: ContactRowProps) {
|
||||
@@ -175,6 +185,7 @@ function ContactRow(props: ContactRowProps) {
|
||||
successMessage: __("Contact deleted successfully"),
|
||||
errorMessage: __("Failed to delete contact"),
|
||||
});
|
||||
const hasAnyAction = props.canUpdate || props.canDelete;
|
||||
|
||||
const handleDelete = () => {
|
||||
confirm(
|
||||
@@ -226,28 +237,32 @@ function ContactRow(props: ContactRowProps) {
|
||||
)}
|
||||
</Td>
|
||||
<Td>{contact.role || __("—")}</Td>
|
||||
{!props.isSnapshotMode && (
|
||||
{!props.isSnapshotMode && hasAnyAction && (
|
||||
<Td width={50} className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
icon={IconPencil}
|
||||
onClick={() => props.onEdit({
|
||||
id: contact.id,
|
||||
fullName: contact.fullName,
|
||||
email: contact.email,
|
||||
phone: contact.phone,
|
||||
role: contact.role,
|
||||
})}
|
||||
>
|
||||
{__("Edit")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
variant="danger"
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
{props.canUpdate && (
|
||||
<DropdownItem
|
||||
icon={IconPencil}
|
||||
onClick={() => props.onEdit({
|
||||
id: contact.id,
|
||||
fullName: contact.fullName,
|
||||
email: contact.email,
|
||||
phone: contact.phone,
|
||||
role: contact.role,
|
||||
})}
|
||||
>
|
||||
{__("Edit")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{props.canDelete && (
|
||||
<DropdownItem
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
variant="danger"
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -59,6 +59,13 @@ export default function VendorOverviewTab() {
|
||||
|
||||
const { __ } = useTranslate();
|
||||
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 }[] = [
|
||||
{ value: "ANALYTICS", label: __("Analytics") },
|
||||
{ value: "CLOUD_MONITORING", label: __("Cloud Monitoring") },
|
||||
@@ -128,8 +135,10 @@ export default function VendorOverviewTab() {
|
||||
|
||||
usePageTitle(vendor.name + " - " + __("Overview"));
|
||||
|
||||
const isFormDisabled = isSubmitting || isSnapshotMode || !canUpdateVendor;
|
||||
|
||||
return (
|
||||
<form onSubmit={isSnapshotMode ? undefined : handleSubmit} className="space-y-12">
|
||||
<form onSubmit={isSnapshotMode || !canUpdateVendor ? undefined : handleSubmit} className="space-y-12">
|
||||
{/* Vendor Details */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-base font-medium">{__("Vendor details")}</h2>
|
||||
@@ -139,14 +148,14 @@ export default function VendorOverviewTab() {
|
||||
label={__("Name")}
|
||||
type="text"
|
||||
error={errors.name?.message}
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
<Field
|
||||
{...register("description")}
|
||||
label={__("Description")}
|
||||
type="textarea"
|
||||
error={errors.description?.message}
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
<ControlledField
|
||||
control={control}
|
||||
@@ -155,7 +164,7 @@ export default function VendorOverviewTab() {
|
||||
label={__("Category")}
|
||||
placeholder={__("Select a category")}
|
||||
error={errors.category?.message}
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
>
|
||||
{vendorCategories.map((category) => (
|
||||
<Option key={category.value} value={category.value}>
|
||||
@@ -168,21 +177,21 @@ export default function VendorOverviewTab() {
|
||||
label={__("Legal name")}
|
||||
type="text"
|
||||
error={errors.legalName?.message}
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
<Field
|
||||
{...register("headquarterAddress")}
|
||||
label={__("Headquarter address")}
|
||||
type="textarea"
|
||||
error={errors.headquarterAddress?.message}
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
<Field
|
||||
{...register("websiteUrl")}
|
||||
label={__("Website URL")}
|
||||
type="text"
|
||||
error={errors.websiteUrl?.message}
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -193,7 +202,7 @@ export default function VendorOverviewTab() {
|
||||
<CountriesField
|
||||
control={control}
|
||||
name="countries"
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -208,7 +217,7 @@ export default function VendorOverviewTab() {
|
||||
name="businessOwnerId"
|
||||
label={__("Business owner")}
|
||||
error={errors.businessOwnerId?.message}
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
optional={true}
|
||||
/>
|
||||
<PeopleSelectField
|
||||
@@ -217,7 +226,7 @@ export default function VendorOverviewTab() {
|
||||
name="securityOwnerId"
|
||||
label={__("Security owner")}
|
||||
error={errors.securityOwnerId?.message}
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
optional={true}
|
||||
/>
|
||||
</Card>
|
||||
@@ -246,7 +255,7 @@ export default function VendorOverviewTab() {
|
||||
type="text"
|
||||
placeholder="https://..."
|
||||
variant="ghost"
|
||||
disabled={isSubmitting || isSnapshotMode}
|
||||
disabled={isFormDisabled}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -287,30 +296,30 @@ export default function VendorOverviewTab() {
|
||||
>
|
||||
{__("Download PDF")}
|
||||
</Button>
|
||||
{!isSnapshotMode && (
|
||||
<>
|
||||
<EditBusinessAssociateAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
agreement={{
|
||||
validFrom: businessAssociateAgreement.validFrom,
|
||||
validUntil: businessAssociateAgreement.validUntil,
|
||||
}}
|
||||
onSuccess={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="quaternary" icon={IconPencil} />
|
||||
</EditBusinessAssociateAgreementDialog>
|
||||
<DeleteBusinessAssociateAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
fileName={businessAssociateAgreement.fileName}
|
||||
onSuccess={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="quaternary" icon={IconTrashCan} />
|
||||
</DeleteBusinessAssociateAgreementDialog>
|
||||
</>
|
||||
{!isSnapshotMode && canUpdateBAA && (
|
||||
<EditBusinessAssociateAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
agreement={{
|
||||
validFrom: businessAssociateAgreement.validFrom,
|
||||
validUntil: businessAssociateAgreement.validUntil,
|
||||
}}
|
||||
onSuccess={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="quaternary" icon={IconPencil} />
|
||||
</EditBusinessAssociateAgreementDialog>
|
||||
)}
|
||||
{!isSnapshotMode && canDeleteBAA && (
|
||||
<DeleteBusinessAssociateAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
fileName={businessAssociateAgreement.fileName}
|
||||
onSuccess={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="quaternary" icon={IconTrashCan} />
|
||||
</DeleteBusinessAssociateAgreementDialog>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
!isSnapshotMode && (
|
||||
!isSnapshotMode && canUploadBAA && (
|
||||
<UploadBusinessAssociateAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
onSuccess={() => window.location.reload()}
|
||||
@@ -354,30 +363,30 @@ export default function VendorOverviewTab() {
|
||||
>
|
||||
{__("Download PDF")}
|
||||
</Button>
|
||||
{!isSnapshotMode && (
|
||||
<>
|
||||
<EditDataPrivacyAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
agreement={{
|
||||
validFrom: dataPrivacyAgreement.validFrom,
|
||||
validUntil: dataPrivacyAgreement.validUntil,
|
||||
}}
|
||||
onSuccess={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="quaternary" icon={IconPencil} />
|
||||
</EditDataPrivacyAgreementDialog>
|
||||
<DeleteDataPrivacyAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
fileName={dataPrivacyAgreement.fileName}
|
||||
onSuccess={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="quaternary" icon={IconTrashCan} />
|
||||
</DeleteDataPrivacyAgreementDialog>
|
||||
</>
|
||||
{!isSnapshotMode && canUpdateDPA && (
|
||||
<EditDataPrivacyAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
agreement={{
|
||||
validFrom: dataPrivacyAgreement.validFrom,
|
||||
validUntil: dataPrivacyAgreement.validUntil,
|
||||
}}
|
||||
onSuccess={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="quaternary" icon={IconPencil} />
|
||||
</EditDataPrivacyAgreementDialog>
|
||||
)}
|
||||
{!isSnapshotMode && canDeleteDPA && (
|
||||
<DeleteDataPrivacyAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
fileName={dataPrivacyAgreement.fileName}
|
||||
onSuccess={() => window.location.reload()}
|
||||
>
|
||||
<Button variant="quaternary" icon={IconTrashCan} />
|
||||
</DeleteDataPrivacyAgreementDialog>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
!isSnapshotMode && (
|
||||
!isSnapshotMode && canUploadDPA && (
|
||||
<UploadDataPrivacyAgreementDialog
|
||||
vendorId={vendor.id}
|
||||
onSuccess={() => window.location.reload()}
|
||||
|
||||
@@ -21,7 +21,8 @@ import type { VendorRiskAssessmentTabFragment_assessment$key } from "./__generat
|
||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||
import { CreateRiskAssessmentDialog } from "../dialogs/CreateRiskAssessmentDialog";
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import { use, useState } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
const riskAssessmentsFragment = graphql`
|
||||
fragment VendorRiskAssessmentTabFragment on Vendor
|
||||
@@ -81,6 +82,8 @@ export default function VendorRiskAssessmentTab() {
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
const { isAuthorized } = use(PermissionsContext);
|
||||
const canCreateRiskAssessment = isAuthorized("Vendor", "createVendorRiskAssessment");
|
||||
|
||||
usePageTitle(vendor.name + " - " + __("Risk Assessments"));
|
||||
|
||||
@@ -88,7 +91,7 @@ export default function VendorRiskAssessmentTab() {
|
||||
return (
|
||||
<div className="text-center text-sm py-6 text-txt-secondary flex flex-col items-center gap-2">
|
||||
{__("No risk assessments found")}
|
||||
{!isSnapshotMode && (
|
||||
{!isSnapshotMode && canCreateRiskAssessment && (
|
||||
<CreateRiskAssessmentDialog
|
||||
vendorId={vendor.id}
|
||||
connection={data.riskAssessments.__id}
|
||||
@@ -116,7 +119,7 @@ export default function VendorRiskAssessmentTab() {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{!isSnapshotMode && (
|
||||
{!isSnapshotMode && canCreateRiskAssessment && (
|
||||
<CreateRiskAssessmentDialog
|
||||
vendorId={vendor.id}
|
||||
connection={data.riskAssessments.__id}
|
||||
|
||||
@@ -25,7 +25,8 @@ import { sprintf } from "@probo/helpers";
|
||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||
import { CreateServiceDialog } from "../dialogs/CreateServiceDialog";
|
||||
import { EditServiceDialog } from "../dialogs/EditServiceDialog";
|
||||
import { useState } from "react";
|
||||
import { use, useState } from "react";
|
||||
import { PermissionsContext } from "/providers/PermissionsContext";
|
||||
|
||||
export const vendorServicesFragment = graphql`
|
||||
fragment VendorServicesTabFragment on Vendor
|
||||
@@ -94,6 +95,11 @@ export default function VendorServicesTab() {
|
||||
name: string;
|
||||
description?: string | 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"));
|
||||
|
||||
@@ -103,7 +109,7 @@ export default function VendorServicesTab() {
|
||||
title={__("Services")}
|
||||
description={__("Manage services provided by this vendor.")}
|
||||
>
|
||||
{!isSnapshotMode && (
|
||||
{!isSnapshotMode && canCreateService && (
|
||||
<CreateServiceDialog
|
||||
vendorId={vendor.id}
|
||||
connectionId={connectionId}
|
||||
@@ -118,7 +124,7 @@ export default function VendorServicesTab() {
|
||||
<Tr>
|
||||
<SortableTh field="NAME">{__("Name")}</SortableTh>
|
||||
<Th>{__("Description")}</Th>
|
||||
{!isSnapshotMode && <Th>{__("Actions")}</Th>}
|
||||
{!isSnapshotMode && hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -129,12 +135,14 @@ export default function VendorServicesTab() {
|
||||
connectionId={connectionId}
|
||||
onEdit={setEditingService}
|
||||
isSnapshotMode={isSnapshotMode}
|
||||
canUpdate={canUpdateService}
|
||||
canDelete={canDeleteService}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
|
||||
{editingService && !isSnapshotMode && (
|
||||
{editingService && !isSnapshotMode && canUpdateService && (
|
||||
<EditServiceDialog
|
||||
serviceId={editingService.id}
|
||||
service={editingService}
|
||||
@@ -154,6 +162,8 @@ type ServiceRowProps = {
|
||||
description?: string | null;
|
||||
}) => void;
|
||||
isSnapshotMode: boolean;
|
||||
canUpdate?: boolean;
|
||||
canDelete?: boolean;
|
||||
};
|
||||
|
||||
function ServiceRow(props: ServiceRowProps) {
|
||||
@@ -167,6 +177,7 @@ function ServiceRow(props: ServiceRowProps) {
|
||||
successMessage: __("Service deleted successfully"),
|
||||
errorMessage: __("Failed to delete service"),
|
||||
});
|
||||
const hasAnyAction = props.canUpdate || props.canDelete;
|
||||
|
||||
const handleDelete = () => {
|
||||
confirm(
|
||||
@@ -194,26 +205,30 @@ function ServiceRow(props: ServiceRowProps) {
|
||||
<Tr>
|
||||
<Td>{service.name}</Td>
|
||||
<Td>{service.description || __("—")}</Td>
|
||||
{!props.isSnapshotMode && (
|
||||
{!props.isSnapshotMode && hasAnyAction && (
|
||||
<Td width={50} className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
icon={IconPencil}
|
||||
onClick={() => props.onEdit({
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
})}
|
||||
>
|
||||
{__("Edit")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
variant="danger"
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
{props.canUpdate && (
|
||||
<DropdownItem
|
||||
icon={IconPencil}
|
||||
onClick={() => props.onEdit({
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
})}
|
||||
>
|
||||
{__("Edit")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
{props.canDelete && (
|
||||
<DropdownItem
|
||||
icon={IconTrashCan}
|
||||
onClick={handleDelete}
|
||||
variant="danger"
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
)}
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
)}
|
||||
|
||||
@@ -165,10 +165,14 @@ const routes = [
|
||||
path: "",
|
||||
Component: () => {
|
||||
const { role } = use(PermissionsContext);
|
||||
if (role === Role.EMPLOYEE) {
|
||||
return <Navigate to="employee" />;
|
||||
switch (role) {
|
||||
case Role.EMPLOYEE:
|
||||
return <Navigate to="employee" />;
|
||||
case Role.AUDITOR:
|
||||
return <Navigate to="measures" />;
|
||||
default:
|
||||
return <Navigate to="tasks" />;
|
||||
}
|
||||
return <Navigate to="tasks" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ export const Role = {
|
||||
OWNER: "OWNER",
|
||||
ADMIN: "ADMIN",
|
||||
VIEWER: "VIEWER",
|
||||
AUDITOR: "AUDITOR",
|
||||
EMPLOYEE: "EMPLOYEE",
|
||||
} as const
|
||||
|
||||
@@ -9,11 +10,11 @@ export type Role = (typeof Role)[keyof typeof Role];
|
||||
|
||||
export function getAssignableRoles(currentRole: Role): Role[] {
|
||||
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) {
|
||||
return [Role.ADMIN, Role.VIEWER, Role.EMPLOYEE];
|
||||
return [Role.ADMIN, Role.VIEWER, Role.AUDITOR, Role.EMPLOYEE];
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
RoleAdmin Role = "ADMIN"
|
||||
RoleEmployee Role = "EMPLOYEE"
|
||||
RoleViewer Role = "VIEWER"
|
||||
RoleAuditor Role = "AUDITOR"
|
||||
RoleFull Role = "FULL"
|
||||
)
|
||||
|
||||
@@ -257,33 +258,32 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
AllRoles = []Role{RoleOwner, RoleAdmin, RoleEmployee, RoleViewer, RoleFull}
|
||||
NonEmployeeRoles = []Role{RoleOwner, RoleAdmin, RoleViewer, RoleFull}
|
||||
EditRoles = []Role{RoleOwner, RoleAdmin, RoleFull}
|
||||
AllRoles = []Role{RoleOwner, RoleAdmin, RoleEmployee, RoleViewer, RoleAuditor, 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{
|
||||
coredata.OrganizationEntityType: {
|
||||
ActionGet: AllRoles,
|
||||
ActionListSignableDocuments: AllRoles,
|
||||
ActionGetLogoUrl: AllRoles,
|
||||
ActionGet: AllRoles,
|
||||
ActionGetLogoUrl: AllRoles,
|
||||
|
||||
ActionListSignableDocuments: InternalRoles,
|
||||
|
||||
ActionListDocuments: NonEmployeeRoles,
|
||||
ActionGetHorizontalLogoUrl: NonEmployeeRoles,
|
||||
ActionMemberships: NonEmployeeRoles,
|
||||
ActionPeoples: NonEmployeeRoles,
|
||||
ActionTotalCount: NonEmployeeRoles,
|
||||
ActionListMembers: NonEmployeeRoles,
|
||||
ActionListInvitations: NonEmployeeRoles,
|
||||
ActionListSlackConnections: NonEmployeeRoles,
|
||||
ActionListFrameworks: NonEmployeeRoles,
|
||||
ActionListControls: NonEmployeeRoles,
|
||||
ActionListVendors: NonEmployeeRoles,
|
||||
ActionListPeople: NonEmployeeRoles,
|
||||
ActionListMeetings: NonEmployeeRoles,
|
||||
ActionListMeasures: NonEmployeeRoles,
|
||||
ActionListRisks: NonEmployeeRoles,
|
||||
ActionListTasks: NonEmployeeRoles,
|
||||
ActionListAssets: NonEmployeeRoles,
|
||||
ActionListData: NonEmployeeRoles,
|
||||
ActionListAudits: NonEmployeeRoles,
|
||||
@@ -292,13 +292,20 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
ActionListContinualImprovements: NonEmployeeRoles,
|
||||
ActionListProcessingActivities: NonEmployeeRoles,
|
||||
ActionListSnapshots: NonEmployeeRoles,
|
||||
ActionListTrustCenterFiles: NonEmployeeRoles,
|
||||
ActionGetTrustCenter: NonEmployeeRoles,
|
||||
ActionGetCustomDomain: NonEmployeeRoles,
|
||||
ActionListSAMLConfigurations: NonEmployeeRoles,
|
||||
ActionConfirmEmail: 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,
|
||||
ActionDeleteOrganizationHorizontalLogo: EditRoles,
|
||||
ActionCreateTrustCenter: EditRoles,
|
||||
@@ -336,11 +343,11 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
ActionDeleteOrganization: {RoleOwner},
|
||||
},
|
||||
coredata.TrustCenterEntityType: {
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionGetNdaFileUrl: NonEmployeeRoles,
|
||||
ActionGetOrganization: NonEmployeeRoles,
|
||||
ActionListAccesses: NonEmployeeRoles,
|
||||
ActionListReferences: NonEmployeeRoles,
|
||||
ActionGet: CoreRoles,
|
||||
ActionGetNdaFileUrl: CoreRoles,
|
||||
ActionGetOrganization: CoreRoles,
|
||||
ActionListAccesses: CoreRoles,
|
||||
ActionListReferences: CoreRoles,
|
||||
|
||||
ActionUpdateTrustCenter: EditRoles,
|
||||
ActionUploadTrustCenterNDA: EditRoles,
|
||||
@@ -349,26 +356,26 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
ActionCreateTrustCenterReference: EditRoles,
|
||||
},
|
||||
coredata.TrustCenterAccessEntityType: {
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionActiveCount: NonEmployeeRoles,
|
||||
ActionPendingRequestCount: NonEmployeeRoles,
|
||||
ActionAvailableDocumentAccesses: NonEmployeeRoles,
|
||||
ActionGetTrustCenterFile: NonEmployeeRoles,
|
||||
ActionGetReport: NonEmployeeRoles,
|
||||
ActionGet: CoreRoles,
|
||||
ActionActiveCount: CoreRoles,
|
||||
ActionPendingRequestCount: CoreRoles,
|
||||
ActionAvailableDocumentAccesses: CoreRoles,
|
||||
ActionGetTrustCenterFile: CoreRoles,
|
||||
ActionGetReport: CoreRoles,
|
||||
|
||||
ActionUpdateTrustCenterAccess: EditRoles,
|
||||
ActionDeleteTrustCenterAccess: EditRoles,
|
||||
},
|
||||
coredata.TrustCenterReferenceEntityType: {
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionGetLogoUrl: NonEmployeeRoles,
|
||||
ActionGet: CoreRoles,
|
||||
ActionGetLogoUrl: CoreRoles,
|
||||
|
||||
ActionUpdateTrustCenterReference: EditRoles,
|
||||
ActionDeleteTrustCenterReference: EditRoles,
|
||||
},
|
||||
coredata.TrustCenterFileEntityType: {
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionGetFileUrl: NonEmployeeRoles,
|
||||
ActionGet: CoreRoles,
|
||||
ActionGetFileUrl: CoreRoles,
|
||||
|
||||
ActionUpdateTrustCenterFile: EditRoles,
|
||||
ActionGetTrustCenterFile: EditRoles,
|
||||
@@ -487,9 +494,10 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
ActionDeleteControlSnapshotMapping: EditRoles,
|
||||
},
|
||||
coredata.MeasureEntityType: {
|
||||
ActionListTasks: CoreRoles,
|
||||
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionListEvidences: NonEmployeeRoles,
|
||||
ActionListTasks: NonEmployeeRoles,
|
||||
ActionListRisks: NonEmployeeRoles,
|
||||
ActionListControls: NonEmployeeRoles,
|
||||
ActionTotalCount: NonEmployeeRoles,
|
||||
@@ -499,11 +507,11 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
ActionUploadMeasureEvidence: EditRoles,
|
||||
},
|
||||
coredata.TaskEntityType: {
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionGetAssignedTo: NonEmployeeRoles,
|
||||
ActionGetOrganization: NonEmployeeRoles,
|
||||
ActionGetMeasure: NonEmployeeRoles,
|
||||
ActionListEvidences: NonEmployeeRoles,
|
||||
ActionGet: CoreRoles,
|
||||
ActionGetAssignedTo: CoreRoles,
|
||||
ActionGetOrganization: CoreRoles,
|
||||
ActionGetMeasure: CoreRoles,
|
||||
ActionListEvidences: CoreRoles,
|
||||
|
||||
ActionUpdateTask: EditRoles,
|
||||
ActionDeleteTask: EditRoles,
|
||||
@@ -513,7 +521,7 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
coredata.EvidenceEntityType: {
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionGetFile: NonEmployeeRoles,
|
||||
ActionGetTask: NonEmployeeRoles,
|
||||
ActionGetTask: CoreRoles,
|
||||
ActionGetMeasure: NonEmployeeRoles,
|
||||
|
||||
ActionDeleteEvidence: EditRoles,
|
||||
@@ -521,7 +529,8 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
coredata.DocumentEntityType: {
|
||||
ActionListSignableDocumentVersion: AllRoles,
|
||||
ActionGetSigned: AllRoles,
|
||||
ActionGetSignableDocument: AllRoles,
|
||||
|
||||
ActionGetSignableDocument: InternalRoles,
|
||||
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionGetOwner: NonEmployeeRoles,
|
||||
@@ -689,14 +698,14 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
ActionDownloadUrl: NonEmployeeRoles,
|
||||
},
|
||||
coredata.TrustCenterDocumentAccessEntityType: {
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionReport: NonEmployeeRoles,
|
||||
ActionTrustCenterFile: NonEmployeeRoles,
|
||||
ActionGet: CoreRoles,
|
||||
ActionReport: CoreRoles,
|
||||
ActionTrustCenterFile: CoreRoles,
|
||||
},
|
||||
coredata.MeetingEntityType: {
|
||||
ActionGet: NonEmployeeRoles,
|
||||
ActionGetOrganization: NonEmployeeRoles,
|
||||
ActionTotalCount: NonEmployeeRoles,
|
||||
ActionGet: CoreRoles,
|
||||
ActionGetOrganization: CoreRoles,
|
||||
ActionTotalCount: CoreRoles,
|
||||
|
||||
ActionUpdateMeeting: EditRoles,
|
||||
ActionDeleteMeeting: EditRoles,
|
||||
|
||||
@@ -220,7 +220,7 @@ var entityRegistry = map[uint16]EntityInfo{
|
||||
Table: "trust_center_references",
|
||||
},
|
||||
TrustCenterDocumentAccessEntityType: {
|
||||
Model: "",
|
||||
Model: "TrustCenterDocumentAccess",
|
||||
Table: "trust_center_document_accesses",
|
||||
},
|
||||
CustomDomainEntityType: {
|
||||
|
||||
@@ -26,6 +26,7 @@ const (
|
||||
MembershipRoleAdmin MembershipRole = "ADMIN"
|
||||
MembershipRoleEmployee MembershipRole = "EMPLOYEE"
|
||||
MembershipRoleViewer MembershipRole = "VIEWER"
|
||||
MembershipRoleAuditor MembershipRole = "AUDITOR"
|
||||
)
|
||||
|
||||
func (r MembershipRole) String() string {
|
||||
@@ -52,6 +53,8 @@ func (r *MembershipRole) Scan(value any) error {
|
||||
*r = MembershipRoleEmployee
|
||||
case "VIEWER":
|
||||
*r = MembershipRoleViewer
|
||||
case "AUDITOR":
|
||||
*r = MembershipRoleAuditor
|
||||
default:
|
||||
return fmt.Errorf("invalid MembershipRole value: %q", s)
|
||||
}
|
||||
|
||||
1
pkg/coredata/migrations/20251210T133440Z.sql
Normal file
1
pkg/coredata/migrations/20251210T133440Z.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TYPE authz_role ADD VALUE 'AUDITOR';
|
||||
@@ -38,6 +38,7 @@ enum Role {
|
||||
OWNER
|
||||
ADMIN
|
||||
VIEWER
|
||||
AUDITOR
|
||||
FULL
|
||||
}
|
||||
|
||||
@@ -98,6 +99,7 @@ enum MembershipRole
|
||||
EMPLOYEE
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee")
|
||||
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") {
|
||||
|
||||
@@ -9592,6 +9592,7 @@ enum Role {
|
||||
OWNER
|
||||
ADMIN
|
||||
VIEWER
|
||||
AUDITOR
|
||||
FULL
|
||||
}
|
||||
|
||||
@@ -9652,6 +9653,7 @@ enum MembershipRole
|
||||
EMPLOYEE
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee")
|
||||
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") {
|
||||
@@ -87915,12 +87917,14 @@ var (
|
||||
"ADMIN": coredata.MembershipRoleAdmin,
|
||||
"EMPLOYEE": coredata.MembershipRoleEmployee,
|
||||
"VIEWER": coredata.MembershipRoleViewer,
|
||||
"AUDITOR": coredata.MembershipRoleAuditor,
|
||||
}
|
||||
marshalNMembershipRole2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐMembershipRole = map[coredata.MembershipRole]string{
|
||||
coredata.MembershipRoleOwner: "OWNER",
|
||||
coredata.MembershipRoleAdmin: "ADMIN",
|
||||
coredata.MembershipRoleEmployee: "EMPLOYEE",
|
||||
coredata.MembershipRoleViewer: "VIEWER",
|
||||
coredata.MembershipRoleAuditor: "AUDITOR",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2537,22 +2537,24 @@ type Viewer struct {
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleOwner Role = "OWNER"
|
||||
RoleAdmin Role = "ADMIN"
|
||||
RoleViewer Role = "VIEWER"
|
||||
RoleFull Role = "FULL"
|
||||
RoleOwner Role = "OWNER"
|
||||
RoleAdmin Role = "ADMIN"
|
||||
RoleViewer Role = "VIEWER"
|
||||
RoleAuditor Role = "AUDITOR"
|
||||
RoleFull Role = "FULL"
|
||||
)
|
||||
|
||||
var AllRole = []Role{
|
||||
RoleOwner,
|
||||
RoleAdmin,
|
||||
RoleViewer,
|
||||
RoleAuditor,
|
||||
RoleFull,
|
||||
}
|
||||
|
||||
func (e Role) IsValid() bool {
|
||||
switch e {
|
||||
case RoleOwner, RoleAdmin, RoleViewer, RoleFull:
|
||||
case RoleOwner, RoleAdmin, RoleViewer, RoleAuditor, RoleFull:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user