From ab077c385ec77ed9f2013ea1d7408d552518627c Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 10 Jun 2025 13:40:24 +0200 Subject: [PATCH] Allow linking controls Signed-off-by: Bryan Frimin Signed-off-by: Sacha Al Himdani --- apps/console2/package.json | 1 + .../controls/LinkedControlsCard.tsx | 47 ++- .../controls/LinkedControlsDialog.tsx | 185 +++++++++ ...nkedControlsDialogControlsQuery.graphql.ts | 337 ++++++++++++++++ .../LinkedControlsDialogFragment.graphql.ts | 232 +++++++++++ .../LinkedControlsDialogQuery.graphql.ts | 239 ++++++++++++ apps/console2/src/hooks/forms/useRiskForm.tsx | 7 +- .../DocumentGraphNodeQuery.graphql.ts | 111 ++++-- .../MeasureGraphNodeQuery.graphql.ts | 87 +++-- .../documents/DocumentDetailPage.tsx | 18 +- ...umentDetailPageDocumentFragment.graphql.ts | 64 +-- .../documents/tabs/DocumentControlsTab.tsx | 68 +++- ...ocumentControlsTabControlsQuery.graphql.ts | 363 ++++++++++++++++++ .../DocumentControlsTabFragment.graphql.ts | 235 ++++++++++++ ...ntrolsTab_attachControlMutation.graphql.ts | 216 +++++++++++ .../measures/MeasureDetailPage.tsx | 31 ++ .../measures/tabs/MeasureControlsTab.tsx | 54 ++- .../measures/tabs/MeasureEvidencesTab.tsx | 2 +- .../measures/tabs/MeasureRisksTab.tsx | 2 +- .../measures/tabs/MeasureTasksTab.tsx | 2 +- ...easureControlsTabAttachMutation.graphql.ts | 216 +++++++++++ ...MeasureControlsTabControlsQuery.graphql.ts | 363 ++++++++++++++++++ .../MeasureControlsTabFragment.graphql.ts | 106 ++++- .../organizations/risks/RiskDetailPage.tsx | 22 +- .../risks/tabs/RiskDocumentsTab.tsx | 2 +- .../risks/tabs/RiskMeasuresTab.tsx | 2 +- .../vendors/VendorDetailPage.tsx | 9 + .../dialogs/ImportAssessmentDialog.tsx | 1 - .../vendors/tabs/VendorComplianceTab.tsx | 2 +- .../vendors/tabs/VendorOverviewTab.tsx | 5 +- .../InfiniteScrollTrigger.tsx | 1 - packages/ui/src/Atoms/Input/Input.tsx | 4 +- 32 files changed, 2841 insertions(+), 193 deletions(-) create mode 100644 apps/console2/src/components/controls/LinkedControlsDialog.tsx create mode 100644 apps/console2/src/components/controls/__generated__/LinkedControlsDialogControlsQuery.graphql.ts create mode 100644 apps/console2/src/components/controls/__generated__/LinkedControlsDialogFragment.graphql.ts create mode 100644 apps/console2/src/components/controls/__generated__/LinkedControlsDialogQuery.graphql.ts create mode 100644 apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTabControlsQuery.graphql.ts create mode 100644 apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTabFragment.graphql.ts create mode 100644 apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTab_attachControlMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabAttachMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabControlsQuery.graphql.ts diff --git a/apps/console2/package.json b/apps/console2/package.json index c163e176f..3196012dc 100644 --- a/apps/console2/package.json +++ b/apps/console2/package.json @@ -31,6 +31,7 @@ "react-relay": "^19.0.0", "react-router": "^7.6.0", "relay-runtime": "^19.0.0", + "use-debounce": "^10.0.5", "usehooks-ts": "^3.1.1", "zod": "^3.25.17" }, diff --git a/apps/console2/src/components/controls/LinkedControlsCard.tsx b/apps/console2/src/components/controls/LinkedControlsCard.tsx index 3e9f28c7c..2f11a5abd 100644 --- a/apps/console2/src/components/controls/LinkedControlsCard.tsx +++ b/apps/console2/src/components/controls/LinkedControlsCard.tsx @@ -3,17 +3,20 @@ import { Button, Tr, Td, - Table, Thead, Tbody, Th, IconTrashCan, Badge, + TrButton, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import type { LinkedControlsCardFragment$key } from "./__generated__/LinkedControlsCardFragment.graphql"; import { useFragment } from "react-relay"; import { useOrganizationId } from "/hooks/useOrganizationId"; +import { LinkedControlsDialog } from "./LinkedControlsDialog"; +import { SortableTable, SortableTh } from "../SortableTable"; +import type { ComponentProps } from "react"; const linkedControlFragment = graphql` fragment LinkedControlsCardFragment on Control { @@ -46,6 +49,10 @@ type Props = { connectionId: string; // Mutation to detach a control (will receive {controlId, ...params}) onDetach: Mutation; + // Mutation to attach a control (will receive {controlId, ...params}) + onAttach?: Mutation; + // Allow sorting in the table + refetch: Pick, "refetch">; }; /** @@ -67,11 +74,26 @@ export function LinkedControlsCard(props: Props) { }); }; + const onAttach = (controlId: string) => { + if (!props.onAttach) { + return; + } + props.onAttach({ + variables: { + input: { + controlId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + return ( - + - + {__("Reference")} @@ -85,16 +107,31 @@ export function LinkedControlsCard(props: Props) { )} {controls.map((control) => ( - + ))} + + {__("Link control")} + -
{__("Reference")} {__("Name")}
+ ); } function ControlRow(props: { control: LinkedControlsCardFragment$key & { id: string }; onClick: (controlId: string) => void; + onAttach?: (controlId: string) => void; }) { const control = useFragment(linkedControlFragment, props.control); const organizationId = useOrganizationId(); diff --git a/apps/console2/src/components/controls/LinkedControlsDialog.tsx b/apps/console2/src/components/controls/LinkedControlsDialog.tsx new file mode 100644 index 000000000..b67d1740f --- /dev/null +++ b/apps/console2/src/components/controls/LinkedControlsDialog.tsx @@ -0,0 +1,185 @@ +import { + Badge, + Button, + Dialog, + DialogContent, + IconMagnifyingGlass, + IconPlusLarge, + IconTrashCan, + InfiniteScrollTrigger, + Input, + Spinner, +} from "@probo/ui"; +import { + Suspense, + useMemo, + useRef, + type ReactNode, + type RefObject, +} from "react"; +import { useTranslate } from "@probo/i18n"; +import { graphql } from "relay-runtime"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; +import type { LinkedControlsDialogQuery } from "./__generated__/LinkedControlsDialogQuery.graphql"; +import type { + LinkedControlsDialogFragment$data, + LinkedControlsDialogFragment$key, +} from "./__generated__/LinkedControlsDialogFragment.graphql"; +import type { NodeOf } from "/types"; +import { useDebounceCallback } from "usehooks-ts"; + +const query = graphql` + query LinkedControlsDialogQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + id + ...LinkedControlsDialogFragment + } + } +`; + +const controlsFragment = graphql` + fragment LinkedControlsDialogFragment on Organization + @argumentDefinitions( + first: { type: "Int", defaultValue: 1 } + after: { type: "CursorKey" } + last: { type: "Int", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + order: { type: "ControlOrder", defaultValue: null } + filter: { type: "ControlFilter", defaultValue: null } + ) + @refetchable(queryName: "LinkedControlsDialogControlsQuery") { + controls( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + filter: $filter + ) @connection(key: "LinkedControlsDialogControlsQuery_controls") { + edges { + node { + id + name + sectionTitle + } + } + } + } +`; + +type Props = { + children: ReactNode; + connectionId: string; + disabled?: boolean; + linkedControls?: { id: string }[]; + onLink: (controlId: string) => void; + onUnlink: (controlId: string) => void; +}; + +type SearchRef = RefObject<{ search: (v: string) => void } | null>; + +export function LinkedControlsDialog(props: Props) { + const { __ } = useTranslate(); + const searchRef: SearchRef = useRef(null); + const onSearch = (v: string) => { + searchRef.current?.search(v); + }; + return ( + + +
+ +
+ }> + + +
+
+ ); +} + +function LinkedControlsDialogContent(props: Props & { ref: SearchRef }) { + const organizationId = useOrganizationId(); + const mainData = useLazyLoadQuery(query, { + organizationId, + }); + const { data, loadNext, hasNext, isLoadingNext, refetch } = + usePaginationFragment( + controlsFragment, + mainData.organization as LinkedControlsDialogFragment$key + ); + + const controls = data.controls?.edges?.map((edge) => edge.node) ?? []; + const controlIds = useMemo(() => { + return new Set(props.linkedControls?.map((c) => c.id) ?? []); + }, [props.linkedControls]); + + props.ref.current = { + search: useDebounceCallback((v: string) => { + refetch({ + first: 20, + filter: { + query: v, + }, + }); + }, 500), + }; + + return ( + <> +
+ {controls.map((control) => ( + + ))} + {hasNext && ( + loadNext(20)} + /> + )} +
+ + ); +} + +function ControlRow( + props: { + control: NodeOf; + controlIds: Set; + } & Props +) { + const { __ } = useTranslate(); + const isLinked = props.controlIds.has(props.control.id); + const onClick = isLinked ? props.onUnlink : props.onLink; + const IconComponent = isLinked ? IconTrashCan : IconPlusLarge; + return ( + + + ); +} diff --git a/apps/console2/src/components/controls/__generated__/LinkedControlsDialogControlsQuery.graphql.ts b/apps/console2/src/components/controls/__generated__/LinkedControlsDialogControlsQuery.graphql.ts new file mode 100644 index 000000000..88c854e08 --- /dev/null +++ b/apps/console2/src/components/controls/__generated__/LinkedControlsDialogControlsQuery.graphql.ts @@ -0,0 +1,337 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type ControlOrderField = "CREATED_AT" | "SECTION_TITLE"; +export type OrderDirection = "ASC" | "DESC"; +export type ControlFilter = { + query?: string | null | undefined; +}; +export type ControlOrder = { + direction: OrderDirection; + field: ControlOrderField; +}; +export type LinkedControlsDialogControlsQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + filter?: ControlFilter | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: ControlOrder | null | undefined; +}; +export type LinkedControlsDialogControlsQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsDialogFragment">; + }; +}; +export type LinkedControlsDialogControlsQuery = { + response: LinkedControlsDialogControlsQuery$data; + variables: LinkedControlsDialogControlsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "filter" +}, +v3 = { + "defaultValue": 1, + "kind": "LocalArgument", + "name": "first" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v5 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v6 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" +}, +v7 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v8 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v9 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v10 = { + "kind": "Variable", + "name": "filter", + "variableName": "filter" +}, +v11 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v12 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v13 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v14 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v15 = [ + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "LinkedControlsDialogControlsQuery", + "selections": [ + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "LinkedControlsDialogFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v4/*: any*/) + ], + "kind": "Operation", + "name": "LinkedControlsDialogControlsQuery", + "selections": [ + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v13/*: any*/), + (v14/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v15/*: any*/), + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "controls", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v14/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "sectionTitle", + "storageKey": null + }, + (v13/*: any*/) + ], + "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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v15/*: any*/), + "filters": [ + "orderBy", + "filter" + ], + "handle": "connection", + "key": "LinkedControlsDialogControlsQuery_controls", + "kind": "LinkedHandle", + "name": "controls" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "ececbd47b66b771cf95244ee81dae1f7", + "id": null, + "metadata": {}, + "name": "LinkedControlsDialogControlsQuery", + "operationKind": "query", + "text": "query LinkedControlsDialogControlsQuery(\n $after: CursorKey\n $before: CursorKey = null\n $filter: ControlFilter = null\n $first: Int = 1\n $last: Int = null\n $order: ControlOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...LinkedControlsDialogFragment_4cFWzS\n id\n }\n}\n\nfragment LinkedControlsDialogFragment_4cFWzS on Organization {\n controls(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: $filter) {\n edges {\n node {\n id\n name\n sectionTitle\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "b4dbf5c3271581e4317726870476da8d"; + +export default node; diff --git a/apps/console2/src/components/controls/__generated__/LinkedControlsDialogFragment.graphql.ts b/apps/console2/src/components/controls/__generated__/LinkedControlsDialogFragment.graphql.ts new file mode 100644 index 000000000..600a7c439 --- /dev/null +++ b/apps/console2/src/components/controls/__generated__/LinkedControlsDialogFragment.graphql.ts @@ -0,0 +1,232 @@ +/** + * @generated SignedSource<<0be68327d7fa04dbceb53404ed1d5df4>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type LinkedControlsDialogFragment$data = { + readonly controls: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly name: string; + readonly sectionTitle: string; + }; + }>; + }; + readonly id: string; + readonly " $fragmentType": "LinkedControlsDialogFragment"; +}; +export type LinkedControlsDialogFragment$key = { + readonly " $data"?: LinkedControlsDialogFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsDialogFragment">; +}; + +import LinkedControlsDialogControlsQuery_graphql from './LinkedControlsDialogControlsQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "controls" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "filter" + }, + { + "defaultValue": 1, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": LinkedControlsDialogControlsQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "LinkedControlsDialogFragment", + "selections": [ + { + "alias": "controls", + "args": [ + { + "kind": "Variable", + "name": "filter", + "variableName": "filter" + }, + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "__LinkedControlsDialogControlsQuery_controls_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "sectionTitle", + "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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + (v1/*: any*/) + ], + "type": "Organization", + "abstractKey": null +}; +})(); + +(node as any).hash = "b4dbf5c3271581e4317726870476da8d"; + +export default node; diff --git a/apps/console2/src/components/controls/__generated__/LinkedControlsDialogQuery.graphql.ts b/apps/console2/src/components/controls/__generated__/LinkedControlsDialogQuery.graphql.ts new file mode 100644 index 000000000..338ce21ca --- /dev/null +++ b/apps/console2/src/components/controls/__generated__/LinkedControlsDialogQuery.graphql.ts @@ -0,0 +1,239 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type LinkedControlsDialogQuery$variables = { + organizationId: string; +}; +export type LinkedControlsDialogQuery$data = { + readonly organization: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsDialogFragment">; + }; +}; +export type LinkedControlsDialogQuery = { + response: LinkedControlsDialogQuery$data; + variables: LinkedControlsDialogQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v4 = [ + { + "kind": "Literal", + "name": "first", + "value": 1 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "LinkedControlsDialogQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedControlsDialogFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "LinkedControlsDialogQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v4/*: any*/), + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "controls", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "sectionTitle", + "storageKey": null + }, + (v3/*: any*/) + ], + "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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "controls(first:1)" + }, + { + "alias": null, + "args": (v4/*: any*/), + "filters": [ + "orderBy", + "filter" + ], + "handle": "connection", + "key": "LinkedControlsDialogControlsQuery_controls", + "kind": "LinkedHandle", + "name": "controls" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "1b0f918add0dcf173a84255896a3b46c", + "id": null, + "metadata": {}, + "name": "LinkedControlsDialogQuery", + "operationKind": "query", + "text": "query LinkedControlsDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...LinkedControlsDialogFragment\n }\n}\n\nfragment LinkedControlsDialogFragment on Organization {\n controls(first: 1) {\n edges {\n node {\n id\n name\n sectionTitle\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "1be60a2dc1efbd8992472862ca32910e"; + +export default node; diff --git a/apps/console2/src/hooks/forms/useRiskForm.tsx b/apps/console2/src/hooks/forms/useRiskForm.tsx index 9a8b022f4..ac3500954 100644 --- a/apps/console2/src/hooks/forms/useRiskForm.tsx +++ b/apps/console2/src/hooks/forms/useRiskForm.tsx @@ -50,7 +50,12 @@ export const useRiskForm = (riskKey?: RiskKey) => { ...risk, ownerId: risk.owner?.id, } - : {}, + : { + inherentLikelihood: 3, + inherentImpact: 3, + residualLikelihood: 3, + residualImpact: 3, + }, }); }; diff --git a/apps/console2/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts index b40eb2712..69b46267e 100644 --- a/apps/console2/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts +++ b/apps/console2/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<660dce2bed93af9863423c7d15ce41a9>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -63,7 +63,7 @@ v5 = [ { "kind": "Literal", "name": "first", - "value": 100 + "value": 20 } ], v6 = { @@ -83,29 +83,18 @@ v7 = { v8 = { "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 - } - ], + "kind": "ScalarField", + "name": "endCursor", "storageKey": null }, v9 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null +}, +v10 = { "kind": "ClientExtension", "selections": [ { @@ -117,13 +106,26 @@ v9 = { } ] }, -v10 = [ +v11 = [ { "kind": "Literal", "name": "first", - "value": 20 + "value": 100 } -]; +], +v12 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + (v8/*: any*/), + (v9/*: any*/) + ], + "storageKey": null +}; return { "fragment": { "argumentDefinitions": (v0/*: any*/), @@ -251,23 +253,52 @@ return { ], "storageKey": null }, - (v8/*: any*/), - (v9/*: any*/) + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + (v8/*: any*/), + (v9/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + (v10/*: any*/) ], - "storageKey": "controls(first:100)" + "storageKey": "controls(first:20)" }, { "alias": null, "args": (v5/*: any*/), - "filters": null, + "filters": [ + "orderBy", + "filter" + ], "handle": "connection", - "key": "DocumentDetailPage_controls", + "key": "DocumentControlsTab_controls", "kind": "LinkedHandle", "name": "controls" }, { "alias": null, - "args": (v10/*: any*/), + "args": (v5/*: any*/), "concreteType": "DocumentVersionConnection", "kind": "LinkedField", "name": "versions", @@ -327,7 +358,7 @@ return { }, { "alias": null, - "args": (v5/*: any*/), + "args": (v11/*: any*/), "concreteType": "DocumentVersionSignatureConnection", "kind": "LinkedField", "name": "signatures", @@ -399,14 +430,14 @@ return { ], "storageKey": null }, - (v8/*: any*/), - (v9/*: any*/) + (v12/*: any*/), + (v10/*: any*/) ], "storageKey": "signatures(first:100)" }, { "alias": null, - "args": (v5/*: any*/), + "args": (v11/*: any*/), "filters": null, "handle": "connection", "key": "DocumentDetailPage_signatures", @@ -441,14 +472,14 @@ return { ], "storageKey": null }, - (v8/*: any*/), - (v9/*: any*/) + (v12/*: any*/), + (v10/*: any*/) ], "storageKey": "versions(first:20)" }, { "alias": null, - "args": (v10/*: any*/), + "args": (v5/*: any*/), "filters": null, "handle": "connection", "key": "DocumentDetailPage_versions", @@ -465,12 +496,12 @@ return { ] }, "params": { - "cacheID": "9660b8b2cc438fd732fcf0eb8265996b", + "cacheID": "010d49252c4c3098beb66f3550885cfc", "id": null, "metadata": {}, "name": "DocumentGraphNodeQuery", "operationKind": "query", - "text": "query DocumentGraphNodeQuery(\n $documentId: ID!\n) {\n node(id: $documentId) {\n __typename\n ... on Document {\n ...DocumentDetailPageDocumentFragment\n }\n id\n }\n}\n\nfragment DocumentDetailPageDocumentFragment on Document {\n id\n title\n owner {\n id\n fullName\n }\n controls(first: 100) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n versions(first: 20) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n signatures(first: 100) {\n edges {\n node {\n id\n state\n signedBy {\n id\n }\n ...DocumentSignaturesDialog_signature\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n ...DocumentVersionHistoryDialogFragment\n ...DocumentSignaturesDialog_version\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment DocumentSignaturesDialog_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n\nfragment DocumentSignaturesDialog_version on DocumentVersion {\n version\n status\n publishedAt\n updatedAt\n}\n\nfragment DocumentVersionHistoryDialogFragment on DocumentVersion {\n id\n version\n status\n content\n changelog\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n name\n id\n }\n}\n" + "text": "query DocumentGraphNodeQuery(\n $documentId: ID!\n) {\n node(id: $documentId) {\n __typename\n ... on Document {\n ...DocumentDetailPageDocumentFragment\n }\n id\n }\n}\n\nfragment DocumentControlsTabFragment on Document {\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 DocumentDetailPageDocumentFragment on Document {\n id\n title\n owner {\n id\n fullName\n }\n ...DocumentControlsTabFragment\n versions(first: 20) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n signatures(first: 100) {\n edges {\n node {\n id\n state\n signedBy {\n id\n }\n ...DocumentSignaturesDialog_signature\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n ...DocumentVersionHistoryDialogFragment\n ...DocumentSignaturesDialog_version\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment DocumentSignaturesDialog_signature on DocumentVersionSignature {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n primaryEmailAddress\n id\n }\n}\n\nfragment DocumentSignaturesDialog_version on DocumentVersion {\n version\n status\n publishedAt\n updatedAt\n}\n\nfragment DocumentVersionHistoryDialogFragment on DocumentVersion {\n id\n version\n status\n content\n changelog\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n name\n id\n }\n}\n" } }; })(); diff --git a/apps/console2/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts index a787f0190..ebcff02f7 100644 --- a/apps/console2/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts +++ b/apps/console2/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<19b79280f8cf27e7e36a700ae2ea003a>> * @lightSyntaxTransform * @nogrep */ @@ -140,6 +140,40 @@ v13 = { ] }, v14 = [ + { + "kind": "Literal", + "name": "first", + "value": 20 + } +], +v15 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + (v10/*: any*/), + (v11/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null +}, +v16 = [ { "kind": "Literal", "name": "first", @@ -387,7 +421,7 @@ return { }, { "alias": null, - "args": (v8/*: any*/), + "args": (v14/*: any*/), "concreteType": "ControlConnection", "kind": "LinkedField", "name": "controls", @@ -439,23 +473,26 @@ return { ], "storageKey": null }, - (v12/*: any*/), + (v15/*: any*/), (v13/*: any*/) ], - "storageKey": "controls(first:100)" + "storageKey": "controls(first:20)" }, { "alias": null, - "args": (v8/*: any*/), - "filters": null, + "args": (v14/*: any*/), + "filters": [ + "orderBy", + "filter" + ], "handle": "connection", - "key": "MeasureControlsTabFragment_controls", + "key": "MeasureControlsTab_controls", "kind": "LinkedHandle", "name": "controls" }, { "alias": null, - "args": (v14/*: any*/), + "args": (v16/*: any*/), "concreteType": "EvidenceConnection", "kind": "LinkedField", "name": "evidences", @@ -528,40 +565,14 @@ return { ], "storageKey": null }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - (v10/*: any*/), - (v11/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasPreviousPage", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "startCursor", - "storageKey": null - } - ], - "storageKey": null - }, + (v15/*: any*/), (v13/*: any*/) ], "storageKey": "evidences(first:50)" }, { "alias": null, - "args": (v14/*: any*/), + "args": (v16/*: any*/), "filters": [ "orderBy" ], @@ -580,12 +591,12 @@ return { ] }, "params": { - "cacheID": "b5f77889cfcec3b09ae7f6707c65200f", + "cacheID": "22ec6d5ae51e192283ffe91b9edc39d0", "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 ...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 name\n id\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: 100) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\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 filename\n size\n type\n createdAt\n fileUrl\n mimeType\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 ...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 name\n id\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 ...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 filename\n size\n type\n createdAt\n fileUrl\n mimeType\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" } }; })(); diff --git a/apps/console2/src/pages/organizations/documents/DocumentDetailPage.tsx b/apps/console2/src/pages/organizations/documents/DocumentDetailPage.tsx index 88425284e..43680d9a1 100644 --- a/apps/console2/src/pages/organizations/documents/DocumentDetailPage.tsx +++ b/apps/console2/src/pages/organizations/documents/DocumentDetailPage.tsx @@ -43,6 +43,8 @@ import UpdateVersionDialog from "./dialogs/UpdateVersionDialog"; import { useRef } from "react"; import { DocumentVersionHistoryDialog } from "./dialogs/DocumentVersionHistoryDialog"; import { DocumentSignaturesDialog } from "./dialogs/DocumentSignaturesDialog"; +import { controlsFragment } from "./tabs/DocumentControlsTab"; +import type { DocumentControlsTabFragment$key } from "./tabs/__generated__/DocumentControlsTabFragment.graphql"; type Props = { queryRef: PreloadedQuery; @@ -56,15 +58,7 @@ const documentFragment = graphql` id fullName } - controls(first: 100) @connection(key: "DocumentDetailPage_controls") { - __id - edges { - node { - id - ...LinkedControlsCardFragment - } - } - } + ...DocumentControlsTabFragment versions(first: 20) @connection(key: "DocumentDetailPage_versions") { __id edges { @@ -182,6 +176,10 @@ export default function DocumentDetailPage(props: Props) { }; const updateDialogRef = useRef<{ open: () => void }>(null); + const controls = useFragment( + controlsFragment, + document as DocumentControlsTabFragment$key + ).controls; return ( <> @@ -254,7 +252,7 @@ export default function DocumentDetailPage(props: Props) { to={`/organizations/${organizationId}/documents/${document.id}/controls`} > {__("Controls")} - {document.controls.edges.length} + {controls.edges.length} diff --git a/apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts b/apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts index d61e47b50..ffaa37a4b 100644 --- a/apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts +++ b/apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<72689566d977ab4171c75ba9ce2c62f4>> + * @generated SignedSource<<6c0262b14b79c61016ad404fab7a1da0>> * @lightSyntaxTransform * @nogrep */ @@ -13,15 +13,6 @@ export type DocumentStatus = "DRAFT" | "PUBLISHED"; export type DocumentVersionSignatureState = "REQUESTED" | "SIGNED"; import { FragmentRefs } from "relay-runtime"; export type DocumentDetailPageDocumentFragment$data = { - readonly controls: { - readonly __id: string; - readonly edges: ReadonlyArray<{ - readonly node: { - readonly id: string; - readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsCardFragment">; - }; - }>; - }; readonly id: string; readonly owner: { readonly fullName: string; @@ -55,6 +46,7 @@ export type DocumentDetailPageDocumentFragment$data = { }; }>; }; + readonly " $fragmentSpreads": FragmentRefs<"DocumentControlsTabFragment">; readonly " $fragmentType": "DocumentDetailPageDocumentFragment"; }; export type DocumentDetailPageDocumentFragment$key = { @@ -126,14 +118,6 @@ return { "kind": "Fragment", "metadata": { "connection": [ - { - "count": null, - "cursor": null, - "direction": "forward", - "path": [ - "controls" - ] - }, { "count": null, "cursor": null, @@ -180,47 +164,9 @@ return { "storageKey": null }, { - "alias": "controls", "args": null, - "concreteType": "ControlConnection", - "kind": "LinkedField", - "name": "__DocumentDetailPage_controls_connection", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "ControlEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Control", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v0/*: any*/), - { - "args": null, - "kind": "FragmentSpread", - "name": "LinkedControlsCardFragment" - }, - (v1/*: any*/) - ], - "storageKey": null - }, - (v2/*: any*/) - ], - "storageKey": null - }, - (v3/*: any*/), - (v4/*: any*/) - ], - "storageKey": null + "kind": "FragmentSpread", + "name": "DocumentControlsTabFragment" }, { "alias": "versions", @@ -373,6 +319,6 @@ return { }; })(); -(node as any).hash = "df93b2fdd0ceea0637ddac2e4186181c"; +(node as any).hash = "02b0af2263f78304097136c2466be18c"; export default node; diff --git a/apps/console2/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx b/apps/console2/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx index fff85fdeb..18e6ce695 100644 --- a/apps/console2/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx +++ b/apps/console2/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx @@ -1,8 +1,39 @@ import { LinkedControlsCard } from "/components/controls/LinkedControlsCard"; import { useOutletContext } from "react-router"; -import type { DocumentDetailPageDocumentFragment$data } from "../__generated__/DocumentDetailPageDocumentFragment.graphql"; import { graphql } from "relay-runtime"; -import { useMutation } from "react-relay"; +import { useMutation, useRefetchableFragment } from "react-relay"; +import type { DocumentControlsTabFragment$key } from "./__generated__/DocumentControlsTabFragment.graphql"; + +export const controlsFragment = graphql` + fragment DocumentControlsTabFragment on Document + @argumentDefinitions( + first: { type: "Int", defaultValue: 20 } + after: { type: "CursorKey" } + last: { type: "Int", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + order: { type: "ControlOrder", defaultValue: null } + filter: { type: "ControlFilter", defaultValue: null } + ) + @refetchable(queryName: "DocumentControlsTabControlsQuery") { + id + controls( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + filter: $filter + ) @connection(key: "DocumentControlsTab_controls") { + __id + edges { + node { + id + ...LinkedControlsCardFragment + } + } + } + } +`; const detachControlMutation = graphql` mutation DocumentControlsTab_detachControlMutation( @@ -15,19 +46,40 @@ const detachControlMutation = graphql` } `; +const attachControlMutation = graphql` + mutation DocumentControlsTab_attachControlMutation( + $input: CreateControlDocumentMappingInput! + $connections: [ID!]! + ) { + createControlDocumentMapping(input: $input) { + controlEdge @prependEdge(connections: $connections) { + node { + id + ...LinkedControlsCardFragment + } + } + } + } +`; + export default function DocumentControlsTab() { const { document } = useOutletContext<{ - document: DocumentDetailPageDocumentFragment$data; + document: DocumentControlsTabFragment$key; }>(); - const controls = document.controls.edges.map((edge) => edge.node); - const [detachControl] = useMutation(detachControlMutation); - console.log(controls.map((c) => c.id)); + const [data, refetch] = useRefetchableFragment(controlsFragment, document); + const controls = data.controls.edges.map((edge) => edge.node); + const [detachControl, isDetaching] = useMutation(detachControlMutation); + const [attachControl, isAttaching] = useMutation(attachControlMutation); + const isLoading = isDetaching || isAttaching; return ( ); } diff --git a/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTabControlsQuery.graphql.ts b/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTabControlsQuery.graphql.ts new file mode 100644 index 000000000..8f6d79f9e --- /dev/null +++ b/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTabControlsQuery.graphql.ts @@ -0,0 +1,363 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type ControlOrderField = "CREATED_AT" | "SECTION_TITLE"; +export type OrderDirection = "ASC" | "DESC"; +export type ControlFilter = { + query?: string | null | undefined; +}; +export type ControlOrder = { + direction: OrderDirection; + field: ControlOrderField; +}; +export type DocumentControlsTabControlsQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + filter?: ControlFilter | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: ControlOrder | null | undefined; +}; +export type DocumentControlsTabControlsQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"DocumentControlsTabFragment">; + }; +}; +export type DocumentControlsTabControlsQuery = { + response: DocumentControlsTabControlsQuery$data; + variables: DocumentControlsTabControlsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "filter" +}, +v3 = { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v5 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v6 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" +}, +v7 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v8 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v9 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v10 = { + "kind": "Variable", + "name": "filter", + "variableName": "filter" +}, +v11 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v12 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v13 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v14 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v15 = [ + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +], +v16 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "DocumentControlsTabControlsQuery", + "selections": [ + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "DocumentControlsTabFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v4/*: any*/) + ], + "kind": "Operation", + "name": "DocumentControlsTabControlsQuery", + "selections": [ + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v13/*: any*/), + (v14/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v15/*: any*/), + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "controls", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v14/*: any*/), + (v16/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "sectionTitle", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + (v16/*: any*/), + (v14/*: any*/) + ], + "storageKey": null + }, + (v13/*: any*/) + ], + "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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v15/*: any*/), + "filters": [ + "orderBy", + "filter" + ], + "handle": "connection", + "key": "DocumentControlsTab_controls", + "kind": "LinkedHandle", + "name": "controls" + } + ], + "type": "Document", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "9db5367f71ee867e25df15eb9babf4ce", + "id": null, + "metadata": {}, + "name": "DocumentControlsTabControlsQuery", + "operationKind": "query", + "text": "query DocumentControlsTabControlsQuery(\n $after: CursorKey\n $before: CursorKey = null\n $filter: ControlFilter = null\n $first: Int = 20\n $last: Int = null\n $order: ControlOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...DocumentControlsTabFragment_4cFWzS\n id\n }\n}\n\nfragment DocumentControlsTabFragment_4cFWzS on Document {\n id\n controls(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: $filter) {\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 LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n name\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "c1b6d5212b3fa33f60c02ddee32d5cea"; + +export default node; diff --git a/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTabFragment.graphql.ts b/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTabFragment.graphql.ts new file mode 100644 index 000000000..a551f40b2 --- /dev/null +++ b/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTabFragment.graphql.ts @@ -0,0 +1,235 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type DocumentControlsTabFragment$data = { + readonly controls: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsCardFragment">; + }; + }>; + }; + readonly id: string; + readonly " $fragmentType": "DocumentControlsTabFragment"; +}; +export type DocumentControlsTabFragment$key = { + readonly " $data"?: DocumentControlsTabFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"DocumentControlsTabFragment">; +}; + +import DocumentControlsTabControlsQuery_graphql from './DocumentControlsTabControlsQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "controls" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "filter" + }, + { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": DocumentControlsTabControlsQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "DocumentControlsTabFragment", + "selections": [ + (v1/*: any*/), + { + "alias": "controls", + "args": [ + { + "kind": "Variable", + "name": "filter", + "variableName": "filter" + }, + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "__DocumentControlsTab_controls_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedControlsCardFragment" + }, + { + "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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + } + ], + "type": "Document", + "abstractKey": null +}; +})(); + +(node as any).hash = "c1b6d5212b3fa33f60c02ddee32d5cea"; + +export default node; diff --git a/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTab_attachControlMutation.graphql.ts b/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTab_attachControlMutation.graphql.ts new file mode 100644 index 000000000..00c30cc70 --- /dev/null +++ b/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTab_attachControlMutation.graphql.ts @@ -0,0 +1,216 @@ +/** + * @generated SignedSource<<74725fda85d702538289d6a97ef17399>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type CreateControlDocumentMappingInput = { + controlId: string; + documentId: string; +}; +export type DocumentControlsTab_attachControlMutation$variables = { + connections: ReadonlyArray; + input: CreateControlDocumentMappingInput; +}; +export type DocumentControlsTab_attachControlMutation$data = { + readonly createControlDocumentMapping: { + readonly controlEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsCardFragment">; + }; + }; + }; +}; +export type DocumentControlsTab_attachControlMutation = { + response: DocumentControlsTab_attachControlMutation$data; + variables: DocumentControlsTab_attachControlMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "DocumentControlsTab_attachControlMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlDocumentMappingPayload", + "kind": "LinkedField", + "name": "createControlDocumentMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "controlEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedControlsCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "DocumentControlsTab_attachControlMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlDocumentMappingPayload", + "kind": "LinkedField", + "name": "createControlDocumentMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "controlEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "sectionTitle", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + (v4/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "controlEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "d283633e6a7ca3af074af6a8b716fc1c", + "id": null, + "metadata": {}, + "name": "DocumentControlsTab_attachControlMutation", + "operationKind": "mutation", + "text": "mutation DocumentControlsTab_attachControlMutation(\n $input: CreateControlDocumentMappingInput!\n) {\n createControlDocumentMapping(input: $input) {\n controlEdge {\n node {\n id\n ...LinkedControlsCardFragment\n }\n }\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n name\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "273f0d619a2be7df7451e7354c53315a"; + +export default node; diff --git a/apps/console2/src/pages/organizations/measures/MeasureDetailPage.tsx b/apps/console2/src/pages/organizations/measures/MeasureDetailPage.tsx index d00362707..bfcce3274 100644 --- a/apps/console2/src/pages/organizations/measures/MeasureDetailPage.tsx +++ b/apps/console2/src/pages/organizations/measures/MeasureDetailPage.tsx @@ -16,6 +16,7 @@ import { Option, PropertyRow, Select, + TabBadge, TabLink, Tabs, useConfirm, @@ -23,6 +24,7 @@ import { import { useTranslate } from "@probo/i18n"; import { ConnectionHandler, + useFragment, usePreloadedQuery, type PreloadedQuery, } from "react-relay"; @@ -38,6 +40,14 @@ import { getMeasureStateLabel, measureStates, slugify } from "@probo/helpers"; import MeasureFormDialog from "./dialog/MeasureFormDialog"; import { sprintf } from "@probo/helpers"; import { useNavigate } from "react-router"; +import { tasksFragment } from "./tabs/MeasureTasksTab"; +import type { MeasureTasksTabFragment$key } from "./tabs/__generated__/MeasureTasksTabFragment.graphql"; +import { evidencesFragment } from "./tabs/MeasureEvidencesTab"; +import type { MeasureEvidencesTabFragment$key } from "./tabs/__generated__/MeasureEvidencesTabFragment.graphql"; +import { controlsFragment } from "./tabs/MeasureControlsTab"; +import type { MeasureControlsTabFragment$key } from "./tabs/__generated__/MeasureControlsTabFragment.graphql"; +import { risksFragment } from "./tabs/MeasureRisksTab"; +import type { MeasureRisksTabFragment$key } from "./tabs/__generated__/MeasureRisksTabFragment.graphql"; type Props = { queryRef: PreloadedQuery; @@ -60,6 +70,23 @@ export default function MeasureDetailPage(props: Props) { ); } + const tasksCount = useFragment( + tasksFragment, + measure as MeasureTasksTabFragment$key + ).tasks.edges.length; + const evidencesCount = useFragment( + evidencesFragment, + measure as MeasureEvidencesTabFragment$key + ).evidences.edges.length; + const controlsCount = useFragment( + controlsFragment, + measure as MeasureControlsTabFragment$key + ).controls.edges.length; + const risksCount = useFragment( + risksFragment, + measure as MeasureRisksTabFragment$key + ).risks.edges.length; + const onDelete = () => { const connectionId = ConnectionHandler.getConnectionID( organizationId, @@ -157,24 +184,28 @@ export default function MeasureDetailPage(props: Props) { > {__("Evidences")} + {evidencesCount > 0 && {evidencesCount}} {__("Tasks")} + {tasksCount > 0 && {tasksCount}} {__("Controls")} + {controlsCount > 0 && {controlsCount}} {__("Risks")} + {risksCount > 0 && {risksCount}} diff --git a/apps/console2/src/pages/organizations/measures/tabs/MeasureControlsTab.tsx b/apps/console2/src/pages/organizations/measures/tabs/MeasureControlsTab.tsx index f24afd42b..b7a0225e4 100644 --- a/apps/console2/src/pages/organizations/measures/tabs/MeasureControlsTab.tsx +++ b/apps/console2/src/pages/organizations/measures/tabs/MeasureControlsTab.tsx @@ -1,13 +1,33 @@ -import { graphql, useFragment, useMutation } from "react-relay"; +import { + graphql, + useFragment, + useMutation, + useRefetchableFragment, +} from "react-relay"; import { useOutletContext } from "react-router"; import { LinkedControlsCard } from "/components/controls/LinkedControlsCard"; import type { MeasureControlsTabFragment$key } from "./__generated__/MeasureControlsTabFragment.graphql"; -const ControlsFragment = graphql` - fragment MeasureControlsTabFragment on Measure { +export const controlsFragment = graphql` + fragment MeasureControlsTabFragment on Measure + @argumentDefinitions( + first: { type: "Int", defaultValue: 20 } + after: { type: "CursorKey" } + last: { type: "Int", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + order: { type: "ControlOrder", defaultValue: null } + filter: { type: "ControlFilter", defaultValue: null } + ) + @refetchable(queryName: "MeasureControlsTabControlsQuery") { id - controls(first: 100) - @connection(key: "MeasureControlsTabFragment_controls") { + controls( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + filter: $filter + ) @connection(key: "MeasureControlsTab_controls") { __id edges { node { @@ -30,23 +50,43 @@ export const detachControlMutation = graphql` } `; +export const attachControlMutation = graphql` + mutation MeasureControlsTabAttachMutation( + $input: CreateControlMeasureMappingInput! + $connections: [ID!]! + ) { + createControlMeasureMapping(input: $input) { + controlEdge @prependEdge(connections: $connections) { + node { + id + ...LinkedControlsCardFragment + } + } + } + } +`; + export default function MeasureControlsTab() { const { measure } = useOutletContext<{ measure: MeasureControlsTabFragment$key & { id: string }; }>(); - const data = useFragment(ControlsFragment, measure); + const [data, refetch] = useRefetchableFragment(controlsFragment, measure); const connectionId = data.controls.__id; const controls = data.controls?.edges?.map((edge) => edge.node) ?? []; const [detachControl, isDetaching] = useMutation(detachControlMutation); + const [attachControl, isAttaching] = useMutation(attachControlMutation); + const isLoading = isDetaching || isAttaching; return ( ); } diff --git a/apps/console2/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx b/apps/console2/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx index a70b0d9fd..7cc1f6ea5 100644 --- a/apps/console2/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx +++ b/apps/console2/src/pages/organizations/measures/tabs/MeasureEvidencesTab.tsx @@ -21,7 +21,7 @@ import type { MeasureEvidencesTabFragment_evidence$key } from "./__generated__/M import { fileSize, fileType } from "@probo/helpers"; import { promisifyMutation, sprintf } from "@probo/helpers"; -const evidencesFragment = graphql` +export const evidencesFragment = graphql` fragment MeasureEvidencesTabFragment on Measure @refetchable(queryName: "MeasureEvidencesTabQuery") @argumentDefinitions( diff --git a/apps/console2/src/pages/organizations/measures/tabs/MeasureRisksTab.tsx b/apps/console2/src/pages/organizations/measures/tabs/MeasureRisksTab.tsx index a7789451f..6400a1b73 100644 --- a/apps/console2/src/pages/organizations/measures/tabs/MeasureRisksTab.tsx +++ b/apps/console2/src/pages/organizations/measures/tabs/MeasureRisksTab.tsx @@ -3,7 +3,7 @@ import type { MeasureRisksTabFragment$key } from "./__generated__/MeasureRisksTa import { useOutletContext } from "react-router"; import { LinkedRisksCard } from "/components/risks/LinkedRisksCard"; -const risksFragment = graphql` +export const risksFragment = graphql` fragment MeasureRisksTabFragment on Measure { id risks(first: 100) @connection(key: "Measure__risks") { diff --git a/apps/console2/src/pages/organizations/measures/tabs/MeasureTasksTab.tsx b/apps/console2/src/pages/organizations/measures/tabs/MeasureTasksTab.tsx index b48109f76..b44d0e4e0 100644 --- a/apps/console2/src/pages/organizations/measures/tabs/MeasureTasksTab.tsx +++ b/apps/console2/src/pages/organizations/measures/tabs/MeasureTasksTab.tsx @@ -7,7 +7,7 @@ import { Button, IconPlusLarge } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import TaskFormDialog from "/components/tasks/TaskFormDialog"; -const tasksFragment = graphql` +export const tasksFragment = graphql` fragment MeasureTasksTabFragment on Measure { tasks(first: 100) @connection(key: "Measure__tasks") { __id diff --git a/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabAttachMutation.graphql.ts b/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabAttachMutation.graphql.ts new file mode 100644 index 000000000..ca26b7395 --- /dev/null +++ b/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabAttachMutation.graphql.ts @@ -0,0 +1,216 @@ +/** + * @generated SignedSource<<70f20e2c5e4b88b101691f90aecde2da>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type CreateControlMeasureMappingInput = { + controlId: string; + measureId: string; +}; +export type MeasureControlsTabAttachMutation$variables = { + connections: ReadonlyArray; + input: CreateControlMeasureMappingInput; +}; +export type MeasureControlsTabAttachMutation$data = { + readonly createControlMeasureMapping: { + readonly controlEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsCardFragment">; + }; + }; + }; +}; +export type MeasureControlsTabAttachMutation = { + response: MeasureControlsTabAttachMutation$data; + variables: MeasureControlsTabAttachMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "MeasureControlsTabAttachMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlMeasureMappingPayload", + "kind": "LinkedField", + "name": "createControlMeasureMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "controlEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedControlsCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "MeasureControlsTabAttachMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlMeasureMappingPayload", + "kind": "LinkedField", + "name": "createControlMeasureMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "controlEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "sectionTitle", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + (v4/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "controlEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "7b0bd217fa037088f76cb4b6fc4a420a", + "id": null, + "metadata": {}, + "name": "MeasureControlsTabAttachMutation", + "operationKind": "mutation", + "text": "mutation MeasureControlsTabAttachMutation(\n $input: CreateControlMeasureMappingInput!\n) {\n createControlMeasureMapping(input: $input) {\n controlEdge {\n node {\n id\n ...LinkedControlsCardFragment\n }\n }\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n name\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "010619cccaa46b2e02738bb0148341c0"; + +export default node; diff --git a/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabControlsQuery.graphql.ts b/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabControlsQuery.graphql.ts new file mode 100644 index 000000000..9e9bc5c96 --- /dev/null +++ b/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabControlsQuery.graphql.ts @@ -0,0 +1,363 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type ControlOrderField = "CREATED_AT" | "SECTION_TITLE"; +export type OrderDirection = "ASC" | "DESC"; +export type ControlFilter = { + query?: string | null | undefined; +}; +export type ControlOrder = { + direction: OrderDirection; + field: ControlOrderField; +}; +export type MeasureControlsTabControlsQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + filter?: ControlFilter | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: ControlOrder | null | undefined; +}; +export type MeasureControlsTabControlsQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment">; + }; +}; +export type MeasureControlsTabControlsQuery = { + response: MeasureControlsTabControlsQuery$data; + variables: MeasureControlsTabControlsQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "filter" +}, +v3 = { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v5 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v6 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" +}, +v7 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v8 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v9 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v10 = { + "kind": "Variable", + "name": "filter", + "variableName": "filter" +}, +v11 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v12 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v13 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v14 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v15 = [ + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +], +v16 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "MeasureControlsTabControlsQuery", + "selections": [ + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "MeasureControlsTabFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + (v4/*: any*/) + ], + "kind": "Operation", + "name": "MeasureControlsTabControlsQuery", + "selections": [ + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v13/*: any*/), + (v14/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v15/*: any*/), + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "controls", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v14/*: any*/), + (v16/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "sectionTitle", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + (v16/*: any*/), + (v14/*: any*/) + ], + "storageKey": null + }, + (v13/*: any*/) + ], + "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 + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v15/*: any*/), + "filters": [ + "orderBy", + "filter" + ], + "handle": "connection", + "key": "MeasureControlsTab_controls", + "kind": "LinkedHandle", + "name": "controls" + } + ], + "type": "Measure", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "33e321fd8342ba1909b806fca25debf4", + "id": null, + "metadata": {}, + "name": "MeasureControlsTabControlsQuery", + "operationKind": "query", + "text": "query MeasureControlsTabControlsQuery(\n $after: CursorKey\n $before: CursorKey = null\n $filter: ControlFilter = null\n $first: Int = 20\n $last: Int = null\n $order: ControlOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MeasureControlsTabFragment_4cFWzS\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n name\n id\n }\n}\n\nfragment MeasureControlsTabFragment_4cFWzS on Measure {\n id\n controls(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: $filter) {\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" + } +}; +})(); + +(node as any).hash = "a9a268e40cc503e7e11fca8464f3e62c"; + +export default node; diff --git a/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabFragment.graphql.ts b/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabFragment.graphql.ts index f2e490092..624ac6f9e 100644 --- a/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabFragment.graphql.ts +++ b/apps/console2/src/pages/organizations/measures/tabs/__generated__/MeasureControlsTabFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<08d4e9940fe4265f66568c81985cb591>> * @lightSyntaxTransform * @nogrep */ @@ -28,8 +28,13 @@ export type MeasureControlsTabFragment$key = { readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment">; }; +import MeasureControlsTabControlsQuery_graphql from './MeasureControlsTabControlsQuery.graphql'; + const node: ReaderFragment = (function(){ -var v0 = { +var v0 = [ + "controls" +], +v1 = { "alias": null, "args": null, "kind": "ScalarField", @@ -37,29 +42,90 @@ var v0 = { "storageKey": null }; return { - "argumentDefinitions": [], + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "filter" + }, + { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" + } + ], "kind": "Fragment", "metadata": { "connection": [ { "count": null, "cursor": null, - "direction": "forward", - "path": [ - "controls" - ] + "direction": "bidirectional", + "path": (v0/*: any*/) } - ] + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": MeasureControlsTabControlsQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } }, "name": "MeasureControlsTabFragment", "selections": [ - (v0/*: any*/), + (v1/*: any*/), { "alias": "controls", - "args": null, + "args": [ + { + "kind": "Variable", + "name": "filter", + "variableName": "filter" + }, + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], "concreteType": "ControlConnection", "kind": "LinkedField", - "name": "__MeasureControlsTabFragment_controls_connection", + "name": "__MeasureControlsTab_controls_connection", "plural": false, "selections": [ { @@ -78,7 +144,7 @@ return { "name": "node", "plural": false, "selections": [ - (v0/*: any*/), + (v1/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -125,6 +191,20 @@ return { "kind": "ScalarField", "name": "hasNextPage", "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null } ], "storageKey": null @@ -150,6 +230,6 @@ return { }; })(); -(node as any).hash = "ce7a8ffb4c5837e7b5f71a781f332064"; +(node as any).hash = "a9a268e40cc503e7e11fca8464f3e62c"; export default node; diff --git a/apps/console2/src/pages/organizations/risks/RiskDetailPage.tsx b/apps/console2/src/pages/organizations/risks/RiskDetailPage.tsx index f64cecd36..b6b603a74 100644 --- a/apps/console2/src/pages/organizations/risks/RiskDetailPage.tsx +++ b/apps/console2/src/pages/organizations/risks/RiskDetailPage.tsx @@ -10,6 +10,7 @@ import { IconTrashCan, PageHeader, PropertyRow, + TabBadge, TabLink, Tabs, useConfirm, @@ -18,7 +19,11 @@ import { Outlet, useNavigate, useParams } from "react-router"; import { useTranslate } from "@probo/i18n"; import { getTreatment, sprintf } from "@probo/helpers"; import { ConnectionHandler } from "relay-runtime"; -import { usePreloadedQuery, type PreloadedQuery } from "react-relay"; +import { + useFragment, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; import FormRiskDialog from "./FormRiskDialog"; import { usePageTitle } from "@probo/hooks"; import { useOrganizationId } from "/hooks/useOrganizationId"; @@ -28,6 +33,10 @@ import { useDeleteRiskMutation, } from "/hooks/graph/RiskGraph"; import type { RiskGraphNodeQuery } from "/hooks/graph/__generated__/RiskGraphNodeQuery.graphql"; +import { documentsFragment } from "./tabs/RiskDocumentsTab"; +import type { RiskDocumentsTabFragment$key } from "./tabs/__generated__/RiskDocumentsTabFragment.graphql"; +import { measuresFragment } from "./tabs/RiskMeasuresTab"; +import type { RiskMeasuresTabFragment$key } from "./tabs/__generated__/RiskMeasuresTabFragment.graphql"; type Props = { queryRef: PreloadedQuery; @@ -80,6 +89,15 @@ export default function RiskDetailPage(props: Props) { ); }; + const documentsCount = useFragment( + documentsFragment, + risk as RiskDocumentsTabFragment$key + ).documents.edges.length; + const measuresCount = useFragment( + measuresFragment, + risk as RiskMeasuresTabFragment$key + ).measures.edges.length; + return (
{/* Header */} @@ -128,11 +146,13 @@ export default function RiskDetailPage(props: Props) { to={`/organizations/${organizationId}/risks/${riskId}/measures`} > {__("Measures")} + {measuresCount > 0 && {measuresCount}} {__("Documents")} + {documentsCount > 0 && {documentsCount}} diff --git a/apps/console2/src/pages/organizations/risks/tabs/RiskDocumentsTab.tsx b/apps/console2/src/pages/organizations/risks/tabs/RiskDocumentsTab.tsx index c355fe1a5..d72c9dd5f 100644 --- a/apps/console2/src/pages/organizations/risks/tabs/RiskDocumentsTab.tsx +++ b/apps/console2/src/pages/organizations/risks/tabs/RiskDocumentsTab.tsx @@ -3,7 +3,7 @@ import { useOutletContext } from "react-router"; import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard"; import type { RiskDocumentsTabFragment$key } from "./__generated__/RiskDocumentsTabFragment.graphql"; -const documentsFragment = graphql` +export const documentsFragment = graphql` fragment RiskDocumentsTabFragment on Risk { id documents(first: 100) @connection(key: "Risk__documents") { diff --git a/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx b/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx index ce81cc4ec..91be49065 100644 --- a/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx +++ b/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx @@ -3,7 +3,7 @@ import type { RiskMeasuresTabFragment$key } from "./__generated__/RiskMeasuresTa import { useOutletContext } from "react-router"; import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard"; -const measuresFragment = graphql` +export const measuresFragment = graphql` fragment RiskMeasuresTabFragment on Risk { id measures(first: 100) @connection(key: "Risk__measures") { diff --git a/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx b/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx index 0f913b600..7de4774e5 100644 --- a/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx +++ b/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx @@ -1,5 +1,6 @@ import { ConnectionHandler, + useFragment, usePreloadedQuery, type PreloadedQuery, } from "react-relay"; @@ -16,6 +17,7 @@ import { DropdownItem, IconPageTextLine, IconTrashCan, + TabBadge, TabLink, Tabs, } from "@probo/ui"; @@ -24,6 +26,8 @@ import { useOrganizationId } from "/hooks/useOrganizationId"; import { Outlet } from "react-router"; import { faviconUrl } from "@probo/helpers"; import { ImportAssessmentDialog } from "./dialogs/ImportAssessmentDialog"; +import { complianceReportsFragment } from "./tabs/VendorComplianceTab"; +import type { VendorComplianceTabFragment$key } from "./tabs/__generated__/VendorComplianceTabFragment.graphql"; type Props = { queryRef: PreloadedQuery; @@ -39,6 +43,10 @@ export default function VendorDetailPage(props: Props) { ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey) ); const logo = faviconUrl(vendor.websiteUrl); + const reportsCount = useFragment( + complianceReportsFragment, + vendor as VendorComplianceTabFragment$key + ).complianceReports.edges.length; return (
@@ -97,6 +105,7 @@ export default function VendorDetailPage(props: Props) { to={`/organizations/${organizationId}/vendors/${vendor.id}/compliance`} > {__("Compliance reports")} + {reportsCount > 0 && {reportsCount}} {__("Links")} {urls.map((url) => ( -
+