From 74c136f6c08a72ee0b183400536f19cdc26baef7 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Mon, 2 Jun 2025 14:24:22 +0200 Subject: [PATCH] Add frameworks detail page Signed-off-by: Bryan Frimin Signed-off-by: Sacha Al Himdani --- .../console2/src/components/FrameworkLogo.tsx | 15 + .../documents/DocumentLinkDialog.tsx | 138 ++++++ .../documents/LinkedDocumentsCard.tsx | 189 ++++++++ .../DocumentLinkDialogQuery.graphql.ts | 246 +++++++++++ .../LinkedDocumentsCardFragment.graphql.ts | 117 +++++ .../measures/LinkedMeasuresCard.tsx | 170 ++++++++ .../components/measures/MeasureLinkDialog.tsx | 166 +++++++ .../LinkedMeasuresCardFragment.graphql.ts | 59 +++ .../MeasureLinkDialogQuery.graphql.ts | 22 +- .../components/risks/MeasureLinkDialog.tsx | 176 +------- ...MeasureLinkDialogCreateMutation.graphql.ts | 192 --------- .../src/hooks/graph/FrameworkGraph.ts | 12 + .../FrameworkGraphNodeQuery.graphql.ts | 407 ++++++++++++++++++ .../RiskGraphNodeQuery.graphql.ts | 36 +- .../frameworks/FrameworkDetailPage.tsx | 211 +++++++++ .../frameworks/FrameworksPage.tsx | 14 +- ...etailPageAttachDocumentMutation.graphql.ts | 249 +++++++++++ ...ameworkDetailPageAttachMutation.graphql.ts | 202 +++++++++ ...etailPageDetachDocumentMutation.graphql.ts | 133 ++++++ ...ameworkDetailPageDetachMutation.graphql.ts | 133 ++++++ .../FrameworkDetailPageFragment.graphql.ts | 288 +++++++++++++ .../dialogs/CreateFrameworkDialog.tsx | 1 - .../risks/tabs/RiskMeasuresTab.tsx | 170 ++------ .../RiskMeasuresTabCreateMutation.graphql.ts | 202 +++++++++ .../RiskMeasuresTabDetachMutation.graphql.ts} | 24 +- .../RiskMeasuresTabFragment.graphql.ts | 45 +- apps/console2/src/routes/frameworkRoutes.ts | 14 +- .../Atoms/ControlItem/ControlItem.stories.tsx | 27 ++ .../ui/src/Atoms/ControlItem/ControlItem.tsx | 49 +++ .../src/Atoms/Markdown/Markdown.stories.tsx | 2 +- .../Atoms/PropertyRow/PropertyRow.stories.tsx | 7 - .../Molecules/Combobox/Combobox.stories.tsx | 7 - .../Dialog/ConfirmDialog.stories.tsx | 34 -- .../src/Molecules/Dialog/Dialog.stories.tsx | 1 - .../src/Molecules/PageHeader/PageHeader.tsx | 4 +- packages/ui/src/index.ts | 1 + 36 files changed, 3117 insertions(+), 646 deletions(-) create mode 100644 apps/console2/src/components/FrameworkLogo.tsx create mode 100644 apps/console2/src/components/documents/DocumentLinkDialog.tsx create mode 100644 apps/console2/src/components/documents/LinkedDocumentsCard.tsx create mode 100644 apps/console2/src/components/documents/__generated__/DocumentLinkDialogQuery.graphql.ts create mode 100644 apps/console2/src/components/documents/__generated__/LinkedDocumentsCardFragment.graphql.ts create mode 100644 apps/console2/src/components/measures/LinkedMeasuresCard.tsx create mode 100644 apps/console2/src/components/measures/MeasureLinkDialog.tsx create mode 100644 apps/console2/src/components/measures/__generated__/LinkedMeasuresCardFragment.graphql.ts rename apps/console2/src/components/{risks => measures}/__generated__/MeasureLinkDialogQuery.graphql.ts (95%) delete mode 100644 apps/console2/src/components/risks/__generated__/MeasureLinkDialogCreateMutation.graphql.ts create mode 100644 apps/console2/src/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql.ts create mode 100644 apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx create mode 100644 apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageAttachDocumentMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageAttachMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageDetachDocumentMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageDetachMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageFragment.graphql.ts create mode 100644 apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabCreateMutation.graphql.ts rename apps/console2/src/{components/risks/__generated__/MeasureLinkDialogDetachMutation.graphql.ts => pages/organizations/risks/tabs/__generated__/RiskMeasuresTabDetachMutation.graphql.ts} (76%) create mode 100644 packages/ui/src/Atoms/ControlItem/ControlItem.stories.tsx create mode 100644 packages/ui/src/Atoms/ControlItem/ControlItem.tsx delete mode 100644 packages/ui/src/Molecules/Dialog/ConfirmDialog.stories.tsx diff --git a/apps/console2/src/components/FrameworkLogo.tsx b/apps/console2/src/components/FrameworkLogo.tsx new file mode 100644 index 000000000..a727f7ea5 --- /dev/null +++ b/apps/console2/src/components/FrameworkLogo.tsx @@ -0,0 +1,15 @@ +import { availableFrameworks } from "@probo/helpers"; +import { Avatar } from "@probo/ui"; + +const availableLogos = new Map( + availableFrameworks.map((framework) => [framework.name, framework.logo]) +); + +export function FrameworkLogo({ name }: { name: string }) { + const logo = availableLogos.get(name); + return logo ? ( + + ) : ( + + ); +} diff --git a/apps/console2/src/components/documents/DocumentLinkDialog.tsx b/apps/console2/src/components/documents/DocumentLinkDialog.tsx new file mode 100644 index 000000000..10e183ff7 --- /dev/null +++ b/apps/console2/src/components/documents/DocumentLinkDialog.tsx @@ -0,0 +1,138 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + IconMagnifyingGlass, + IconPlusLarge, + IconTrashCan, + 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 } from "./__generated__/DocumentLinkDialogQuery.graphql"; +import { useOrganizationId } from "/hooks/useOrganizationId"; + +const documentsQuery = graphql` + query DocumentLinkDialogQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + id + ... on Organization { + documents(first: 100) @connection(key: "Organization__documents") { + edges { + node { + id + title + } + } + } + } + } + } +`; + +type Props = { + children: ReactNode; + connectionId: string; + disabled?: boolean; + linkedDocuments?: { id: string }[]; + onLink: (documentId: string) => void; + onUnlink: (documentId: string) => void; +}; + +export function DocumentLinkDialog({ children, ...props }: Props) { + const { __ } = useTranslate(); + + return ( + + + }> + + + + + + ); +} + +function DocumentLinkDialogContent(props: Omit) { + const organizationId = useOrganizationId(); + const data = useLazyLoadQuery(documentsQuery, { + organizationId, + }); + const { __ } = useTranslate(); + const [search, setSearch] = useState(""); + const documents = + data.organization?.documents?.edges?.map((edge) => edge.node) ?? []; + const linkedIds = useMemo(() => { + return new Set(props.linkedDocuments?.map((m) => m.id) ?? []); + }, [props.linkedDocuments]); + + const filteredDocuments = useMemo(() => { + return documents.filter((document) => + document.title.toLowerCase().includes(search.toLowerCase()) + ); + }, [documents, search]); + + return ( + <> +
+ +
+
+ {filteredDocuments.map((document) => ( + + ))} +
+ + ); +} + +type RowProps = { + document: { title: string; id: string }; + linkedDocuments: Set; + disabled?: boolean; + onLink: (documentId: string) => void; + onUnlink: (documentId: string) => void; +}; + +function DocumentRow(props: RowProps) { + const { __ } = useTranslate(); + + const isLinked = props.linkedDocuments.has(props.document.id); + const onClick = isLinked ? props.onUnlink : props.onLink; + const IconComponent = isLinked ? IconTrashCan : IconPlusLarge; + + return ( + + + ); +} diff --git a/apps/console2/src/components/documents/LinkedDocumentsCard.tsx b/apps/console2/src/components/documents/LinkedDocumentsCard.tsx new file mode 100644 index 000000000..10757c584 --- /dev/null +++ b/apps/console2/src/components/documents/LinkedDocumentsCard.tsx @@ -0,0 +1,189 @@ +import { graphql } from "relay-runtime"; +import { + Card, + IconPlusLarge, + Button, + Tr, + Td, + Table, + Thead, + Tbody, + Th, + IconChevronDown, + IconTrashCan, + DocumentVersionBadge, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import type { LinkedDocumentsCardFragment$key } from "./__generated__/LinkedDocumentsCardFragment.graphql"; +import { useFragment } from "react-relay"; +import { useMemo, useState } from "react"; +import { sprintf } from "@probo/helpers"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { DocumentLinkDialog } from "./DocumentLinkDialog"; + +const linkedDocumentFragment = graphql` + fragment LinkedDocumentsCardFragment on Document { + id + title + createdAt + versions(first: 1) { + edges { + node { + id + status + } + } + } + } +`; + +type Mutation = (p: { + variables: { + input: { + documentId: string; + } & Params; + connections: string[]; + }; +}) => void; + +type Props = { + // Documents linked to the element + documents: (LinkedDocumentsCardFragment$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 document (will receive {documentId, ...params}) + onAttach: Mutation; + // Mutation to detach a document (will receive {documentId, ...params}) + onDetach: Mutation; +}; + +/** + * Reusable component that displays a list of linked documents + */ +export function LinkedDocumentsCard(props: Props) { + const { __ } = useTranslate(); + const [limit, setLimit] = useState(4); + const documents = useMemo(() => { + return limit ? props.documents.slice(0, limit) : props.documents; + }, [props.documents, limit]); + const showMoreButton = limit !== null && props.documents.length > limit; + + const onAttach = (documentId: string) => { + props.onAttach({ + variables: { + input: { + documentId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + const onDetach = (documentId: string) => { + props.onDetach({ + variables: { + input: { + documentId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + return ( + +
+
{__("Documents")}
+ + + +
+ {documents.length > 0 ? ( + + + + + + + + + + {documents.map((document) => ( + + ))} + +
{__("Name")}{__("State")}
+ ) : ( +
+ {__("No documents linked")} +
+ )} + {showMoreButton && ( + + )} +
+ ); +} + +function DocumentRow(props: { + document: LinkedDocumentsCardFragment$key & { id: string }; + onClick: (documentId: string) => void; +}) { + const document = useFragment(linkedDocumentFragment, props.document); + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + + return ( + + +
+ + {document.title} +
+ + + + + + + + + ); +} diff --git a/apps/console2/src/components/documents/__generated__/DocumentLinkDialogQuery.graphql.ts b/apps/console2/src/components/documents/__generated__/DocumentLinkDialogQuery.graphql.ts new file mode 100644 index 000000000..7f3702a2f --- /dev/null +++ b/apps/console2/src/components/documents/__generated__/DocumentLinkDialogQuery.graphql.ts @@ -0,0 +1,246 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DocumentLinkDialogQuery$variables = { + organizationId: string; +}; +export type DocumentLinkDialogQuery$data = { + readonly organization: { + readonly documents?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly title: string; + }; + }>; + }; + readonly id: string; + }; +}; +export type DocumentLinkDialogQuery = { + response: DocumentLinkDialogQuery$data; + variables: DocumentLinkDialogQuery$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": "DocumentEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Document", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "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": "DocumentLinkDialogQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": "documents", + "args": null, + "concreteType": "DocumentConnection", + "kind": "LinkedField", + "name": "__Organization__documents_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": "DocumentLinkDialogQuery", + "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": "DocumentConnection", + "kind": "LinkedField", + "name": "documents", + "plural": false, + "selections": (v4/*: any*/), + "storageKey": "documents(first:100)" + }, + { + "alias": null, + "args": (v5/*: any*/), + "filters": null, + "handle": "connection", + "key": "Organization__documents", + "kind": "LinkedHandle", + "name": "documents" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "fa671324c6ab818f40ccf9f83ca9af8a", + "id": null, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "organization", + "documents" + ] + } + ] + }, + "name": "DocumentLinkDialogQuery", + "operationKind": "query", + "text": "query DocumentLinkDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n documents(first: 100) {\n edges {\n node {\n id\n title\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "70a09260215ae0f74ccf06a0c0872e75"; + +export default node; diff --git a/apps/console2/src/components/documents/__generated__/LinkedDocumentsCardFragment.graphql.ts b/apps/console2/src/components/documents/__generated__/LinkedDocumentsCardFragment.graphql.ts new file mode 100644 index 000000000..ddbfc4ddb --- /dev/null +++ b/apps/console2/src/components/documents/__generated__/LinkedDocumentsCardFragment.graphql.ts @@ -0,0 +1,117 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type DocumentStatus = "DRAFT" | "PUBLISHED"; +import { FragmentRefs } from "relay-runtime"; +export type LinkedDocumentsCardFragment$data = { + readonly createdAt: any; + readonly id: string; + readonly title: string; + readonly versions: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly status: DocumentStatus; + }; + }>; + }; + readonly " $fragmentType": "LinkedDocumentsCardFragment"; +}; +export type LinkedDocumentsCardFragment$key = { + readonly " $data"?: LinkedDocumentsCardFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"LinkedDocumentsCardFragment">; +}; + +const node: ReaderFragment = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LinkedDocumentsCardFragment", + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "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": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "versions(first:1)" + } + ], + "type": "Document", + "abstractKey": null +}; +})(); + +(node as any).hash = "907fbdc23df596e4f76e396a11878706"; + +export default node; diff --git a/apps/console2/src/components/measures/LinkedMeasuresCard.tsx b/apps/console2/src/components/measures/LinkedMeasuresCard.tsx new file mode 100644 index 000000000..fce1637ac --- /dev/null +++ b/apps/console2/src/components/measures/LinkedMeasuresCard.tsx @@ -0,0 +1,170 @@ +import { graphql } from "relay-runtime"; +import { + Card, + IconPlusLarge, + Button, + Tr, + Td, + Table, + Thead, + Tbody, + Th, + IconChevronDown, + MeasureBadge, + IconTrashCan, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import type { LinkedMeasuresCardFragment$key } from "./__generated__/LinkedMeasuresCardFragment.graphql"; +import { useFragment } from "react-relay"; +import { useMemo, useState } from "react"; +import { sprintf } from "@probo/helpers"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { MeasureLinkDialog } from "./MeasureLinkDialog"; + +const linkedMeasureFragment = graphql` + fragment LinkedMeasuresCardFragment on Measure { + id + name + state + } +`; + +type Mutation = (p: { + variables: { + input: { + measureId: string; + } & Params; + connections: string[]; + }; +}) => void; + +type Props = { + // Measures linked to the element + measures: (LinkedMeasuresCardFragment$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 measure (will receive {measureId, ...params}) + onAttach: Mutation; + // Mutation to detach a measure (will receive {measureId, ...params}) + onDetach: Mutation; +}; + +/** + * Reusable component that displays a list of linked measures + */ +export function LinkedMeasuresCard(props: Props) { + const { __ } = useTranslate(); + const [limit, setLimit] = useState(4); + const measures = useMemo(() => { + return limit ? props.measures.slice(0, limit) : props.measures; + }, [props.measures, limit]); + const showMoreButton = limit !== null && props.measures.length > limit; + + const onAttach = (measureId: string) => { + props.onAttach({ + variables: { + input: { + measureId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + const onDetach = (measureId: string) => { + props.onDetach({ + variables: { + input: { + measureId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + return ( + +
+
{__("Measures")}
+ + + +
+ {measures.length > 0 ? ( + + + + + + + + + + {measures.map((measure) => ( + + ))} + +
{__("Name")}{__("State")}
+ ) : ( +
+ {__("No measures linked")} +
+ )} + {showMoreButton && ( + + )} +
+ ); +} + +function MeasureRow(props: { + measure: LinkedMeasuresCardFragment$key & { id: string }; + onClick: (measureId: string) => void; +}) { + const measure = useFragment(linkedMeasureFragment, props.measure); + const organizationId = useOrganizationId(); + const { __ } = useTranslate(); + + return ( + + {measure.name} + + + + + + + + ); +} diff --git a/apps/console2/src/components/measures/MeasureLinkDialog.tsx b/apps/console2/src/components/measures/MeasureLinkDialog.tsx new file mode 100644 index 000000000..4bd8868fa --- /dev/null +++ b/apps/console2/src/components/measures/MeasureLinkDialog.tsx @@ -0,0 +1,166 @@ +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 { MeasureLinkDialogQuery } from "./__generated__/MeasureLinkDialogQuery.graphql"; +import { useOrganizationId } from "/hooks/useOrganizationId"; + +const measuresQuery = graphql` + query MeasureLinkDialogQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + id + ... on Organization { + measures(first: 100) @connection(key: "Organization__measures") { + edges { + node { + id + name + state + description + category + } + } + } + } + } + } +`; + +type Props = { + children: ReactNode; + connectionId: string; + disabled?: boolean; + linkedMeasures?: { id: string }[]; + onLink: (measureId: string) => void; + onUnlink: (measureId: string) => void; +}; + +export function MeasureLinkDialog({ children, ...props }: Props) { + const { __ } = useTranslate(); + + return ( + + + }> + + + + + + ); +} + +function MeasureLinkDialogContent(props: Omit) { + const organizationId = useOrganizationId(); + const data = useLazyLoadQuery(measuresQuery, { + organizationId, + }); + const { __ } = useTranslate(); + const [search, setSearch] = useState(""); + const [category, setCategory] = useState(null); + const measures = + data.organization?.measures?.edges?.map((edge) => edge.node) ?? []; + const linkedIds = useMemo(() => { + return new Set(props.linkedMeasures?.map((m) => m.id) ?? []); + }, [props.linkedMeasures]); + + const filteredMeasures = useMemo(() => { + return measures.filter( + (measure) => + (category === null || measure.category === category) && + (measure.name.toLowerCase().includes(search.toLowerCase()) || + measure.description?.toLowerCase().includes(search.toLowerCase())) + ); + }, [measures, search, category]); + + const categories = useMemo( + () => Array.from(new Set(measures.map((m) => m.category))), + [measures] + ); + + return ( + <> +
+ + +
+
+ {filteredMeasures.map((measure) => ( + + ))} +
+ + ); +} + +type RowProps = { + measure: { name: string; category: string; id: string }; + linkedMeasures: Set; + disabled?: boolean; + onLink: (measureId: string) => void; + onUnlink: (measureId: string) => void; +}; + +function MeasureRow(props: RowProps) { + const { __ } = useTranslate(); + + const isLinked = props.linkedMeasures.has(props.measure.id); + const onClick = isLinked ? props.onUnlink : props.onLink; + const IconComponent = isLinked ? IconTrashCan : IconPlusLarge; + + return ( + + + ); +} diff --git a/apps/console2/src/components/measures/__generated__/LinkedMeasuresCardFragment.graphql.ts b/apps/console2/src/components/measures/__generated__/LinkedMeasuresCardFragment.graphql.ts new file mode 100644 index 000000000..c4fb8097d --- /dev/null +++ b/apps/console2/src/components/measures/__generated__/LinkedMeasuresCardFragment.graphql.ts @@ -0,0 +1,59 @@ +/** + * @generated SignedSource<> + * @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 LinkedMeasuresCardFragment$data = { + readonly id: string; + readonly name: string; + readonly state: MeasureState; + readonly " $fragmentType": "LinkedMeasuresCardFragment"; +}; +export type LinkedMeasuresCardFragment$key = { + readonly " $data"?: LinkedMeasuresCardFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresCardFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LinkedMeasuresCardFragment", + "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": "state", + "storageKey": null + } + ], + "type": "Measure", + "abstractKey": null +}; + +(node as any).hash = "741a216c02732c1ff97b265ac8dbf39b"; + +export default node; diff --git a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogQuery.graphql.ts b/apps/console2/src/components/measures/__generated__/MeasureLinkDialogQuery.graphql.ts similarity index 95% rename from apps/console2/src/components/risks/__generated__/MeasureLinkDialogQuery.graphql.ts rename to apps/console2/src/components/measures/__generated__/MeasureLinkDialogQuery.graphql.ts index cde38a9e4..430a51bc1 100644 --- a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogQuery.graphql.ts +++ b/apps/console2/src/components/measures/__generated__/MeasureLinkDialogQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<6a9b2c4428ed2255dda81771894c0c6c>> + * @generated SignedSource<<0b725ea8709fbf5da3f17545fcd2b8c4>> * @lightSyntaxTransform * @nogrep */ @@ -88,6 +88,13 @@ v4 = [ "name": "name", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + }, { "alias": null, "args": null, @@ -102,13 +109,6 @@ v4 = [ "name": "category", "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "state", - "storageKey": null - }, (v3/*: any*/) ], "storageKey": null @@ -244,7 +244,7 @@ return { ] }, "params": { - "cacheID": "13eb493b76ecc90bc9dd27963b5dba84", + "cacheID": "54fff8a0c07ebe02ef34f3bb35e7582e", "id": null, "metadata": { "connection": [ @@ -261,11 +261,11 @@ return { }, "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 description\n category\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" + "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 = "468d35ade53ae7655cd0fbab242b7d1a"; +(node as any).hash = "cf01fa3a766badd4f6150e1a544403ba"; export default node; diff --git a/apps/console2/src/components/risks/MeasureLinkDialog.tsx b/apps/console2/src/components/risks/MeasureLinkDialog.tsx index 2bd2efdcf..313f1e729 100644 --- a/apps/console2/src/components/risks/MeasureLinkDialog.tsx +++ b/apps/console2/src/components/risks/MeasureLinkDialog.tsx @@ -1,48 +1,4 @@ -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, useMutation } from "react-relay"; -import type { - MeasureLinkDialogQuery, - MeasureLinkDialogQuery$data, -} from "./__generated__/MeasureLinkDialogQuery.graphql"; -import type { NodeOf } from "/types"; - -const measuresQuery = graphql` - query MeasureLinkDialogQuery($organizationId: ID!) { - organization: node(id: $organizationId) { - id - ... on Organization { - measures(first: 100) @connection(key: "Organization__measures") { - edges { - node { - id - name - description - category - state - } - } - } - } - } - } -`; - +/* const attachMeasureMutation = graphql` mutation MeasureLinkDialogCreateMutation( $input: CreateRiskMeasureMappingInput! @@ -72,132 +28,4 @@ export const detachMeasureMutation = graphql` } } `; - -type Props = { - trigger: ReactNode; - organizationId: string; - connectionId: string; - riskId: string; - linkedIds?: Set; -}; - -export function MeasureLinkDialog({ trigger, ...props }: Props) { - const { __ } = useTranslate(); - - return ( - - -

- {__("Link or unlink measures to manage this risk.")} -

- }> - - -
- -
- ); -} - -function MeasureLinkDialogContent(props: Omit) { - const data = useLazyLoadQuery(measuresQuery, { - organizationId: props.organizationId, - }); - const { __ } = useTranslate(); - const [search, setSearch] = useState(""); - const [category, setCategory] = useState(null); - const measures = - data.organization?.measures?.edges?.map((edge) => edge.node) ?? []; - - const filteredMeasures = useMemo(() => { - return measures.filter( - (measure) => - (category === null || measure.category === category) && - (measure.name.toLowerCase().includes(search.toLowerCase()) || - measure.description?.toLowerCase().includes(search.toLowerCase())) - ); - }, [measures, search, category]); - - const categories = useMemo( - () => Array.from(new Set(measures.map((m) => m.category))), - [measures] - ); - - return ( - <> -
- - -
-
- {filteredMeasures.map((measure) => ( - - ))} -
- - ); -} - -type RowProps = { - measure: NodeOf< - Required["measures"] - >; -} & Omit; - -function MeasureRow(props: RowProps) { - const isLinked = props.linkedIds?.has(props.measure.id) ?? false; - const { __ } = useTranslate(); - const [attachMeasure, isFetchingAttach] = useMutation(attachMeasureMutation); - const [detachMeasure, isFetchingDetach] = useMutation(detachMeasureMutation); - - const isFetching = isFetchingAttach || isFetchingDetach; - - const onClick = () => { - if (isFetching) { - return; - } - const action = isLinked ? detachMeasure : attachMeasure; - action({ - variables: { - input: { - riskId: props.riskId, - measureId: props.measure.id, - }, - connections: [props.connectionId], - }, - }); - }; - - return ( -
- {props.measure.name} - {props.measure.category} - -
- ); -} +*/ diff --git a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogCreateMutation.graphql.ts b/apps/console2/src/components/risks/__generated__/MeasureLinkDialogCreateMutation.graphql.ts deleted file mode 100644 index 18c6385d0..000000000 --- a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogCreateMutation.graphql.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * @generated SignedSource<<043e8cc5beda1ac8949a7c02adcff4bf>> - * @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 CreateRiskMeasureMappingInput = { - measureId: string; - riskId: string; -}; -export type MeasureLinkDialogCreateMutation$variables = { - connections: ReadonlyArray; - input: CreateRiskMeasureMappingInput; -}; -export type MeasureLinkDialogCreateMutation$data = { - readonly createRiskMeasureMapping: { - readonly measureEdge: { - readonly node: { - readonly category: string; - readonly description: string; - readonly id: string; - readonly name: string; - readonly state: MeasureState; - }; - }; - }; -}; -export type MeasureLinkDialogCreateMutation = { - response: MeasureLinkDialogCreateMutation$data; - variables: MeasureLinkDialogCreateMutation$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, - "concreteType": "MeasureEdge", - "kind": "LinkedField", - "name": "measureEdge", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "Measure", - "kind": "LinkedField", - "name": "node", - "plural": false, - "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": "description", - "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 -}; -return { - "fragment": { - "argumentDefinitions": [ - (v0/*: any*/), - (v1/*: any*/) - ], - "kind": "Fragment", - "metadata": null, - "name": "MeasureLinkDialogCreateMutation", - "selections": [ - { - "alias": null, - "args": (v2/*: any*/), - "concreteType": "CreateRiskMeasureMappingPayload", - "kind": "LinkedField", - "name": "createRiskMeasureMapping", - "plural": false, - "selections": [ - (v3/*: any*/) - ], - "storageKey": null - } - ], - "type": "Mutation", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": [ - (v1/*: any*/), - (v0/*: any*/) - ], - "kind": "Operation", - "name": "MeasureLinkDialogCreateMutation", - "selections": [ - { - "alias": null, - "args": (v2/*: any*/), - "concreteType": "CreateRiskMeasureMappingPayload", - "kind": "LinkedField", - "name": "createRiskMeasureMapping", - "plural": false, - "selections": [ - (v3/*: any*/), - { - "alias": null, - "args": null, - "filters": null, - "handle": "prependEdge", - "key": "", - "kind": "LinkedHandle", - "name": "measureEdge", - "handleArgs": [ - { - "kind": "Variable", - "name": "connections", - "variableName": "connections" - } - ] - } - ], - "storageKey": null - } - ] - }, - "params": { - "cacheID": "309f3c90d91577e2f1c95ea69848ce5a", - "id": null, - "metadata": {}, - "name": "MeasureLinkDialogCreateMutation", - "operationKind": "mutation", - "text": "mutation MeasureLinkDialogCreateMutation(\n $input: CreateRiskMeasureMappingInput!\n) {\n createRiskMeasureMapping(input: $input) {\n measureEdge {\n node {\n id\n name\n description\n category\n state\n }\n }\n }\n}\n" - } -}; -})(); - -(node as any).hash = "bd012f06e39d5a04f4c1a2d2026f497e"; - -export default node; diff --git a/apps/console2/src/hooks/graph/FrameworkGraph.ts b/apps/console2/src/hooks/graph/FrameworkGraph.ts index df6feb670..c4c95c4f8 100644 --- a/apps/console2/src/hooks/graph/FrameworkGraph.ts +++ b/apps/console2/src/hooks/graph/FrameworkGraph.ts @@ -70,3 +70,15 @@ export const useDeleteFrameworkMutation = ( ); }, [framework, connectionId, commitDelete]); }; + +export const frameworkNodeQuery = graphql` + query FrameworkGraphNodeQuery($frameworkId: ID!) { + node(id: $frameworkId) { + ... on Framework { + id + name + ...FrameworkDetailPageFragment + } + } + } +`; diff --git a/apps/console2/src/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql.ts new file mode 100644 index 000000000..42bcbaad4 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql.ts @@ -0,0 +1,407 @@ +/** + * @generated SignedSource<<64acca659bff60ca637937d933fc839f>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type FrameworkGraphNodeQuery$variables = { + frameworkId: string; +}; +export type FrameworkGraphNodeQuery$data = { + readonly node: { + readonly id?: string; + readonly name?: string; + readonly " $fragmentSpreads": FragmentRefs<"FrameworkDetailPageFragment">; + }; +}; +export type FrameworkGraphNodeQuery = { + response: FrameworkGraphNodeQuery$data; + variables: FrameworkGraphNodeQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "frameworkId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "frameworkId" + } +], +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": "__typename", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null +}, +v6 = [ + { + "kind": "Literal", + "name": "first", + "value": 100 + } +], +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null +}, +v9 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FrameworkGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "FrameworkDetailPageFragment" + } + ], + "type": "Framework", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FrameworkGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v4/*: any*/), + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + (v5/*: any*/), + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": "ControlConnection", + "kind": "LinkedField", + "name": "controls", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ControlEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Control", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "referenceId", + "storageKey": null + }, + (v3/*: any*/), + (v5/*: any*/), + { + "alias": null, + "args": (v6/*: 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*/), + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v8/*: any*/), + (v9/*: any*/) + ], + "storageKey": "measures(first:100)" + }, + { + "alias": null, + "args": (v6/*: any*/), + "filters": null, + "handle": "connection", + "key": "FrameworkDetailPage_measures", + "kind": "LinkedHandle", + "name": "measures" + }, + { + "alias": null, + "args": (v6/*: 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": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "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": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "versions(first:1)" + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v7/*: any*/) + ], + "storageKey": null + }, + (v8/*: any*/), + (v9/*: any*/) + ], + "storageKey": "documents(first:100)" + }, + { + "alias": null, + "args": (v6/*: any*/), + "filters": null, + "handle": "connection", + "key": "FrameworkDetailPage_documents", + "kind": "LinkedHandle", + "name": "documents" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "controls(first:100)" + } + ], + "type": "Framework", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "b10b304da4ff5b00e15b881ee486e8fe", + "id": null, + "metadata": {}, + "name": "FrameworkGraphNodeQuery", + "operationKind": "query", + "text": "query FrameworkGraphNodeQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n ... on Framework {\n id\n name\n ...FrameworkDetailPageFragment\n }\n id\n }\n}\n\nfragment FrameworkDetailPageFragment on Framework {\n id\n name\n description\n controls(first: 100) {\n edges {\n node {\n id\n referenceId\n name\n description\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 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 }\n }\n}\n\nfragment LinkedDocumentsCardFragment on Document {\n id\n title\n createdAt\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" + } +}; +})(); + +(node as any).hash = "69a32dc8fbcaefbbaeb02dd2b89752c4"; + +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 667a1bc09..8a9d79f2e 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<<18341e1d2a7f480ad73ef32506121c5e>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -108,14 +108,7 @@ v8 = { "name": "__typename", "storageKey": null }, -v9 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "category", - "storageKey": null -}, -v10 = [ +v9 = [ { "kind": "Literal", "name": "first", @@ -195,7 +188,13 @@ return { (v4/*: any*/), (v6/*: any*/), (v7/*: any*/), - (v9/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null + }, { "alias": null, "args": null, @@ -240,7 +239,7 @@ return { }, { "alias": null, - "args": (v10/*: any*/), + "args": (v9/*: any*/), "concreteType": "MeasureConnection", "kind": "LinkedField", "name": "measures", @@ -264,15 +263,6 @@ return { "selections": [ (v5/*: any*/), (v2/*: any*/), - (v3/*: any*/), - (v9/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, { "alias": null, "args": null, @@ -336,7 +326,7 @@ return { }, { "alias": null, - "args": (v10/*: any*/), + "args": (v9/*: any*/), "filters": null, "handle": "connection", "key": "Risk__measures", @@ -353,12 +343,12 @@ return { ] }, "params": { - "cacheID": "fc9da3522dfacf8b5f887a0ebbd3e21e", + "cacheID": "295ecc0e99f46b7790fe04db0deed1d8", "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 RiskMeasuresTabFragment on Risk {\n id\n measures(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n createdAt\n state\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 }\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" } }; })(); diff --git a/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx b/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx new file mode 100644 index 000000000..b916c3034 --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx @@ -0,0 +1,211 @@ +import { + useFragment, + useMutation, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; +import { usePageTitle } from "@probo/hooks"; +import { graphql } from "relay-runtime"; +import { ControlItem, PageHeader } from "@probo/ui"; +import { FrameworkLogo } from "/components/FrameworkLogo"; +import { frameworkNodeQuery } from "/hooks/graph/FrameworkGraph"; +import { useTranslate } from "@probo/i18n"; +import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard"; +import { useParams } from "react-router"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import type { FrameworkGraphNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql"; +import type { FrameworkDetailPageFragment$key } from "./__generated__/FrameworkDetailPageFragment.graphql"; +import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard"; + +const frameworkDetailFragment = graphql` + fragment FrameworkDetailPageFragment on Framework { + id + name + description + controls(first: 100) { + edges { + node { + id + referenceId + name + description + measures(first: 100) + @connection(key: "FrameworkDetailPage_measures") { + __id + edges { + node { + id + ...LinkedMeasuresCardFragment + } + } + } + documents(first: 100) + @connection(key: "FrameworkDetailPage_documents") { + __id + edges { + node { + id + ...LinkedDocumentsCardFragment + } + } + } + } + } + } + } +`; + +const attachMeasureMutation = graphql` + mutation FrameworkDetailPageAttachMutation( + $input: CreateControlMeasureMappingInput! + $connections: [ID!]! + ) { + createControlMeasureMapping(input: $input) { + measureEdge @prependEdge(connections: $connections) { + node { + id + ...LinkedMeasuresCardFragment + } + } + } + } +`; + +const detachMeasureMutation = graphql` + mutation FrameworkDetailPageDetachMutation( + $input: DeleteControlMeasureMappingInput! + $connections: [ID!]! + ) { + deleteControlMeasureMapping(input: $input) { + deletedMeasureId @deleteEdge(connections: $connections) + } + } +`; + +const attachDocumentMutation = graphql` + mutation FrameworkDetailPageAttachDocumentMutation( + $input: CreateControlDocumentMappingInput! + $connections: [ID!]! + ) { + createControlDocumentMapping(input: $input) { + documentEdge @prependEdge(connections: $connections) { + node { + id + ...LinkedDocumentsCardFragment + } + } + } + } +`; + +const detachDocumentMutation = graphql` + mutation FrameworkDetailPageDetachDocumentMutation( + $input: DeleteControlDocumentMappingInput! + $connections: [ID!]! + ) { + deleteControlDocumentMapping(input: $input) { + deletedDocumentId @deleteEdge(connections: $connections) + } + } +`; + +type Props = { + queryRef: PreloadedQuery; +}; + +export default function FrameworkDetailPage(props: Props) { + const { __ } = useTranslate(); + const { controlId } = useParams<{ controlId?: string }>(); + const organizationId = useOrganizationId(); + const data = usePreloadedQuery(frameworkNodeQuery, props.queryRef); + const framework = useFragment( + frameworkDetailFragment, + data.node + ); + + const [detachMeasure, isDetachingMeasure] = useMutation( + detachMeasureMutation + ); + const [attachMeasure, isAttachingMeasure] = useMutation( + attachMeasureMutation + ); + const [detachDocument, isDetachingDocument] = useMutation( + detachDocumentMutation + ); + const [attachDocument, isAttachingDocument] = useMutation( + attachDocumentMutation + ); + + const controls = framework.controls.edges.map((edge) => edge.node); + + const selectedControl = controlId + ? controls.find((control) => control.id === controlId) + : controls[0]; + usePageTitle(`${framework.name} | ${selectedControl?.referenceId}`); + + return ( +
+ + + {framework.name} + + } + /> +
+ {__("Requirement categories")} +
+
+
+ {controls.map((control) => ( + + ))} +
+ {selectedControl ? ( +
+
+ {selectedControl.referenceId} +
+
{selectedControl.name}
+ edge.node) ?? [] + } + params={{ controlId: selectedControl.id }} + connectionId={selectedControl.measures.__id!} + onAttach={attachMeasure} + onDetach={detachMeasure} + disabled={isAttachingMeasure || isDetachingMeasure} + /> + edge.node) ?? [] + } + params={{ controlId: selectedControl.id }} + connectionId={selectedControl.documents.__id!} + onAttach={attachDocument} + onDetach={detachDocument} + disabled={isAttachingDocument || isDetachingDocument} + /> +
+ ) : ( +
+
+ {__("No control selected")} +
+
+ )} +
+
+ ); +} diff --git a/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx b/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx index 9ce5690c0..47bac620e 100644 --- a/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx +++ b/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx @@ -2,7 +2,6 @@ import { usePageTitle } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; import { ActionDropdown, - Avatar, Card, DropdownItem, FileButton, @@ -27,8 +26,8 @@ import { Link } from "react-router"; import type { FrameworksPageCardFragment$key } from "./__generated__/FrameworksPageCardFragment.graphql"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { useState, type ChangeEventHandler } from "react"; -import { availableFrameworks } from "@probo/helpers"; import { CreateFrameworkDialog } from "./dialogs/CreateFrameworkDialog"; +import { FrameworkLogo } from "/components/FrameworkLogo"; type Props = { queryRef: PreloadedQuery; @@ -50,10 +49,6 @@ const importFrameworkMutation = graphql` } `; -const availableLogos = new Map( - availableFrameworks.map((framework) => [framework.name, framework.logo]) -); - export default function FrameworksPage(props: Props) { const { __ } = useTranslate(); usePageTitle(__("Frameworks")); @@ -182,15 +177,10 @@ function FrameworkCard(props: FrameworkCardProps) { props.connectionId ); const { __ } = useTranslate(); - const logo = availableLogos.get(framework.name); return (
- {logo ? ( - - ) : ( - - )} + > + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type CreateControlDocumentMappingInput = { + controlId: string; + documentId: string; +}; +export type FrameworkDetailPageAttachDocumentMutation$variables = { + connections: ReadonlyArray; + input: CreateControlDocumentMappingInput; +}; +export type FrameworkDetailPageAttachDocumentMutation$data = { + readonly createControlDocumentMapping: { + readonly documentEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedDocumentsCardFragment">; + }; + }; + }; +}; +export type FrameworkDetailPageAttachDocumentMutation = { + response: FrameworkDetailPageAttachDocumentMutation$data; + variables: FrameworkDetailPageAttachDocumentMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "FrameworkDetailPageAttachDocumentMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlDocumentMappingPayload", + "kind": "LinkedField", + "name": "createControlDocumentMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentEdge", + "kind": "LinkedField", + "name": "documentEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Document", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedDocumentsCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "FrameworkDetailPageAttachDocumentMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlDocumentMappingPayload", + "kind": "LinkedField", + "name": "createControlDocumentMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "DocumentEdge", + "kind": "LinkedField", + "name": "documentEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Document", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "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": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "status", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "versions(first:1)" + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "documentEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "9100dabaa4861920eab52dc47e551b06", + "id": null, + "metadata": {}, + "name": "FrameworkDetailPageAttachDocumentMutation", + "operationKind": "mutation", + "text": "mutation FrameworkDetailPageAttachDocumentMutation(\n $input: CreateControlDocumentMappingInput!\n) {\n createControlDocumentMapping(input: $input) {\n documentEdge {\n node {\n id\n ...LinkedDocumentsCardFragment\n }\n }\n }\n}\n\nfragment LinkedDocumentsCardFragment on Document {\n id\n title\n createdAt\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "26c53db439feba2b2d65c3e3a9a973c7"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageAttachMutation.graphql.ts b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageAttachMutation.graphql.ts new file mode 100644 index 000000000..eedb63fd4 --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageAttachMutation.graphql.ts @@ -0,0 +1,202 @@ +/** + * @generated SignedSource<<81d008fc4116a78a92e2d925f8d6daa7>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type CreateControlMeasureMappingInput = { + controlId: string; + measureId: string; +}; +export type FrameworkDetailPageAttachMutation$variables = { + connections: ReadonlyArray; + input: CreateControlMeasureMappingInput; +}; +export type FrameworkDetailPageAttachMutation$data = { + readonly createControlMeasureMapping: { + readonly measureEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresCardFragment">; + }; + }; + }; +}; +export type FrameworkDetailPageAttachMutation = { + response: FrameworkDetailPageAttachMutation$data; + variables: FrameworkDetailPageAttachMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "FrameworkDetailPageAttachMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlMeasureMappingPayload", + "kind": "LinkedField", + "name": "createControlMeasureMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MeasureEdge", + "kind": "LinkedField", + "name": "measureEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedMeasuresCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "FrameworkDetailPageAttachMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlMeasureMappingPayload", + "kind": "LinkedField", + "name": "createControlMeasureMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MeasureEdge", + "kind": "LinkedField", + "name": "measureEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "measureEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "9fcb599cd78f3108d6d71aa571d7005d", + "id": null, + "metadata": {}, + "name": "FrameworkDetailPageAttachMutation", + "operationKind": "mutation", + "text": "mutation FrameworkDetailPageAttachMutation(\n $input: CreateControlMeasureMappingInput!\n) {\n createControlMeasureMapping(input: $input) {\n measureEdge {\n node {\n id\n ...LinkedMeasuresCardFragment\n }\n }\n }\n}\n\nfragment LinkedMeasuresCardFragment on Measure {\n id\n name\n state\n}\n" + } +}; +})(); + +(node as any).hash = "d3834b8c2656d347e13189469894be61"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageDetachDocumentMutation.graphql.ts b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageDetachDocumentMutation.graphql.ts new file mode 100644 index 000000000..8381ead9f --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageDetachDocumentMutation.graphql.ts @@ -0,0 +1,133 @@ +/** + * @generated SignedSource<<6f2850042c0413963bd82f90236fe220>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteControlDocumentMappingInput = { + controlId: string; + documentId: string; +}; +export type FrameworkDetailPageDetachDocumentMutation$variables = { + connections: ReadonlyArray; + input: DeleteControlDocumentMappingInput; +}; +export type FrameworkDetailPageDetachDocumentMutation$data = { + readonly deleteControlDocumentMapping: { + readonly deletedDocumentId: string; + }; +}; +export type FrameworkDetailPageDetachDocumentMutation = { + response: FrameworkDetailPageDetachDocumentMutation$data; + variables: FrameworkDetailPageDetachDocumentMutation$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": "deletedDocumentId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "FrameworkDetailPageDetachDocumentMutation", + "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": "FrameworkDetailPageDetachDocumentMutation", + "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": "deletedDocumentId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "53618baa1678e3a1b496a08597bdd364", + "id": null, + "metadata": {}, + "name": "FrameworkDetailPageDetachDocumentMutation", + "operationKind": "mutation", + "text": "mutation FrameworkDetailPageDetachDocumentMutation(\n $input: DeleteControlDocumentMappingInput!\n) {\n deleteControlDocumentMapping(input: $input) {\n deletedDocumentId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "233b182104744a67a21d21d60b6df67a"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageDetachMutation.graphql.ts b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageDetachMutation.graphql.ts new file mode 100644 index 000000000..953e08a4e --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageDetachMutation.graphql.ts @@ -0,0 +1,133 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteControlMeasureMappingInput = { + controlId: string; + measureId: string; +}; +export type FrameworkDetailPageDetachMutation$variables = { + connections: ReadonlyArray; + input: DeleteControlMeasureMappingInput; +}; +export type FrameworkDetailPageDetachMutation$data = { + readonly deleteControlMeasureMapping: { + readonly deletedMeasureId: string; + }; +}; +export type FrameworkDetailPageDetachMutation = { + response: FrameworkDetailPageDetachMutation$data; + variables: FrameworkDetailPageDetachMutation$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": "deletedMeasureId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "FrameworkDetailPageDetachMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteControlMeasureMappingPayload", + "kind": "LinkedField", + "name": "deleteControlMeasureMapping", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "FrameworkDetailPageDetachMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteControlMeasureMappingPayload", + "kind": "LinkedField", + "name": "deleteControlMeasureMapping", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedMeasureId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "0bede5dc29caa4e2aba9b0dd96e58014", + "id": null, + "metadata": {}, + "name": "FrameworkDetailPageDetachMutation", + "operationKind": "mutation", + "text": "mutation FrameworkDetailPageDetachMutation(\n $input: DeleteControlMeasureMappingInput!\n) {\n deleteControlMeasureMapping(input: $input) {\n deletedMeasureId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "96945bf8e909e7425af2297f7ee6ea0a"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageFragment.graphql.ts b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageFragment.graphql.ts new file mode 100644 index 000000000..d2a2dc76a --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworkDetailPageFragment.graphql.ts @@ -0,0 +1,288 @@ +/** + * @generated SignedSource<<15cc2290d512a0cdde5241edd045d3f5>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type FrameworkDetailPageFragment$data = { + readonly controls: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly description: string; + readonly documents: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedDocumentsCardFragment">; + }; + }>; + }; + readonly id: string; + readonly measures: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresCardFragment">; + }; + }>; + }; + readonly name: string; + readonly referenceId: string; + }; + }>; + }; + readonly description: string; + readonly id: string; + readonly name: string; + readonly " $fragmentType": "FrameworkDetailPageFragment"; +}; +export type FrameworkDetailPageFragment$key = { + readonly " $data"?: FrameworkDetailPageFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"FrameworkDetailPageFragment">; +}; + +const node: ReaderFragment = (function(){ +var v0 = { + "count": null, + "cursor": null, + "direction": "forward", + "path": null +}, +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v6 = { + "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 +}, +v7 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}; +return { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "connection": [ + (v0/*: any*/), + (v0/*: any*/) + ] + }, + "name": "FrameworkDetailPageFragment", + "selections": [ + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "first", + "value": 100 + } + ], + "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": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "referenceId", + "storageKey": null + }, + (v2/*: any*/), + (v3/*: any*/), + { + "alias": "measures", + "args": null, + "concreteType": "MeasureConnection", + "kind": "LinkedField", + "name": "__FrameworkDetailPage_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*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedMeasuresCardFragment" + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v5/*: any*/) + ], + "storageKey": null + }, + (v6/*: any*/), + (v7/*: any*/) + ], + "storageKey": null + }, + { + "alias": "documents", + "args": null, + "concreteType": "DocumentConnection", + "kind": "LinkedField", + "name": "__FrameworkDetailPage_documents_connection", + "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": [ + (v1/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedDocumentsCardFragment" + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v5/*: any*/) + ], + "storageKey": null + }, + (v6/*: any*/), + (v7/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "controls(first:100)" + } + ], + "type": "Framework", + "abstractKey": null +}; +})(); + +(node as any).hash = "15b523ca06d1a1e1a80379f6d51b92e2"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx b/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx index ea0100fc7..d39ab1bde 100644 --- a/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx +++ b/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx @@ -67,7 +67,6 @@ export function CreateFrameworkDialog(props: Props) { return ( } > diff --git a/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx b/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx index f425821e3..ce81cc4ec 100644 --- a/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx +++ b/apps/console2/src/pages/organizations/risks/tabs/RiskMeasuresTab.tsx @@ -1,28 +1,7 @@ import { graphql, useFragment, useMutation } from "react-relay"; -import type { - RiskMeasuresTabFragment$data, - RiskMeasuresTabFragment$key, -} from "./__generated__/RiskMeasuresTabFragment.graphql"; -import { useTranslate } from "@probo/i18n"; -import { - Button, - IconPlusLarge, - Table, - Thead, - Tbody, - Td, - Th, - Tr, - IconTrashCan, -} from "@probo/ui"; -import { - detachMeasureMutation, - MeasureLinkDialog, -} from "/components/risks/MeasureLinkDialog"; -import { useOrganizationId } from "/hooks/useOrganizationId"; +import type { RiskMeasuresTabFragment$key } from "./__generated__/RiskMeasuresTabFragment.graphql"; import { useOutletContext } from "react-router"; -import type { NodeOf } from "/types"; -import { useMemo } from "react"; +import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard"; const measuresFragment = graphql` fragment RiskMeasuresTabFragment on Risk { @@ -32,127 +11,60 @@ const measuresFragment = graphql` edges { node { id - name - description - category - createdAt - state + ...LinkedMeasuresCardFragment } } } } `; +const attachMeasureMutation = graphql` + mutation RiskMeasuresTabCreateMutation( + $input: CreateRiskMeasureMappingInput! + $connections: [ID!]! + ) { + createRiskMeasureMapping(input: $input) { + measureEdge @prependEdge(connections: $connections) { + node { + id + ...LinkedMeasuresCardFragment + } + } + } + } +`; + +export const detachMeasureMutation = graphql` + mutation RiskMeasuresTabDetachMutation( + $input: DeleteRiskMeasureMappingInput! + $connections: [ID!]! + ) { + deleteRiskMeasureMapping(input: $input) { + deletedMeasureId @deleteEdge(connections: $connections) + } + } +`; + export default function RiskMeasuresTab() { const { risk } = useOutletContext<{ risk: RiskMeasuresTabFragment$key & { id: string }; }>(); const data = useFragment(measuresFragment, risk); - const organizationId = useOrganizationId(); - const { __ } = useTranslate(); const connectionId = data.measures.__id; const measures = data.measures?.edges?.map((edge) => edge.node) ?? []; - const linkedIds = useMemo( - () => new Set(measures.map((measure) => measure.id)), - [measures] - ); - if (measures.length === 0) { - return ( -
- {__("No measures associated with this risk.")} - - {__("Link measure")} - - } - /> -
- ); - } + const [detachMeasure, isDetaching] = useMutation(detachMeasureMutation); + const [attachMeasure, isAttaching] = useMutation(attachMeasureMutation); + const isLoading = isDetaching || isAttaching; return ( -
- - {__("Link measure")} - - } - /> - - - - - - - - - {measures.map((measure) => ( - - ))} - -
{__("Measure")}
-
- ); -} - -function MeasureRow({ - measure, - riskId, - connectionId, -}: { - measure: NodeOf; - riskId: string; - connectionId: string; -}) { - const { __ } = useTranslate(); - - const [detachMeasure, isFetching] = useMutation(detachMeasureMutation); - - const onClick = () => { - detachMeasure({ - variables: { - input: { - riskId: riskId, - measureId: measure.id, - }, - connections: [connectionId], - }, - }); - }; - return ( - - {measure.name} - - - - + ); } diff --git a/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabCreateMutation.graphql.ts b/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabCreateMutation.graphql.ts new file mode 100644 index 000000000..90fb0a82d --- /dev/null +++ b/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabCreateMutation.graphql.ts @@ -0,0 +1,202 @@ +/** + * @generated SignedSource<<6e27a069ae5675b9c83e369566a91d01>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type CreateRiskMeasureMappingInput = { + measureId: string; + riskId: string; +}; +export type RiskMeasuresTabCreateMutation$variables = { + connections: ReadonlyArray; + input: CreateRiskMeasureMappingInput; +}; +export type RiskMeasuresTabCreateMutation$data = { + readonly createRiskMeasureMapping: { + readonly measureEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresCardFragment">; + }; + }; + }; +}; +export type RiskMeasuresTabCreateMutation = { + response: RiskMeasuresTabCreateMutation$data; + variables: RiskMeasuresTabCreateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "RiskMeasuresTabCreateMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateRiskMeasureMappingPayload", + "kind": "LinkedField", + "name": "createRiskMeasureMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MeasureEdge", + "kind": "LinkedField", + "name": "measureEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedMeasuresCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "RiskMeasuresTabCreateMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateRiskMeasureMappingPayload", + "kind": "LinkedField", + "name": "createRiskMeasureMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MeasureEdge", + "kind": "LinkedField", + "name": "measureEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Measure", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "state", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "measureEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "3b141d4117959b1f6efed67e8aa655cf", + "id": null, + "metadata": {}, + "name": "RiskMeasuresTabCreateMutation", + "operationKind": "mutation", + "text": "mutation RiskMeasuresTabCreateMutation(\n $input: CreateRiskMeasureMappingInput!\n) {\n createRiskMeasureMapping(input: $input) {\n measureEdge {\n node {\n id\n ...LinkedMeasuresCardFragment\n }\n }\n }\n}\n\nfragment LinkedMeasuresCardFragment on Measure {\n id\n name\n state\n}\n" + } +}; +})(); + +(node as any).hash = "982d31266eda233518c48f1fd0cd2f46"; + +export default node; diff --git a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogDetachMutation.graphql.ts b/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabDetachMutation.graphql.ts similarity index 76% rename from apps/console2/src/components/risks/__generated__/MeasureLinkDialogDetachMutation.graphql.ts rename to apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabDetachMutation.graphql.ts index 3759cbd47..eecc9b4d3 100644 --- a/apps/console2/src/components/risks/__generated__/MeasureLinkDialogDetachMutation.graphql.ts +++ b/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabDetachMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<327cdb825c0029c21d8812b782ecf14f>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -13,18 +13,18 @@ export type DeleteRiskMeasureMappingInput = { measureId: string; riskId: string; }; -export type MeasureLinkDialogDetachMutation$variables = { +export type RiskMeasuresTabDetachMutation$variables = { connections: ReadonlyArray; input: DeleteRiskMeasureMappingInput; }; -export type MeasureLinkDialogDetachMutation$data = { +export type RiskMeasuresTabDetachMutation$data = { readonly deleteRiskMeasureMapping: { readonly deletedMeasureId: string; }; }; -export type MeasureLinkDialogDetachMutation = { - response: MeasureLinkDialogDetachMutation$data; - variables: MeasureLinkDialogDetachMutation$variables; +export type RiskMeasuresTabDetachMutation = { + response: RiskMeasuresTabDetachMutation$data; + variables: RiskMeasuresTabDetachMutation$variables; }; const node: ConcreteRequest = (function(){ @@ -60,7 +60,7 @@ return { ], "kind": "Fragment", "metadata": null, - "name": "MeasureLinkDialogDetachMutation", + "name": "RiskMeasuresTabDetachMutation", "selections": [ { "alias": null, @@ -85,7 +85,7 @@ return { (v0/*: any*/) ], "kind": "Operation", - "name": "MeasureLinkDialogDetachMutation", + "name": "RiskMeasuresTabDetachMutation", "selections": [ { "alias": null, @@ -118,16 +118,16 @@ return { ] }, "params": { - "cacheID": "8a54f580a892f60f24c65ec927c7340c", + "cacheID": "81260f49b2f31c4a12f87b38bab7a683", "id": null, "metadata": {}, - "name": "MeasureLinkDialogDetachMutation", + "name": "RiskMeasuresTabDetachMutation", "operationKind": "mutation", - "text": "mutation MeasureLinkDialogDetachMutation(\n $input: DeleteRiskMeasureMappingInput!\n) {\n deleteRiskMeasureMapping(input: $input) {\n deletedMeasureId\n }\n}\n" + "text": "mutation RiskMeasuresTabDetachMutation(\n $input: DeleteRiskMeasureMappingInput!\n) {\n deleteRiskMeasureMapping(input: $input) {\n deletedMeasureId\n }\n}\n" } }; })(); -(node as any).hash = "e02a17e00e2c07881dcae17fef9513eb"; +(node as any).hash = "c5adc6d900f0da15ec3acf7db304d410"; export default node; diff --git a/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabFragment.graphql.ts b/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabFragment.graphql.ts index 3d696ed04..847264c8c 100644 --- a/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabFragment.graphql.ts +++ b/apps/console2/src/pages/organizations/risks/tabs/__generated__/RiskMeasuresTabFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<62ef6617862f528f26edaffe3adc3069>> + * @generated SignedSource<<5a37b1ce3c0c17ce36fc3e1157eadf8e>> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,6 @@ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; -export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED"; import { FragmentRefs } from "relay-runtime"; export type RiskMeasuresTabFragment$data = { readonly id: string; @@ -17,12 +16,8 @@ export type RiskMeasuresTabFragment$data = { readonly __id: string; readonly edges: ReadonlyArray<{ readonly node: { - readonly category: string; - readonly createdAt: any; - readonly description: string; readonly id: string; - readonly name: string; - readonly state: MeasureState; + readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresCardFragment">; }; }>; }; @@ -85,39 +80,9 @@ return { "selections": [ (v0/*: any*/), { - "alias": null, "args": null, - "kind": "ScalarField", - "name": "name", - "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": "createdAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "state", - "storageKey": null + "kind": "FragmentSpread", + "name": "LinkedMeasuresCardFragment" }, { "alias": null, @@ -185,6 +150,6 @@ return { }; })(); -(node as any).hash = "e04c5962a40b48aade0d1e4a31f0c583"; +(node as any).hash = "e928c83f7fea74daa5663d6f80518419"; export default node; diff --git a/apps/console2/src/routes/frameworkRoutes.ts b/apps/console2/src/routes/frameworkRoutes.ts index 4062c8583..26cd18b11 100644 --- a/apps/console2/src/routes/frameworkRoutes.ts +++ b/apps/console2/src/routes/frameworkRoutes.ts @@ -1,7 +1,10 @@ import { loadQuery } from "react-relay"; import { PageSkeleton } from "/components/skeletons/PageSkeleton"; import { relayEnvironment } from "/providers/RelayProviders"; -import { frameworksQuery } from "/hooks/graph/FrameworkGraph"; +import { + frameworksQuery, + frameworkNodeQuery, +} from "/hooks/graph/FrameworkGraph"; import type { AppRoute } from "/routes"; import { lazy } from "react"; @@ -15,4 +18,13 @@ export const frameworkRoutes = [ () => import("/pages/organizations/frameworks/FrameworksPage") ), }, + { + path: "frameworks/:frameworkId/:controlId?", + fallback: PageSkeleton, + queryLoader: ({ frameworkId }) => + loadQuery(relayEnvironment, frameworkNodeQuery, { frameworkId }), + Component: lazy( + () => import("/pages/organizations/frameworks/FrameworkDetailPage") + ), + }, ] satisfies AppRoute[]; diff --git a/packages/ui/src/Atoms/ControlItem/ControlItem.stories.tsx b/packages/ui/src/Atoms/ControlItem/ControlItem.stories.tsx new file mode 100644 index 000000000..0a8ab1f2b --- /dev/null +++ b/packages/ui/src/Atoms/ControlItem/ControlItem.stories.tsx @@ -0,0 +1,27 @@ +import { ControlItem } from "./ControlItem"; +import type { Meta, StoryObj } from "@storybook/react"; + +export default { + title: "Atoms/ControlItem", + component: ControlItem, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + id: "CC1.1", + description: + "The entity obtains privacy commitments from vendors and other third parties who have access to personal information to meet the entity’s objectives related to privacy. The entity assesses those parties’ compliance on a periodic and as-needed basis and takes corrective action, if necessary.", + }, + render: (args) => ( +
+ + + + + +
+ ), +}; diff --git a/packages/ui/src/Atoms/ControlItem/ControlItem.tsx b/packages/ui/src/Atoms/ControlItem/ControlItem.tsx new file mode 100644 index 000000000..06e9ad7c4 --- /dev/null +++ b/packages/ui/src/Atoms/ControlItem/ControlItem.tsx @@ -0,0 +1,49 @@ +import type { HTMLAttributes } from "react"; +import { Link } from "react-router"; +import { tv } from "tailwind-variants"; + +type Props = { + active?: boolean; + id: string; + description?: string; + to: string; +} & HTMLAttributes; + +const classNames = tv({ + slots: { + wrapper: "block p-4 space-y-[6px] rounded-xl cursor-pointer text-start", + id: "px-[6px] py-[2px] text-base font-medium border border-border-low rounded-lg w-max", + description: "text-sm text-txt-tertiary line-clamp-3", + }, + variants: { + active: { + true: { + wrapper: "bg-tertiary-pressed", + id: "bg-active", + }, + false: { + wrapper: "hover:bg-tertiary-hover", + id: "bg-highlight", + }, + }, + }, + defaultVariants: { + active: false, + }, +}); + +export function ControlItem({ active, id, description, to, ...props }: Props) { + const { + wrapper, + id: idCls, + description: descriptionCls, + } = classNames({ + active, + }); + return ( + +
{id}
+
{description}
+ + ); +} diff --git a/packages/ui/src/Atoms/Markdown/Markdown.stories.tsx b/packages/ui/src/Atoms/Markdown/Markdown.stories.tsx index b76d8be8c..21b011bec 100644 --- a/packages/ui/src/Atoms/Markdown/Markdown.stories.tsx +++ b/packages/ui/src/Atoms/Markdown/Markdown.stories.tsx @@ -11,7 +11,7 @@ type Story = StoryObj; export const Default: Story = { args: { - children: `# Siquis facies post + content: `# Siquis facies post ## Venit Ianigenam egressus a tamen terra frater diff --git a/packages/ui/src/Atoms/PropertyRow/PropertyRow.stories.tsx b/packages/ui/src/Atoms/PropertyRow/PropertyRow.stories.tsx index f38ed29f8..ce81de06c 100644 --- a/packages/ui/src/Atoms/PropertyRow/PropertyRow.stories.tsx +++ b/packages/ui/src/Atoms/PropertyRow/PropertyRow.stories.tsx @@ -18,13 +18,6 @@ export const Default: Story = { }, }; -export const Disabled: Story = { - args: { - ...Default.args, - disabled: true, - }, -}; - export const WithError: Story = { args: { ...Default.args, diff --git a/packages/ui/src/Molecules/Combobox/Combobox.stories.tsx b/packages/ui/src/Molecules/Combobox/Combobox.stories.tsx index 3dd1ae2b2..7e8daff43 100644 --- a/packages/ui/src/Molecules/Combobox/Combobox.stories.tsx +++ b/packages/ui/src/Molecules/Combobox/Combobox.stories.tsx @@ -12,9 +12,6 @@ export default { type Story = StoryObj; export const Default: Story = { - args: { - open: true, - }, render: () => { const [items, setItems] = useState(["a", "b", "c"] as string[]); const onSearch = (query: string) => { @@ -22,10 +19,6 @@ export const Default: Story = { }; return ( <> - Lorem ipsum dolor sit, amet consectetur adipisicing elit. - Nostrum labore repellat facere voluptatum, in voluptatem eaque - quidem nam quod nesciunt repudiandae a illo non placeat nulla. - Ratione ipsa at sint? {items.map((item) => ( {item} diff --git a/packages/ui/src/Molecules/Dialog/ConfirmDialog.stories.tsx b/packages/ui/src/Molecules/Dialog/ConfirmDialog.stories.tsx deleted file mode 100644 index 4defcf6ef..000000000 --- a/packages/ui/src/Molecules/Dialog/ConfirmDialog.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Button } from "../../Atoms/Button/Button"; -import { ConfirmDialog } from "./ConfirmDialog"; -import type { Meta, StoryObj } from "@storybook/react"; - -export default { - title: "Atoms/ConfirmDialog", - component: ConfirmDialog, - argTypes: {}, -} satisfies Meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - message: - 'This will permanently delete the risk "Demo". This action cannot be undone.', - }, - render() { - return ( - { - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1000); - }); - }} - > - - - ); - }, -}; diff --git a/packages/ui/src/Molecules/Dialog/Dialog.stories.tsx b/packages/ui/src/Molecules/Dialog/Dialog.stories.tsx index 1e442949c..26dbfa677 100644 --- a/packages/ui/src/Molecules/Dialog/Dialog.stories.tsx +++ b/packages/ui/src/Molecules/Dialog/Dialog.stories.tsx @@ -15,7 +15,6 @@ export const Default: Story = { return ( {}} trigger={} title="Edit profile" > diff --git a/packages/ui/src/Molecules/PageHeader/PageHeader.tsx b/packages/ui/src/Molecules/PageHeader/PageHeader.tsx index e940115e7..762f07b60 100644 --- a/packages/ui/src/Molecules/PageHeader/PageHeader.tsx +++ b/packages/ui/src/Molecules/PageHeader/PageHeader.tsx @@ -9,7 +9,9 @@ export function PageHeader({ title, description, children }: Props) { return (
-

{title}

+

+ {title} +

{description && (

{description}

)} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index d6c5d627a..a0d30a8dd 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -33,6 +33,7 @@ export { Table, Thead, Tr, Tbody, Td, Th } from "./Atoms/Table/Table"; export { Tabs, TabLink } from "./Atoms/Tabs/Tabs"; export { Markdown } from "./Atoms/Markdown/Markdown"; export { Dropzone } from "./Atoms/Dropzone/Dropzone"; +export { ControlItem } from "./Atoms/ControlItem/ControlItem"; // Molecules export {