diff --git a/apps/console2/src/components/PageError.tsx b/apps/console2/src/components/PageError.tsx new file mode 100644 index 000000000..3144909f3 --- /dev/null +++ b/apps/console2/src/components/PageError.tsx @@ -0,0 +1,35 @@ +import { useRouteError } from "react-router"; +import { IconPageCross } from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; + +const classNames = { + wrapper: "py-10 text-center space-y-2 ", + title: "text-2xl flex gap-2 font-semibold items-center justify-center", + description: "text-base text-txt-tertiary", +}; + +export function PageError() { + const error = useRouteError(); + const { __ } = useTranslate(); + + if (!error) { + return ( +
+

+ + {__("Page not found")} +

+

+ {__("The page you are looking for does not exist")} +

+
+ ); + } + + return ( +
+

{__("Unexpected error :(")}

+

{error.toString()}

+
+ ); +} diff --git a/apps/console2/src/components/controls/LinkedControlsCard.tsx b/apps/console2/src/components/controls/LinkedControlsCard.tsx new file mode 100644 index 000000000..03df58547 --- /dev/null +++ b/apps/console2/src/components/controls/LinkedControlsCard.tsx @@ -0,0 +1,123 @@ +import { graphql } from "relay-runtime"; +import { + Button, + Tr, + Td, + Table, + Thead, + Tbody, + Th, + IconTrashCan, + Badge, +} 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"; + +const linkedControlFragment = graphql` + fragment LinkedControlsCardFragment on Control { + id + name + referenceId + framework { + name + } + } +`; + +type Mutation = (p: { + variables: { + input: { + controlId: string; + } & Params; + connections: string[]; + }; +}) => void; + +type Props = { + // Controls linked to the element + controls: (LinkedControlsCardFragment$key & { id: string })[]; + // Extra params to send to the mutation + params: Params; + // Disable (action when loading for instance) + disabled?: boolean; + // ID of the connection to update + connectionId: string; + // Mutation to detach a control (will receive {controlId, ...params}) + onDetach: Mutation; +}; + +/** + * Reusable component that displays a list of linked controls + */ +export function LinkedControlsCard(props: Props) { + const { __ } = useTranslate(); + const controls = props.controls; + + const onDetach = (controlId: string) => { + props.onDetach({ + variables: { + input: { + controlId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + return ( + + + + + + + + + + {controls.length === 0 && ( + + + + )} + {controls.map((control) => ( + + ))} + +
{__("Reference")}{__("Name")}
+ {__("No controls linked")} +
+ ); +} + +function ControlRow(props: { + control: LinkedControlsCardFragment$key & { id: string }; + onClick: (controlId: string) => void; +}) { + const control = useFragment(linkedControlFragment, props.control); + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + + return ( + + + + {control.framework.name}{" "} + {control.referenceId} + + + {control.name} + + + + + ); +} diff --git a/apps/console2/src/components/controls/__generated__/LinkedControlsCardFragment.graphql.ts b/apps/console2/src/components/controls/__generated__/LinkedControlsCardFragment.graphql.ts new file mode 100644 index 000000000..e542ca6f1 --- /dev/null +++ b/apps/console2/src/components/controls/__generated__/LinkedControlsCardFragment.graphql.ts @@ -0,0 +1,76 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type LinkedControlsCardFragment$data = { + readonly framework: { + readonly name: string; + }; + readonly id: string; + readonly name: string; + readonly referenceId: string; + readonly " $fragmentType": "LinkedControlsCardFragment"; +}; +export type LinkedControlsCardFragment$key = { + readonly " $data"?: LinkedControlsCardFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsCardFragment">; +}; + +const node: ReaderFragment = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}; +return { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LinkedControlsCardFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "referenceId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + (v0/*: any*/) + ], + "storageKey": null + } + ], + "type": "Control", + "abstractKey": null +}; +})(); + +(node as any).hash = "1b72ca232511c698e0cbdc84cbc81c02"; + +export default node; diff --git a/apps/console2/src/components/documents/LinkedDocumentsCard.tsx b/apps/console2/src/components/documents/LinkedDocumentsCard.tsx index 4921e4fde..d09632117 100644 --- a/apps/console2/src/components/documents/LinkedDocumentsCard.tsx +++ b/apps/console2/src/components/documents/LinkedDocumentsCard.tsx @@ -13,6 +13,7 @@ import { IconTrashCan, DocumentVersionBadge, DocumentTypeBadge, + TrButton, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import type { LinkedDocumentsCardFragment$key } from "./__generated__/LinkedDocumentsCardFragment.graphql"; @@ -20,7 +21,8 @@ import { useFragment } from "react-relay"; import { useMemo, useState } from "react"; import { sprintf } from "@probo/helpers"; import { useOrganizationId } from "/hooks/useOrganizationId"; -import { DocumentLinkDialog } from "./DocumentLinkDialog"; +import { LinkedDocumentDialog } from "./LinkedDocumentsDialog.tsx"; +import clsx from "clsx"; const linkedDocumentFragment = graphql` fragment LinkedDocumentsCardFragment on Document { @@ -61,6 +63,7 @@ type Props = { onAttach: Mutation; // Mutation to detach a document (will receive {documentId, ...params}) onDetach: Mutation; + variant?: "card" | "table"; }; /** @@ -73,6 +76,7 @@ export function LinkedDocumentsCard(props: Props) { return limit ? props.documents.slice(0, limit) : props.documents; }, [props.documents, limit]); const showMoreButton = limit !== null && props.documents.length > limit; + const variant = props.variant ?? "table"; const onAttach = (documentId: string) => { props.onAttach({ @@ -98,47 +102,65 @@ export function LinkedDocumentsCard(props: Props) { }); }; + const Wrapper = variant === "card" ? Card : "div"; + return ( - -
-
{__("Documents")}
- - - -
- {documents.length > 0 ? ( - - - - - - - - - - - {documents.map((document) => ( - - ))} - -
{__("Name")}{__("Type")}{__("State")}
- ) : ( -
- {__("No documents linked")} + + {variant === "card" && ( +
+
{__("Documents")}
+ + +
)} + + + + + + + + + + + {documents.length === 0 && ( + + + + )} + {documents.map((document) => ( + + ))} + {variant === "table" && ( + + + {__("Link document")} + + + )} + +
{__("Name")}{__("Type")}{__("State")}
+ {__("No documents linked")} +
{showMoreButton && ( )} - +
); } diff --git a/apps/console2/src/components/documents/DocumentLinkDialog.tsx b/apps/console2/src/components/documents/LinkedDocumentsDialog.tsx similarity index 62% rename from apps/console2/src/components/documents/DocumentLinkDialog.tsx rename to apps/console2/src/components/documents/LinkedDocumentsDialog.tsx index f5a1ba1d3..cac0fe479 100644 --- a/apps/console2/src/components/documents/DocumentLinkDialog.tsx +++ b/apps/console2/src/components/documents/LinkedDocumentsDialog.tsx @@ -7,33 +7,55 @@ import { IconMagnifyingGlass, IconPlusLarge, IconTrashCan, + InfiniteScrollTrigger, Input, Spinner, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { Suspense, useMemo, useState, type ReactNode } from "react"; import { graphql } from "relay-runtime"; -import { useLazyLoadQuery } from "react-relay"; -import type { - DocumentLinkDialogQuery, - DocumentLinkDialogQuery$data, -} from "./__generated__/DocumentLinkDialogQuery.graphql"; +import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; +import type { LinkedDocumentsDialogQuery } from "./__generated__/LinkedDocumentsDialogQuery.graphql"; import { useOrganizationId } from "/hooks/useOrganizationId"; import type { NodeOf } from "/types"; +import type { + LinkedDocumentsDialogFragment$data, + LinkedDocumentsDialogFragment$key, +} from "./__generated__/LinkedDocumentsDialogFragment.graphql"; const documentsQuery = graphql` - query DocumentLinkDialogQuery($organizationId: ID!) { + query LinkedDocumentsDialogQuery($organizationId: ID!) { organization: node(id: $organizationId) { id ... on Organization { - documents(first: 100) @connection(key: "Organization__documents") { - edges { - node { - id - title - documentType - } - } + ...LinkedDocumentsDialogFragment + } + } + } +`; + +const documentsFragment = graphql` + fragment LinkedDocumentsDialogFragment on Organization + @refetchable(queryName: "LinkedDocumentsDialogQuery_fragment") + @argumentDefinitions( + first: { type: "Int", defaultValue: 20 } + order: { type: "DocumentOrder", defaultValue: null } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + ) { + documents( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + ) @connection(key: "LinkedDocumentsDialogQuery_documents") { + edges { + node { + id + title + documentType } } } @@ -49,14 +71,14 @@ type Props = { onUnlink: (documentId: string) => void; }; -export function DocumentLinkDialog({ children, ...props }: Props) { +export function LinkedDocumentDialog({ children, ...props }: Props) { const { __ } = useTranslate(); return ( }> - + @@ -64,15 +86,18 @@ export function DocumentLinkDialog({ children, ...props }: Props) { ); } -function DocumentLinkDialogContent(props: Omit) { +function LinkedDocumentsDialogContent(props: Omit) { const organizationId = useOrganizationId(); - const data = useLazyLoadQuery(documentsQuery, { + const query = useLazyLoadQuery(documentsQuery, { organizationId, }); + const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment( + documentsFragment, + query.organization as LinkedDocumentsDialogFragment$key + ); const { __ } = useTranslate(); const [search, setSearch] = useState(""); - const documents = - data.organization?.documents?.edges?.map((edge) => edge.node) ?? []; + const documents = data.documents?.edges?.map((edge) => edge.node) ?? []; const linkedIds = useMemo(() => { return new Set(props.linkedDocuments?.map((m) => m.id) ?? []); }, [props.linkedDocuments]); @@ -103,14 +128,18 @@ function DocumentLinkDialogContent(props: Omit) { disabled={props.disabled} /> ))} + {hasNext && ( + loadNext(20)} + /> + )}
); } -type Document = NodeOf< - DocumentLinkDialogQuery$data["organization"]["documents"] ->; +type Document = NodeOf; type RowProps = { document: Document; @@ -129,7 +158,7 @@ function DocumentRow(props: RowProps) { return ( - - - {measures.length > 0 ? ( - - - - - - - - - - {measures.map((measure) => ( - - ))} - -
{__("Name")}{__("State")}
- ) : ( -
- {__("No measures linked")} + + {variant === "card" && ( +
+
{__("Measures")}
+ + +
)} + + + + + + + + + + {measures.length === 0 && ( + + + + )} + {measures.map((measure) => ( + + ))} + {variant === "table" && ( + + + {__("Link measure")} + + + )} + +
{__("Name")}{__("State")}
+ {__("No measures linked")} +
{showMoreButton && ( )} - +
); } diff --git a/apps/console2/src/components/measures/MeasureLinkDialog.tsx b/apps/console2/src/components/measures/LinkedMeasuresDialog.tsx similarity index 68% rename from apps/console2/src/components/measures/MeasureLinkDialog.tsx rename to apps/console2/src/components/measures/LinkedMeasuresDialog.tsx index 4bd8868fa..b2f0573db 100644 --- a/apps/console2/src/components/measures/MeasureLinkDialog.tsx +++ b/apps/console2/src/components/measures/LinkedMeasuresDialog.tsx @@ -7,6 +7,7 @@ import { IconMagnifyingGlass, IconPlusLarge, IconTrashCan, + InfiniteScrollTrigger, Input, Option, Select, @@ -15,25 +16,46 @@ import { import { useTranslate } from "@probo/i18n"; import { Suspense, useMemo, useState, type ReactNode } from "react"; import { graphql } from "relay-runtime"; -import { useLazyLoadQuery } from "react-relay"; -import type { MeasureLinkDialogQuery } from "./__generated__/MeasureLinkDialogQuery.graphql"; +import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; +import type { LinkedMeasuresDialogQuery } from "./__generated__/LinkedMeasuresDialogQuery.graphql"; import { useOrganizationId } from "/hooks/useOrganizationId"; +import type { LinkedMeasuresDialogFragment$key } from "./__generated__/LinkedMeasuresDialogFragment.graphql"; const measuresQuery = graphql` - query MeasureLinkDialogQuery($organizationId: ID!) { + query LinkedMeasuresDialogQuery($organizationId: ID!) { organization: node(id: $organizationId) { id ... on Organization { - measures(first: 100) @connection(key: "Organization__measures") { - edges { - node { - id - name - state - description - category - } - } + ...LinkedMeasuresDialogFragment + } + } + } +`; + +const measuresFragment = graphql` + fragment LinkedMeasuresDialogFragment on Organization + @refetchable(queryName: "LinkedMeasuresDialogQuery_fragment") + @argumentDefinitions( + first: { type: "Int", defaultValue: 20 } + order: { type: "MeasureOrder", defaultValue: null } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + ) { + measures( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + ) @connection(key: "LinkedMeasuresDialogQuery_measures") { + edges { + node { + id + name + state + description + category } } } @@ -49,14 +71,14 @@ type Props = { onUnlink: (measureId: string) => void; }; -export function MeasureLinkDialog({ children, ...props }: Props) { +export function LinkedMeasureDialog({ children, ...props }: Props) { const { __ } = useTranslate(); return ( }> - + @@ -64,16 +86,19 @@ export function MeasureLinkDialog({ children, ...props }: Props) { ); } -function MeasureLinkDialogContent(props: Omit) { +function LinkedMeasuresDialogContent(props: Omit) { const organizationId = useOrganizationId(); - const data = useLazyLoadQuery(measuresQuery, { + const query = useLazyLoadQuery(measuresQuery, { organizationId, }); + const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment( + measuresFragment, + query.organization as LinkedMeasuresDialogFragment$key + ); const { __ } = useTranslate(); const [search, setSearch] = useState(""); const [category, setCategory] = useState(null); - const measures = - data.organization?.measures?.edges?.map((edge) => edge.node) ?? []; + const measures = data.measures?.edges?.map((edge) => edge.node) ?? []; const linkedIds = useMemo(() => { return new Set(props.linkedMeasures?.map((m) => m.id) ?? []); }, [props.linkedMeasures]); @@ -124,6 +149,12 @@ function MeasureLinkDialogContent(props: Omit) { disabled={props.disabled} /> ))} + {hasNext && ( + loadNext(20)} + /> + )}
); diff --git a/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogFragment.graphql.ts b/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogFragment.graphql.ts new file mode 100644 index 000000000..52f4d1bc2 --- /dev/null +++ b/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogFragment.graphql.ts @@ -0,0 +1,239 @@ +/** + * @generated SignedSource<<47d66bf5d9b99eb598728999a7b2ce85>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; +import { FragmentRefs } from "relay-runtime"; +export type LinkedMeasuresDialogFragment$data = { + readonly id: string; + readonly measures: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly category: string; + readonly description: string; + readonly id: string; + readonly name: string; + readonly state: MeasureState; + }; + }>; + }; + readonly " $fragmentType": "LinkedMeasuresDialogFragment"; +}; +export type LinkedMeasuresDialogFragment$key = { + readonly " $data"?: LinkedMeasuresDialogFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresDialogFragment">; +}; + +import LinkedMeasuresDialogQuery_fragment_graphql from './LinkedMeasuresDialogQuery_fragment.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "measures" +], +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": 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": LinkedMeasuresDialogQuery_fragment_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "LinkedMeasuresDialogFragment", + "selections": [ + { + "alias": "measures", + "args": [ + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "MeasureConnection", + "kind": "LinkedField", + "name": "__LinkedMeasuresDialogQuery_measures_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MeasureEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "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": "state", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "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 = "69458f9aed8aee19f69cd58090b68947"; + +export default node; diff --git a/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogQuery.graphql.ts b/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogQuery.graphql.ts new file mode 100644 index 000000000..79a808220 --- /dev/null +++ b/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogQuery.graphql.ts @@ -0,0 +1,259 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type LinkedMeasuresDialogQuery$variables = { + organizationId: string; +}; +export type LinkedMeasuresDialogQuery$data = { + readonly organization: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresDialogFragment">; + }; +}; +export type LinkedMeasuresDialogQuery = { + response: LinkedMeasuresDialogQuery$data; + variables: LinkedMeasuresDialogQuery$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": 20 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "LinkedMeasuresDialogQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedMeasuresDialogFragment" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "LinkedMeasuresDialogQuery", + "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": "MeasureConnection", + "kind": "LinkedField", + "name": "measures", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MeasureEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "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": "state", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "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": "measures(first:20)" + }, + { + "alias": null, + "args": (v4/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "LinkedMeasuresDialogQuery_measures", + "kind": "LinkedHandle", + "name": "measures" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "ecb3ffb566c178a40f6ea72b1947b3ed", + "id": null, + "metadata": {}, + "name": "LinkedMeasuresDialogQuery", + "operationKind": "query", + "text": "query LinkedMeasuresDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n ...LinkedMeasuresDialogFragment\n }\n }\n}\n\nfragment LinkedMeasuresDialogFragment on Organization {\n measures(first: 20) {\n edges {\n node {\n id\n name\n state\n description\n category\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 = "8592f3602ea10f6c00f5eb32c3f6b638"; + +export default node; diff --git a/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogQuery_fragment.graphql.ts b/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogQuery_fragment.graphql.ts new file mode 100644 index 000000000..b8b1fd530 --- /dev/null +++ b/apps/console2/src/components/measures/__generated__/LinkedMeasuresDialogQuery_fragment.graphql.ts @@ -0,0 +1,332 @@ +/** + * @generated SignedSource<<412b6fd2ef44aa7bc3c9e66ec2284c37>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type MeasureOrderField = "CREATED_AT"; +export type OrderDirection = "ASC" | "DESC"; +export type MeasureOrder = { + direction: OrderDirection; + field: MeasureOrderField; +}; +export type LinkedMeasuresDialogQuery_fragment$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: MeasureOrder | null | undefined; +}; +export type LinkedMeasuresDialogQuery_fragment$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresDialogFragment">; + }; +}; +export type LinkedMeasuresDialogQuery_fragment = { + response: LinkedMeasuresDialogQuery_fragment$data; + variables: LinkedMeasuresDialogQuery_fragment$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v5 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" +}, +v6 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v7 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v8 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v9 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v10 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v13 = [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "LinkedMeasuresDialogQuery_fragment", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "LinkedMeasuresDialogFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v3/*: any*/) + ], + "kind": "Operation", + "name": "LinkedMeasuresDialogQuery_fragment", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v13/*: any*/), + "concreteType": "MeasureConnection", + "kind": "LinkedField", + "name": "measures", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MeasureEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v12/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null + }, + (v11/*: 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": (v13/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "LinkedMeasuresDialogQuery_measures", + "kind": "LinkedHandle", + "name": "measures" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "9de5e8b136a2952dbb169f6b27aade30", + "id": null, + "metadata": {}, + "name": "LinkedMeasuresDialogQuery_fragment", + "operationKind": "query", + "text": "query LinkedMeasuresDialogQuery_fragment(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MeasureOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...LinkedMeasuresDialogFragment_16fISc\n id\n }\n}\n\nfragment LinkedMeasuresDialogFragment_16fISc on Organization {\n measures(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n name\n state\n description\n category\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 = "69458f9aed8aee19f69cd58090b68947"; + +export default node; diff --git a/apps/console2/src/components/measures/__generated__/MeasureLinkDialogQuery.graphql.ts b/apps/console2/src/components/measures/__generated__/MeasureLinkDialogQuery.graphql.ts deleted file mode 100644 index 430a51bc1..000000000 --- a/apps/console2/src/components/measures/__generated__/MeasureLinkDialogQuery.graphql.ts +++ /dev/null @@ -1,271 +0,0 @@ -/** - * @generated SignedSource<<0b725ea8709fbf5da3f17545fcd2b8c4>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; -export type MeasureLinkDialogQuery$variables = { - organizationId: string; -}; -export type MeasureLinkDialogQuery$data = { - readonly organization: { - readonly id: string; - readonly measures?: { - readonly edges: ReadonlyArray<{ - readonly node: { - readonly category: string; - readonly description: string; - readonly id: string; - readonly name: string; - readonly state: MeasureState; - }; - }>; - }; - }; -}; -export type MeasureLinkDialogQuery = { - response: MeasureLinkDialogQuery$data; - variables: MeasureLinkDialogQuery$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 = [ - { - "alias": null, - "args": null, - "concreteType": "MeasureEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Measure", - "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": "state", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "description", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "category", - "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 - } - ], - "storageKey": null - } -], -v5 = [ - { - "kind": "Literal", - "name": "first", - "value": 100 - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "MeasureLinkDialogQuery", - "selections": [ - { - "alias": "organization", - "args": (v1/*: any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - (v2/*: any*/), - { - "kind": "InlineFragment", - "selections": [ - { - "alias": "measures", - "args": null, - "concreteType": "MeasureConnection", - "kind": "LinkedField", - "name": "__Organization__measures_connection", - "plural": false, - "selections": (v4/*: any*/), - "storageKey": null - } - ], - "type": "Organization", - "abstractKey": null - } - ], - "storageKey": null - } - ], - "type": "Query", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "MeasureLinkDialogQuery", - "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": (v5/*: any*/), - "concreteType": "MeasureConnection", - "kind": "LinkedField", - "name": "measures", - "plural": false, - "selections": (v4/*: any*/), - "storageKey": "measures(first:100)" - }, - { - "alias": null, - "args": (v5/*: any*/), - "filters": null, - "handle": "connection", - "key": "Organization__measures", - "kind": "LinkedHandle", - "name": "measures" - } - ], - "type": "Organization", - "abstractKey": null - } - ], - "storageKey": null - } - ] - }, - "params": { - "cacheID": "54fff8a0c07ebe02ef34f3bb35e7582e", - "id": null, - "metadata": { - "connection": [ - { - "count": null, - "cursor": null, - "direction": "forward", - "path": [ - "organization", - "measures" - ] - } - ] - }, - "name": "MeasureLinkDialogQuery", - "operationKind": "query", - "text": "query MeasureLinkDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n measures(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n category\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" - } -}; -})(); - -(node as any).hash = "cf01fa3a766badd4f6150e1a544403ba"; - -export default node; diff --git a/apps/console2/src/components/risks/LinkedRisksCard.tsx b/apps/console2/src/components/risks/LinkedRisksCard.tsx new file mode 100644 index 000000000..aa05d2119 --- /dev/null +++ b/apps/console2/src/components/risks/LinkedRisksCard.tsx @@ -0,0 +1,167 @@ +import { graphql } from "relay-runtime"; +import { + IconPlusLarge, + Button, + Tr, + Td, + Table, + Thead, + Tbody, + Th, + IconChevronDown, + RiskBadge, + IconTrashCan, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import type { LinkedRisksCardFragment$key } from "./__generated__/LinkedRisksCardFragment.graphql"; +import { useFragment } from "react-relay"; +import { useMemo, useState } from "react"; +import { sprintf } from "@probo/helpers"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { LinkedRisksDialog } from "./LinkedRisksDialog.tsx"; + +const linkedRiskFragment = graphql` + fragment LinkedRisksCardFragment on Risk { + id + name + inherentRiskScore + residualRiskScore + } +`; + +type Mutation = (p: { + variables: { + input: { + riskId: string; + } & Params; + connections: string[]; + }; +}) => void; + +type Props = { + // Risks linked to the element + risks: (LinkedRisksCardFragment$key & { id: string })[]; + // Extra params to send to the mutation + params: Params; + // Disable (action when loading for instance) + disabled?: boolean; + // ID of the connection to update + connectionId: string; + // Mutation to attach a risk (will receive {riskId, ...params}) + onAttach: Mutation; + // Mutation to detach a risk (will receive {riskId, ...params}) + onDetach: Mutation; +}; + +/** + * Reusable component that displays a list of linked risks + */ +export function LinkedRisksCard(props: Props) { + const { __ } = useTranslate(); + const [limit, setLimit] = useState(4); + const risks = useMemo(() => { + return limit ? props.risks.slice(0, limit) : props.risks; + }, [props.risks, limit]); + const showMoreButton = limit !== null && props.risks.length > limit; + + const onAttach = (riskId: string) => { + props.onAttach({ + variables: { + input: { + riskId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + const onDetach = (riskId: string) => { + props.onDetach({ + variables: { + input: { + riskId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + return ( +
+ {risks.length > 0 ? ( + + + + + + + + + + + {risks.map((risk) => ( + + ))} + +
{__("Name")}{__("Inherent Risk")}{__("Residual Risk")}
+ ) : ( +
+ {__("No risks linked")} +
+ )} + {showMoreButton && ( + + )} + + + +
+ ); +} + +function RiskRow(props: { + risk: LinkedRisksCardFragment$key & { id: string }; + onClick: (riskId: string) => void; +}) { + const risk = useFragment(linkedRiskFragment, props.risk); + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + + return ( + + {risk.name} + + + + + + + + + + + ); +} diff --git a/apps/console2/src/components/risks/LinkedRisksDialog.tsx b/apps/console2/src/components/risks/LinkedRisksDialog.tsx new file mode 100644 index 000000000..5d4b01ae4 --- /dev/null +++ b/apps/console2/src/components/risks/LinkedRisksDialog.tsx @@ -0,0 +1,172 @@ +import { + Badge, + Button, + Dialog, + DialogContent, + DialogFooter, + IconMagnifyingGlass, + IconPlusLarge, + IconTrashCan, + Input, + Option, + Select, + Spinner, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { Suspense, useMemo, useState, type ReactNode } from "react"; +import { graphql } from "relay-runtime"; +import { useLazyLoadQuery } from "react-relay"; +import type { LinkedRisksDialogQuery } from "./__generated__/LinkedRisksDialogQuery.graphql"; +import { useOrganizationId } from "/hooks/useOrganizationId"; + +const risksQuery = graphql` + query LinkedRisksDialogQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + id + ... on Organization { + risks(first: 100) { + edges { + node { + id + name + category + description + inherentRiskScore + residualRiskScore + } + } + } + } + } + } +`; + +type Props = { + children: ReactNode; + connectionId: string; + disabled?: boolean; + linkedRisks?: { id: string }[]; + onLink: (riskId: string) => void; + onUnlink: (riskId: string) => void; +}; + +export function LinkedRisksDialog({ children, ...props }: Props) { + const { __ } = useTranslate(); + + return ( + + + }> + + + + + + ); +} + +function LinkedRisksDialogContent(props: Omit) { + const organizationId = useOrganizationId(); + const data = useLazyLoadQuery(risksQuery, { + organizationId, + }); + const { __ } = useTranslate(); + const [search, setSearch] = useState(""); + const [category, setCategory] = useState(null); + const risks = data.organization?.risks?.edges?.map((edge) => edge.node) ?? []; + const linkedIds = useMemo(() => { + return new Set(props.linkedRisks?.map((r) => r.id) ?? []); + }, [props.linkedRisks]); + + const filteredRisks = useMemo(() => { + return risks.filter( + (risk) => + (category === null || risk.category === category) && + (risk.name.toLowerCase().includes(search.toLowerCase()) || + risk.description?.toLowerCase().includes(search.toLowerCase())) + ); + }, [risks, search, category]); + + const categories = useMemo( + () => Array.from(new Set(risks.map((r) => r.category))), + [risks] + ); + + return ( + <> +
+ + +
+
+ {filteredRisks.map((risk) => ( + + ))} +
+ + ); +} + +type RowProps = { + risk: { + name: string; + category: string; + id: string; + inherentRiskScore: number; + residualRiskScore: number; + }; + linkedRisks: Set; + disabled?: boolean; + onLink: (riskId: string) => void; + onUnlink: (riskId: string) => void; +}; + +function RiskRow(props: RowProps) { + const { __ } = useTranslate(); + + const isLinked = props.linkedRisks.has(props.risk.id); + const onClick = isLinked ? props.onUnlink : props.onLink; + const IconComponent = isLinked ? IconTrashCan : IconPlusLarge; + + return ( + + + ); +} diff --git a/apps/console2/src/components/risks/MeasureLinkDialog.tsx b/apps/console2/src/components/risks/MeasureLinkDialog.tsx deleted file mode 100644 index 313f1e729..000000000 --- a/apps/console2/src/components/risks/MeasureLinkDialog.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* -const attachMeasureMutation = graphql` - mutation MeasureLinkDialogCreateMutation( - $input: CreateRiskMeasureMappingInput! - $connections: [ID!]! - ) { - createRiskMeasureMapping(input: $input) { - measureEdge @prependEdge(connections: $connections) { - node { - id - name - description - category - state - } - } - } - } -`; - -export const detachMeasureMutation = graphql` - mutation MeasureLinkDialogDetachMutation( - $input: DeleteRiskMeasureMappingInput! - $connections: [ID!]! - ) { - deleteRiskMeasureMapping(input: $input) { - deletedMeasureId @deleteEdge(connections: $connections) - } - } -`; -*/ diff --git a/apps/console2/src/components/risks/__generated__/LinkedRisksCardFragment.graphql.ts b/apps/console2/src/components/risks/__generated__/LinkedRisksCardFragment.graphql.ts new file mode 100644 index 000000000..262289f0b --- /dev/null +++ b/apps/console2/src/components/risks/__generated__/LinkedRisksCardFragment.graphql.ts @@ -0,0 +1,66 @@ +/** + * @generated SignedSource<<6e092be20526b76ee767836880803c34>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type LinkedRisksCardFragment$data = { + readonly id: string; + readonly inherentRiskScore: number; + readonly name: string; + readonly residualRiskScore: number; + readonly " $fragmentType": "LinkedRisksCardFragment"; +}; +export type LinkedRisksCardFragment$key = { + readonly " $data"?: LinkedRisksCardFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"LinkedRisksCardFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LinkedRisksCardFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentRiskScore", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualRiskScore", + "storageKey": null + } + ], + "type": "Risk", + "abstractKey": null +}; + +(node as any).hash = "32a84445ae1cc071f56139fa44d67690"; + +export default node; diff --git a/apps/console2/src/components/risks/__generated__/LinkedRisksDialogQuery.graphql.ts b/apps/console2/src/components/risks/__generated__/LinkedRisksDialogQuery.graphql.ts new file mode 100644 index 000000000..b0c9871e4 --- /dev/null +++ b/apps/console2/src/components/risks/__generated__/LinkedRisksDialogQuery.graphql.ts @@ -0,0 +1,206 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type LinkedRisksDialogQuery$variables = { + organizationId: string; +}; +export type LinkedRisksDialogQuery$data = { + readonly organization: { + readonly id: string; + readonly risks?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly category: string; + readonly description: string; + readonly id: string; + readonly inherentRiskScore: number; + readonly name: string; + readonly residualRiskScore: number; + }; + }>; + }; + }; +}; +export type LinkedRisksDialogQuery = { + response: LinkedRisksDialogQuery$data; + variables: LinkedRisksDialogQuery$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 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 100 + } + ], + "concreteType": "RiskConnection", + "kind": "LinkedField", + "name": "risks", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "RiskEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "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": "category", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentRiskScore", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualRiskScore", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "risks(first:100)" + } + ], + "type": "Organization", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "LinkedRisksDialogQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "LinkedRisksDialogQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "ccac9f72628186de135aeb2ee176207d", + "id": null, + "metadata": {}, + "name": "LinkedRisksDialogQuery", + "operationKind": "query", + "text": "query LinkedRisksDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n risks(first: 100) {\n edges {\n node {\n id\n name\n category\n description\n inherentRiskScore\n residualRiskScore\n }\n }\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "a0a7e39135cc4c1a4a74c4728902f944"; + +export default node; diff --git a/apps/console2/src/hooks/graph/DocumentGraph.ts b/apps/console2/src/hooks/graph/DocumentGraph.ts index fc626950b..9e10aaa48 100644 --- a/apps/console2/src/hooks/graph/DocumentGraph.ts +++ b/apps/console2/src/hooks/graph/DocumentGraph.ts @@ -41,7 +41,7 @@ export const documentNodeQuery = graphql` query DocumentGraphNodeQuery($documentId: ID!) { node(id: $documentId) { ... on Document { - ...DocumentPageDocumentFragment + ...DocumentDetailPageDocumentFragment } } } diff --git a/apps/console2/src/hooks/graph/MeasureGraph.ts b/apps/console2/src/hooks/graph/MeasureGraph.ts index 2b4e71011..bda967724 100644 --- a/apps/console2/src/hooks/graph/MeasureGraph.ts +++ b/apps/console2/src/hooks/graph/MeasureGraph.ts @@ -23,6 +23,8 @@ const deleteMeasureMutation = graphql` } `; +export const MeasureConnectionKey = "MeasuresGraphListQuery__measures"; + export function useDeleteMeasureMutation() { const { __ } = useTranslate(); @@ -34,3 +36,40 @@ export function useDeleteMeasureMutation() { } ); } + +export const measureNodeQuery = graphql` + query MeasureGraphNodeQuery($measureId: ID!) { + node(id: $measureId) { + ... on Measure { + id + name + description + state + category + ...MeasureRisksTabFragment + ...MeasureControlsTabFragment + ...MeasureFormDialogMeasureFragment + ...MeasureEvidencesTabFragment + } + } + } +`; + +const measureUpdateMutation = graphql` + mutation MeasureGraphUpdateMutation($input: UpdateMeasureInput!) { + updateMeasure(input: $input) { + measure { + ...MeasureFormDialogMeasureFragment + } + } + } +`; + +export const useUpdateMeasure = () => { + const { __ } = useTranslate(); + + return useMutationWithToasts(measureUpdateMutation, { + successMessage: __("Measure updated successfully."), + errorMessage: __("Failed to update measure. Please try again."), + }); +}; diff --git a/apps/console2/src/hooks/graph/PeopleGraph.ts b/apps/console2/src/hooks/graph/PeopleGraph.ts index 7c3c2b80a..588ab6e19 100644 --- a/apps/console2/src/hooks/graph/PeopleGraph.ts +++ b/apps/console2/src/hooks/graph/PeopleGraph.ts @@ -12,7 +12,7 @@ import type { PeopleGraphPaginatedQuery } from "./__generated__/PeopleGraphPagin import type { PeopleGraphPaginatedFragment$key } from "./__generated__/PeopleGraphPaginatedFragment.graphql"; import { useConfirm } from "@probo/ui"; import type { PeopleGraphDeleteMutation } from "./__generated__/PeopleGraphDeleteMutation.graphql"; -import { sprintf } from "@probo/helpers"; +import { promisifyMutation, sprintf } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; const peopleQuery = graphql` @@ -130,16 +130,13 @@ export const useDeletePeople = ( } confirm( () => - new Promise((resolve) => { - mutate({ - variables: { - input: { - peopleId: people.id!, - }, - connections: [connectionId], + promisifyMutation(mutate)({ + variables: { + input: { + peopleId: people.id!, }, - onCompleted: () => resolve(), - }); + connections: [connectionId], + }, }), { message: sprintf( diff --git a/apps/console2/src/hooks/graph/RiskGraph.ts b/apps/console2/src/hooks/graph/RiskGraph.ts index fa7c48937..9cf5ba6c4 100644 --- a/apps/console2/src/hooks/graph/RiskGraph.ts +++ b/apps/console2/src/hooks/graph/RiskGraph.ts @@ -109,6 +109,7 @@ export const riskNodeQuery = graphql` ...useRiskFormFragment ...RiskOverviewTabFragment ...RiskMeasuresTabFragment + ...RiskDocumentsTabFragment } } } diff --git a/apps/console2/src/hooks/graph/VendorGraph.ts b/apps/console2/src/hooks/graph/VendorGraph.ts index 997191ce9..88d256a1c 100644 --- a/apps/console2/src/hooks/graph/VendorGraph.ts +++ b/apps/console2/src/hooks/graph/VendorGraph.ts @@ -5,7 +5,7 @@ import type { VendorGraphCreateMutation } from "./__generated__/VendorGraphCreat import type { VendorGraphDeleteMutation } from "./__generated__/VendorGraphDeleteMutation.graphql.ts"; import { useMutation } from "react-relay"; import { useConfirm } from "@probo/ui"; -import { sprintf } from "@probo/helpers"; +import { promisifyMutation, sprintf } from "@probo/helpers"; const createVendorMutation = graphql` mutation VendorGraphCreateMutation( @@ -64,16 +64,13 @@ export const useDeleteVendor = ( } confirm( () => - new Promise((resolve) => { - mutate({ - variables: { - input: { - vendorId: vendor.id!, - }, - connections: [connectionId], + promisifyMutation(mutate)({ + variables: { + input: { + vendorId: vendor.id!, }, - onCompleted: () => resolve(), - }); + connections: [connectionId], + }, }), { message: sprintf( diff --git a/apps/console2/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/DocumentGraphNodeQuery.graphql.ts index 78d5557f5..2bf3242e2 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<<2cb115c83f67688124df197a8f944329>> + * @generated SignedSource<<1c8d31af825eefa7bfb5d58a2534f190>> * @lightSyntaxTransform * @nogrep */ @@ -15,7 +15,7 @@ export type DocumentGraphNodeQuery$variables = { }; export type DocumentGraphNodeQuery$data = { readonly node: { - readonly " $fragmentSpreads": FragmentRefs<"DocumentPageDocumentFragment">; + readonly " $fragmentSpreads": FragmentRefs<"DocumentDetailPageDocumentFragment">; }; }; export type DocumentGraphNodeQuery = { @@ -60,19 +60,19 @@ v4 = { "storageKey": null }, v5 = [ - { - "kind": "Literal", - "name": "first", - "value": 20 - } -], -v6 = [ { "kind": "Literal", "name": "first", "value": 100 } ], +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, v7 = { "alias": null, "args": null, @@ -116,7 +116,14 @@ v9 = { "storageKey": null } ] -}; +}, +v10 = [ + { + "kind": "Literal", + "name": "first", + "value": 20 + } +]; return { "fragment": { "argumentDefinitions": (v0/*: any*/), @@ -138,7 +145,7 @@ return { { "args": null, "kind": "FragmentSpread", - "name": "DocumentPageDocumentFragment" + "name": "DocumentDetailPageDocumentFragment" } ], "type": "Document", @@ -193,6 +200,74 @@ return { { "alias": null, "args": (v5/*: 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": [ + (v3/*: any*/), + (v6/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "referenceId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + (v6/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + }, + (v2/*: any*/) + ], + "storageKey": null + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v8/*: any*/), + (v9/*: any*/) + ], + "storageKey": "controls(first:100)" + }, + { + "alias": null, + "args": (v5/*: any*/), + "filters": null, + "handle": "connection", + "key": "DocumentDetailPage_controls", + "kind": "LinkedHandle", + "name": "controls" + }, + { + "alias": null, + "args": (v10/*: any*/), "concreteType": "DocumentVersionConnection", "kind": "LinkedField", "name": "versions", @@ -252,7 +327,7 @@ return { }, { "alias": null, - "args": (v6/*: any*/), + "args": (v5/*: any*/), "concreteType": "DocumentVersionSignatureConnection", "kind": "LinkedField", "name": "signatures", @@ -331,10 +406,10 @@ return { }, { "alias": null, - "args": (v6/*: any*/), + "args": (v5/*: any*/), "filters": null, "handle": "connection", - "key": "DocumentPage_signatures", + "key": "DocumentDetailPage_signatures", "kind": "LinkedHandle", "name": "signatures" }, @@ -373,10 +448,10 @@ return { }, { "alias": null, - "args": (v5/*: any*/), + "args": (v10/*: any*/), "filters": null, "handle": "connection", - "key": "DocumentPage_versions", + "key": "DocumentDetailPage_versions", "kind": "LinkedHandle", "name": "versions" } @@ -390,16 +465,16 @@ return { ] }, "params": { - "cacheID": "671e1a9b133593847c68af3abd9a721f", + "cacheID": "cc52945231b42fbaf4c86eba5c3ee433", "id": null, "metadata": {}, "name": "DocumentGraphNodeQuery", "operationKind": "query", - "text": "query DocumentGraphNodeQuery(\n $documentId: ID!\n) {\n node(id: $documentId) {\n __typename\n ... on Document {\n ...DocumentPageDocumentFragment\n }\n id\n }\n}\n\nfragment DocumentPageDocumentFragment on Document {\n id\n title\n owner {\n id\n fullName\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" + "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 referenceId\n framework {\n name\n id\n }\n}\n" } }; })(); -(node as any).hash = "d6f55d4636b86d853b97b44e68e435f2"; +(node as any).hash = "f7e5a8ebb67a7668627c01ca1c4c6b19"; export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/MeasureGraphListQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/MeasureGraphListQuery.graphql.ts index e6abc0ff7..19f5a3ed9 100644 --- a/apps/console2/src/hooks/graph/__generated__/MeasureGraphListQuery.graphql.ts +++ b/apps/console2/src/hooks/graph/__generated__/MeasureGraphListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<4d7510366af238f19e3bff7949776f0a>> + * @generated SignedSource<<792fbba36453d167b7844222f287183e>> * @lightSyntaxTransform * @nogrep */ @@ -153,6 +153,13 @@ return { "name": "state", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, (v3/*: any*/) ], "storageKey": null @@ -226,12 +233,12 @@ return { ] }, "params": { - "cacheID": "876c92c41e08247a4900883b56ac6762", + "cacheID": "aeaecdca869ba61b6602c2ebd701061d", "id": null, "metadata": {}, "name": "MeasureGraphListQuery", "operationKind": "query", - "text": "query MeasureGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...MeasuresPageFragment\n }\n}\n\nfragment MeasuresPageFragment on Organization {\n measures(first: 100) {\n edges {\n node {\n id\n name\n category\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" + "text": "query MeasureGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...MeasuresPageFragment\n }\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasuresPageFragment on Organization {\n measures(first: 100) {\n edges {\n node {\n id\n name\n category\n state\n ...MeasureFormDialogMeasureFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\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 new file mode 100644 index 000000000..c402ddc6a --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql.ts @@ -0,0 +1,495 @@ +/** + * @generated SignedSource<<3f0bf5e0e474802b13df43d692ee926d>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; +export type MeasureGraphNodeQuery$variables = { + measureId: string; +}; +export type MeasureGraphNodeQuery$data = { + readonly node: { + readonly category?: string; + readonly description?: string; + readonly id?: string; + readonly name?: string; + readonly state?: MeasureState; + readonly " $fragmentSpreads": FragmentRefs<"MeasureControlsTabFragment" | "MeasureEvidencesTabFragment" | "MeasureFormDialogMeasureFragment" | "MeasureRisksTabFragment">; + }; +}; +export type MeasureGraphNodeQuery = { + response: MeasureGraphNodeQuery$data; + variables: MeasureGraphNodeQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "measureId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "measureId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null +}, +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v8 = [ + { + "kind": "Literal", + "name": "first", + "value": 100 + } +], +v9 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v10 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + (v10/*: any*/), + (v11/*: any*/) + ], + "storageKey": null +}, +v13 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}, +v14 = [ + { + "kind": "Literal", + "name": "first", + "value": 50 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "MeasureGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "MeasureRisksTabFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "MeasureControlsTabFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "MeasureFormDialogMeasureFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "MeasureEvidencesTabFragment" + } + ], + "type": "Measure", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "MeasureGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v7/*: any*/), + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v6/*: any*/), + { + "alias": null, + "args": (v8/*: any*/), + "concreteType": "RiskConnection", + "kind": "LinkedField", + "name": "risks", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "RiskEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentRiskScore", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualRiskScore", + "storageKey": null + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/) + ], + "storageKey": null + }, + (v12/*: any*/), + (v13/*: any*/) + ], + "storageKey": "risks(first:100)" + }, + { + "alias": null, + "args": (v8/*: any*/), + "filters": null, + "handle": "connection", + "key": "Measure__risks", + "kind": "LinkedHandle", + "name": "risks" + }, + { + "alias": null, + "args": (v8/*: 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*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "referenceId", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + (v3/*: any*/), + (v2/*: any*/) + ], + "storageKey": null + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/) + ], + "storageKey": null + }, + (v12/*: any*/), + (v13/*: any*/) + ], + "storageKey": "controls(first:100)" + }, + { + "alias": null, + "args": (v8/*: any*/), + "filters": null, + "handle": "connection", + "key": "MeasureControlsTabFragment_controls", + "kind": "LinkedHandle", + "name": "controls" + }, + { + "alias": null, + "args": (v14/*: any*/), + "concreteType": "EvidenceConnection", + "kind": "LinkedField", + "name": "evidences", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "EvidenceEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Evidence", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "filename", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "size", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "type", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "mimeType", + "storageKey": null + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/) + ], + "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 + }, + (v13/*: any*/) + ], + "storageKey": "evidences(first:50)" + }, + { + "alias": null, + "args": (v14/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "MeasureEvidencesTabFragment_evidences", + "kind": "LinkedHandle", + "name": "evidences" + } + ], + "type": "Measure", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "a4e9a325cbe97ad3b4846200c7e88a22", + "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 ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n referenceId\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" + } +}; +})(); + +(node as any).hash = "5c4bd0699414117e0ed408348554ca7d"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/MeasureGraphUpdateMutation.graphql.ts b/apps/console2/src/hooks/graph/__generated__/MeasureGraphUpdateMutation.graphql.ts new file mode 100644 index 000000000..3a8aed82a --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/MeasureGraphUpdateMutation.graphql.ts @@ -0,0 +1,167 @@ +/** + * @generated SignedSource<<0568c40c0caa32ce6daf11d414e2cc72>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; +export type UpdateMeasureInput = { + category?: string | null | undefined; + description?: string | null | undefined; + id: string; + name?: string | null | undefined; + state?: MeasureState | null | undefined; +}; +export type MeasureGraphUpdateMutation$variables = { + input: UpdateMeasureInput; +}; +export type MeasureGraphUpdateMutation$data = { + readonly updateMeasure: { + readonly measure: { + readonly " $fragmentSpreads": FragmentRefs<"MeasureFormDialogMeasureFragment">; + }; + }; +}; +export type MeasureGraphUpdateMutation = { + response: MeasureGraphUpdateMutation$data; + variables: MeasureGraphUpdateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "MeasureGraphUpdateMutation", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateMeasurePayload", + "kind": "LinkedField", + "name": "updateMeasure", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "measure", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "MeasureFormDialogMeasureFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "MeasureGraphUpdateMutation", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateMeasurePayload", + "kind": "LinkedField", + "name": "updateMeasure", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "measure", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "17d35db59a42796407f7f0d6bb4798ba", + "id": null, + "metadata": {}, + "name": "MeasureGraphUpdateMutation", + "operationKind": "mutation", + "text": "mutation MeasureGraphUpdateMutation(\n $input: UpdateMeasureInput!\n) {\n updateMeasure(input: $input) {\n measure {\n ...MeasureFormDialogMeasureFragment\n id\n }\n }\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n" + } +}; +})(); + +(node as any).hash = "9f74c8c6cea82050d077d26e6b7547f6"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/RiskGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/RiskGraphNodeQuery.graphql.ts index 8a9d79f2e..cfc0c55c6 100644 --- a/apps/console2/src/hooks/graph/__generated__/RiskGraphNodeQuery.graphql.ts +++ b/apps/console2/src/hooks/graph/__generated__/RiskGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<1fce2cc80e82f95488675ab257cb1087>> * @lightSyntaxTransform * @nogrep */ @@ -24,7 +24,7 @@ export type RiskGraphNodeQuery$data = { readonly id: string; } | null | undefined; readonly treatment?: RiskTreatment; - readonly " $fragmentSpreads": FragmentRefs<"RiskMeasuresTabFragment" | "RiskOverviewTabFragment" | "useRiskFormFragment">; + readonly " $fragmentSpreads": FragmentRefs<"RiskDocumentsTabFragment" | "RiskMeasuresTabFragment" | "RiskOverviewTabFragment" | "useRiskFormFragment">; }; }; export type RiskGraphNodeQuery = { @@ -114,7 +114,51 @@ v9 = [ "name": "first", "value": 100 } -]; +], +v10 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v11 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null +}, +v12 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}; return { "fragment": { "argumentDefinitions": (v0/*: any*/), @@ -152,6 +196,11 @@ return { "args": null, "kind": "FragmentSpread", "name": "RiskMeasuresTabFragment" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "RiskDocumentsTabFragment" } ], "type": "Risk", @@ -274,53 +323,12 @@ return { ], "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "cursor", - "storageKey": null - } + (v10/*: any*/) ], "storageKey": null }, - { - "alias": null, - "args": null, - "concreteType": "PageInfo", - "kind": "LinkedField", - "name": "pageInfo", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "endCursor", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "hasNextPage", - "storageKey": null - } - ], - "storageKey": null - }, - { - "kind": "ClientExtension", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__id", - "storageKey": null - } - ] - } + (v11/*: any*/), + (v12/*: any*/) ], "storageKey": "measures(first:100)" }, @@ -332,6 +340,121 @@ return { "key": "Risk__measures", "kind": "LinkedHandle", "name": "measures" + }, + { + "alias": null, + "args": (v9/*: any*/), + "concreteType": "DocumentConnection", + "kind": "LinkedField", + "name": "documents", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Document", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v5/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "documentType", + "storageKey": null + }, + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 1 + } + ], + "concreteType": "DocumentVersionConnection", + "kind": "LinkedField", + "name": "versions", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentVersionEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentVersion", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v5/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "versions(first:1)" + }, + (v8/*: any*/) + ], + "storageKey": null + }, + (v10/*: any*/) + ], + "storageKey": null + }, + (v11/*: any*/), + (v12/*: any*/) + ], + "storageKey": "documents(first:100)" + }, + { + "alias": null, + "args": (v9/*: any*/), + "filters": null, + "handle": "connection", + "key": "Risk__documents", + "kind": "LinkedHandle", + "name": "documents" } ], "type": "Risk", @@ -343,16 +466,16 @@ return { ] }, "params": { - "cacheID": "295ecc0e99f46b7790fe04db0deed1d8", + "cacheID": "bb52cb7161bdf1bbba40f782f5ff22c2", "id": null, "metadata": {}, "name": "RiskGraphNodeQuery", "operationKind": "query", - "text": "query RiskGraphNodeQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n ... on Risk {\n name\n description\n treatment\n owner {\n id\n fullName\n }\n note\n ...useRiskFormFragment\n ...RiskOverviewTabFragment\n ...RiskMeasuresTabFragment\n }\n id\n }\n}\n\nfragment LinkedMeasuresCardFragment on Measure {\n id\n name\n state\n}\n\nfragment RiskMeasuresTabFragment on Risk {\n id\n measures(first: 100) {\n edges {\n node {\n id\n ...LinkedMeasuresCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment RiskOverviewTabFragment on Risk {\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" + "text": "query RiskGraphNodeQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n ... on Risk {\n name\n description\n treatment\n owner {\n id\n fullName\n }\n note\n ...useRiskFormFragment\n ...RiskOverviewTabFragment\n ...RiskMeasuresTabFragment\n ...RiskDocumentsTabFragment\n }\n id\n }\n}\n\nfragment LinkedDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment LinkedMeasuresCardFragment on Measure {\n id\n name\n state\n}\n\nfragment RiskDocumentsTabFragment on Risk {\n id\n documents(first: 100) {\n edges {\n node {\n id\n ...LinkedDocumentsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment RiskMeasuresTabFragment on Risk {\n id\n measures(first: 100) {\n edges {\n node {\n id\n ...LinkedMeasuresCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment RiskOverviewTabFragment on Risk {\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentRiskScore\n residualRiskScore\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" } }; })(); -(node as any).hash = "c742a5b3e536eabc8dca8b680f763a04"; +(node as any).hash = "fad84c61c3943be5d63e810325aa8f62"; export default node; diff --git a/apps/console2/src/layouts/MainLayout.tsx b/apps/console2/src/layouts/MainLayout.tsx index 6d669d1e3..1aa7a2100 100644 --- a/apps/console2/src/layouts/MainLayout.tsx +++ b/apps/console2/src/layouts/MainLayout.tsx @@ -28,6 +28,8 @@ import { graphql } from "relay-runtime"; import { useLazyLoadQuery } from "react-relay"; import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql"; import { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { PageError } from "/components/PageError"; const MainLayoutQuery = graphql` query MainLayoutQuery { @@ -123,7 +125,9 @@ export function MainLayout() { } > - + + + ); } diff --git a/apps/console2/src/pages/organizations/documents/DocumentPage.tsx b/apps/console2/src/pages/organizations/documents/DocumentDetailPage.tsx similarity index 84% rename from apps/console2/src/pages/organizations/documents/DocumentPage.tsx rename to apps/console2/src/pages/organizations/documents/DocumentDetailPage.tsx index cd8be0da0..88425284e 100644 --- a/apps/console2/src/pages/organizations/documents/DocumentPage.tsx +++ b/apps/console2/src/pages/organizations/documents/DocumentDetailPage.tsx @@ -13,13 +13,12 @@ import { useDeleteDocumentMutation, } from "/hooks/graph/DocumentGraph"; import { usePageTitle } from "@probo/hooks"; -import type { DocumentPageDocumentFragment$key } from "./__generated__/DocumentPageDocumentFragment.graphql"; +import type { DocumentDetailPageDocumentFragment$key } from "./__generated__/DocumentDetailPageDocumentFragment.graphql"; import { useTranslate } from "@probo/i18n"; import { PageHeader, Breadcrumb, IconCheckmark1, - Markdown, PropertyRow, Drawer, Badge, @@ -31,12 +30,15 @@ import { IconClock, IconSignature, useConfirm, + Tabs, + TabLink, + TabBadge, } from "@probo/ui"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { Button } from "@probo/ui"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { sprintf } from "@probo/helpers"; -import { useNavigate } from "react-router"; +import { Outlet, useNavigate } from "react-router"; import UpdateVersionDialog from "./dialogs/UpdateVersionDialog"; import { useRef } from "react"; import { DocumentVersionHistoryDialog } from "./dialogs/DocumentVersionHistoryDialog"; @@ -47,14 +49,23 @@ type Props = { }; const documentFragment = graphql` - fragment DocumentPageDocumentFragment on Document { + fragment DocumentDetailPageDocumentFragment on Document { id title owner { id fullName } - versions(first: 20) @connection(key: "DocumentPage_versions") { + controls(first: 100) @connection(key: "DocumentDetailPage_controls") { + __id + edges { + node { + id + ...LinkedControlsCardFragment + } + } + } + versions(first: 20) @connection(key: "DocumentDetailPage_versions") { __id edges { node { @@ -64,7 +75,8 @@ const documentFragment = graphql` publishedAt version updatedAt - signatures(first: 100) @connection(key: "DocumentPage_signatures") { + signatures(first: 100) + @connection(key: "DocumentDetailPage_signatures") { __id edges { node { @@ -86,7 +98,9 @@ const documentFragment = graphql` `; const publishDocumentVersionMutation = graphql` - mutation DocumentPagePublishMutation($input: PublishDocumentVersionInput!) { + mutation DocumentDetailPagePublishMutation( + $input: PublishDocumentVersionInput! + ) { publishDocumentVersion(input: $input) { document { id @@ -95,11 +109,11 @@ const publishDocumentVersionMutation = graphql` } `; -export default function DocumentPage(props: Props) { +export default function DocumentDetailPage(props: Props) { const node = usePreloadedQuery(documentNodeQuery, props.queryRef).node; const document = useFragment( documentFragment, - node as DocumentPageDocumentFragment$key + node as DocumentDetailPageDocumentFragment$key ); const { __, dateFormat } = useTranslate(); const organizationId = useOrganizationId(); @@ -229,7 +243,22 @@ export default function DocumentPage(props: Props) { - + + + + {__("Description")} + + + {__("Controls")} + {document.controls.edges.length} + + + +
diff --git a/apps/console2/src/pages/organizations/documents/__generated__/DocumentPageDocumentFragment.graphql.ts b/apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts similarity index 79% rename from apps/console2/src/pages/organizations/documents/__generated__/DocumentPageDocumentFragment.graphql.ts rename to apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts index a2d471953..d61e47b50 100644 --- a/apps/console2/src/pages/organizations/documents/__generated__/DocumentPageDocumentFragment.graphql.ts +++ b/apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPageDocumentFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<72689566d977ab4171c75ba9ce2c62f4>> * @lightSyntaxTransform * @nogrep */ @@ -12,7 +12,16 @@ import { ReaderFragment } from 'relay-runtime'; export type DocumentStatus = "DRAFT" | "PUBLISHED"; export type DocumentVersionSignatureState = "REQUESTED" | "SIGNED"; import { FragmentRefs } from "relay-runtime"; -export type DocumentPageDocumentFragment$data = { +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; @@ -46,11 +55,11 @@ export type DocumentPageDocumentFragment$data = { }; }>; }; - readonly " $fragmentType": "DocumentPageDocumentFragment"; + readonly " $fragmentType": "DocumentDetailPageDocumentFragment"; }; -export type DocumentPageDocumentFragment$key = { - readonly " $data"?: DocumentPageDocumentFragment$data; - readonly " $fragmentSpreads": FragmentRefs<"DocumentPageDocumentFragment">; +export type DocumentDetailPageDocumentFragment$key = { + readonly " $data"?: DocumentDetailPageDocumentFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"DocumentDetailPageDocumentFragment">; }; const node: ReaderFragment = (function(){ @@ -117,6 +126,14 @@ return { "kind": "Fragment", "metadata": { "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "controls" + ] + }, { "count": null, "cursor": null, @@ -133,7 +150,7 @@ return { } ] }, - "name": "DocumentPageDocumentFragment", + "name": "DocumentDetailPageDocumentFragment", "selections": [ (v0/*: any*/), { @@ -162,12 +179,55 @@ 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 + }, { "alias": "versions", "args": null, "concreteType": "DocumentVersionConnection", "kind": "LinkedField", - "name": "__DocumentPage_versions_connection", + "name": "__DocumentDetailPage_versions_connection", "plural": false, "selections": [ { @@ -227,7 +287,7 @@ return { "args": null, "concreteType": "DocumentVersionSignatureConnection", "kind": "LinkedField", - "name": "__DocumentPage_signatures_connection", + "name": "__DocumentDetailPage_signatures_connection", "plural": false, "selections": [ { @@ -313,6 +373,6 @@ return { }; })(); -(node as any).hash = "b42e98e8e26bc94a2d412bd73cd601f2"; +(node as any).hash = "df93b2fdd0ceea0637ddac2e4186181c"; export default node; diff --git a/apps/console2/src/pages/organizations/documents/__generated__/DocumentPagePublishMutation.graphql.ts b/apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPagePublishMutation.graphql.ts similarity index 67% rename from apps/console2/src/pages/organizations/documents/__generated__/DocumentPagePublishMutation.graphql.ts rename to apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPagePublishMutation.graphql.ts index 9d6a248eb..429d5a4ad 100644 --- a/apps/console2/src/pages/organizations/documents/__generated__/DocumentPagePublishMutation.graphql.ts +++ b/apps/console2/src/pages/organizations/documents/__generated__/DocumentDetailPagePublishMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<3ef23505e9378ce7ea92943b394ba290>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -10,21 +10,22 @@ import { ConcreteRequest } from 'relay-runtime'; export type PublishDocumentVersionInput = { + changelog?: string | null | undefined; documentId: string; }; -export type DocumentPagePublishMutation$variables = { +export type DocumentDetailPagePublishMutation$variables = { input: PublishDocumentVersionInput; }; -export type DocumentPagePublishMutation$data = { +export type DocumentDetailPagePublishMutation$data = { readonly publishDocumentVersion: { readonly document: { readonly id: string; }; }; }; -export type DocumentPagePublishMutation = { - response: DocumentPagePublishMutation$data; - variables: DocumentPagePublishMutation$variables; +export type DocumentDetailPagePublishMutation = { + response: DocumentDetailPagePublishMutation$data; + variables: DocumentDetailPagePublishMutation$variables; }; const node: ConcreteRequest = (function(){ @@ -77,7 +78,7 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, - "name": "DocumentPagePublishMutation", + "name": "DocumentDetailPagePublishMutation", "selections": (v1/*: any*/), "type": "Mutation", "abstractKey": null @@ -86,20 +87,20 @@ return { "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", - "name": "DocumentPagePublishMutation", + "name": "DocumentDetailPagePublishMutation", "selections": (v1/*: any*/) }, "params": { - "cacheID": "fad75c1a396f60d6d13e1605fe85f33e", + "cacheID": "bbee8d20f444cdc0dbb012e8fecfa9c1", "id": null, "metadata": {}, - "name": "DocumentPagePublishMutation", + "name": "DocumentDetailPagePublishMutation", "operationKind": "mutation", - "text": "mutation DocumentPagePublishMutation(\n $input: PublishDocumentVersionInput!\n) {\n publishDocumentVersion(input: $input) {\n document {\n id\n }\n }\n}\n" + "text": "mutation DocumentDetailPagePublishMutation(\n $input: PublishDocumentVersionInput!\n) {\n publishDocumentVersion(input: $input) {\n document {\n id\n }\n }\n}\n" } }; })(); -(node as any).hash = "488029ccd88b4c637b63cd11d1bef986"; +(node as any).hash = "2f5cc9855133b614896a1ac3768e669f"; export default node; diff --git a/apps/console2/src/pages/organizations/documents/dialogs/DocumentSignaturesDialog.tsx b/apps/console2/src/pages/organizations/documents/dialogs/DocumentSignaturesDialog.tsx index 97ba3440f..01b19a879 100644 --- a/apps/console2/src/pages/organizations/documents/dialogs/DocumentSignaturesDialog.tsx +++ b/apps/console2/src/pages/organizations/documents/dialogs/DocumentSignaturesDialog.tsx @@ -13,7 +13,6 @@ import { } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { useState, type ReactNode, Suspense } from "react"; -import type { DocumentPageDocumentFragment$data } from "../__generated__/DocumentPageDocumentFragment.graphql"; import clsx from "clsx"; import type { ItemOf, NodeOf } from "/types"; import { graphql, useFragment } from "react-relay"; @@ -23,13 +22,14 @@ import { usePeople } from "/hooks/graph/PeopleGraph.ts"; import { useOrganizationId } from "/hooks/useOrganizationId.ts"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts.ts"; import { sprintf } from "@probo/helpers"; +import type { DocumentDetailPageDocumentFragment$data } from "../__generated__/DocumentDetailPageDocumentFragment.graphql"; type Props = { - document: DocumentPageDocumentFragment$data; + document: DocumentDetailPageDocumentFragment$data; children?: ReactNode; }; -type Version = NodeOf; +type Version = NodeOf; export function DocumentSignaturesDialog(props: Props) { const { __ } = useTranslate(); diff --git a/apps/console2/src/pages/organizations/documents/dialogs/DocumentVersionHistoryDialog.tsx b/apps/console2/src/pages/organizations/documents/dialogs/DocumentVersionHistoryDialog.tsx index 2c965f377..e62db98bb 100644 --- a/apps/console2/src/pages/organizations/documents/dialogs/DocumentVersionHistoryDialog.tsx +++ b/apps/console2/src/pages/organizations/documents/dialogs/DocumentVersionHistoryDialog.tsx @@ -9,11 +9,11 @@ import { import { graphql } from "relay-runtime"; import { useTranslate } from "@probo/i18n"; import { useState, type ReactNode } from "react"; -import type { DocumentPageDocumentFragment$data } from "../__generated__/DocumentPageDocumentFragment.graphql"; import { useFragment } from "react-relay"; import type { DocumentVersionHistoryDialogFragment$key } from "./__generated__/DocumentVersionHistoryDialogFragment.graphql"; import clsx from "clsx"; import type { NodeOf } from "/types"; +import type { DocumentDetailPageDocumentFragment$data } from "../__generated__/DocumentDetailPageDocumentFragment.graphql"; const historyFragment = graphql` fragment DocumentVersionHistoryDialogFragment on DocumentVersion { @@ -31,11 +31,11 @@ const historyFragment = graphql` `; type Props = { - document: DocumentPageDocumentFragment$data; + document: DocumentDetailPageDocumentFragment$data; children?: ReactNode; }; -type Version = NodeOf; +type Version = NodeOf; export function DocumentVersionHistoryDialog(props: Props) { const { __ } = useTranslate(); @@ -68,7 +68,7 @@ export function DocumentVersionHistoryDialog(props: Props) { } function VersionItem(props: { - document: DocumentPageDocumentFragment$data; + document: DocumentDetailPageDocumentFragment$data; version: Version; active?: boolean; onSelect: (v: Version) => void; diff --git a/apps/console2/src/pages/organizations/documents/dialogs/UpdateVersionDialog.tsx b/apps/console2/src/pages/organizations/documents/dialogs/UpdateVersionDialog.tsx index 1fda7112c..9d20c24da 100644 --- a/apps/console2/src/pages/organizations/documents/dialogs/UpdateVersionDialog.tsx +++ b/apps/console2/src/pages/organizations/documents/dialogs/UpdateVersionDialog.tsx @@ -13,12 +13,12 @@ import { import { type RefObject } from "react"; import { graphql } from "relay-runtime"; import { useMutation } from "react-relay"; -import type { DocumentPageDocumentFragment$data } from "../__generated__/DocumentPageDocumentFragment.graphql"; import type { UpdateVersionDialogCreateMutation } from "./__generated__/UpdateVersionDialogCreateMutation.graphql"; import type { UpdateVersionDialogUpdateMutation } from "./__generated__/UpdateVersionDialogUpdateMutation.graphql"; import { z } from "zod"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import type { DocumentDetailPageDocumentFragment$data } from "../__generated__/DocumentDetailPageDocumentFragment.graphql"; const createDraftDocument = graphql` mutation UpdateVersionDialogCreateMutation( @@ -62,7 +62,7 @@ const UpdateDocumentMutation = graphql` `; type Props = { - document: DocumentPageDocumentFragment$data; + document: DocumentDetailPageDocumentFragment$data; connectionId: string; ref: RefObject<{ open: () => void } | null>; }; diff --git a/apps/console2/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx b/apps/console2/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx new file mode 100644 index 000000000..fff85fdeb --- /dev/null +++ b/apps/console2/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx @@ -0,0 +1,33 @@ +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"; + +const detachControlMutation = graphql` + mutation DocumentControlsTab_detachControlMutation( + $input: DeleteControlDocumentMappingInput! + $connections: [ID!]! + ) { + deleteControlDocumentMapping(input: $input) { + deletedControlId @deleteEdge(connections: $connections) + } + } +`; + +export default function DocumentControlsTab() { + const { document } = useOutletContext<{ + document: DocumentDetailPageDocumentFragment$data; + }>(); + const controls = document.controls.edges.map((edge) => edge.node); + const [detachControl] = useMutation(detachControlMutation); + console.log(controls.map((c) => c.id)); + return ( + + ); +} diff --git a/apps/console2/src/pages/organizations/documents/tabs/DocumentDescriptionTab.tsx b/apps/console2/src/pages/organizations/documents/tabs/DocumentDescriptionTab.tsx new file mode 100644 index 000000000..bfd5e0341 --- /dev/null +++ b/apps/console2/src/pages/organizations/documents/tabs/DocumentDescriptionTab.tsx @@ -0,0 +1,15 @@ +import { useOutletContext } from "react-router"; +import type { DocumentDetailPageDocumentFragment$data } from "../__generated__/DocumentDetailPageDocumentFragment.graphql"; +import type { NodeOf } from "/types"; +import { Markdown } from "@probo/ui"; + +export default function DocumentDescriptionTab() { + const { lastVersion } = useOutletContext<{ + lastVersion: NodeOf; + }>(); + return ( +
+ +
+ ); +} diff --git a/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTab_detachControlMutation.graphql.ts b/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTab_detachControlMutation.graphql.ts new file mode 100644 index 000000000..0ab2dc982 --- /dev/null +++ b/apps/console2/src/pages/organizations/documents/tabs/__generated__/DocumentControlsTab_detachControlMutation.graphql.ts @@ -0,0 +1,133 @@ +/** + * @generated SignedSource<<5c230fb025256fb7a5281ee5ecd3ccf4>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteControlDocumentMappingInput = { + controlId: string; + documentId: string; +}; +export type DocumentControlsTab_detachControlMutation$variables = { + connections: ReadonlyArray; + input: DeleteControlDocumentMappingInput; +}; +export type DocumentControlsTab_detachControlMutation$data = { + readonly deleteControlDocumentMapping: { + readonly deletedControlId: string; + }; +}; +export type DocumentControlsTab_detachControlMutation = { + response: DocumentControlsTab_detachControlMutation$data; + variables: DocumentControlsTab_detachControlMutation$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": "deletedControlId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "DocumentControlsTab_detachControlMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteControlDocumentMappingPayload", + "kind": "LinkedField", + "name": "deleteControlDocumentMapping", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "DocumentControlsTab_detachControlMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteControlDocumentMappingPayload", + "kind": "LinkedField", + "name": "deleteControlDocumentMapping", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedControlId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "fce801689ab578a47c33a50c60fc4cd7", + "id": null, + "metadata": {}, + "name": "DocumentControlsTab_detachControlMutation", + "operationKind": "mutation", + "text": "mutation DocumentControlsTab_detachControlMutation(\n $input: DeleteControlDocumentMappingInput!\n) {\n deleteControlDocumentMapping(input: $input) {\n deletedControlId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "39c5b39c08ba37d06f5e0cfedc707a25"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx b/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx index 362e7e7cf..0a2b3a908 100644 --- a/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx +++ b/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx @@ -205,7 +205,7 @@ export default function FrameworkDetailPage(props: Props) { key={control.id} id={control.referenceId} description={control.name ?? control.description} - to={`/organizations/${organizationId}/frameworks/${framework.id}/${control.id}`} + to={`/organizations/${organizationId}/frameworks/${framework.id}/controls/${control.id}`} active={selectedControl?.id === control.id} /> ))} @@ -217,6 +217,7 @@ export default function FrameworkDetailPage(props: Props) {
{selectedControl.name}
edge.node) ?? [] } @@ -227,6 +228,7 @@ export default function FrameworkDetailPage(props: Props) { disabled={isAttachingMeasure || isDetachingMeasure} /> edge.node) ?? [] } diff --git a/apps/console2/src/pages/organizations/measures/MeasureDetailPage.tsx b/apps/console2/src/pages/organizations/measures/MeasureDetailPage.tsx new file mode 100644 index 000000000..d00362707 --- /dev/null +++ b/apps/console2/src/pages/organizations/measures/MeasureDetailPage.tsx @@ -0,0 +1,190 @@ +import { Outlet, useParams } from "react-router"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { + ActionDropdown, + Breadcrumb, + Button, + Drawer, + DropdownItem, + IconCheckmark1, + IconFrame2, + IconPageTextLine, + IconPencil, + IconTrashCan, + IconWarning, + MeasureBadge, + Option, + PropertyRow, + Select, + TabLink, + Tabs, + useConfirm, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { + ConnectionHandler, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; +import type { MeasureGraphNodeQuery } from "/hooks/graph/__generated__/MeasureGraphNodeQuery.graphql"; +import { + MeasureConnectionKey, + measureNodeQuery, + useDeleteMeasureMutation, + useUpdateMeasure, +} from "/hooks/graph/MeasureGraph"; +import { PageHeader } from "@probo/ui"; +import { getMeasureStateLabel, measureStates, slugify } from "@probo/helpers"; +import MeasureFormDialog from "./dialog/MeasureFormDialog"; +import { sprintf } from "@probo/helpers"; +import { useNavigate } from "react-router"; + +type Props = { + queryRef: PreloadedQuery; +}; + +export default function MeasureDetailPage(props: Props) { + const { measureId } = useParams<{ measureId: string }>(); + const organizationId = useOrganizationId(); + const data = usePreloadedQuery(measureNodeQuery, props.queryRef); + const measure = data.node; + const { __ } = useTranslate(); + const [deleteMeasure] = useDeleteMeasureMutation(); + const navigate = useNavigate(); + const confirm = useConfirm(); + const [updateMeasure, isUpdating] = useUpdateMeasure(); + + if (!measureId) { + throw new Error( + "Cannot load measure detail page without measureId parameter" + ); + } + + const onDelete = () => { + const connectionId = ConnectionHandler.getConnectionID( + organizationId, + MeasureConnectionKey + ); + confirm( + () => + new Promise((resolve) => { + deleteMeasure({ + variables: { + input: { measureId }, + connections: [connectionId], + }, + onSuccess() { + navigate(`/organizations/${organizationId}/measures`); + resolve(); + }, + }); + }), + { + message: sprintf( + __( + 'This will permanently delete the measure "%s". This action cannot be undone.' + ), + measure.name + ), + } + ); + }; + + const onStateChange = (state: string) => { + updateMeasure({ + variables: { + input: { + id: measureId, + state, + }, + }, + }); + }; + + return ( +
+ {/* Header */} + + + + + + + + + + {__("Delete")} + + + + + + + + {__("Evidences")} + + + + {__("Tasks")} + + + + {__("Controls")} + + + + {__("Risks")} + + + + + + + + + + +
+ ); +} diff --git a/apps/console2/src/pages/organizations/measures/MeasuresPage.tsx b/apps/console2/src/pages/organizations/measures/MeasuresPage.tsx index 0e92692c1..726c27d36 100644 --- a/apps/console2/src/pages/organizations/measures/MeasuresPage.tsx +++ b/apps/console2/src/pages/organizations/measures/MeasuresPage.tsx @@ -44,6 +44,8 @@ import type { MeasuresPageImportMutation } from "./__generated__/MeasuresPageImp import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { Link, useParams } from "react-router"; +import MeasureFormDialog from "./dialog/MeasureFormDialog"; +import { usePageTitle } from "@probo/hooks"; type Props = { queryRef: PreloadedQuery; @@ -59,6 +61,7 @@ const measuresFragment = graphql` name category state + ...MeasureFormDialogMeasureFragment } } } @@ -106,6 +109,7 @@ export default function MeasuresPage(props: Props) { } ); const importFileRef = useRef(null); + usePageTitle(__("Measures")); const handleImport: ChangeEventHandler = (event) => { const file = event.target.files?.[0]; @@ -145,9 +149,11 @@ export default function MeasuresPage(props: Props) { > {__("Import")} - + + +
{objectKeys(measuresPerCategory) diff --git a/apps/console2/src/pages/organizations/measures/__generated__/MeasuresPageFragment.graphql.ts b/apps/console2/src/pages/organizations/measures/__generated__/MeasuresPageFragment.graphql.ts index 179458bed..ef330ee45 100644 --- a/apps/console2/src/pages/organizations/measures/__generated__/MeasuresPageFragment.graphql.ts +++ b/apps/console2/src/pages/organizations/measures/__generated__/MeasuresPageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -20,6 +20,7 @@ export type MeasuresPageFragment$data = { readonly id: string; readonly name: string; readonly state: MeasureState; + readonly " $fragmentSpreads": FragmentRefs<"MeasureFormDialogMeasureFragment">; }; }>; }; @@ -99,6 +100,11 @@ const node: ReaderFragment = { "name": "state", "storageKey": null }, + { + "args": null, + "kind": "FragmentSpread", + "name": "MeasureFormDialogMeasureFragment" + }, { "alias": null, "args": null, @@ -164,6 +170,6 @@ const node: ReaderFragment = { "abstractKey": null }; -(node as any).hash = "6a45bee844bf79dc4540e20edf10ad78"; +(node as any).hash = "b9f66d2e9305aa568c33f708d4ce8fac"; export default node; diff --git a/apps/console2/src/pages/organizations/measures/dialog/MeasureFormDialog.tsx b/apps/console2/src/pages/organizations/measures/dialog/MeasureFormDialog.tsx new file mode 100644 index 000000000..7f611494d --- /dev/null +++ b/apps/console2/src/pages/organizations/measures/dialog/MeasureFormDialog.tsx @@ -0,0 +1,190 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Input, + Label, + Option, + PropertyRow, + Textarea, + useDialogRef, +} from "@probo/ui"; +import type { ReactNode } from "react"; +import { useTranslate } from "@probo/i18n"; +import { Breadcrumb } from "@probo/ui"; +import { graphql } from "relay-runtime"; +import type { MeasureFormDialogMeasureFragment$key } from "./__generated__/MeasureFormDialogMeasureFragment.graphql"; +import { useFragment } from "react-relay"; +import { z } from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { getMeasureStateLabel, measureStates } from "@probo/helpers"; +import { ControlledSelect } from "/components/form/ControlledField"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { useUpdateMeasure } from "/hooks/graph/MeasureGraph"; + +type Props = { + children: ReactNode; + measure?: MeasureFormDialogMeasureFragment$key; + connection?: string; +}; + +const measureFragment = graphql` + fragment MeasureFormDialogMeasureFragment on Measure { + id + description + name + category + state + } +`; + +const measureCreateMutation = graphql` + mutation MeasureFormDialogCreateMutation( + $input: CreateMeasureInput! + $connections: [ID!]! + ) { + createMeasure(input: $input) { + measureEdge @prependEdge(connections: $connections) { + node { + ...MeasureFormDialogMeasureFragment + } + } + } + } +`; + +const measureSchema = z.object({ + name: z.string(), + description: z.string(), + category: z.string(), + state: z.enum(measureStates), +}); + +export default function MeasureFormDialog(props: Props) { + const { __ } = useTranslate(); + const measure = useFragment(measureFragment, props.measure); + const dialogRef = useDialogRef(); + const organizationId = useOrganizationId(); + const [mutate] = props.measure + ? useUpdateMeasure() + : useMutationWithToasts(measureCreateMutation, { + successMessage: __("Measure created successfully."), + errorMessage: __("Failed to create measure. Please try again."), + }); + + const { control, handleSubmit, register, formState } = useFormWithSchema( + measureSchema, + { + defaultValues: { + name: measure?.name ?? "", + description: measure?.description ?? "", + category: measure?.category ?? "", + state: "NOT_STARTED", + }, + } + ); + + const onSubmit = handleSubmit(async (data) => { + if (measure) { + await mutate({ + variables: { + input: { + id: measure.id, + name: data.name, + description: data.description, + category: data.category, + state: data.state, + }, + }, + }); + } else { + await mutate({ + variables: { + input: { + organizationId, + name: data.name, + description: data.description, + category: data.category, + }, + connections: [props.connection!], + }, + }); + } + dialogRef.current?.close(); + }); + + return ( + + } + > +
+ +
+ +