From c2a69b54281f86db2c7591712e5c49fa36cd605a Mon Sep 17 00:00:00 2001 From: Jonathan Date: Thu, 22 May 2025 21:29:42 +0200 Subject: [PATCH] Add vendor creation Signed-off-by: Bryan Frimin Signed-off-by: Sacha Al Himdani --- apps/console2/package.json | 6 +- .../src/components/form/PeopleSelect.tsx | 1 + .../Risks.ts => graph/RiskGraph.ts} | 9 +- apps/console2/src/graph/VendorGraph.ts | 60 +++ .../RiskGraphDeleteMutation.graphql.ts} | 24 +- .../VendorGraphCreateMutation.graphql.ts | 214 +++++++++++ .../VendorGraphDeleteMutation.graphql.ts | 132 +++++++ apps/console2/src/hooks/useVendorSearch.ts | 33 ++ .../organizations/risks/FormRiskDialog.tsx | 13 +- .../organizations/risks/RiskDetailPage.tsx | 2 +- .../pages/organizations/risks/RisksPage.tsx | 28 +- .../__generated__/RisksPageQuery.graphql.ts | 29 +- .../organizations/risks/forms/useRiskForm.tsx | 16 +- .../vendors/CreateVendorDialog.tsx | 95 +++++ .../organizations/vendors/VendorsPage.tsx | 199 ++++++++++ .../__generated__/VendorsPageQuery.graphql.ts | 362 ++++++++++++++++++ apps/console2/src/routes.tsx | 11 +- packages/helpers/src/array.test.ts | 12 + packages/helpers/src/array.ts | 10 + packages/helpers/src/index.ts | 4 +- packages/helpers/src/string.ts | 13 + packages/i18n/TranslatorProvider.tsx | 28 ++ packages/ui/.storybook/preview.tsx | 4 +- packages/ui/package.json | 1 + packages/ui/src/Atoms/Badge/Badge.tsx | 2 +- packages/ui/src/Atoms/Button/Button.tsx | 2 +- packages/ui/src/Atoms/Dropdown/Dropdown.tsx | 25 +- packages/ui/src/Atoms/Input/Input.tsx | 2 +- packages/ui/src/Atoms/Select/Select.tsx | 3 - .../ui/src/Atoms/Spinner/Spinner.stories.tsx | 12 + packages/ui/src/Atoms/Spinner/Spinner.tsx | 12 + .../src/Molecules/Badge/RiskBadge.stories.tsx | 6 +- packages/ui/src/Molecules/Badge/RiskBadge.tsx | 58 ++- .../Molecules/Combobox/Combobox.stories.tsx | 37 ++ .../ui/src/Molecules/Combobox/Combobox.tsx | 46 +++ .../ui/src/Molecules/Dialog/ConfirmDialog.tsx | 7 +- packages/ui/src/Molecules/Dialog/Dialog.tsx | 9 +- packages/ui/src/index.ts | 2 + 38 files changed, 1426 insertions(+), 103 deletions(-) rename apps/console2/src/{mutations/Risks.ts => graph/RiskGraph.ts} (65%) create mode 100644 apps/console2/src/graph/VendorGraph.ts rename apps/console2/src/{mutations/__generated__/RisksDeleteMutation.graphql.ts => graph/__generated__/RiskGraphDeleteMutation.graphql.ts} (77%) create mode 100644 apps/console2/src/graph/__generated__/VendorGraphCreateMutation.graphql.ts create mode 100644 apps/console2/src/graph/__generated__/VendorGraphDeleteMutation.graphql.ts create mode 100644 apps/console2/src/hooks/useVendorSearch.ts create mode 100644 apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx create mode 100644 apps/console2/src/pages/organizations/vendors/VendorsPage.tsx create mode 100644 apps/console2/src/pages/organizations/vendors/__generated__/VendorsPageQuery.graphql.ts create mode 100644 packages/helpers/src/array.test.ts create mode 100644 packages/ui/src/Atoms/Spinner/Spinner.stories.tsx create mode 100644 packages/ui/src/Atoms/Spinner/Spinner.tsx create mode 100644 packages/ui/src/Molecules/Combobox/Combobox.stories.tsx create mode 100644 packages/ui/src/Molecules/Combobox/Combobox.tsx diff --git a/apps/console2/package.json b/apps/console2/package.json index 1ae3cbcf2..9dbd4846a 100644 --- a/apps/console2/package.json +++ b/apps/console2/package.json @@ -6,21 +6,23 @@ "scripts": { "dev": "vite", "relay": "npx relay-compiler", - "build": "tsc -b && vite build", + "build": "vite build", "lint": "eslint .", "check": "tsc --noEmit -p tsconfig.app.json", "preview": "vite preview" }, "dependencies": { "@hookform/resolvers": "^5.0.1", - "@probo/hooks": "1.0.0", "@probo/helpers": "1.0.0", + "@probo/hooks": "1.0.0", "@probo/i18n": "1.0.0", "@probo/ui": "1.0.0", + "@probo/vendors": "0.0.1", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.5", "@tanstack/react-query": "^5.76.1", + "minisearch": "^7.1.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-error-boundary": "^6.0.0", diff --git a/apps/console2/src/components/form/PeopleSelect.tsx b/apps/console2/src/components/form/PeopleSelect.tsx index 477cd5adb..a44d69a9d 100644 --- a/apps/console2/src/components/form/PeopleSelect.tsx +++ b/apps/console2/src/components/form/PeopleSelect.tsx @@ -71,6 +71,7 @@ function PeopleSelectWithQuery({ id={name} variant="editor" placeholder={__("Select an owner")} + onValueChange={field.onChange} {...field} > {people?.map((p) => ( diff --git a/apps/console2/src/mutations/Risks.ts b/apps/console2/src/graph/RiskGraph.ts similarity index 65% rename from apps/console2/src/mutations/Risks.ts rename to apps/console2/src/graph/RiskGraph.ts index 2f4fe511b..a211907e7 100644 --- a/apps/console2/src/mutations/Risks.ts +++ b/apps/console2/src/graph/RiskGraph.ts @@ -1,10 +1,13 @@ import { graphql } from "relay-runtime"; import { useTranslate } from "@probo/i18n"; import { useMutationWithToasts } from "../hooks/useMutationWithToasts"; -import type { RisksDeleteMutation } from "./__generated__/RisksDeleteMutation.graphql"; +import type { RiskGraphDeleteMutation } from "./__generated__/RiskGraphDeleteMutation.graphql"; const deleteRiskMutation = graphql` - mutation RisksDeleteMutation($input: DeleteRiskInput!, $connections: [ID!]!) { + mutation RiskGraphDeleteMutation( + $input: DeleteRiskInput! + $connections: [ID!]! + ) { deleteRisk(input: $input) { deletedRiskId @deleteEdge(connections: $connections) } @@ -14,7 +17,7 @@ const deleteRiskMutation = graphql` export function useDeleteRiskMutation() { const { __ } = useTranslate(); - return useMutationWithToasts(deleteRiskMutation, { + return useMutationWithToasts(deleteRiskMutation, { successMessage: __("Risk deleted successfully."), errorMessage: __("Failed to delete risk. Please try again."), }); diff --git a/apps/console2/src/graph/VendorGraph.ts b/apps/console2/src/graph/VendorGraph.ts new file mode 100644 index 000000000..11a5aa8f3 --- /dev/null +++ b/apps/console2/src/graph/VendorGraph.ts @@ -0,0 +1,60 @@ +import { useTranslate } from "@probo/i18n"; +import { graphql } from "relay-runtime"; +import { useMutationWithToasts } from "../hooks/useMutationWithToasts"; +import type { VendorGraphCreateMutation } from "./__generated__/VendorGraphCreateMutation.graphql"; +import type { VendorGraphDeleteMutation } from "./__generated__/VendorGraphDeleteMutation.graphql"; + +const createVendorMutation = graphql` + mutation VendorGraphCreateMutation( + $input: CreateVendorInput! + $connections: [ID!]! + ) { + createVendor(input: $input) { + vendorEdge @prependEdge(connections: $connections) { + node { + id + name + description + websiteUrl + createdAt + updatedAt + } + } + } + } +`; + +export function useCreateVendorMutation() { + const { __ } = useTranslate(); + + return useMutationWithToasts( + createVendorMutation, + { + successMessage: __("Vendor created successfully."), + errorMessage: __("Failed to create vendor. Please try again."), + } + ); +} + +const deleteVendorMutation = graphql` + mutation VendorGraphDeleteMutation( + $input: DeleteVendorInput! + $connections: [ID!]! + ) { + deleteVendor(input: $input) { + deletedVendorId @deleteEdge(connections: $connections) + } + } +`; + +export function useDeleteVendorMutation() { + const { __ } = useTranslate(); + + return useMutationWithToasts( + deleteVendorMutation, + { + successMessage: __("Vendor deleted successfully."), + errorMessage: __("Failed to delete vendor. Please try again."), + } + ); +} diff --git a/apps/console2/src/mutations/__generated__/RisksDeleteMutation.graphql.ts b/apps/console2/src/graph/__generated__/RiskGraphDeleteMutation.graphql.ts similarity index 77% rename from apps/console2/src/mutations/__generated__/RisksDeleteMutation.graphql.ts rename to apps/console2/src/graph/__generated__/RiskGraphDeleteMutation.graphql.ts index b0047a5cc..6288dd970 100644 --- a/apps/console2/src/mutations/__generated__/RisksDeleteMutation.graphql.ts +++ b/apps/console2/src/graph/__generated__/RiskGraphDeleteMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<5102c0f9ea660855dc5db7c6b16fd64b>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime'; export type DeleteRiskInput = { riskId: string; }; -export type RisksDeleteMutation$variables = { +export type RiskGraphDeleteMutation$variables = { connections: ReadonlyArray; input: DeleteRiskInput; }; -export type RisksDeleteMutation$data = { +export type RiskGraphDeleteMutation$data = { readonly deleteRisk: { readonly deletedRiskId: string; }; }; -export type RisksDeleteMutation = { - response: RisksDeleteMutation$data; - variables: RisksDeleteMutation$variables; +export type RiskGraphDeleteMutation = { + response: RiskGraphDeleteMutation$data; + variables: RiskGraphDeleteMutation$variables; }; const node: ConcreteRequest = (function(){ @@ -59,7 +59,7 @@ return { ], "kind": "Fragment", "metadata": null, - "name": "RisksDeleteMutation", + "name": "RiskGraphDeleteMutation", "selections": [ { "alias": null, @@ -84,7 +84,7 @@ return { (v0/*: any*/) ], "kind": "Operation", - "name": "RisksDeleteMutation", + "name": "RiskGraphDeleteMutation", "selections": [ { "alias": null, @@ -117,16 +117,16 @@ return { ] }, "params": { - "cacheID": "f4273ec8d2cd76543169a6d1b7cd72b3", + "cacheID": "57b2b12cd623a9c0a99bc5d13284e21e", "id": null, "metadata": {}, - "name": "RisksDeleteMutation", + "name": "RiskGraphDeleteMutation", "operationKind": "mutation", - "text": "mutation RisksDeleteMutation(\n $input: DeleteRiskInput!\n) {\n deleteRisk(input: $input) {\n deletedRiskId\n }\n}\n" + "text": "mutation RiskGraphDeleteMutation(\n $input: DeleteRiskInput!\n) {\n deleteRisk(input: $input) {\n deletedRiskId\n }\n}\n" } }; })(); -(node as any).hash = "ad609ad0f9c749fe47f2bd0d382777bb"; +(node as any).hash = "a930aa55cb8dcca220dfb26783550877"; export default node; diff --git a/apps/console2/src/graph/__generated__/VendorGraphCreateMutation.graphql.ts b/apps/console2/src/graph/__generated__/VendorGraphCreateMutation.graphql.ts new file mode 100644 index 000000000..417e9b44d --- /dev/null +++ b/apps/console2/src/graph/__generated__/VendorGraphCreateMutation.graphql.ts @@ -0,0 +1,214 @@ +/** + * @generated SignedSource<<37dd4db659813ffa6fab65ffec643844>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type CreateVendorInput = { + businessOwnerId?: string | null | undefined; + category?: string | null | undefined; + certifications?: ReadonlyArray | null | undefined; + dataProcessingAgreementUrl?: string | null | undefined; + description?: string | null | undefined; + headquarterAddress?: string | null | undefined; + legalName?: string | null | undefined; + name: string; + organizationId: string; + privacyPolicyUrl?: string | null | undefined; + securityOwnerId?: string | null | undefined; + securityPageUrl?: string | null | undefined; + serviceLevelAgreementUrl?: string | null | undefined; + statusPageUrl?: string | null | undefined; + termsOfServiceUrl?: string | null | undefined; + trustPageUrl?: string | null | undefined; + websiteUrl?: string | null | undefined; +}; +export type VendorGraphCreateMutation$variables = { + connections: ReadonlyArray; + input: CreateVendorInput; +}; +export type VendorGraphCreateMutation$data = { + readonly createVendor: { + readonly vendorEdge: { + readonly node: { + readonly createdAt: any; + readonly description: string | null | undefined; + readonly id: string; + readonly name: string; + readonly updatedAt: any; + readonly websiteUrl: string | null | undefined; + }; + }; + }; +}; +export type VendorGraphCreateMutation = { + response: VendorGraphCreateMutation$data; + variables: VendorGraphCreateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "concreteType": "VendorEdge", + "kind": "LinkedField", + "name": "vendorEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "VendorGraphCreateMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateVendorPayload", + "kind": "LinkedField", + "name": "createVendor", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "VendorGraphCreateMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateVendorPayload", + "kind": "LinkedField", + "name": "createVendor", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "vendorEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "b6e686d8c3e4add7f11f09fd0d1e1de3", + "id": null, + "metadata": {}, + "name": "VendorGraphCreateMutation", + "operationKind": "mutation", + "text": "mutation VendorGraphCreateMutation(\n $input: CreateVendorInput!\n) {\n createVendor(input: $input) {\n vendorEdge {\n node {\n id\n name\n description\n websiteUrl\n createdAt\n updatedAt\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "ae71994587599f61caf6b79d9159ad44"; + +export default node; diff --git a/apps/console2/src/graph/__generated__/VendorGraphDeleteMutation.graphql.ts b/apps/console2/src/graph/__generated__/VendorGraphDeleteMutation.graphql.ts new file mode 100644 index 000000000..614aa3cf1 --- /dev/null +++ b/apps/console2/src/graph/__generated__/VendorGraphDeleteMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<6b924850fd9bad48c4fe32c331a7770b>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteVendorInput = { + vendorId: string; +}; +export type VendorGraphDeleteMutation$variables = { + connections: ReadonlyArray; + input: DeleteVendorInput; +}; +export type VendorGraphDeleteMutation$data = { + readonly deleteVendor: { + readonly deletedVendorId: string; + }; +}; +export type VendorGraphDeleteMutation = { + response: VendorGraphDeleteMutation$data; + variables: VendorGraphDeleteMutation$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": "deletedVendorId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "VendorGraphDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteVendorPayload", + "kind": "LinkedField", + "name": "deleteVendor", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "VendorGraphDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteVendorPayload", + "kind": "LinkedField", + "name": "deleteVendor", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedVendorId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "866254027729839312bc72c942de3111", + "id": null, + "metadata": {}, + "name": "VendorGraphDeleteMutation", + "operationKind": "mutation", + "text": "mutation VendorGraphDeleteMutation(\n $input: DeleteVendorInput!\n) {\n deleteVendor(input: $input) {\n deletedVendorId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "82648f6825e035f9abe617d5f641d537"; + +export default node; diff --git a/apps/console2/src/hooks/useVendorSearch.ts b/apps/console2/src/hooks/useVendorSearch.ts new file mode 100644 index 000000000..c01221809 --- /dev/null +++ b/apps/console2/src/hooks/useVendorSearch.ts @@ -0,0 +1,33 @@ +import { useEffect, useRef, useState } from "react"; +import MiniSearch from "minisearch"; +import type { Vendor } from "@probo/vendors"; + +export function useVendorSearch() { + const searchRef = useRef<(s: string) => Vendor[]>(() => []); + const [query, setQuery] = useState(""); + const [vendors, setVendors] = useState([]); + useEffect(() => { + import("@probo/vendors").then((module) => { + const ms = new MiniSearch({ + fields: ["name"], + storeFields: Object.keys(module.default[0]), + searchOptions: { + fuzzy: 0.1, + prefix: true, + }, + }); + ms.addAll(module.default.map((v) => ({ ...v, id: v.name }))); + // @ts-expect-error not enough types to handle this case + searchRef.current = ms.search.bind(ms); + }); + }, []); + + return { + query, + vendors: vendors.slice(0, 20), + search: (s: string) => { + setQuery(s); + setVendors(searchRef.current(s)); + }, + }; +} diff --git a/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx b/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx index bf38d3205..6a71bb4a8 100644 --- a/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx +++ b/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx @@ -36,6 +36,7 @@ type Props = { open?: boolean; risk?: RiskKey; onSuccess?: () => void; + connection: string; }; type RiskTemplate = { @@ -77,6 +78,7 @@ export default function FormRiskDialog({ risk, onSuccess, open, + connection, }: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); @@ -113,17 +115,13 @@ export default function FormRiskDialog({ }); return; } - const connectionID = ConnectionHandler.getConnectionID( - organizationId, - "RisksPage_risks" - ); createRisk({ variables: { input: { ...data, organizationId, }, - connections: [connectionID], + connections: [connection], }, successMessage: __("Risk created successfully."), errorMessage: __("Failed to create risk. Please try again."), @@ -236,6 +234,7 @@ export default function FormRiskDialog({ /> {showNote && (