@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user