From fc45d2f00d81d74947137cbd105fc77ab2fa61ed Mon Sep 17 00:00:00 2001 From: Jonathan Date: Thu, 22 May 2025 12:40:12 +0200 Subject: [PATCH] Add risk forms Signed-off-by: Bryan Frimin Signed-off-by: Sacha Al Himdani --- apps/console2/package.json | 1 + .../src/components/form/ControlledField.tsx | 2 +- .../form/{UserSelect.tsx => PeopleSelect.tsx} | 58 +-- .../PeopleSelectQuery.graphql.ts | 180 +++++++ .../src/hooks/useMutationWithToasts.ts | 57 +++ apps/console2/src/pages/OrganizationsPage.tsx | 2 +- apps/console2/src/pages/auth/LoginPage.tsx | 6 +- .../organizations/NewOrganizationPage.tsx | 4 +- .../{NewRiskDialog.tsx => FormRiskDialog.tsx} | 142 +++--- .../pages/organizations/risks/RisksPage.tsx | 182 +++++++- .../FormRiskDialogMutation.graphql.ts | 271 +++++++++++ ...ormRiskDialogUpdateRiskMutation.graphql.ts | 221 +++++++++ .../RisksPageDeleteMutation.graphql.ts | 132 ++++++ .../__generated__/RisksPageQuery.graphql.ts | 441 ++++++++++++++++++ .../RisksPageUpdateRiskMutation.graphql.ts | 220 +++++++++ .../useRiskFormFragment.graphql.ts | 133 ++++++ .../organizations/risks/forms/useRiskForm.tsx | 81 +++- apps/console2/src/routes.tsx | 3 +- apps/console2/src/types.ts | 7 + bun.lock | 24 +- packages/helpers/src/index.ts | 1 + packages/helpers/src/string.test.ts | 10 + packages/helpers/src/string.ts | 9 + packages/i18n/package.json | 4 +- packages/ui/package.json | 1 + packages/ui/src/Atoms/Badge/Badge.stories.tsx | 31 ++ packages/ui/src/Atoms/Badge/Badge.tsx | 24 + packages/ui/src/Atoms/Dropdown/Dropdown.tsx | 20 +- packages/ui/src/Atoms/Select/Select.tsx | 5 +- packages/ui/src/Atoms/Table/Table.stories.tsx | 12 + packages/ui/src/Atoms/Table/Table.tsx | 64 +++ .../src/Molecules/Badge/RiskBadge.stories.tsx | 20 + packages/ui/src/Molecules/Badge/RiskBadge.tsx | 30 ++ .../Molecules/Badge/SeverityBadge.stories.tsx | 21 + .../ui/src/Molecules/Badge/SeverityBadge.tsx | 36 ++ .../Dialog/ConfirmDialog.stories.tsx | 28 ++ .../ui/src/Molecules/Dialog/ConfirmDialog.tsx | 66 +++ packages/ui/src/Molecules/Dialog/Dialog.tsx | 4 +- packages/ui/src/Molecules/Field/Field.tsx | 2 + packages/ui/src/index.ts | 5 + packages/ui/tsconfig.app.json | 2 +- 41 files changed, 2426 insertions(+), 136 deletions(-) rename apps/console2/src/components/form/{UserSelect.tsx => PeopleSelect.tsx} (50%) create mode 100644 apps/console2/src/components/form/__generated__/PeopleSelectQuery.graphql.ts create mode 100644 apps/console2/src/hooks/useMutationWithToasts.ts rename apps/console2/src/pages/organizations/risks/{NewRiskDialog.tsx => FormRiskDialog.tsx} (72%) create mode 100644 apps/console2/src/pages/organizations/risks/__generated__/FormRiskDialogMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/risks/__generated__/FormRiskDialogUpdateRiskMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/risks/__generated__/RisksPageDeleteMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/risks/__generated__/RisksPageQuery.graphql.ts create mode 100644 apps/console2/src/pages/organizations/risks/__generated__/RisksPageUpdateRiskMutation.graphql.ts create mode 100644 apps/console2/src/pages/organizations/risks/forms/__generated__/useRiskFormFragment.graphql.ts create mode 100644 apps/console2/src/types.ts create mode 100644 packages/helpers/src/string.test.ts create mode 100644 packages/helpers/src/string.ts create mode 100644 packages/ui/src/Atoms/Badge/Badge.stories.tsx create mode 100644 packages/ui/src/Atoms/Badge/Badge.tsx create mode 100644 packages/ui/src/Atoms/Table/Table.stories.tsx create mode 100644 packages/ui/src/Atoms/Table/Table.tsx create mode 100644 packages/ui/src/Molecules/Badge/RiskBadge.stories.tsx create mode 100644 packages/ui/src/Molecules/Badge/RiskBadge.tsx create mode 100644 packages/ui/src/Molecules/Badge/SeverityBadge.stories.tsx create mode 100644 packages/ui/src/Molecules/Badge/SeverityBadge.tsx create mode 100644 packages/ui/src/Molecules/Dialog/ConfirmDialog.stories.tsx create mode 100644 packages/ui/src/Molecules/Dialog/ConfirmDialog.tsx diff --git a/apps/console2/package.json b/apps/console2/package.json index f5e733661..1ae3cbcf2 100644 --- a/apps/console2/package.json +++ b/apps/console2/package.json @@ -14,6 +14,7 @@ "dependencies": { "@hookform/resolvers": "^5.0.1", "@probo/hooks": "1.0.0", + "@probo/helpers": "1.0.0", "@probo/i18n": "1.0.0", "@probo/ui": "1.0.0", "@radix-ui/react-dialog": "^1.1.14", diff --git a/apps/console2/src/components/form/ControlledField.tsx b/apps/console2/src/components/form/ControlledField.tsx index 9c967fda1..1dd316b10 100644 --- a/apps/console2/src/components/form/ControlledField.tsx +++ b/apps/console2/src/components/form/ControlledField.tsx @@ -1,4 +1,4 @@ -import type { ComponentProps, JSXElementConstructor } from "react"; +import type { ComponentProps, JSX, JSXElementConstructor } from "react"; import { Field } from "@probo/ui"; import { Controller, type Control } from "react-hook-form"; import { Select } from "@probo/ui"; diff --git a/apps/console2/src/components/form/UserSelect.tsx b/apps/console2/src/components/form/PeopleSelect.tsx similarity index 50% rename from apps/console2/src/components/form/UserSelect.tsx rename to apps/console2/src/components/form/PeopleSelect.tsx index 357d290cb..477cd5adb 100644 --- a/apps/console2/src/components/form/UserSelect.tsx +++ b/apps/console2/src/components/form/PeopleSelect.tsx @@ -2,15 +2,15 @@ import { Avatar, Option, Select } from "@probo/ui"; import { Suspense } from "react"; import { useLazyLoadQuery } from "react-relay"; import { graphql } from "relay-runtime"; -import type { UserSelectQuery as UserSelectQueryType } from "./__generated__/UserSelectQuery.graphql"; +import type { PeopleSelectQuery as PeopleSelectQueryType } from "./__generated__/PeopleSelectQuery.graphql"; import { useTranslate } from "@probo/i18n"; import { Controller, type Control } from "react-hook-form"; -const usersQuery = graphql` - query UserSelectQuery($organizationId: ID!) { +const peopleQuery = graphql` + query PeopleSelectQuery($organizationId: ID!) { organization: node(id: $organizationId) { ... on Organization { - users(first: 100, orderBy: { direction: ASC, field: CREATED_AT }) { + peoples(first: 100, orderBy: { direction: ASC, field: CREATED_AT }) { edges { node { id @@ -23,7 +23,7 @@ const usersQuery = graphql` } `; -export function UserSelect({ +export function PeopleSelect({ organization, name, control, @@ -36,7 +36,7 @@ export function UserSelect({ } > - (usersQuery, { + const data = useLazyLoadQuery(peopleQuery, { organizationId: organization, }); - const users = data.organization?.users?.edges.map((edge) => edge.node); + const people = data.organization?.peoples?.edges.map((edge) => edge.node); return ( - ( - - )} - /> + <> + ( + + )} + /> + ); } diff --git a/apps/console2/src/components/form/__generated__/PeopleSelectQuery.graphql.ts b/apps/console2/src/components/form/__generated__/PeopleSelectQuery.graphql.ts new file mode 100644 index 000000000..24b0fbc67 --- /dev/null +++ b/apps/console2/src/components/form/__generated__/PeopleSelectQuery.graphql.ts @@ -0,0 +1,180 @@ +/** + * @generated SignedSource<<758e3ece2d7d1d52975886d41b6ae905>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type PeopleSelectQuery$variables = { + organizationId: string; +}; +export type PeopleSelectQuery$data = { + readonly organization: { + readonly peoples?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly fullName: string; + readonly id: string; + }; + }>; + }; + }; +}; +export type PeopleSelectQuery = { + response: PeopleSelectQuery$data; + variables: PeopleSelectQuery$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 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "ASC", + "field": "CREATED_AT" + } + } + ], + "concreteType": "PeopleConnection", + "kind": "LinkedField", + "name": "peoples", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PeopleEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})" + } + ], + "type": "Organization", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "PeopleSelectQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "PeopleSelectQuery", + "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 + }, + (v3/*: any*/), + (v2/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "4e58fc42b352af93ba7067921531af3e", + "id": null, + "metadata": {}, + "name": "PeopleSelectQuery", + "operationKind": "query", + "text": "query PeopleSelectQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n peoples(first: 100, orderBy: {direction: ASC, field: CREATED_AT}) {\n edges {\n node {\n id\n fullName\n }\n }\n }\n }\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "77119544e99cf9dd74d8a0f446df108e"; + +export default node; diff --git a/apps/console2/src/hooks/useMutationWithToasts.ts b/apps/console2/src/hooks/useMutationWithToasts.ts new file mode 100644 index 000000000..3a5325612 --- /dev/null +++ b/apps/console2/src/hooks/useMutationWithToasts.ts @@ -0,0 +1,57 @@ +import { useCallback } from "react"; +import { useMutation, type UseMutationConfig } from "react-relay"; +import { useToast } from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import type { MutationParameters, GraphQLTaggedNode } from "relay-runtime"; + +/** + * A decorated useMutation hook that emits toast notifications on success or error. + */ +export function useMutationWithToasts( + query: GraphQLTaggedNode +) { + const [mutate, isLoading] = useMutation(query); + const { toast } = useToast(); + const { __ } = useTranslate(); + const mutateWithToast = useCallback( + ( + options: UseMutationConfig & { + onSuccess?: () => void; + successMessage: string; + errorMessage?: string; + } + ) => { + mutate({ + ...options, + onCompleted: (_, error) => { + if (error) { + toast({ + title: __("Error"), + description: + options.errorMessage ?? __("Failed to commit this operation."), + variant: "error", + }); + return; + } + toast({ + title: __("Success"), + description: options.successMessage, + variant: "success", + }); + options.onSuccess?.(); + }, + onError: () => { + toast({ + title: __("Error"), + description: + options.errorMessage ?? __("Failed to commit this operation."), + variant: "error", + }); + }, + }); + }, + [mutate] + ); + + return [mutateWithToast, isLoading] as const; +} diff --git a/apps/console2/src/pages/OrganizationsPage.tsx b/apps/console2/src/pages/OrganizationsPage.tsx index 6d9361b9d..7f43814d8 100644 --- a/apps/console2/src/pages/OrganizationsPage.tsx +++ b/apps/console2/src/pages/OrganizationsPage.tsx @@ -37,7 +37,7 @@ export default function OrganizationsPage() { // Redirect to the first organization if only one exists useEffect(() => { if (organizations.length === 1) { - // navigate(`/organizations/${organizations[0].id}`); + navigate(`/organizations/${organizations[0].id}`); } }, [organizations]); diff --git a/apps/console2/src/pages/auth/LoginPage.tsx b/apps/console2/src/pages/auth/LoginPage.tsx index 84c6f996c..16468cc1c 100644 --- a/apps/console2/src/pages/auth/LoginPage.tsx +++ b/apps/console2/src/pages/auth/LoginPage.tsx @@ -1,5 +1,5 @@ import { useTranslate } from "@probo/i18n"; -import { Button, Input, useToast } from "@probo/ui"; +import { Button, Field, useToast } from "@probo/ui"; import type { FormEventHandler } from "react"; import { Link, useNavigate } from "react-router"; @@ -46,14 +46,14 @@ export default function LoginPage() {

{__("Enter your email below to login to your account")}

- - {__("Enter the basic information about your organization.")}

- void; }; -type Risk = { +type RiskTemplate = { category: string; name: string; description: string; }; const createRiskMutation = graphql` - mutation NewRiskDialogMutation( + mutation FormRiskDialogMutation( $input: CreateRiskInput! $connections: [ID!]! ) { createRisk(input: $input) { riskEdge @prependEdge(connections: $connections) { node { - id - name - description - category - inherentLikelihood - inherentImpact - residualLikelihood - residualImpact - treatment - createdAt - updatedAt + ...useRiskFormFragment } } } } `; -export default function NewRiskDialog({ trigger }: Props) { +const updateRiskMutation = graphql` + mutation FormRiskDialogUpdateRiskMutation($input: UpdateRiskInput!) { + updateRisk(input: $input) { + risk { + ...useRiskFormFragment + } + } + } +`; + +/** + * Dialog to create or update a risk + */ +export default function FormRiskDialog({ trigger, risk, onSuccess }: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); - const { control, handleSubmit, setValue, register, watch, formState } = - useRiskForm(organizationId); + const { control, handleSubmit, setValue, register, watch, formState, reset } = + useRiskForm(risk); const errors = formState.errors ?? {}; - const [createRisk, isLoading] = useMutation(createRiskMutation); - const { toast } = useToast(); + const [createRisk, isLoadingCreate] = + useMutationWithToasts(createRiskMutation); + const [updateRisk, isLoadingUpdate] = + useMutationWithToasts(updateRiskMutation); + const isLoading = isLoadingCreate || isLoadingUpdate; - const onTemplateChange = (risk: Risk) => { + const onTemplateChange = (risk: RiskTemplate) => { setValue("name", risk.name); setValue("description", risk.description); }; const onSubmit = handleSubmit((data) => { + if (risk) { + updateRisk({ + variables: { + input: { + id: risk.id, + ...data, + }, + }, + successMessage: __("Risk updated successfully."), + errorMessage: __("Failed to update risk. Please try again."), + onSuccess: () => { + setOpen(false); + onSuccess?.(); + }, + }); + return; + } + const connectionID = ConnectionHandler.getConnectionID( + organizationId, + "RisksPage_risks" + ); createRisk({ variables: { - input: data, + input: { + ...data, + organizationId, + }, + connections: [connectionID], }, - onCompleted: (response, error) => { - if (error) { - toast({ - title: __("Error"), - description: __("Failed to create risk. Please try again."), - variant: "error", - }); - return; - } - - toast({ - title: __("Success"), - description: __("Risk created successfully."), - variant: "success", - }); - + successMessage: __("Risk created successfully."), + errorMessage: __("Failed to create risk. Please try again."), + onSuccess: () => { setOpen(false); + reset(); + onSuccess?.(); }, }); }); const [showNote, toggleNote] = useToggle(false); - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(!!risk); return ( } + title={ + + } >
@@ -124,6 +153,7 @@ export default function NewRiskDialog({ trigger }: Props) { watch={watch} /> - @@ -224,9 +254,14 @@ function ImpactAndLikelihood({ errors, }: { label: string; - prefix: string; + prefix: "inherent" | "residual"; control: RiskForm["control"]; - errors: Record; + errors: FieldErrors<{ + inherentImpact: string; + inherentLikelihood: string; + residualImpact: string; + residualLikelihood: string; + }>; }) { const { __ } = useTranslate(); return ( @@ -271,14 +306,17 @@ function TemplateSelector({ control, watch, }: { - onChange: (risk: Risk) => void; + onChange: (risk: RiskTemplate) => void; control: RiskForm["control"]; watch: RiskForm["watch"]; }) { const { __ } = useTranslate(); - const { data: risks } = useFetchQuery("/data/risks/risks.json", { - staleTime: 100_000, - }); + const { data: risks } = useFetchQuery( + "/data/risks/risks.json", + { + staleTime: 100_000, + } + ); const categories = useMemo( () => Array.from(new Set(risks?.map((t) => t.category))), diff --git a/apps/console2/src/pages/organizations/risks/RisksPage.tsx b/apps/console2/src/pages/organizations/risks/RisksPage.tsx index 669541de2..6b68241d6 100644 --- a/apps/console2/src/pages/organizations/risks/RisksPage.tsx +++ b/apps/console2/src/pages/organizations/risks/RisksPage.tsx @@ -1,17 +1,193 @@ -import { Button, PageHeader } from "@probo/ui"; +import { + Button, + PageHeader, + Td, + Tr, + Th, + Thead, + Tbody, + Table, + RiskBadge, + SeverityBadge, + ActionDropdown, + DropdownItem, + IconTrashCan, + IconPencil, + ConfirmDialog, +} from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { IconPlusLarge } from "@probo/ui"; -import NewRiskDialog from "./NewRiskDialog"; +import FormRiskDialog from "./FormRiskDialog"; +import { ConnectionHandler, graphql } from "relay-runtime"; +import { useLazyLoadQuery } from "react-relay"; +import type { + RisksPageQuery as RisksPageQueryType, + RiskTreatment, +} from "./__generated__/RisksPageQuery.graphql"; +import { useOrganizationId } from "../../../hooks/useOrganizationId"; +import { useMemo, useState } from "react"; +import { usePageTitle } from "@probo/hooks"; +import type { RisksPageDeleteMutation } from "./__generated__/RisksPageDeleteMutation.graphql"; +import { useMutationWithToasts } from "../../../hooks/useMutationWithToasts"; +import { sprintf } from "@probo/helpers"; +import type { ItemOf } from "../../../types"; + +const risksQuery = graphql` + query RisksPageQuery( + $organizationId: ID! + $first: Int + $after: CursorKey + $last: Int + $before: CursorKey + ) { + organization: node(id: $organizationId) { + ... on Organization { + risks(first: $first, after: $after, last: $last, before: $before) + @connection(key: "RisksPage_risks") { + edges { + node { + id + name + category + treatment + inherentLikelihood + inherentImpact + residualLikelihood + residualImpact + inherentSeverity + ...useRiskFormFragment + } + } + } + } + } + } +`; + +const deleteRiskMutation = graphql` + mutation RisksPageDeleteMutation( + $input: DeleteRiskInput! + $connections: [ID!]! + ) { + deleteRisk(input: $input) { + deletedRiskId @deleteEdge(connections: $connections) + } + } +`; export default function RisksPage() { const { __ } = useTranslate(); + const data = useLazyLoadQuery(risksQuery, { + organizationId: useOrganizationId(), + }); + const risks = data.organization?.risks?.edges.map((edge) => edge.node); + const [editedRisk, setEditedRisk] = useState | null>( + null + ); + const treatmentLabels = useMemo(() => { + return { + MITIGATED: __("Mitigate"), + ACCEPTED: __("Accept"), + TRANSFERRED: __("Transfer"), + AVOIDED: __("Avoid"), + } as Record; + }, [__]); + const organizationId = useOrganizationId(); + + const [deleteRisk] = + useMutationWithToasts(deleteRiskMutation); + + const onDelete = (riskId: string) => { + const connectionId = ConnectionHandler.getConnectionID( + organizationId, + "RisksPage_risks" + ); + deleteRisk({ + variables: { + input: { riskId }, + connections: [connectionId], + }, + successMessage: __("Risk deleted successfully."), + errorMessage: __("Failed to delete risk. Please try again."), + }); + }; + + usePageTitle(__("Risks")); + return (
- {__("New Risk")}} /> + {editedRisk && ( + setEditedRisk(null)} + /> + )} + + + + + + + + + + + + + + {risks?.map((risk) => ( + + + + + + + + + + ))} + +
{__("Risk name")}{__("Category")}{__("Treatment")}{__("Initial Risk")}{__("Residual Risk")}{__("Severity")}
{risk.name}{risk.category}{treatmentLabels[risk.treatment]} + + + + + + + + setEditedRisk(risk)} + > + {__("Edit")} + + onDelete(risk.id)} + > + + {__("Delete")} + + + +
); } diff --git a/apps/console2/src/pages/organizations/risks/__generated__/FormRiskDialogMutation.graphql.ts b/apps/console2/src/pages/organizations/risks/__generated__/FormRiskDialogMutation.graphql.ts new file mode 100644 index 000000000..2e9ffb1b0 --- /dev/null +++ b/apps/console2/src/pages/organizations/risks/__generated__/FormRiskDialogMutation.graphql.ts @@ -0,0 +1,271 @@ +/** + * @generated SignedSource<<05dd3d535fb39d12fc0c015864a85d31>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type RiskTreatment = "ACCEPTED" | "AVOIDED" | "MITIGATED" | "TRANSFERRED" | "%future added value"; +export type CreateRiskInput = { + category: string; + description: string; + inherentImpact: number; + inherentLikelihood: number; + name: string; + note?: string | null | undefined; + organizationId: string; + ownerId?: string | null | undefined; + residualImpact?: number | null | undefined; + residualLikelihood?: number | null | undefined; + treatment: RiskTreatment; +}; +export type FormRiskDialogMutation$variables = { + connections: ReadonlyArray; + input: CreateRiskInput; +}; +export type FormRiskDialogMutation$data = { + readonly createRisk: { + readonly riskEdge: { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"useRiskFormFragment">; + }; + }; + }; +}; +export type FormRiskDialogMutation = { + response: FormRiskDialogMutation$data; + variables: FormRiskDialogMutation$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": "FormRiskDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateRiskPayload", + "kind": "LinkedField", + "name": "createRisk", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "RiskEdge", + "kind": "LinkedField", + "name": "riskEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "useRiskFormFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "FormRiskDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateRiskPayload", + "kind": "LinkedField", + "name": "createRisk", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "RiskEdge", + "kind": "LinkedField", + "name": "riskEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "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": "category", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "treatment", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentLikelihood", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentImpact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualLikelihood", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualImpact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "note", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "riskEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "e2131409b62d6cd6bd8a6d008f2ac1f5", + "id": null, + "metadata": {}, + "name": "FormRiskDialogMutation", + "operationKind": "mutation", + "text": "mutation FormRiskDialogMutation(\n $input: CreateRiskInput!\n) {\n createRisk(input: $input) {\n riskEdge {\n node {\n ...useRiskFormFragment\n id\n }\n }\n }\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "0429ad2b3af194de47f4b5af4231e22b"; + +export default node; diff --git a/apps/console2/src/pages/organizations/risks/__generated__/FormRiskDialogUpdateRiskMutation.graphql.ts b/apps/console2/src/pages/organizations/risks/__generated__/FormRiskDialogUpdateRiskMutation.graphql.ts new file mode 100644 index 000000000..c7b7130d3 --- /dev/null +++ b/apps/console2/src/pages/organizations/risks/__generated__/FormRiskDialogUpdateRiskMutation.graphql.ts @@ -0,0 +1,221 @@ +/** + * @generated SignedSource<<71792ccd18aa886a5df0ecfa8a23fda9>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type RiskTreatment = "ACCEPTED" | "AVOIDED" | "MITIGATED" | "TRANSFERRED" | "%future added value"; +export type UpdateRiskInput = { + category?: string | null | undefined; + description?: string | null | undefined; + id: string; + inherentImpact?: number | null | undefined; + inherentLikelihood?: number | null | undefined; + name?: string | null | undefined; + note?: string | null | undefined; + ownerId?: string | null | undefined; + residualImpact?: number | null | undefined; + residualLikelihood?: number | null | undefined; + treatment?: RiskTreatment | null | undefined; +}; +export type FormRiskDialogUpdateRiskMutation$variables = { + input: UpdateRiskInput; +}; +export type FormRiskDialogUpdateRiskMutation$data = { + readonly updateRisk: { + readonly risk: { + readonly " $fragmentSpreads": FragmentRefs<"useRiskFormFragment">; + }; + }; +}; +export type FormRiskDialogUpdateRiskMutation = { + response: FormRiskDialogUpdateRiskMutation$data; + variables: FormRiskDialogUpdateRiskMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FormRiskDialogUpdateRiskMutation", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateRiskPayload", + "kind": "LinkedField", + "name": "updateRisk", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "kind": "LinkedField", + "name": "risk", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "useRiskFormFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FormRiskDialogUpdateRiskMutation", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateRiskPayload", + "kind": "LinkedField", + "name": "updateRisk", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "kind": "LinkedField", + "name": "risk", + "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": "treatment", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentLikelihood", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentImpact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualLikelihood", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualImpact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "note", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "546b1e60a2e87bf71a1851ca76485767", + "id": null, + "metadata": {}, + "name": "FormRiskDialogUpdateRiskMutation", + "operationKind": "mutation", + "text": "mutation FormRiskDialogUpdateRiskMutation(\n $input: UpdateRiskInput!\n) {\n updateRisk(input: $input) {\n risk {\n ...useRiskFormFragment\n id\n }\n }\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "0299493042b2b5e7e464c991bab94375"; + +export default node; diff --git a/apps/console2/src/pages/organizations/risks/__generated__/RisksPageDeleteMutation.graphql.ts b/apps/console2/src/pages/organizations/risks/__generated__/RisksPageDeleteMutation.graphql.ts new file mode 100644 index 000000000..c1161a030 --- /dev/null +++ b/apps/console2/src/pages/organizations/risks/__generated__/RisksPageDeleteMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteRiskInput = { + riskId: string; +}; +export type RisksPageDeleteMutation$variables = { + connections: ReadonlyArray; + input: DeleteRiskInput; +}; +export type RisksPageDeleteMutation$data = { + readonly deleteRisk: { + readonly deletedRiskId: string; + }; +}; +export type RisksPageDeleteMutation = { + response: RisksPageDeleteMutation$data; + variables: RisksPageDeleteMutation$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": "deletedRiskId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "RisksPageDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteRiskPayload", + "kind": "LinkedField", + "name": "deleteRisk", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "RisksPageDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteRiskPayload", + "kind": "LinkedField", + "name": "deleteRisk", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedRiskId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "a08977dc1d81b5ab63e431014b2d2b41", + "id": null, + "metadata": {}, + "name": "RisksPageDeleteMutation", + "operationKind": "mutation", + "text": "mutation RisksPageDeleteMutation(\n $input: DeleteRiskInput!\n) {\n deleteRisk(input: $input) {\n deletedRiskId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "fb17a17779665063283d3d73fcb39785"; + +export default node; diff --git a/apps/console2/src/pages/organizations/risks/__generated__/RisksPageQuery.graphql.ts b/apps/console2/src/pages/organizations/risks/__generated__/RisksPageQuery.graphql.ts new file mode 100644 index 000000000..06add716f --- /dev/null +++ b/apps/console2/src/pages/organizations/risks/__generated__/RisksPageQuery.graphql.ts @@ -0,0 +1,441 @@ +/** + * @generated SignedSource<<5ce40300f73da895316795164a9bcf88>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type RiskTreatment = "ACCEPTED" | "AVOIDED" | "MITIGATED" | "TRANSFERRED" | "%future added value"; +export type RisksPageQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + first?: number | null | undefined; + last?: number | null | undefined; + organizationId: string; +}; +export type RisksPageQuery$data = { + readonly organization: { + readonly risks?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly category: string; + readonly id: string; + readonly inherentImpact: number; + readonly inherentLikelihood: number; + readonly inherentSeverity: number; + readonly name: string; + readonly residualImpact: number; + readonly residualLikelihood: number; + readonly treatment: RiskTreatment; + readonly " $fragmentSpreads": FragmentRefs<"useRiskFormFragment">; + }; + }>; + }; + }; +}; +export type RisksPageQuery = { + response: RisksPageQuery$data; + variables: RisksPageQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "first" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" +}, +v5 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v6 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v7 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v8 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "category", + "storageKey": null +}, +v9 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "treatment", + "storageKey": null +}, +v10 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentLikelihood", + "storageKey": null +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentImpact", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualLikelihood", + "storageKey": null +}, +v13 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualImpact", + "storageKey": null +}, +v14 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentSeverity", + "storageKey": null +}, +v15 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v16 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v17 = { + "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 +}, +v18 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "after" + }, + { + "kind": "Variable", + "name": "before", + "variableName": "before" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "first" + }, + { + "kind": "Variable", + "name": "last", + "variableName": "last" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "RisksPageQuery", + "selections": [ + { + "alias": "organization", + "args": (v5/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + { + "alias": "risks", + "args": null, + "concreteType": "RiskConnection", + "kind": "LinkedField", + "name": "__RisksPage_risks_connection", + "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": [ + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + (v13/*: any*/), + (v14/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "useRiskFormFragment" + }, + (v15/*: any*/) + ], + "storageKey": null + }, + (v16/*: any*/) + ], + "storageKey": null + }, + (v17/*: any*/) + ], + "storageKey": null + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v4/*: any*/), + (v2/*: any*/), + (v0/*: any*/), + (v3/*: any*/), + (v1/*: any*/) + ], + "kind": "Operation", + "name": "RisksPageQuery", + "selections": [ + { + "alias": "organization", + "args": (v5/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v15/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v18/*: any*/), + "concreteType": "RiskConnection", + "kind": "LinkedField", + "name": "risks", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "RiskEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v6/*: any*/), + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + (v11/*: any*/), + (v12/*: any*/), + (v13/*: any*/), + (v14/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "note", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v6/*: any*/) + ], + "storageKey": null + }, + (v15/*: any*/) + ], + "storageKey": null + }, + (v16/*: any*/) + ], + "storageKey": null + }, + (v17/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": (v18/*: any*/), + "filters": null, + "handle": "connection", + "key": "RisksPage_risks", + "kind": "LinkedHandle", + "name": "risks" + } + ], + "type": "Organization", + "abstractKey": null + }, + (v6/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "6bc1d32f53846f76f66d3dd065f5b038", + "id": null, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": [ + "organization", + "risks" + ] + } + ] + }, + "name": "RisksPageQuery", + "operationKind": "query", + "text": "query RisksPageQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n risks(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n category\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n inherentSeverity\n ...useRiskFormFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n }\n id\n }\n}\n\nfragment useRiskFormFragment on Risk {\n id\n name\n category\n description\n treatment\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n owner {\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "bf112f97f377b95f3b83ec949e5d5e2f"; + +export default node; diff --git a/apps/console2/src/pages/organizations/risks/__generated__/RisksPageUpdateRiskMutation.graphql.ts b/apps/console2/src/pages/organizations/risks/__generated__/RisksPageUpdateRiskMutation.graphql.ts new file mode 100644 index 000000000..af1bd5bcb --- /dev/null +++ b/apps/console2/src/pages/organizations/risks/__generated__/RisksPageUpdateRiskMutation.graphql.ts @@ -0,0 +1,220 @@ +/** + * @generated SignedSource<<78e5a6f1db510ffe28be99140539b234>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type RiskTreatment = "ACCEPTED" | "AVOIDED" | "MITIGATED" | "TRANSFERRED" | "%future added value"; +export type UpdateRiskInput = { + category?: string | null | undefined; + description?: string | null | undefined; + id: string; + inherentImpact?: number | null | undefined; + inherentLikelihood?: number | null | undefined; + name?: string | null | undefined; + note?: string | null | undefined; + ownerId?: string | null | undefined; + residualImpact?: number | null | undefined; + residualLikelihood?: number | null | undefined; + treatment?: RiskTreatment | null | undefined; +}; +export type RisksPageUpdateRiskMutation$variables = { + input: UpdateRiskInput; +}; +export type RisksPageUpdateRiskMutation$data = { + readonly updateRisk: { + readonly risk: { + readonly category: string; + readonly description: string; + readonly id: string; + readonly inherentImpact: number; + readonly inherentLikelihood: number; + readonly name: string; + readonly note: string; + readonly owner: { + readonly fullName: string; + readonly id: string; + } | null | undefined; + readonly residualImpact: number; + readonly residualLikelihood: number; + readonly treatment: RiskTreatment; + readonly updatedAt: any; + }; + }; +}; +export type RisksPageUpdateRiskMutation = { + response: RisksPageUpdateRiskMutation$data; + variables: RisksPageUpdateRiskMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateRiskPayload", + "kind": "LinkedField", + "name": "updateRisk", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "kind": "LinkedField", + "name": "risk", + "plural": false, + "selections": [ + (v1/*: 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": "inherentLikelihood", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentImpact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualLikelihood", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualImpact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "treatment", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "note", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "RisksPageUpdateRiskMutation", + "selections": (v2/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "RisksPageUpdateRiskMutation", + "selections": (v2/*: any*/) + }, + "params": { + "cacheID": "a3c421bf079967f96acf2215e72cf8a7", + "id": null, + "metadata": {}, + "name": "RisksPageUpdateRiskMutation", + "operationKind": "mutation", + "text": "mutation RisksPageUpdateRiskMutation(\n $input: UpdateRiskInput!\n) {\n updateRisk(input: $input) {\n risk {\n id\n name\n description\n category\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n treatment\n note\n updatedAt\n owner {\n id\n fullName\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "e3c8daa449e937742695adf8deb6dbe5"; + +export default node; diff --git a/apps/console2/src/pages/organizations/risks/forms/__generated__/useRiskFormFragment.graphql.ts b/apps/console2/src/pages/organizations/risks/forms/__generated__/useRiskFormFragment.graphql.ts new file mode 100644 index 000000000..9579ea90a --- /dev/null +++ b/apps/console2/src/pages/organizations/risks/forms/__generated__/useRiskFormFragment.graphql.ts @@ -0,0 +1,133 @@ +/** + * @generated SignedSource<<6cf0470d3c2e5d8d9d6ca52bf3375026>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type RiskTreatment = "ACCEPTED" | "AVOIDED" | "MITIGATED" | "TRANSFERRED" | "%future added value"; +import { FragmentRefs } from "relay-runtime"; +export type useRiskFormFragment$data = { + readonly category: string; + readonly description: string; + readonly id: string; + readonly inherentImpact: number; + readonly inherentLikelihood: number; + readonly name: string; + readonly note: string; + readonly owner: { + readonly id: string; + } | null | undefined; + readonly residualImpact: number; + readonly residualLikelihood: number; + readonly treatment: RiskTreatment; + readonly " $fragmentType": "useRiskFormFragment"; +}; +export type useRiskFormFragment$key = { + readonly " $data"?: useRiskFormFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"useRiskFormFragment">; +}; + +const node: ReaderFragment = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "useRiskFormFragment", + "selections": [ + (v0/*: 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": "treatment", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentLikelihood", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "inherentImpact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualLikelihood", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "residualImpact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "note", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "owner", + "plural": false, + "selections": [ + (v0/*: any*/) + ], + "storageKey": null + } + ], + "type": "Risk", + "abstractKey": null +}; +})(); + +(node as any).hash = "7ad9386b4049af3f139d7269bb5229c2"; + +export default node; diff --git a/apps/console2/src/pages/organizations/risks/forms/useRiskForm.tsx b/apps/console2/src/pages/organizations/risks/forms/useRiskForm.tsx index 6dce2bcf3..fa7b509ee 100644 --- a/apps/console2/src/pages/organizations/risks/forms/useRiskForm.tsx +++ b/apps/console2/src/pages/organizations/risks/forms/useRiskForm.tsx @@ -1,38 +1,77 @@ import { z } from "zod"; import { useFormWithSchema } from "../../../../hooks/useFormWithSchema"; +import { graphql } from "relay-runtime"; +import type { + useRiskFormFragment$data, + useRiskFormFragment$key, +} from "./__generated__/useRiskFormFragment.graphql"; +import { useFragment } from "react-relay"; + +const RiskFragment = graphql` + fragment useRiskFormFragment on Risk { + id + name + category + description + treatment + inherentLikelihood + inherentImpact + residualLikelihood + residualImpact + note + owner { + id + } + } +`; + +export type RiskNode = useRiskFormFragment$data; +export type RiskKey = useRiskFormFragment$key & { id: string }; // Export the schema so it can be used elsewhere export const riskSchema = z.object({ category: z.string(), name: z.string(), description: z.string(), - organizationId: z.string(), ownerId: z.string(), - treatment: z.enum(["AVOIDED", "MITIGATED", "TRANSFERRED", "ACCEPTED"]), - inherentLikelihood: z.number().min(1).max(5), - inherentImpact: z.number().min(1).max(5), - residualLikelihood: z.number().min(1).max(5), - residualImpact: z.number().min(1).max(5), + treatment: z.enum([ + "AVOIDED", + "MITIGATED", + "TRANSFERRED", + "ACCEPTED", + "%future added value", + ]), + inherentLikelihood: z.number({ coerce: true }).min(1).max(5), + inherentImpact: z.number({ coerce: true }).min(1).max(5), + residualLikelihood: z.number({ coerce: true }).min(1).max(5), + residualImpact: z.number({ coerce: true }).min(1).max(5), note: z.string(), }); -export const useRiskForm = (organizationId: string) => { +export const useRiskForm = (riskKey?: RiskKey) => { + const risk = useFragment(RiskFragment, riskKey); return useFormWithSchema(riskSchema, { - defaultValues: { - category: "Compliance & Legal", - name: "Contractual risk due to poorly drafted agreements", - description: - "Weak contracts leads to disputes, missed deliverables, or revenue leakage", - organizationId: organizationId, - ownerId: "a_YJLJ5RAAIACAAAAZbuSmzPhFPhoMEQ", - treatment: "MITIGATED", - inherentLikelihood: 5, - inherentImpact: 5, - residualLikelihood: 5, - residualImpact: 5, - note: "Hello worlds this is a test", - }, + defaultValues: risk + ? { + ...risk, + ownerId: risk.owner?.id, + } + : { + category: "Compliance & Legal", + name: "Contractual risk due to poorly drafted agreements", + description: + "Weak contracts leads to disputes, missed deliverables, or revenue leakage", + ownerId: "a_YJLJ5RAAIACAAAAZbuSmzPhFPhoMEQ", + treatment: "MITIGATED", + inherentLikelihood: 5, + inherentImpact: 5, + residualLikelihood: 5, + residualImpact: 5, + note: "Hello worlds this is a test", + }, }); }; export type RiskForm = ReturnType; + +export type RiskData = z.infer; diff --git a/apps/console2/src/routes.tsx b/apps/console2/src/routes.tsx index 660adf21e..7c87c2639 100644 --- a/apps/console2/src/routes.tsx +++ b/apps/console2/src/routes.tsx @@ -71,11 +71,12 @@ const routes = [ */ function routeTransformer(route: Route): RouteObject { if ("fallback" in route && route.fallback) { + const fallback = ; return { ...route, children: route.children?.map(routeTransformer), Component: () => ( - }> + ), diff --git a/apps/console2/src/types.ts b/apps/console2/src/types.ts new file mode 100644 index 000000000..eb2e6dd79 --- /dev/null +++ b/apps/console2/src/types.ts @@ -0,0 +1,7 @@ +export type NodeOf = T extends + | { readonly edges: ReadonlyArray<{ readonly node: infer U }> } + | undefined + ? U + : never; + +export type ItemOf = T extends (infer U)[] ? U : never; diff --git a/bun.lock b/bun.lock index 36ffdf99e..5e3822cef 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^5.0.1", + "@probo/helpers": "1.0.0", "@probo/hooks": "1.0.0", "@probo/i18n": "1.0.0", "@probo/ui": "1.0.0", @@ -88,10 +89,10 @@ "name": "@probo/i18n", "version": "1.0.0", "dependencies": { - "date-fns": "^3.6.0", + "date-fns": "^4.1.0", }, "devDependencies": { - "@types/react": "^18.2.78", + "@types/react": "^19.1.2", }, "peerDependencies": { "react": "^18.0", @@ -119,6 +120,7 @@ "@probo/helpers": "1.0.0", "@probo/i18n": "1.0.0", "@radix-ui/react-dropdown-menu": "^2.1.14", + "@radix-ui/react-portal": "^1.1.9", "@radix-ui/react-scroll-area": "^1.2.9", "@tailwindcss/vite": "^4.1.7", "class-variance-authority": "^0.7.1", @@ -574,9 +576,7 @@ "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], - "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], - - "@types/react": ["@types/react@18.3.21", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw=="], + "@types/react": ["@types/react@19.1.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g=="], "@types/react-dom": ["@types/react-dom@19.1.5", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg=="], @@ -718,7 +718,7 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], @@ -1310,9 +1310,9 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@3.25.17", "", {}, "sha512-8hQzQ/kMOIFbwOgPrm9Sf9rtFHpFUMy4HvN0yEB0spw14aYi0uT5xG5CE2DB9cd51GWNsz+DNO7se1kztHMKnw=="], + "zod": ["zod@3.25.20", "", {}, "sha512-z03fqpTMDF1G02VLKUMt6vyACE7rNWkh3gpXVHgPTw28NPtDFRGvcpTtPwn2kMKtQ0idtYJUTxchytmnqYswcw=="], - "zustand": ["zustand@5.0.4", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ=="], + "zustand": ["zustand@5.0.5", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg=="], "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], @@ -1324,8 +1324,6 @@ "@joshwooding/vite-plugin-react-docgen-typescript/magic-string": ["magic-string@0.27.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.13" } }, "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA=="], - "@probo/ui/@types/react": ["@types/react@19.1.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g=="], - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@storybook/core/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -1352,10 +1350,6 @@ "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], - "@types/react-dom/@types/react": ["@types/react@19.1.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g=="], - - "@types/react-relay/@types/react": ["@types/react@19.1.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1370,8 +1364,6 @@ "caller-callsite/callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], - "console2/@types/react": ["@types/react@19.1.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g=="], - "cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="], "cosmiconfig/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index a675cb8ee..73bf5db5b 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -1 +1,2 @@ export { objectKeys } from "./object"; +export { sprintf } from "./string"; diff --git a/packages/helpers/src/string.test.ts b/packages/helpers/src/string.test.ts new file mode 100644 index 000000000..e2955e77b --- /dev/null +++ b/packages/helpers/src/string.test.ts @@ -0,0 +1,10 @@ +import { describe, it, expect } from "vitest"; +import { sprintf } from "./string"; + +describe("strings", () => { + it("should format a string", () => { + expect(sprintf("Hello %s", "world")).toBe("Hello world"); + expect(sprintf("Hello %s %s", "John", "Doe")).toBe("Hello John Doe"); + expect(sprintf("%2s %1s", "world", "Hello")).toBe("Hello world"); + }); +}); diff --git a/packages/helpers/src/string.ts b/packages/helpers/src/string.ts new file mode 100644 index 000000000..fe10c6c82 --- /dev/null +++ b/packages/helpers/src/string.ts @@ -0,0 +1,9 @@ +export function sprintf(str: string, ...params: unknown[]): string { + let i = 0; + return str.replace(/%(\d+)?s/g, (_, ...args) => { + if (args[0]) { + return params[parseInt(args[0]) - 1] as string; + } + return params[i++] as string; + }); +} diff --git a/packages/i18n/package.json b/packages/i18n/package.json index a3369910d..4a3cff806 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -11,9 +11,9 @@ "author": "", "license": "ISC", "devDependencies": { - "@types/react": "^18.2.78" + "@types/react": "^19.1.2" }, "dependencies": { - "date-fns": "^3.6.0" + "date-fns": "^4.1.0" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 84a7470e8..06e0d7cba 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -15,6 +15,7 @@ "@probo/helpers": "1.0.0", "@probo/i18n": "1.0.0", "@radix-ui/react-dropdown-menu": "^2.1.14", + "@radix-ui/react-portal": "^1.1.9", "@radix-ui/react-scroll-area": "^1.2.9", "@tailwindcss/vite": "^4.1.7", "class-variance-authority": "^0.7.1", diff --git a/packages/ui/src/Atoms/Badge/Badge.stories.tsx b/packages/ui/src/Atoms/Badge/Badge.stories.tsx new file mode 100644 index 000000000..952cf0da5 --- /dev/null +++ b/packages/ui/src/Atoms/Badge/Badge.stories.tsx @@ -0,0 +1,31 @@ +import { Badge } from "./Badge"; +import type { Meta, StoryObj } from "@storybook/react"; + +export default { + title: "Atoms/Badge", + component: Badge, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + <> + {( + [ + "success", + "warning", + "danger", + "info", + "neutral", + "outline", + ] as const + ).map((variant) => ( + + {variant} + + ))} + + ), +}; diff --git a/packages/ui/src/Atoms/Badge/Badge.tsx b/packages/ui/src/Atoms/Badge/Badge.tsx new file mode 100644 index 000000000..fe219b62a --- /dev/null +++ b/packages/ui/src/Atoms/Badge/Badge.tsx @@ -0,0 +1,24 @@ +import type { HTMLAttributes } from "react"; +import { tv } from "tailwind-variants"; + +type Props = { + variant?: "success" | "warning" | "danger" | "info" | "neutral" | "outline"; +} & HTMLAttributes; + +const badge = tv({ + base: "text-xs font-medium py-[2px] px-[6px] rounded-lg w-max", + variants: { + variant: { + success: "bg-success text-txt-success", + warning: "bg-warning text-txt-warning", + danger: "bg-danger text-txt-danger", + info: "bg-info text-txt-info", + neutral: "bg-subtle text-txt-secondary", + outline: "text-txt-tertiary border border-border-low", + }, + }, +}); + +export function Badge(props: Props) { + return
; +} diff --git a/packages/ui/src/Atoms/Dropdown/Dropdown.tsx b/packages/ui/src/Atoms/Dropdown/Dropdown.tsx index 858d433b4..678844436 100644 --- a/packages/ui/src/Atoms/Dropdown/Dropdown.tsx +++ b/packages/ui/src/Atoms/Dropdown/Dropdown.tsx @@ -1,8 +1,10 @@ -import type { FC, PropsWithChildren, ReactNode } from "react"; +import type { ComponentProps, FC, PropsWithChildren, ReactNode } from "react"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { clsx } from "clsx"; import type { IconProps } from "../Icons/type.ts"; import { tv } from "tailwind-variants"; +import { IconDotGrid1x3Horizontal } from "../Icons/IconDotGrid1x3Horizontal.tsx"; +import { Button } from "../Button/Button.tsx"; type Props = PropsWithChildren<{ toggle: ReactNode; @@ -38,7 +40,8 @@ type DropdownItemProps = PropsWithChildren<{ icon?: FC; asChild?: boolean; variant?: "primary" | "tertiary" | "danger"; -}>; +}> & + ComponentProps; const dropdownItem = tv({ base: "text-txt-primary flex items-center gap-2 hover:bg-tertiary-hover active:bg-tertiary-pressed cursor-pointer p-2", @@ -54,15 +57,28 @@ const dropdownItem = tv({ }, }); +export function ActionDropdown(props: Omit) { + return ( + + } + /> + ); +} + export function DropdownItem({ icon: IconComponent, variant, className, children, asChild, + ...props }: DropdownItemProps) { return ( diff --git a/packages/ui/src/Atoms/Select/Select.tsx b/packages/ui/src/Atoms/Select/Select.tsx index 160f3a964..4da51bd9f 100644 --- a/packages/ui/src/Atoms/Select/Select.tsx +++ b/packages/ui/src/Atoms/Select/Select.tsx @@ -85,6 +85,9 @@ const findSelectedOption = ( children: unknown[], condition: (c: { props: Record }) => boolean, ): ReactNode => { + if (children.length === 1) { + debugger; + } const selectedOptions = children.find( // @ts-expect-error We know that the children are ReactElements with props (c) => isValidElement(c) && condition(c), @@ -109,7 +112,7 @@ export function Select({ (c) => c.props.value?.toString() === value?.toString(), ) : undefined; - + console.log(childrenArr, valueNode); return ( diff --git a/packages/ui/src/Atoms/Table/Table.stories.tsx b/packages/ui/src/Atoms/Table/Table.stories.tsx new file mode 100644 index 000000000..81a43931a --- /dev/null +++ b/packages/ui/src/Atoms/Table/Table.stories.tsx @@ -0,0 +1,12 @@ +import { Table } from "./Table"; +import type { Meta, StoryObj } from "@storybook/react"; + +export default { + title: "Atoms/Table", + component: Table, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/ui/src/Atoms/Table/Table.tsx b/packages/ui/src/Atoms/Table/Table.tsx new file mode 100644 index 000000000..953adddcb --- /dev/null +++ b/packages/ui/src/Atoms/Table/Table.tsx @@ -0,0 +1,64 @@ +import { createContext, useContext, type PropsWithChildren } from "react"; +import { Card } from "../Card/Card"; +import { Link } from "react-router"; +import clsx from "clsx"; + +export function Table({ children }: PropsWithChildren) { + return ( + + {children}
+
+ ); +} + +export function Thead({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} + +export function Th({ children }: PropsWithChildren) { + return {children}; +} + +const TrContext = createContext({} as { to?: string }); + +export function Tr({ children, to }: PropsWithChildren<{ to?: string }>) { + return ( + + + {children} + + + ); +} + +export function Tbody({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} + +export function Td({ + children, + noLink, +}: PropsWithChildren<{ noLink?: boolean }>) { + const { to } = useContext(TrContext); + if (!to || noLink) { + return {children}; + } + return ( + + {children} + + ); +} diff --git a/packages/ui/src/Molecules/Badge/RiskBadge.stories.tsx b/packages/ui/src/Molecules/Badge/RiskBadge.stories.tsx new file mode 100644 index 000000000..b2452d9f7 --- /dev/null +++ b/packages/ui/src/Molecules/Badge/RiskBadge.stories.tsx @@ -0,0 +1,20 @@ +import { RiskBadge } from "./RiskBadge"; +import type { Meta, StoryObj } from "@storybook/react"; + +export default { + title: "Molecules/Badges/RiskBadge", + component: RiskBadge, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( +
+ + + +
+ ), +}; diff --git a/packages/ui/src/Molecules/Badge/RiskBadge.tsx b/packages/ui/src/Molecules/Badge/RiskBadge.tsx new file mode 100644 index 000000000..0a07b79f2 --- /dev/null +++ b/packages/ui/src/Molecules/Badge/RiskBadge.tsx @@ -0,0 +1,30 @@ +import { useTranslate } from "@probo/i18n"; +import { Badge } from "../../Atoms/Badge/Badge"; + +type Props = { + score: number; +}; + +const badgeVariant = (score: number) => { + if (score >= 15) { + return "danger"; + } + if (score >= 8) { + return "warning"; + } + return "success"; +}; + +export function RiskBadge({ score }: Props) { + const { __ } = useTranslate(); + const label = () => { + if (score >= 15) { + return __("High"); + } + if (score >= 8) { + return __("Medium"); + } + return __("Low"); + }; + return {label()}; +} diff --git a/packages/ui/src/Molecules/Badge/SeverityBadge.stories.tsx b/packages/ui/src/Molecules/Badge/SeverityBadge.stories.tsx new file mode 100644 index 000000000..919d04ddc --- /dev/null +++ b/packages/ui/src/Molecules/Badge/SeverityBadge.stories.tsx @@ -0,0 +1,21 @@ +import { SeverityBadge } from "./SeverityBadge.tsx"; +import type { Meta, StoryObj } from "@storybook/react"; + +export default { + title: "Molecules/Badges/SeverityBadge", + component: SeverityBadge, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( +
+ + + + +
+ ), +}; diff --git a/packages/ui/src/Molecules/Badge/SeverityBadge.tsx b/packages/ui/src/Molecules/Badge/SeverityBadge.tsx new file mode 100644 index 000000000..90b13c333 --- /dev/null +++ b/packages/ui/src/Molecules/Badge/SeverityBadge.tsx @@ -0,0 +1,36 @@ +import { useTranslate } from "@probo/i18n"; +import { Badge } from "../../Atoms/Badge/Badge.tsx"; + +type Props = { + severity: number; +}; + +const badgeVariant = (severity: number) => { + if (severity >= 75) { + return "danger"; + } + if (severity >= 50) { + return "warning"; + } + if (severity >= 25) { + return "info"; + } + return "success"; +}; + +export function SeverityBadge({ severity }: Props) { + const { __ } = useTranslate(); + const label = () => { + if (severity >= 75) { + return __("Critical"); + } + if (severity >= 50) { + return __("High"); + } + if (severity >= 25) { + return __("Low"); + } + return __("None"); + }; + return {label()}; +} diff --git a/packages/ui/src/Molecules/Dialog/ConfirmDialog.stories.tsx b/packages/ui/src/Molecules/Dialog/ConfirmDialog.stories.tsx new file mode 100644 index 000000000..09d45faf1 --- /dev/null +++ b/packages/ui/src/Molecules/Dialog/ConfirmDialog.stories.tsx @@ -0,0 +1,28 @@ +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 ( + {}} + > + + + ); + }, +}; diff --git a/packages/ui/src/Molecules/Dialog/ConfirmDialog.tsx b/packages/ui/src/Molecules/Dialog/ConfirmDialog.tsx new file mode 100644 index 000000000..af4376348 --- /dev/null +++ b/packages/ui/src/Molecules/Dialog/ConfirmDialog.tsx @@ -0,0 +1,66 @@ +import { useTranslate } from "@probo/i18n"; +import { Dialog, DialogContent, DialogFooter } from "./Dialog"; +import { + Children, + cloneElement, + isValidElement, + useState, + type ComponentProps, + type MouseEvent, + type ReactNode, +} from "react"; +import { Button } from "../../Atoms/Button/Button"; +import { Root as Portal } from "@radix-ui/react-portal"; + +type Props = { + children?: ReactNode; + title?: ReactNode; + message: string; + variant?: ComponentProps["variant"]; + label?: string; + onConfirm?: () => void; +}; + +export function ConfirmDialog(props: Props) { + const { __ } = useTranslate(); + const title = props.title ?? __("Are you sure ?"); + const variant = props.variant ?? "danger"; + const label = variant === "danger" ? __("Delete") : props.label; + const [open, setOpen] = useState(false); + const onConfirm = () => { + setOpen(false); + props.onConfirm?.(); + document.body.click(); + }; + const children = Children.map(props.children, (child) => { + if (!isValidElement(child)) { + throw new Error("Cannot use non element children"); + } + return cloneElement(child, { + // @ts-expect-error We inject a handler on an unknown element + onClick: (e: MouseEvent) => { + e.preventDefault(); + setOpen(true); + }, + }); + }); + return ( + <> + {children} + + + +

+ {props.message} +

+
+ + + +
+
+ + ); +} diff --git a/packages/ui/src/Molecules/Dialog/Dialog.tsx b/packages/ui/src/Molecules/Dialog/Dialog.tsx index d8a5c56a4..1e8b37850 100644 --- a/packages/ui/src/Molecules/Dialog/Dialog.tsx +++ b/packages/ui/src/Molecules/Dialog/Dialog.tsx @@ -14,7 +14,7 @@ import { useTranslate } from "@probo/i18n"; import clsx from "clsx"; type Props = { - trigger: ReactNode; + trigger?: ReactNode; title: ReactNode; children?: ReactNode; onOpenChange?: (open: boolean) => void; @@ -30,7 +30,7 @@ export function Dialog({ }: Props) { return ( - {trigger} + {trigger && {trigger}} = { type Props = | BaseProps> | BaseProps<"text", ComponentProps> + | BaseProps<"email", ComponentProps> + | BaseProps<"password", ComponentProps> | BaseProps<"textarea", ComponentProps> | BaseProps<"select", ComponentProps>; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 4e82b7532..c9a1efce5 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -16,6 +16,7 @@ export { Card } from "./Atoms/Card/Card"; export { Breadcrumb } from "./Atoms/Breadcrumb/Breadcrumb"; export { Dropdown, + ActionDropdown, DropdownSeparator, DropdownItem, } from "./Atoms/Dropdown/Dropdown"; @@ -26,6 +27,7 @@ export { Textarea } from "./Atoms/Textarea/Textarea.tsx"; export { Select, Option } from "./Atoms/Select/Select.tsx"; export { Label } from "./Atoms/Label/Label"; export { PropertyRow } from "./Atoms/PropertyRow/PropertyRow"; +export { Table, Thead, Tr, Tbody, Td, Th } from "./Atoms/Table/Table"; // Molecules export { @@ -35,6 +37,9 @@ export { export { PageHeader } from "./Molecules/PageHeader/PageHeader"; export { Skeleton } from "./Atoms/Skeleton/Skeleton"; export { Dialog, DialogContent, DialogFooter } from "./Molecules/Dialog/Dialog"; +export { RiskBadge } from "./Molecules/Badge/RiskBadge"; +export { SeverityBadge } from "./Molecules/Badge/SeverityBadge.tsx"; +export { ConfirmDialog } from "./Molecules/Dialog/ConfirmDialog.tsx"; // Hooks export { useToast, Toasts } from "./Atoms/Toasts/Toasts"; diff --git a/packages/ui/tsconfig.app.json b/packages/ui/tsconfig.app.json index 6e3061836..068b7fe56 100644 --- a/packages/ui/tsconfig.app.json +++ b/packages/ui/tsconfig.app.json @@ -23,6 +23,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"], + "include": ["src", "../../apps/console2/src/types.ts"], "exclude": ["src/Atoms/Icons/generator.ts"] }