Add measures evidences tab

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Jonathan
2025-06-09 12:14:56 +02:00
committed by Sacha Al Himdani
parent b660c0ce7f
commit 140857bfb3
92 changed files with 8427 additions and 865 deletions

View File

@@ -0,0 +1,35 @@
import { useRouteError } from "react-router";
import { IconPageCross } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
const classNames = {
wrapper: "py-10 text-center space-y-2 ",
title: "text-2xl flex gap-2 font-semibold items-center justify-center",
description: "text-base text-txt-tertiary",
};
export function PageError() {
const error = useRouteError();
const { __ } = useTranslate();
if (!error) {
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>
<IconPageCross size={26} />
{__("Page not found")}
</h1>
<p className={classNames.description}>
{__("The page you are looking for does not exist")}
</p>
</div>
);
}
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>{__("Unexpected error :(")}</h1>
<p className={classNames.description}>{error.toString()}</p>
</div>
);
}

View File

@@ -0,0 +1,123 @@
import { graphql } from "relay-runtime";
import {
Button,
Tr,
Td,
Table,
Thead,
Tbody,
Th,
IconTrashCan,
Badge,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { LinkedControlsCardFragment$key } from "./__generated__/LinkedControlsCardFragment.graphql";
import { useFragment } from "react-relay";
import { useOrganizationId } from "/hooks/useOrganizationId";
const linkedControlFragment = graphql`
fragment LinkedControlsCardFragment on Control {
id
name
referenceId
framework {
name
}
}
`;
type Mutation<Params> = (p: {
variables: {
input: {
controlId: string;
} & Params;
connections: string[];
};
}) => void;
type Props<Params> = {
// Controls linked to the element
controls: (LinkedControlsCardFragment$key & { id: string })[];
// Extra params to send to the mutation
params: Params;
// Disable (action when loading for instance)
disabled?: boolean;
// ID of the connection to update
connectionId: string;
// Mutation to detach a control (will receive {controlId, ...params})
onDetach: Mutation<Params>;
};
/**
* Reusable component that displays a list of linked controls
*/
export function LinkedControlsCard<Params>(props: Props<Params>) {
const { __ } = useTranslate();
const controls = props.controls;
const onDetach = (controlId: string) => {
props.onDetach({
variables: {
input: {
controlId,
...props.params,
},
connections: [props.connectionId],
},
});
};
return (
<Table>
<Thead>
<Tr>
<Th>{__("Reference")}</Th>
<Th>{__("Name")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{controls.length === 0 && (
<Tr>
<Td colSpan={4} className="text-center text-txt-secondary">
{__("No controls linked")}
</Td>
</Tr>
)}
{controls.map((control) => (
<ControlRow key={control.id} control={control} onClick={onDetach} />
))}
</Tbody>
</Table>
);
}
function ControlRow(props: {
control: LinkedControlsCardFragment$key & { id: string };
onClick: (controlId: string) => void;
}) {
const control = useFragment(linkedControlFragment, props.control);
const organizationId = useOrganizationId();
const { __ } = useTranslate();
return (
<Tr to={`/organizations/${organizationId}/controls/${control.id}`}>
<Td>
<span className="inline-flex gap-2 items-center">
{control.framework.name}{" "}
<Badge size="md">{control.referenceId}</Badge>
</span>
</Td>
<Td>{control.name}</Td>
<Td noLink width={50} className="text-end">
<Button
variant="secondary"
onClick={() => props.onClick(control.id)}
icon={IconTrashCan}
>
{__("Unlink")}
</Button>
</Td>
</Tr>
);
}

View File

@@ -0,0 +1,76 @@
/**
* @generated SignedSource<<c922ab2420ac172a8d9c18948fc4f006>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type LinkedControlsCardFragment$data = {
readonly framework: {
readonly name: string;
};
readonly id: string;
readonly name: string;
readonly referenceId: string;
readonly " $fragmentType": "LinkedControlsCardFragment";
};
export type LinkedControlsCardFragment$key = {
readonly " $data"?: LinkedControlsCardFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"LinkedControlsCardFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "LinkedControlsCardFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "referenceId",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "Framework",
"kind": "LinkedField",
"name": "framework",
"plural": false,
"selections": [
(v0/*: any*/)
],
"storageKey": null
}
],
"type": "Control",
"abstractKey": null
};
})();
(node as any).hash = "1b72ca232511c698e0cbdc84cbc81c02";
export default node;

View File

@@ -13,6 +13,7 @@ import {
IconTrashCan,
DocumentVersionBadge,
DocumentTypeBadge,
TrButton,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { LinkedDocumentsCardFragment$key } from "./__generated__/LinkedDocumentsCardFragment.graphql";
@@ -20,7 +21,8 @@ import { useFragment } from "react-relay";
import { useMemo, useState } from "react";
import { sprintf } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { DocumentLinkDialog } from "./DocumentLinkDialog";
import { LinkedDocumentDialog } from "./LinkedDocumentsDialog.tsx";
import clsx from "clsx";
const linkedDocumentFragment = graphql`
fragment LinkedDocumentsCardFragment on Document {
@@ -61,6 +63,7 @@ type Props<Params> = {
onAttach: Mutation<Params>;
// Mutation to detach a document (will receive {documentId, ...params})
onDetach: Mutation<Params>;
variant?: "card" | "table";
};
/**
@@ -73,6 +76,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
return limit ? props.documents.slice(0, limit) : props.documents;
}, [props.documents, limit]);
const showMoreButton = limit !== null && props.documents.length > limit;
const variant = props.variant ?? "table";
const onAttach = (documentId: string) => {
props.onAttach({
@@ -98,47 +102,65 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
});
};
const Wrapper = variant === "card" ? Card : "div";
return (
<Card padded className="space-y-[10px]">
<div className="flex justify-between">
<div className="text-lg font-semibold">{__("Documents")}</div>
<DocumentLinkDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedDocuments={props.documents}
onLink={onAttach}
onUnlink={onDetach}
>
<Button variant="tertiary" icon={IconPlusLarge}>
{__("Link document")}
</Button>
</DocumentLinkDialog>
</div>
{documents.length > 0 ? (
<Table className="bg-invert">
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("Type")}</Th>
<Th>{__("State")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{documents.map((document) => (
<DocumentRow
key={document.id}
document={document}
onClick={onDetach}
/>
))}
</Tbody>
</Table>
) : (
<div className="text-center text-sm text-txt-secondary">
{__("No documents linked")}
<Wrapper padded className="space-y-[10px]">
{variant === "card" && (
<div className="flex justify-between">
<div className="text-lg font-semibold">{__("Documents")}</div>
<LinkedDocumentDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedDocuments={props.documents}
onLink={onAttach}
onUnlink={onDetach}
>
<Button variant="tertiary" icon={IconPlusLarge}>
{__("Link document")}
</Button>
</LinkedDocumentDialog>
</div>
)}
<Table className={clsx(variant === "card" && "bg-invert")}>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("Type")}</Th>
<Th>{__("State")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{documents.length === 0 && (
<Tr>
<Td colSpan={4} className="text-center text-txt-secondary">
{__("No documents linked")}
</Td>
</Tr>
)}
{documents.map((document) => (
<DocumentRow
key={document.id}
document={document}
onClick={onDetach}
/>
))}
{variant === "table" && (
<LinkedDocumentDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedDocuments={props.documents}
onLink={onAttach}
onUnlink={onDetach}
>
<TrButton colspan={4} icon={IconPlusLarge}>
{__("Link document")}
</TrButton>
</LinkedDocumentDialog>
)}
</Tbody>
</Table>
{showMoreButton && (
<Button
variant="tertiary"
@@ -149,7 +171,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
{sprintf(__("Show %s more"), props.documents.length - limit)}
</Button>
)}
</Card>
</Wrapper>
);
}

View File

@@ -7,33 +7,55 @@ import {
IconMagnifyingGlass,
IconPlusLarge,
IconTrashCan,
InfiniteScrollTrigger,
Input,
Spinner,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { Suspense, useMemo, useState, type ReactNode } from "react";
import { graphql } from "relay-runtime";
import { useLazyLoadQuery } from "react-relay";
import type {
DocumentLinkDialogQuery,
DocumentLinkDialogQuery$data,
} from "./__generated__/DocumentLinkDialogQuery.graphql";
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
import type { LinkedDocumentsDialogQuery } from "./__generated__/LinkedDocumentsDialogQuery.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { NodeOf } from "/types";
import type {
LinkedDocumentsDialogFragment$data,
LinkedDocumentsDialogFragment$key,
} from "./__generated__/LinkedDocumentsDialogFragment.graphql";
const documentsQuery = graphql`
query DocumentLinkDialogQuery($organizationId: ID!) {
query LinkedDocumentsDialogQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
id
... on Organization {
documents(first: 100) @connection(key: "Organization__documents") {
edges {
node {
id
title
documentType
}
}
...LinkedDocumentsDialogFragment
}
}
}
`;
const documentsFragment = graphql`
fragment LinkedDocumentsDialogFragment on Organization
@refetchable(queryName: "LinkedDocumentsDialogQuery_fragment")
@argumentDefinitions(
first: { type: "Int", defaultValue: 20 }
order: { type: "DocumentOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
) {
documents(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
) @connection(key: "LinkedDocumentsDialogQuery_documents") {
edges {
node {
id
title
documentType
}
}
}
@@ -49,14 +71,14 @@ type Props = {
onUnlink: (documentId: string) => void;
};
export function DocumentLinkDialog({ children, ...props }: Props) {
export function LinkedDocumentDialog({ children, ...props }: Props) {
const { __ } = useTranslate();
return (
<Dialog trigger={children} title={__("Link documents")}>
<DialogContent>
<Suspense fallback={<Spinner centered />}>
<DocumentLinkDialogContent {...props} />
<LinkedDocumentsDialogContent {...props} />
</Suspense>
</DialogContent>
<DialogFooter exitLabel={__("Close")} />
@@ -64,15 +86,18 @@ export function DocumentLinkDialog({ children, ...props }: Props) {
);
}
function DocumentLinkDialogContent(props: Omit<Props, "children">) {
function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
const organizationId = useOrganizationId();
const data = useLazyLoadQuery<DocumentLinkDialogQuery>(documentsQuery, {
const query = useLazyLoadQuery<LinkedDocumentsDialogQuery>(documentsQuery, {
organizationId,
});
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
documentsFragment,
query.organization as LinkedDocumentsDialogFragment$key
);
const { __ } = useTranslate();
const [search, setSearch] = useState("");
const documents =
data.organization?.documents?.edges?.map((edge) => edge.node) ?? [];
const documents = data.documents?.edges?.map((edge) => edge.node) ?? [];
const linkedIds = useMemo(() => {
return new Set(props.linkedDocuments?.map((m) => m.id) ?? []);
}, [props.linkedDocuments]);
@@ -103,14 +128,18 @@ function DocumentLinkDialogContent(props: Omit<Props, "children">) {
disabled={props.disabled}
/>
))}
{hasNext && (
<InfiniteScrollTrigger
loading={isLoadingNext}
onView={() => loadNext(20)}
/>
)}
</div>
</>
);
}
type Document = NodeOf<
DocumentLinkDialogQuery$data["organization"]["documents"]
>;
type Document = NodeOf<LinkedDocumentsDialogFragment$data["documents"]>;
type RowProps = {
document: Document;
@@ -129,7 +158,7 @@ function DocumentRow(props: RowProps) {
return (
<button
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full"
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full h-[100px]"
onClick={() => onClick(props.document.id)}
>
{props.document.title}

View File

@@ -1,255 +0,0 @@
/**
* @generated SignedSource<<ec5910871c59f229db361be238007976>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DocumentType = "ISMS" | "OTHER" | "POLICY";
export type DocumentLinkDialogQuery$variables = {
organizationId: string;
};
export type DocumentLinkDialogQuery$data = {
readonly organization: {
readonly documents?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly documentType: DocumentType;
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
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"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": "0905fffe448540bf9c3d8abcbb5b7583",
"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 documentType\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "6994e268de74f2102f48dc3e8156a797";
export default node;

View File

@@ -0,0 +1,223 @@
/**
* @generated SignedSource<<7b0ec493ab146db602cae612158407cb>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type DocumentType = "ISMS" | "OTHER" | "POLICY";
import { FragmentRefs } from "relay-runtime";
export type LinkedDocumentsDialogFragment$data = {
readonly documents: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly documentType: DocumentType;
readonly id: string;
readonly title: string;
};
}>;
};
readonly id: string;
readonly " $fragmentType": "LinkedDocumentsDialogFragment";
};
export type LinkedDocumentsDialogFragment$key = {
readonly " $data"?: LinkedDocumentsDialogFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"LinkedDocumentsDialogFragment">;
};
import LinkedDocumentsDialogQuery_fragment_graphql from './LinkedDocumentsDialogQuery_fragment.graphql';
const node: ReaderFragment = (function(){
var v0 = [
"documents"
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
{
"defaultValue": 20,
"kind": "LocalArgument",
"name": "first"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "order"
}
],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "bidirectional",
"path": (v0/*: any*/)
}
],
"refetch": {
"connection": {
"forward": {
"count": "first",
"cursor": "after"
},
"backward": {
"count": "last",
"cursor": "before"
},
"path": (v0/*: any*/)
},
"fragmentPathInResult": [
"node"
],
"operation": LinkedDocumentsDialogQuery_fragment_graphql,
"identifierInfo": {
"identifierField": "id",
"identifierQueryVariableName": "id"
}
}
},
"name": "LinkedDocumentsDialogFragment",
"selections": [
{
"alias": "documents",
"args": [
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
}
],
"concreteType": "DocumentConnection",
"kind": "LinkedField",
"name": "__LinkedDocumentsDialogQuery_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*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
(v1/*: any*/)
],
"type": "Organization",
"abstractKey": null
};
})();
(node as any).hash = "392d33b379a58cb3da08ab783c1cda5d";
export default node;

View File

@@ -0,0 +1,245 @@
/**
* @generated SignedSource<<6316cd819a27efb5989750b5f8de0444>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type LinkedDocumentsDialogQuery$variables = {
organizationId: string;
};
export type LinkedDocumentsDialogQuery$data = {
readonly organization: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"LinkedDocumentsDialogFragment">;
};
};
export type LinkedDocumentsDialogQuery = {
response: LinkedDocumentsDialogQuery$data;
variables: LinkedDocumentsDialogQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v4 = [
{
"kind": "Literal",
"name": "first",
"value": 20
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "LinkedDocumentsDialogQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "LinkedDocumentsDialogFragment"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "LinkedDocumentsDialogQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v4/*: any*/),
"concreteType": "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": "documentType",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "documents(first:20)"
},
{
"alias": null,
"args": (v4/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "LinkedDocumentsDialogQuery_documents",
"kind": "LinkedHandle",
"name": "documents"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "fd4ab41131ead5f74dceb4610ef3f2c5",
"id": null,
"metadata": {},
"name": "LinkedDocumentsDialogQuery",
"operationKind": "query",
"text": "query LinkedDocumentsDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n ...LinkedDocumentsDialogFragment\n }\n }\n}\n\nfragment LinkedDocumentsDialogFragment on Organization {\n documents(first: 20) {\n edges {\n node {\n id\n title\n documentType\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "f97a3016285b1a39cc6925ac39d82acc";
export default node;

View File

@@ -0,0 +1,318 @@
/**
* @generated SignedSource<<29bf3465293095c44e826394c602cde1>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DocumentOrderField = "CREATED_AT" | "TITLE";
export type OrderDirection = "ASC" | "DESC";
export type DocumentOrder = {
direction: OrderDirection;
field: DocumentOrderField;
};
export type LinkedDocumentsDialogQuery_fragment$variables = {
after?: any | null | undefined;
before?: any | null | undefined;
first?: number | null | undefined;
id: string;
last?: number | null | undefined;
order?: DocumentOrder | null | undefined;
};
export type LinkedDocumentsDialogQuery_fragment$data = {
readonly node: {
readonly " $fragmentSpreads": FragmentRefs<"LinkedDocumentsDialogFragment">;
};
};
export type LinkedDocumentsDialogQuery_fragment = {
response: LinkedDocumentsDialogQuery_fragment$data;
variables: LinkedDocumentsDialogQuery_fragment$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
v2 = {
"defaultValue": 20,
"kind": "LocalArgument",
"name": "first"
},
v3 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "id"
},
v4 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
v5 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "order"
},
v6 = [
{
"kind": "Variable",
"name": "id",
"variableName": "id"
}
],
v7 = {
"kind": "Variable",
"name": "after",
"variableName": "after"
},
v8 = {
"kind": "Variable",
"name": "before",
"variableName": "before"
},
v9 = {
"kind": "Variable",
"name": "first",
"variableName": "first"
},
v10 = {
"kind": "Variable",
"name": "last",
"variableName": "last"
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v13 = [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
}
];
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "LinkedDocumentsDialogQuery_fragment",
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{
"kind": "Variable",
"name": "order",
"variableName": "order"
}
],
"kind": "FragmentSpread",
"name": "LinkedDocumentsDialogFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v3/*: any*/)
],
"kind": "Operation",
"name": "LinkedDocumentsDialogQuery_fragment",
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v11/*: any*/),
(v12/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v13/*: any*/),
"concreteType": "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": [
(v12/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"storageKey": null
},
(v11/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": (v13/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "LinkedDocumentsDialogQuery_documents",
"kind": "LinkedHandle",
"name": "documents"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "e72639ede26aa4300b69ba3181de482b",
"id": null,
"metadata": {},
"name": "LinkedDocumentsDialogQuery_fragment",
"operationKind": "query",
"text": "query LinkedDocumentsDialogQuery_fragment(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: DocumentOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...LinkedDocumentsDialogFragment_16fISc\n id\n }\n}\n\nfragment LinkedDocumentsDialogFragment_16fISc on Organization {\n documents(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n title\n documentType\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "392d33b379a58cb3da08ab783c1cda5d";
export default node;

View File

@@ -12,6 +12,7 @@ import {
IconChevronDown,
MeasureBadge,
IconTrashCan,
TrButton,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { LinkedMeasuresCardFragment$key } from "./__generated__/LinkedMeasuresCardFragment.graphql";
@@ -19,7 +20,8 @@ import { useFragment } from "react-relay";
import { useMemo, useState } from "react";
import { sprintf } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { MeasureLinkDialog } from "./MeasureLinkDialog";
import { LinkedMeasureDialog } from "./LinkedMeasuresDialog.tsx";
import clsx from "clsx";
const linkedMeasureFragment = graphql`
fragment LinkedMeasuresCardFragment on Measure {
@@ -51,6 +53,7 @@ type Props<Params> = {
onAttach: Mutation<Params>;
// Mutation to detach a measure (will receive {measureId, ...params})
onDetach: Mutation<Params>;
variant?: "card" | "table";
};
/**
@@ -58,11 +61,14 @@ type Props<Params> = {
*/
export function LinkedMeasuresCard<Params>(props: Props<Params>) {
const { __ } = useTranslate();
const [limit, setLimit] = useState<number | null>(4);
const [limit, setLimit] = useState<number | null>(
props.variant === "card" ? 4 : null
);
const measures = useMemo(() => {
return limit ? props.measures.slice(0, limit) : props.measures;
}, [props.measures, limit]);
const showMoreButton = limit !== null && props.measures.length > limit;
const variant = props.variant ?? "table";
const onAttach = (measureId: string) => {
props.onAttach({
@@ -88,46 +94,60 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
});
};
const Wrapper = variant === "card" ? Card : "div";
return (
<Card padded className="space-y-[10px]">
<div className="flex justify-between">
<div className="text-lg font-semibold">{__("Measures")}</div>
<MeasureLinkDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedMeasures={props.measures}
onLink={onAttach}
onUnlink={onDetach}
>
<Button variant="tertiary" icon={IconPlusLarge}>
{__("Link measure")}
</Button>
</MeasureLinkDialog>
</div>
{measures.length > 0 ? (
<Table className="bg-invert">
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("State")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{measures.map((measure) => (
<MeasureRow
key={measure.id}
measure={measure}
onClick={onDetach}
/>
))}
</Tbody>
</Table>
) : (
<div className="text-center text-sm text-txt-secondary">
{__("No measures linked")}
<Wrapper padded className="space-y-[10px]">
{variant === "card" && (
<div className="flex justify-between">
<div className="text-lg font-semibold">{__("Measures")}</div>
<LinkedMeasureDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedMeasures={props.measures}
onLink={onAttach}
onUnlink={onDetach}
>
<Button variant="tertiary" icon={IconPlusLarge}>
{__("Link measure")}
</Button>
</LinkedMeasureDialog>
</div>
)}
<Table className={clsx(variant === "card" && "bg-invert")}>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("State")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{measures.length === 0 && (
<Tr>
<Td colSpan={3} className="text-center text-txt-secondary">
{__("No measures linked")}
</Td>
</Tr>
)}
{measures.map((measure) => (
<MeasureRow key={measure.id} measure={measure} onClick={onDetach} />
))}
{variant === "table" && (
<LinkedMeasureDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedMeasures={props.measures}
onLink={onAttach}
onUnlink={onDetach}
>
<TrButton colspan={3} icon={IconPlusLarge}>
{__("Link measure")}
</TrButton>
</LinkedMeasureDialog>
)}
</Tbody>
</Table>
{showMoreButton && (
<Button
variant="tertiary"
@@ -138,7 +158,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
{sprintf(__("Show %s more"), props.measures.length - limit)}
</Button>
)}
</Card>
</Wrapper>
);
}

View File

@@ -7,6 +7,7 @@ import {
IconMagnifyingGlass,
IconPlusLarge,
IconTrashCan,
InfiniteScrollTrigger,
Input,
Option,
Select,
@@ -15,25 +16,46 @@ import {
import { useTranslate } from "@probo/i18n";
import { Suspense, useMemo, useState, type ReactNode } from "react";
import { graphql } from "relay-runtime";
import { useLazyLoadQuery } from "react-relay";
import type { MeasureLinkDialogQuery } from "./__generated__/MeasureLinkDialogQuery.graphql";
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
import type { LinkedMeasuresDialogQuery } from "./__generated__/LinkedMeasuresDialogQuery.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { LinkedMeasuresDialogFragment$key } from "./__generated__/LinkedMeasuresDialogFragment.graphql";
const measuresQuery = graphql`
query MeasureLinkDialogQuery($organizationId: ID!) {
query LinkedMeasuresDialogQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
id
... on Organization {
measures(first: 100) @connection(key: "Organization__measures") {
edges {
node {
id
name
state
description
category
}
}
...LinkedMeasuresDialogFragment
}
}
}
`;
const measuresFragment = graphql`
fragment LinkedMeasuresDialogFragment on Organization
@refetchable(queryName: "LinkedMeasuresDialogQuery_fragment")
@argumentDefinitions(
first: { type: "Int", defaultValue: 20 }
order: { type: "MeasureOrder", defaultValue: null }
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
) {
measures(
first: $first
after: $after
last: $last
before: $before
orderBy: $order
) @connection(key: "LinkedMeasuresDialogQuery_measures") {
edges {
node {
id
name
state
description
category
}
}
}
@@ -49,14 +71,14 @@ type Props = {
onUnlink: (measureId: string) => void;
};
export function MeasureLinkDialog({ children, ...props }: Props) {
export function LinkedMeasureDialog({ children, ...props }: Props) {
const { __ } = useTranslate();
return (
<Dialog trigger={children} title={__("Link measures")}>
<DialogContent>
<Suspense fallback={<Spinner centered />}>
<MeasureLinkDialogContent {...props} />
<LinkedMeasuresDialogContent {...props} />
</Suspense>
</DialogContent>
<DialogFooter exitLabel={__("Close")} />
@@ -64,16 +86,19 @@ export function MeasureLinkDialog({ children, ...props }: Props) {
);
}
function MeasureLinkDialogContent(props: Omit<Props, "children">) {
function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
const organizationId = useOrganizationId();
const data = useLazyLoadQuery<MeasureLinkDialogQuery>(measuresQuery, {
const query = useLazyLoadQuery<LinkedMeasuresDialogQuery>(measuresQuery, {
organizationId,
});
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
measuresFragment,
query.organization as LinkedMeasuresDialogFragment$key
);
const { __ } = useTranslate();
const [search, setSearch] = useState("");
const [category, setCategory] = useState<string | null>(null);
const measures =
data.organization?.measures?.edges?.map((edge) => edge.node) ?? [];
const measures = data.measures?.edges?.map((edge) => edge.node) ?? [];
const linkedIds = useMemo(() => {
return new Set(props.linkedMeasures?.map((m) => m.id) ?? []);
}, [props.linkedMeasures]);
@@ -124,6 +149,12 @@ function MeasureLinkDialogContent(props: Omit<Props, "children">) {
disabled={props.disabled}
/>
))}
{hasNext && (
<InfiniteScrollTrigger
loading={isLoadingNext}
onView={() => loadNext(20)}
/>
)}
</div>
</>
);

View File

@@ -0,0 +1,239 @@
/**
* @generated SignedSource<<47d66bf5d9b99eb598728999a7b2ce85>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
import { FragmentRefs } from "relay-runtime";
export type LinkedMeasuresDialogFragment$data = {
readonly id: string;
readonly measures: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly category: string;
readonly description: string;
readonly id: string;
readonly name: string;
readonly state: MeasureState;
};
}>;
};
readonly " $fragmentType": "LinkedMeasuresDialogFragment";
};
export type LinkedMeasuresDialogFragment$key = {
readonly " $data"?: LinkedMeasuresDialogFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresDialogFragment">;
};
import LinkedMeasuresDialogQuery_fragment_graphql from './LinkedMeasuresDialogQuery_fragment.graphql';
const node: ReaderFragment = (function(){
var v0 = [
"measures"
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
{
"defaultValue": 20,
"kind": "LocalArgument",
"name": "first"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "order"
}
],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "bidirectional",
"path": (v0/*: any*/)
}
],
"refetch": {
"connection": {
"forward": {
"count": "first",
"cursor": "after"
},
"backward": {
"count": "last",
"cursor": "before"
},
"path": (v0/*: any*/)
},
"fragmentPathInResult": [
"node"
],
"operation": LinkedMeasuresDialogQuery_fragment_graphql,
"identifierInfo": {
"identifierField": "id",
"identifierQueryVariableName": "id"
}
}
},
"name": "LinkedMeasuresDialogFragment",
"selections": [
{
"alias": "measures",
"args": [
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
}
],
"concreteType": "MeasureConnection",
"kind": "LinkedField",
"name": "__LinkedMeasuresDialogQuery_measures_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MeasureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
(v1/*: any*/)
],
"type": "Organization",
"abstractKey": null
};
})();
(node as any).hash = "69458f9aed8aee19f69cd58090b68947";
export default node;

View File

@@ -0,0 +1,259 @@
/**
* @generated SignedSource<<d357c78d6043fd24cd4b9df8e6ba5057>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type LinkedMeasuresDialogQuery$variables = {
organizationId: string;
};
export type LinkedMeasuresDialogQuery$data = {
readonly organization: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresDialogFragment">;
};
};
export type LinkedMeasuresDialogQuery = {
response: LinkedMeasuresDialogQuery$data;
variables: LinkedMeasuresDialogQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v4 = [
{
"kind": "Literal",
"name": "first",
"value": 20
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "LinkedMeasuresDialogQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "LinkedMeasuresDialogFragment"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "LinkedMeasuresDialogQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v4/*: any*/),
"concreteType": "MeasureConnection",
"kind": "LinkedField",
"name": "measures",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MeasureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "measures(first:20)"
},
{
"alias": null,
"args": (v4/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "LinkedMeasuresDialogQuery_measures",
"kind": "LinkedHandle",
"name": "measures"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "ecb3ffb566c178a40f6ea72b1947b3ed",
"id": null,
"metadata": {},
"name": "LinkedMeasuresDialogQuery",
"operationKind": "query",
"text": "query LinkedMeasuresDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n ...LinkedMeasuresDialogFragment\n }\n }\n}\n\nfragment LinkedMeasuresDialogFragment on Organization {\n measures(first: 20) {\n edges {\n node {\n id\n name\n state\n description\n category\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "8592f3602ea10f6c00f5eb32c3f6b638";
export default node;

View File

@@ -0,0 +1,332 @@
/**
* @generated SignedSource<<412b6fd2ef44aa7bc3c9e66ec2284c37>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MeasureOrderField = "CREATED_AT";
export type OrderDirection = "ASC" | "DESC";
export type MeasureOrder = {
direction: OrderDirection;
field: MeasureOrderField;
};
export type LinkedMeasuresDialogQuery_fragment$variables = {
after?: any | null | undefined;
before?: any | null | undefined;
first?: number | null | undefined;
id: string;
last?: number | null | undefined;
order?: MeasureOrder | null | undefined;
};
export type LinkedMeasuresDialogQuery_fragment$data = {
readonly node: {
readonly " $fragmentSpreads": FragmentRefs<"LinkedMeasuresDialogFragment">;
};
};
export type LinkedMeasuresDialogQuery_fragment = {
response: LinkedMeasuresDialogQuery_fragment$data;
variables: LinkedMeasuresDialogQuery_fragment$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
v2 = {
"defaultValue": 20,
"kind": "LocalArgument",
"name": "first"
},
v3 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "id"
},
v4 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
v5 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "order"
},
v6 = [
{
"kind": "Variable",
"name": "id",
"variableName": "id"
}
],
v7 = {
"kind": "Variable",
"name": "after",
"variableName": "after"
},
v8 = {
"kind": "Variable",
"name": "before",
"variableName": "before"
},
v9 = {
"kind": "Variable",
"name": "first",
"variableName": "first"
},
v10 = {
"kind": "Variable",
"name": "last",
"variableName": "last"
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v13 = [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{
"kind": "Variable",
"name": "orderBy",
"variableName": "order"
}
];
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "LinkedMeasuresDialogQuery_fragment",
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{
"kind": "Variable",
"name": "order",
"variableName": "order"
}
],
"kind": "FragmentSpread",
"name": "LinkedMeasuresDialogFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v3/*: any*/)
],
"kind": "Operation",
"name": "LinkedMeasuresDialogQuery_fragment",
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v11/*: any*/),
(v12/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v13/*: any*/),
"concreteType": "MeasureConnection",
"kind": "LinkedField",
"name": "measures",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "MeasureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v12/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
(v11/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": (v13/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "LinkedMeasuresDialogQuery_measures",
"kind": "LinkedHandle",
"name": "measures"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "9de5e8b136a2952dbb169f6b27aade30",
"id": null,
"metadata": {},
"name": "LinkedMeasuresDialogQuery_fragment",
"operationKind": "query",
"text": "query LinkedMeasuresDialogQuery_fragment(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MeasureOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...LinkedMeasuresDialogFragment_16fISc\n id\n }\n}\n\nfragment LinkedMeasuresDialogFragment_16fISc on Organization {\n measures(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n name\n state\n description\n category\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "69458f9aed8aee19f69cd58090b68947";
export default node;

View File

@@ -1,271 +0,0 @@
/**
* @generated SignedSource<<0b725ea8709fbf5da3f17545fcd2b8c4>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type MeasureLinkDialogQuery$variables = {
organizationId: string;
};
export type MeasureLinkDialogQuery$data = {
readonly organization: {
readonly id: string;
readonly measures?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly category: string;
readonly description: string;
readonly id: string;
readonly name: string;
readonly state: MeasureState;
};
}>;
};
};
};
export type MeasureLinkDialogQuery = {
response: MeasureLinkDialogQuery$data;
variables: MeasureLinkDialogQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v4 = [
{
"alias": null,
"args": null,
"concreteType": "MeasureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
}
],
v5 = [
{
"kind": "Literal",
"name": "first",
"value": 100
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "MeasureLinkDialogQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": "measures",
"args": null,
"concreteType": "MeasureConnection",
"kind": "LinkedField",
"name": "__Organization__measures_connection",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "MeasureLinkDialogQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v5/*: any*/),
"concreteType": "MeasureConnection",
"kind": "LinkedField",
"name": "measures",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": "measures(first:100)"
},
{
"alias": null,
"args": (v5/*: any*/),
"filters": null,
"handle": "connection",
"key": "Organization__measures",
"kind": "LinkedHandle",
"name": "measures"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "54fff8a0c07ebe02ef34f3bb35e7582e",
"id": null,
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"organization",
"measures"
]
}
]
},
"name": "MeasureLinkDialogQuery",
"operationKind": "query",
"text": "query MeasureLinkDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n measures(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n category\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "cf01fa3a766badd4f6150e1a544403ba";
export default node;

View File

@@ -0,0 +1,167 @@
import { graphql } from "relay-runtime";
import {
IconPlusLarge,
Button,
Tr,
Td,
Table,
Thead,
Tbody,
Th,
IconChevronDown,
RiskBadge,
IconTrashCan,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { LinkedRisksCardFragment$key } from "./__generated__/LinkedRisksCardFragment.graphql";
import { useFragment } from "react-relay";
import { useMemo, useState } from "react";
import { sprintf } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { LinkedRisksDialog } from "./LinkedRisksDialog.tsx";
const linkedRiskFragment = graphql`
fragment LinkedRisksCardFragment on Risk {
id
name
inherentRiskScore
residualRiskScore
}
`;
type Mutation<Params> = (p: {
variables: {
input: {
riskId: string;
} & Params;
connections: string[];
};
}) => void;
type Props<Params> = {
// Risks linked to the element
risks: (LinkedRisksCardFragment$key & { id: string })[];
// Extra params to send to the mutation
params: Params;
// Disable (action when loading for instance)
disabled?: boolean;
// ID of the connection to update
connectionId: string;
// Mutation to attach a risk (will receive {riskId, ...params})
onAttach: Mutation<Params>;
// Mutation to detach a risk (will receive {riskId, ...params})
onDetach: Mutation<Params>;
};
/**
* Reusable component that displays a list of linked risks
*/
export function LinkedRisksCard<Params>(props: Props<Params>) {
const { __ } = useTranslate();
const [limit, setLimit] = useState<number | null>(4);
const risks = useMemo(() => {
return limit ? props.risks.slice(0, limit) : props.risks;
}, [props.risks, limit]);
const showMoreButton = limit !== null && props.risks.length > limit;
const onAttach = (riskId: string) => {
props.onAttach({
variables: {
input: {
riskId,
...props.params,
},
connections: [props.connectionId],
},
});
};
const onDetach = (riskId: string) => {
props.onDetach({
variables: {
input: {
riskId,
...props.params,
},
connections: [props.connectionId],
},
});
};
return (
<div className="space-y-4 relative">
{risks.length > 0 ? (
<Table>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("Inherent Risk")}</Th>
<Th>{__("Residual Risk")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{risks.map((risk) => (
<RiskRow key={risk.id} risk={risk} onClick={onDetach} />
))}
</Tbody>
</Table>
) : (
<div className="text-center text-sm text-txt-secondary">
{__("No risks linked")}
</div>
)}
{showMoreButton && (
<Button
variant="tertiary"
onClick={() => setLimit(null)}
className="mt-3 mx-auto"
icon={IconChevronDown}
>
{sprintf(__("Show %s more"), props.risks.length - limit)}
</Button>
)}
<LinkedRisksDialog
connectionId={props.connectionId}
disabled={props.disabled}
linkedRisks={props.risks}
onLink={onAttach}
onUnlink={onDetach}
>
<Button variant="secondary" icon={IconPlusLarge} className="ml-auto">
{__("Link risk")}
</Button>
</LinkedRisksDialog>
</div>
);
}
function RiskRow(props: {
risk: LinkedRisksCardFragment$key & { id: string };
onClick: (riskId: string) => void;
}) {
const risk = useFragment(linkedRiskFragment, props.risk);
const organizationId = useOrganizationId();
const { __ } = useTranslate();
return (
<Tr to={`/organizations/${organizationId}/risks/${risk.id}`}>
<Td>{risk.name}</Td>
<Td>
<RiskBadge level={risk.inherentRiskScore} />
</Td>
<Td>
<RiskBadge level={risk.residualRiskScore} />
</Td>
<Td noLink width={50} className="text-end">
<Button
variant="secondary"
onClick={() => props.onClick(risk.id)}
icon={IconTrashCan}
>
{__("Unlink")}
</Button>
</Td>
</Tr>
);
}

View File

@@ -0,0 +1,172 @@
import {
Badge,
Button,
Dialog,
DialogContent,
DialogFooter,
IconMagnifyingGlass,
IconPlusLarge,
IconTrashCan,
Input,
Option,
Select,
Spinner,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { Suspense, useMemo, useState, type ReactNode } from "react";
import { graphql } from "relay-runtime";
import { useLazyLoadQuery } from "react-relay";
import type { LinkedRisksDialogQuery } from "./__generated__/LinkedRisksDialogQuery.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
const risksQuery = graphql`
query LinkedRisksDialogQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
id
... on Organization {
risks(first: 100) {
edges {
node {
id
name
category
description
inherentRiskScore
residualRiskScore
}
}
}
}
}
}
`;
type Props = {
children: ReactNode;
connectionId: string;
disabled?: boolean;
linkedRisks?: { id: string }[];
onLink: (riskId: string) => void;
onUnlink: (riskId: string) => void;
};
export function LinkedRisksDialog({ children, ...props }: Props) {
const { __ } = useTranslate();
return (
<Dialog trigger={children} title={__("Link risks")}>
<DialogContent>
<Suspense fallback={<Spinner centered />}>
<LinkedRisksDialogContent {...props} />
</Suspense>
</DialogContent>
<DialogFooter exitLabel={__("Close")} />
</Dialog>
);
}
function LinkedRisksDialogContent(props: Omit<Props, "children">) {
const organizationId = useOrganizationId();
const data = useLazyLoadQuery<LinkedRisksDialogQuery>(risksQuery, {
organizationId,
});
const { __ } = useTranslate();
const [search, setSearch] = useState("");
const [category, setCategory] = useState<string | null>(null);
const risks = data.organization?.risks?.edges?.map((edge) => edge.node) ?? [];
const linkedIds = useMemo(() => {
return new Set(props.linkedRisks?.map((r) => r.id) ?? []);
}, [props.linkedRisks]);
const filteredRisks = useMemo(() => {
return risks.filter(
(risk) =>
(category === null || risk.category === category) &&
(risk.name.toLowerCase().includes(search.toLowerCase()) ||
risk.description?.toLowerCase().includes(search.toLowerCase()))
);
}, [risks, search, category]);
const categories = useMemo(
() => Array.from(new Set(risks.map((r) => r.category))),
[risks]
);
return (
<>
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
<Input
icon={IconMagnifyingGlass}
placeholder={__("Search risks...")}
onValueChange={setSearch}
/>
<Select
value={category ?? ""}
placeholder={__("All categories")}
onValueChange={setCategory}
className="max-w-[180px]"
>
{categories.map((category) => (
<Option key={category} value={category}>
{category}
</Option>
))}
</Select>
</div>
<div className="divide-y divide-border-low">
{filteredRisks.map((risk) => (
<RiskRow
key={risk.id}
risk={risk}
linkedRisks={linkedIds}
onLink={props.onLink}
onUnlink={props.onUnlink}
disabled={props.disabled}
/>
))}
</div>
</>
);
}
type RowProps = {
risk: {
name: string;
category: string;
id: string;
inherentRiskScore: number;
residualRiskScore: number;
};
linkedRisks: Set<string>;
disabled?: boolean;
onLink: (riskId: string) => void;
onUnlink: (riskId: string) => void;
};
function RiskRow(props: RowProps) {
const { __ } = useTranslate();
const isLinked = props.linkedRisks.has(props.risk.id);
const onClick = isLinked ? props.onUnlink : props.onLink;
const IconComponent = isLinked ? IconTrashCan : IconPlusLarge;
return (
<button
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full"
onClick={() => onClick(props.risk.id)}
>
<div className="text-left">{props.risk.name}</div>
<Badge variant="neutral">{props.risk.category}</Badge>
<Button
disabled={props.disabled}
className="ml-auto"
variant={isLinked ? "secondary" : "primary"}
asChild
>
<span>
<IconComponent size={16} /> {isLinked ? __("Unlink") : __("Link")}
</span>
</Button>
</button>
);
}

View File

@@ -1,31 +0,0 @@
/*
const attachMeasureMutation = graphql`
mutation MeasureLinkDialogCreateMutation(
$input: CreateRiskMeasureMappingInput!
$connections: [ID!]!
) {
createRiskMeasureMapping(input: $input) {
measureEdge @prependEdge(connections: $connections) {
node {
id
name
description
category
state
}
}
}
}
`;
export const detachMeasureMutation = graphql`
mutation MeasureLinkDialogDetachMutation(
$input: DeleteRiskMeasureMappingInput!
$connections: [ID!]!
) {
deleteRiskMeasureMapping(input: $input) {
deletedMeasureId @deleteEdge(connections: $connections)
}
}
`;
*/

View File

@@ -0,0 +1,66 @@
/**
* @generated SignedSource<<6e092be20526b76ee767836880803c34>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type LinkedRisksCardFragment$data = {
readonly id: string;
readonly inherentRiskScore: number;
readonly name: string;
readonly residualRiskScore: number;
readonly " $fragmentType": "LinkedRisksCardFragment";
};
export type LinkedRisksCardFragment$key = {
readonly " $data"?: LinkedRisksCardFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"LinkedRisksCardFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "LinkedRisksCardFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "inherentRiskScore",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualRiskScore",
"storageKey": null
}
],
"type": "Risk",
"abstractKey": null
};
(node as any).hash = "32a84445ae1cc071f56139fa44d67690";
export default node;

View File

@@ -0,0 +1,206 @@
/**
* @generated SignedSource<<ac10f3fa76fc76951f4c08e38ef0a185>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type LinkedRisksDialogQuery$variables = {
organizationId: string;
};
export type LinkedRisksDialogQuery$data = {
readonly organization: {
readonly id: string;
readonly risks?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly category: string;
readonly description: string;
readonly id: string;
readonly inherentRiskScore: number;
readonly name: string;
readonly residualRiskScore: number;
};
}>;
};
};
};
export type LinkedRisksDialogQuery = {
response: LinkedRisksDialogQuery$data;
variables: LinkedRisksDialogQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "RiskConnection",
"kind": "LinkedField",
"name": "risks",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "RiskEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Risk",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "inherentRiskScore",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualRiskScore",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "risks(first:100)"
}
],
"type": "Organization",
"abstractKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "LinkedRisksDialogQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "LinkedRisksDialogQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
(v3/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "ccac9f72628186de135aeb2ee176207d",
"id": null,
"metadata": {},
"name": "LinkedRisksDialogQuery",
"operationKind": "query",
"text": "query LinkedRisksDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n risks(first: 100) {\n edges {\n node {\n id\n name\n category\n description\n inherentRiskScore\n residualRiskScore\n }\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "a0a7e39135cc4c1a4a74c4728902f944";
export default node;