diff --git a/apps/console2/src/components/form/PeopleSelect.tsx b/apps/console2/src/components/form/PeopleSelectField.tsx similarity index 56% rename from apps/console2/src/components/form/PeopleSelect.tsx rename to apps/console2/src/components/form/PeopleSelectField.tsx index 676d2b422..5f704f559 100644 --- a/apps/console2/src/components/form/PeopleSelect.tsx +++ b/apps/console2/src/components/form/PeopleSelectField.tsx @@ -1,42 +1,43 @@ -import { Avatar, Option, Select } from "@probo/ui"; +import { Avatar, Field, Option, Select } from "@probo/ui"; import { Suspense } from "react"; import { useTranslate } from "@probo/i18n"; import { type Control, Controller } from "react-hook-form"; import { usePeople } from "/hooks/graph/PeopleGraph.ts"; -export function PeopleSelect({ - organization, - name, - control, -}: { - organization: string; - name: string; +type Props = { + organizationId: string; control: Control; -}) { + name: string; + label?: string; + error?: string; +}; + +export function PeopleSelectField({ + organizationId, + control, + ...props +}: Props) { return ( - } - > - - + + } + > + + + ); } -function PeopleSelectWithQuery({ - organization, - name, - control, -}: { - organization: string; - name: string; - control: Control; -}) { +function PeopleSelectWithQuery( + props: Pick +) { const { __ } = useTranslate(); - const people = usePeople(organization); + const { name, organizationId, control } = props; + const people = usePeople(organizationId); return ( <> @@ -51,6 +52,7 @@ function PeopleSelectWithQuery({ onValueChange={field.onChange} key={people?.length.toString() ?? "0"} {...field} + className="w-full" value={field.value ?? ""} > {people?.map((p) => ( diff --git a/apps/console2/src/hooks/forms/__generated__/useVendorFormFragment.graphql.ts b/apps/console2/src/hooks/forms/__generated__/useVendorFormFragment.graphql.ts new file mode 100644 index 000000000..c8f5e4b2c --- /dev/null +++ b/apps/console2/src/hooks/forms/__generated__/useVendorFormFragment.graphql.ts @@ -0,0 +1,178 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type useVendorFormFragment$data = { + readonly businessOwner: { + readonly id: string; + } | null | undefined; + readonly certifications: ReadonlyArray; + readonly dataProcessingAgreementUrl: string | null | undefined; + readonly description: string | null | undefined; + readonly headquarterAddress: string | null | undefined; + readonly id: string; + readonly legalName: string | null | undefined; + readonly name: string; + readonly privacyPolicyUrl: string | null | undefined; + readonly securityOwner: { + readonly id: string; + } | null | undefined; + readonly securityPageUrl: string | null | undefined; + readonly serviceLevelAgreementUrl: string | null | undefined; + readonly statusPageUrl: string | null | undefined; + readonly termsOfServiceUrl: string | null | undefined; + readonly trustPageUrl: string | null | undefined; + readonly websiteUrl: string | null | undefined; + readonly " $fragmentType": "useVendorFormFragment"; +}; +export type useVendorFormFragment$key = { + readonly " $data"?: useVendorFormFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"useVendorFormFragment">; +}; + +const node: ReaderFragment = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v1 = [ + (v0/*: any*/) +]; +return { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "useVendorFormFragment", + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "statusPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "termsOfServiceUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "privacyPolicyUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "serviceLevelAgreementUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataProcessingAgreementUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "legalName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "headquarterAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "certifications", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "securityPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "trustPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "businessOwner", + "plural": false, + "selections": (v1/*: any*/), + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "securityOwner", + "plural": false, + "selections": (v1/*: any*/), + "storageKey": null + } + ], + "type": "Vendor", + "abstractKey": null +}; +})(); + +(node as any).hash = "87f1029f8c634a5f7efdb6d7abe17709"; + +export default node; diff --git a/apps/console2/src/hooks/forms/__generated__/useVendorFormMutation.graphql.ts b/apps/console2/src/hooks/forms/__generated__/useVendorFormMutation.graphql.ts new file mode 100644 index 000000000..5f0106b8f --- /dev/null +++ b/apps/console2/src/hooks/forms/__generated__/useVendorFormMutation.graphql.ts @@ -0,0 +1,265 @@ +/** + * @generated SignedSource<<6a680118da31fa610f9638d27a9b48d1>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type UpdateVendorInput = { + businessOwnerId?: string | null | undefined; + category?: string | null | undefined; + certifications?: ReadonlyArray | null | undefined; + dataProcessingAgreementUrl?: string | null | undefined; + description?: string | null | undefined; + headquarterAddress?: string | null | undefined; + id: string; + legalName?: string | null | undefined; + name?: string | null | undefined; + 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 useVendorFormMutation$variables = { + input: UpdateVendorInput; +}; +export type useVendorFormMutation$data = { + readonly updateVendor: { + readonly vendor: { + readonly " $fragmentSpreads": FragmentRefs<"useVendorFormFragment">; + }; + }; +}; +export type useVendorFormMutation = { + response: useVendorFormMutation$data; + variables: useVendorFormMutation$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 +}, +v3 = [ + (v2/*: any*/) +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "useVendorFormMutation", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateVendorPayload", + "kind": "LinkedField", + "name": "updateVendor", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "vendor", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "useVendorFormFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "useVendorFormMutation", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "UpdateVendorPayload", + "kind": "LinkedField", + "name": "updateVendor", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "vendor", + "plural": false, + "selections": [ + (v2/*: 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": "statusPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "termsOfServiceUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "privacyPolicyUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "serviceLevelAgreementUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataProcessingAgreementUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "legalName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "headquarterAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "certifications", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "securityPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "trustPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "businessOwner", + "plural": false, + "selections": (v3/*: any*/), + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "securityOwner", + "plural": false, + "selections": (v3/*: any*/), + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "324b4589b4692806f555489703521bcd", + "id": null, + "metadata": {}, + "name": "useVendorFormMutation", + "operationKind": "mutation", + "text": "mutation useVendorFormMutation(\n $input: UpdateVendorInput!\n) {\n updateVendor(input: $input) {\n vendor {\n ...useVendorFormFragment\n id\n }\n }\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "f49cfca25ffd99e8bbdc2e2e3c78b400"; + +export default node; diff --git a/apps/console2/src/hooks/forms/useVendorForm.tsx b/apps/console2/src/hooks/forms/useVendorForm.tsx new file mode 100644 index 000000000..7f0d36821 --- /dev/null +++ b/apps/console2/src/hooks/forms/useVendorForm.tsx @@ -0,0 +1,108 @@ +import { z } from "zod"; +import { useFormWithSchema } from "../useFormWithSchema"; +import { graphql } from "relay-runtime"; +import { useFragment, useMutation } from "react-relay"; +import type { useVendorFormFragment$key } from "./__generated__/useVendorFormFragment.graphql"; +import { useMutationWithToasts } from "../useMutationWithToasts"; +import { useTranslate } from "@probo/i18n"; + +const schema = z.object({ + name: z.string(), + description: z.string(), + statusPageUrl: z.string(), + termsOfServiceUrl: z.string(), + privacyPolicyUrl: z.string(), + serviceLevelAgreementUrl: z.string(), + dataProcessingAgreementUrl: z.string(), + websiteUrl: z.string(), + legalName: z.string(), + headquarterAddress: z.string(), + certifications: z.array(z.string()), + securityPageUrl: z.string(), + trustPageUrl: z.string(), + businessOwnerId: z.string(), + securityOwnerId: z.string(), +}); + +const vendorFormFragment = graphql` + fragment useVendorFormFragment on Vendor { + id + name + description + statusPageUrl + termsOfServiceUrl + privacyPolicyUrl + serviceLevelAgreementUrl + dataProcessingAgreementUrl + websiteUrl + legalName + headquarterAddress + certifications + securityPageUrl + trustPageUrl + businessOwner { + id + } + securityOwner { + id + } + } +`; + +const vendorUpdateQuery = graphql` + mutation useVendorFormMutation($input: UpdateVendorInput!) { + updateVendor(input: $input) { + vendor { + ...useVendorFormFragment + } + } + } +`; + +export function useVendorForm(vendorKey: useVendorFormFragment$key) { + const vendor = useFragment(vendorFormFragment, vendorKey); + const { __ } = useTranslate(); + + const [mutate] = useMutationWithToasts(vendorUpdateQuery, { + successMessage: __("Vendor updated successfully."), + errorMessage: __("Failed to update vendor. Please try again."), + }); + + const form = useFormWithSchema(schema, { + defaultValues: { + name: vendor.name, + description: vendor.description ?? "", + statusPageUrl: vendor.statusPageUrl ?? "", + termsOfServiceUrl: vendor.termsOfServiceUrl ?? "", + privacyPolicyUrl: vendor.privacyPolicyUrl ?? "", + serviceLevelAgreementUrl: vendor.serviceLevelAgreementUrl ?? "", + dataProcessingAgreementUrl: vendor.dataProcessingAgreementUrl ?? "", + websiteUrl: vendor.websiteUrl ?? "", + legalName: vendor.legalName ?? "", + headquarterAddress: vendor.headquarterAddress ?? "", + certifications: [...(vendor.certifications ?? [])], + securityPageUrl: vendor.securityPageUrl ?? "", + trustPageUrl: vendor.trustPageUrl ?? "", + businessOwnerId: vendor.businessOwner?.id, + securityOwnerId: vendor.securityOwner?.id, + }, + }); + + const handleSubmit = form.handleSubmit((data) => { + return mutate({ + variables: { + input: { + id: vendor.id, + ...data, + }, + }, + }).then(() => { + form.reset(data); + }); + }); + + return { + ...form, + handleSubmit, + }; +} diff --git a/apps/console2/src/hooks/graph/PeopleGraph.ts b/apps/console2/src/hooks/graph/PeopleGraph.ts index 08f3173d4..7c3c2b80a 100644 --- a/apps/console2/src/hooks/graph/PeopleGraph.ts +++ b/apps/console2/src/hooks/graph/PeopleGraph.ts @@ -7,14 +7,13 @@ import { useRefetchableFragment, type PreloadedQuery, } from "react-relay"; -import { useMemo, useTransition } from "react"; +import { useMemo } from "react"; import type { PeopleGraphPaginatedQuery } from "./__generated__/PeopleGraphPaginatedQuery.graphql"; import type { PeopleGraphPaginatedFragment$key } from "./__generated__/PeopleGraphPaginatedFragment.graphql"; import { useConfirm } from "@probo/ui"; import type { PeopleGraphDeleteMutation } from "./__generated__/PeopleGraphDeleteMutation.graphql"; import { sprintf } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; -import type { PeopleGraphNodeQuery$data } from "./__generated__/PeopleGraphNodeQuery.graphql"; const peopleQuery = graphql` query PeopleGraphQuery($organizationId: ID!) { diff --git a/apps/console2/src/hooks/graph/VendorGraph.ts b/apps/console2/src/hooks/graph/VendorGraph.ts index 7b1fe25bc..643b7ca70 100644 --- a/apps/console2/src/hooks/graph/VendorGraph.ts +++ b/apps/console2/src/hooks/graph/VendorGraph.ts @@ -3,6 +3,9 @@ import { graphql } from "relay-runtime"; import { useMutationWithToasts } from "../useMutationWithToasts.ts"; import type { VendorGraphCreateMutation } from "./__generated__/VendorGraphCreateMutation.graphql.ts"; import type { VendorGraphDeleteMutation } from "./__generated__/VendorGraphDeleteMutation.graphql.ts"; +import { useMutation } from "react-relay"; +import { useConfirm } from "@probo/ui"; +import { sprintf } from "@probo/helpers"; const createVendorMutation = graphql` mutation VendorGraphCreateMutation( @@ -32,7 +35,7 @@ export function useCreateVendorMutation() { { successMessage: __("Vendor created successfully."), errorMessage: __("Failed to create vendor. Please try again."), - }, + } ); } @@ -47,14 +50,88 @@ const deleteVendorMutation = graphql` } `; -export function useDeleteVendorMutation() { +export const useDeleteVendor = ( + vendor: { id?: string; name?: string }, + connectionId: string +) => { + const [mutate] = useMutation(deleteVendorMutation); + const confirm = useConfirm(); const { __ } = useTranslate(); - return useMutationWithToasts( - deleteVendorMutation, - { - successMessage: __("Vendor deleted successfully."), - errorMessage: __("Failed to delete vendor. Please try again."), - }, - ); -} + return () => { + if (!vendor.id || !vendor.name) { + return alert(__("Failed to delete vendor: missing id or name")); + } + confirm( + () => + new Promise((resolve) => { + mutate({ + variables: { + input: { + vendorId: vendor.id!, + }, + connections: [connectionId], + }, + onCompleted: () => resolve(), + }); + }), + { + message: sprintf( + __( + 'This will permanently delete vendor "%s". This action cannot be undone.' + ), + vendor.name + ), + } + ); + }; +}; + +export const vendorConnectionKey = "VendorsPage_vendors"; + +export const vendorsQuery = graphql` + query VendorGraphListQuery($organizationId: ID!) { + node(id: $organizationId) { + ... on Organization { + vendors(first: 25) @connection(key: "VendorsPage_vendors") { + __id + edges { + node { + id + name + websiteUrl + updatedAt + riskAssessments( + first: 1 + orderBy: { direction: DESC, field: ASSESSED_AT } + ) { + edges { + node { + id + assessedAt + expiresAt + dataSensitivity + businessImpact + } + } + } + } + } + } + } + } + } +`; + +export const vendorNodeQuery = graphql` + query VendorGraphNodeQuery($vendorId: ID!) { + node(id: $vendorId) { + ... on Vendor { + id + name + websiteUrl + ...useVendorFormFragment + } + } + } +`; diff --git a/apps/console2/src/pages/organizations/vendors/__generated__/VendorsPageQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/VendorGraphListQuery.graphql.ts similarity index 86% rename from apps/console2/src/pages/organizations/vendors/__generated__/VendorsPageQuery.graphql.ts rename to apps/console2/src/hooks/graph/__generated__/VendorGraphListQuery.graphql.ts index 2e0202ceb..ed7f51806 100644 --- a/apps/console2/src/pages/organizations/vendors/__generated__/VendorsPageQuery.graphql.ts +++ b/apps/console2/src/hooks/graph/__generated__/VendorGraphListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<2de1bbc55eb7b45a9546c8ec306ad979>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -11,10 +11,10 @@ import { ConcreteRequest } from 'relay-runtime'; export type BusinessImpact = "CRITICAL" | "HIGH" | "LOW" | "MEDIUM"; export type DataSensitivity = "CRITICAL" | "HIGH" | "LOW" | "MEDIUM" | "NONE"; -export type VendorsPageQuery$variables = { +export type VendorGraphListQuery$variables = { organizationId: string; }; -export type VendorsPageQuery$data = { +export type VendorGraphListQuery$data = { readonly node: { readonly vendors?: { readonly __id: string; @@ -40,9 +40,9 @@ export type VendorsPageQuery$data = { }; }; }; -export type VendorsPageQuery = { - response: VendorsPageQuery$data; - variables: VendorsPageQuery$variables; +export type VendorGraphListQuery = { + response: VendorGraphListQuery$data; + variables: VendorGraphListQuery$variables; }; const node: ConcreteRequest = (function(){ @@ -253,7 +253,7 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Fragment", "metadata": null, - "name": "VendorsPageQuery", + "name": "VendorGraphListQuery", "selections": [ { "alias": null, @@ -291,7 +291,7 @@ return { "operation": { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", - "name": "VendorsPageQuery", + "name": "VendorGraphListQuery", "selections": [ { "alias": null, @@ -335,7 +335,7 @@ return { ] }, "params": { - "cacheID": "cfdef0fe4c1b737de9dfbf0a27a3de44", + "cacheID": "ce175c4c5d08117d3acdbd3eed92ca95", "id": null, "metadata": { "connection": [ @@ -350,13 +350,13 @@ return { } ] }, - "name": "VendorsPageQuery", + "name": "VendorGraphListQuery", "operationKind": "query", - "text": "query VendorsPageQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n vendors(first: 25) {\n edges {\n node {\n id\n name\n websiteUrl\n updatedAt\n riskAssessments(first: 1, orderBy: {direction: DESC, field: ASSESSED_AT}) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n" + "text": "query VendorGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n vendors(first: 25) {\n edges {\n node {\n id\n name\n websiteUrl\n updatedAt\n riskAssessments(first: 1, orderBy: {direction: DESC, field: ASSESSED_AT}) {\n edges {\n node {\n id\n assessedAt\n expiresAt\n dataSensitivity\n businessImpact\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "ce37725ed4c8f58ae2e4af7cf6ee8564"; +(node as any).hash = "ef2b4fb44af8cef6cd3f50a6746c6de3"; export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts new file mode 100644 index 000000000..e3eb43ba7 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/VendorGraphNodeQuery.graphql.ts @@ -0,0 +1,251 @@ +/** + * @generated SignedSource<<57ff86dca8786cdaf42f34f9cbe379a1>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type VendorGraphNodeQuery$variables = { + vendorId: string; +}; +export type VendorGraphNodeQuery$data = { + readonly node: { + readonly id?: string; + readonly name?: string; + readonly websiteUrl?: string | null | undefined; + readonly " $fragmentSpreads": FragmentRefs<"useVendorFormFragment">; + }; +}; +export type VendorGraphNodeQuery = { + response: VendorGraphNodeQuery$data; + variables: VendorGraphNodeQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "vendorId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "vendorId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "websiteUrl", + "storageKey": null +}, +v5 = [ + (v2/*: any*/) +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "VendorGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "useVendorFormFragment" + } + ], + "type": "Vendor", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "VendorGraphNodeQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + (v4/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "statusPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "termsOfServiceUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "privacyPolicyUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "serviceLevelAgreementUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "dataProcessingAgreementUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "legalName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "headquarterAddress", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "certifications", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "securityPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "trustPageUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "businessOwner", + "plural": false, + "selections": (v5/*: any*/), + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "People", + "kind": "LinkedField", + "name": "securityOwner", + "plural": false, + "selections": (v5/*: any*/), + "storageKey": null + } + ], + "type": "Vendor", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "3e7b2628c65e007a1603b982e41fea12", + "id": null, + "metadata": {}, + "name": "VendorGraphNodeQuery", + "operationKind": "query", + "text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n name\n websiteUrl\n ...useVendorFormFragment\n }\n id\n }\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "9a718caa45c75f17f4b910ca2c555781"; + +export default node; diff --git a/apps/console2/src/hooks/useMutationWithToasts.ts b/apps/console2/src/hooks/useMutationWithToasts.ts index 125dfe400..4ab71cfed 100644 --- a/apps/console2/src/hooks/useMutationWithToasts.ts +++ b/apps/console2/src/hooks/useMutationWithToasts.ts @@ -26,36 +26,43 @@ export function useMutationWithToasts( } ) => { const options = { ...baseOptions, ...queryOptions }; - mutate({ - ...options, - onCompleted: (response, error) => { - options.onCompleted?.(response, error); - if (error) { + return new Promise((resolve, reject) => + mutate({ + ...options, + onCompleted: (response, error) => { + options.onCompleted?.(response, error); + if (error) { + toast({ + title: __("Error"), + description: + options.errorMessage ?? + __("Failed to commit this operation."), + variant: "error", + }); + reject(error); + return; + } + toast({ + title: __("Success"), + description: + options.successMessage ?? + __("Operation completed successfully"), + variant: "success", + }); + options.onSuccess?.(); + resolve(); + }, + onError: (error) => { toast({ title: __("Error"), description: options.errorMessage ?? __("Failed to commit this operation."), variant: "error", }); - return; - } - toast({ - title: __("Success"), - description: - options.successMessage ?? __("Operation completed successfully"), - variant: "success", - }); - options.onSuccess?.(); - }, - onError: () => { - toast({ - title: __("Error"), - description: - options.errorMessage ?? __("Failed to commit this operation."), - variant: "error", - }); - }, - }); + reject(error); + }, + }) + ); }, [mutate] ); diff --git a/apps/console2/src/pages/organizations/policies/dialogs/CreatePolicyDialog.tsx b/apps/console2/src/pages/organizations/policies/dialogs/CreatePolicyDialog.tsx index d00b4a377..bcc9dab22 100644 --- a/apps/console2/src/pages/organizations/policies/dialogs/CreatePolicyDialog.tsx +++ b/apps/console2/src/pages/organizations/policies/dialogs/CreatePolicyDialog.tsx @@ -12,12 +12,12 @@ import { Textarea, useDialogRef, } from "@probo/ui"; -import { useState, type ReactNode } from "react"; +import { type ReactNode } from "react"; import { graphql } from "relay-runtime"; import { useOrganizationId } from "/hooks/useOrganizationId"; -import { usePolicyForm } from "../../../../hooks/forms/usePolicyForm"; +import { usePolicyForm } from "/hooks/forms/usePolicyForm"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; -import { PeopleSelect } from "/components/form/PeopleSelect"; +import { PeopleSelectField } from "/components/form/PeopleSelectField"; import type { CreatePolicyDialogMutation } from "./__generated__/CreatePolicyDialogMutation.graphql"; type Props = { @@ -112,10 +112,10 @@ export function CreatePolicyDialog({ trigger, connection }: Props) { label={__("Owner")} error={errors.ownerId?.message} > - diff --git a/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx b/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx index 3cd6c316b..ea7bdbd12 100644 --- a/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx +++ b/apps/console2/src/pages/organizations/risks/FormRiskDialog.tsx @@ -15,10 +15,10 @@ import { Textarea, useDialogRef, } from "@probo/ui"; -import { useMemo, useState, type ReactNode } from "react"; +import { useMemo, type ReactNode } from "react"; import { useFetchQuery } from "/hooks/useFetchQuery"; import { graphql } from "relay-runtime"; -import { PeopleSelect } from "/components/form/PeopleSelect"; +import { PeopleSelectField } from "/components/form/PeopleSelectField"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { useToggle } from "@probo/hooks"; import { @@ -196,10 +196,10 @@ export default function FormRiskDialog({ label={__("Owner")} error={errors.ownerId?.message} > - diff --git a/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx b/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx index aff3c53ee..cbdfbadda 100644 --- a/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx +++ b/apps/console2/src/pages/organizations/vendors/CreateVendorDialog.tsx @@ -7,6 +7,7 @@ import { DialogFooter, Avatar, IconPlusLarge, + useDialogRef, } from "@probo/ui"; import { useVendorSearch } from "/hooks/useVendorSearch"; import { faviconUrl } from "@probo/helpers"; @@ -15,20 +16,19 @@ import { useCreateVendorMutation } from "/hooks/graph/VendorGraph"; import { type ReactNode, useState } from "react"; type Props = { - trigger: ReactNode; + children: ReactNode; organizationId: string; connection: string; }; export function CreateVendorDialog({ - trigger, + children, organizationId, connection, }: Props) { const { __ } = useTranslate(); const { search, vendors, query } = useVendorSearch(); const [createVendor] = useCreateVendorMutation(); - const [open, setOpen] = useState(false); const onSelect = (vendor: Vendor | string) => { const input = @@ -61,18 +61,15 @@ export function CreateVendorDialog({ connections: [connection], }, onSuccess: () => { - setOpen(false); + dialogRef.current?.close(); }, }); }; + const dialogRef = useDialogRef(); + return ( - + {vendors.map((vendor) => ( diff --git a/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx b/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx new file mode 100644 index 000000000..997e8487b --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/VendorDetailPage.tsx @@ -0,0 +1,97 @@ +import { + ConnectionHandler, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; +import type { VendorGraphNodeQuery } from "/hooks/graph/__generated__/VendorGraphNodeQuery.graphql"; +import { + useDeleteVendor, + vendorConnectionKey, + vendorNodeQuery, +} from "/hooks/graph/VendorGraph"; +import { + ActionDropdown, + Avatar, + Breadcrumb, + DropdownItem, + IconTrashCan, + TabLink, + Tabs, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { Outlet } from "react-router"; +import { faviconUrl } from "@probo/helpers"; + +type Props = { + queryRef: PreloadedQuery; +}; + +export default function VendorDetailPage(props: Props) { + const data = usePreloadedQuery(vendorNodeQuery, props.queryRef); + const vendor = data.node; + const { __ } = useTranslate(); + const organizationId = useOrganizationId(); + const deleteVendor = useDeleteVendor( + vendor, + ConnectionHandler.getConnectionID(organizationId, vendorConnectionKey) + ); + const logo = faviconUrl(vendor.websiteUrl); + + return ( +
+ +
+
+ {logo && ( + {vendor.name + )} +
{vendor.name}
+
+ + + {__("Delete")} + + +
+ + + + {__("Overview")} + + + {__("Certifications")} + + + {__("Compliance reports")} + + + + +
+ ); +} diff --git a/apps/console2/src/pages/organizations/vendors/VendorsPage.tsx b/apps/console2/src/pages/organizations/vendors/VendorsPage.tsx index 588e568b0..a89b4b6cc 100644 --- a/apps/console2/src/pages/organizations/vendors/VendorsPage.tsx +++ b/apps/console2/src/pages/organizations/vendors/VendorsPage.tsx @@ -14,68 +14,34 @@ import { ActionDropdown, DropdownItem, IconTrashCan, - useConfirm, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { usePageTitle } from "@probo/hooks"; -import { ConnectionHandler, graphql } from "relay-runtime"; -import { useLazyLoadQuery } from "react-relay"; +import { usePreloadedQuery, type PreloadedQuery } from "react-relay"; import { useOrganizationId } from "/hooks/useOrganizationId"; -import type { - VendorsPageQuery, - VendorsPageQuery$data, -} from "./__generated__/VendorsPageQuery.graphql"; -import { faviconUrl, sprintf } from "@probo/helpers"; +import { faviconUrl } from "@probo/helpers"; import type { NodeOf } from "/types"; import { CreateVendorDialog } from "./CreateVendorDialog"; -import { useDeleteVendorMutation } from "/hooks/graph/VendorGraph"; +import { useDeleteVendor, vendorsQuery } from "/hooks/graph/VendorGraph"; +import type { + VendorGraphListQuery, + VendorGraphListQuery$data, +} from "/hooks/graph/__generated__/VendorGraphListQuery.graphql"; -const vendorsQuery = graphql` - query VendorsPageQuery($organizationId: ID!) { - node(id: $organizationId) { - ... on Organization { - vendors(first: 25) @connection(key: "VendorsPage_vendors") { - __id - edges { - node { - id - name - websiteUrl - updatedAt - riskAssessments( - first: 1 - orderBy: { direction: DESC, field: ASSESSED_AT } - ) { - edges { - node { - id - assessedAt - expiresAt - dataSensitivity - businessImpact - } - } - } - } - } - } - } - } - } -`; +type Vendor = NodeOf["vendors"]>; -type Vendor = NodeOf["vendors"]>; +type Props = { + queryRef: PreloadedQuery; +}; -export default function VendorsPage() { +export default function VendorsPage(props: Props) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); - const data = useLazyLoadQuery(vendorsQuery, { - organizationId, - }); - console.log(data); + const data = usePreloadedQuery(vendorsQuery, props.queryRef); const vendors = data.node?.vendors?.edges.map((edge) => edge.node); + const connectionId = data.node!.vendors!.__id; usePageTitle(__("Vendors")); @@ -90,8 +56,9 @@ export default function VendorsPage() { {__("Add vendor")}} - /> + > + + @@ -110,6 +77,7 @@ export default function VendorsPage() { key={vendor.id} vendor={vendor} organizationId={organizationId} + connectionId={connectionId} /> ))} @@ -121,9 +89,11 @@ export default function VendorsPage() { function VendorRow({ vendor, organizationId, + connectionId, }: { vendor: Vendor; organizationId: string; + connectionId: string; }) { const { __, dateFormat } = useTranslate(); const latestAssessment = vendor.riskAssessments?.edges[0]?.node; @@ -131,42 +101,11 @@ function VendorRow({ ? new Date(latestAssessment.expiresAt) < new Date() : false; - const [deleteVendor] = useDeleteVendorMutation(); - const confirm = useConfirm(); - - const onDelete = () => { - confirm( - () => - new Promise((resolve) => { - deleteVendor({ - variables: { - input: { - vendorId: vendor.id, - }, - connections: [ - ConnectionHandler.getConnectionID( - organizationId, - "VendorsPage_vendors" - ), - ], - }, - onCompleted: () => resolve(), - }); - }), - { - message: sprintf( - __( - 'This will permanently delete the vendor "%s". This action cannot be undone.' - ), - vendor.name - ), - } - ); - }; + const deleteVendor = useDeleteVendor(vendor, connectionId); return ( <> - +
@@ -196,7 +135,7 @@ function VendorRow({
diff --git a/apps/console2/src/pages/organizations/vendors/tabs/VendorCertificationsTab.tsx b/apps/console2/src/pages/organizations/vendors/tabs/VendorCertificationsTab.tsx new file mode 100644 index 000000000..3cb480926 --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/tabs/VendorCertificationsTab.tsx @@ -0,0 +1,167 @@ +import { useTranslate } from "@probo/i18n"; +import { + Badge, + Button, + Card, + Combobox, + ComboboxItem, + IconCrossLargeX, + IconPlusLarge, +} from "@probo/ui"; +import { Controller } from "react-hook-form"; +import { useVendorForm } from "/hooks/forms/useVendorForm"; +import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVendorFormFragment.graphql"; +import { useOutletContext } from "react-router"; +import { + certificationCategoryLabel, + certifications, + objectEntries, +} from "@probo/helpers"; +import { useRef, useState } from "react"; +import clsx from "clsx"; + +/** + * Vendor certifications tab + */ +export default function VendorCertificationsTab() { + const { vendor } = useOutletContext<{ + vendor: useVendorFormFragment$key & { name: string }; + }>(); + const { __ } = useTranslate(); + const { control, handleSubmit } = useVendorForm(vendor); + + return ( +
+ + ( + + )} + /> + +
+ +
+
+ ); +} + +type CertificationsProps = { + value: string[]; + onValueChange: (value: string[]) => void; +}; + +/** + * List all certifications badges + */ +function Certifications(props: CertificationsProps) { + const categorizedCertifications = Object.values(certifications).flat(); + const { __ } = useTranslate(); + const animateBadge = useRef(false); + const categories = objectEntries(certifications) + .map( + ([key, value]) => + [key, value.filter((c) => props.value.includes(c))] as const + ) + .filter(([_, certifications]) => certifications.length > 0); + categories.push([ + "custom", + props.value.filter((c) => !categorizedCertifications.includes(c)), + ]); + + const addCertificate = (name: string) => { + animateBadge.current = true; + props.onValueChange([...props.value, name]); + }; + + const removeCertificate = (name: string) => { + animateBadge.current = true; + props.onValueChange(props.value.filter((v) => v !== name)); + }; + + return ( +
+ {categories.map(([key, certifications]) => ( +
+
+ {certificationCategoryLabel(__, key)} +
+
+ {certifications.map((certification) => ( + + + + ))} +
+
+ ))} + !props.value.includes(c) + )} + onAdd={addCertificate} + /> +
+ ); +} + +/** + * Input to add a new certification + */ +function CertificationInput({ + certifications, + onAdd, +}: { + certifications: string[]; + onAdd: (name: string) => void; +}) { + const { __ } = useTranslate(); + const [search, setSearch] = useState(""); + const isCustom = !certifications.includes(search.trim()); + const filteredCertifications = certifications.filter((c) => + c.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ {search} + + {filteredCertifications.map((certification) => ( + + {certification} + + ))} + {isCustom && search.trim().length >= 2 && ( + + + {__("Add a custom certification")} : {search} + + )} + +
+ ); +} diff --git a/apps/console2/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx b/apps/console2/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx new file mode 100644 index 000000000..fc24a62af --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/tabs/VendorComplianceTab.tsx @@ -0,0 +1,51 @@ +import { useOutletContext } from "react-router"; +import { graphql } from "relay-runtime"; +import type { VendorComplianceTabFragment$key } from "./__generated__/VendorComplianceTabFragment.graphql"; +import { useTranslate } from "@probo/i18n"; +import { usePageTitle } from "@probo/hooks"; + +const complianceReportsFragment = graphql` + fragment VendorComplianceTabFragment on Vendor + @refetchable(queryName: "ComplianceReportListQuery") + @argumentDefinitions( + first: { type: "Int", defaultValue: 50 } + order: { type: "VendorComplianceReportOrder", defaultValue: null } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + ) { + complianceReports( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + ) @connection(key: "VendorComplianceTabFragment_complianceReports") { + edges { + node { + ...VendorComplianceTabFragment_report + } + } + } + } +`; + +const complianceReportFragment = graphql` + fragment VendorComplianceTabFragment_report on VendorComplianceReport { + reportDate + validUntil + reportName + fileUrl + fileSize + } +`; + +export default function VendorComplianceTab() { + const { vendor } = useOutletContext<{ + vendor: VendorComplianceTabFragment$key & { name: string }; + }>(); + const { __ } = useTranslate(); + + usePageTitle(vendor.name + " - " + __("Compliance reports")); + return
Vendor Compliance
; +} diff --git a/apps/console2/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx b/apps/console2/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx new file mode 100644 index 000000000..416c3d36a --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/tabs/VendorOverviewTab.tsx @@ -0,0 +1,142 @@ +import { useTranslate } from "@probo/i18n"; +import { useVendorForm } from "/hooks/forms/useVendorForm"; +import type { useVendorFormFragment$key } from "/hooks/forms/__generated__/useVendorFormFragment.graphql"; +import { useOutletContext } from "react-router"; +import { Button, Card, Field, Input } from "@probo/ui"; +import { PeopleSelectField } from "/components/form/PeopleSelectField"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { useMemo } from "react"; +import { usePageTitle } from "@probo/hooks"; + +export default function VendorOverviewTab() { + const { vendor } = useOutletContext<{ + vendor: useVendorFormFragment$key & { name: string }; + }>(); + const { __ } = useTranslate(); + const organizationId = useOrganizationId(); + const { + control, + register, + handleSubmit, + formState: { errors, isDirty, isSubmitting }, + } = useVendorForm(vendor); + + const urls = useMemo( + () => + [ + { name: "statusPageUrl", label: __("Status page URL") }, + { name: "termsOfServiceUrl", label: __("Terms of service URL") }, + { name: "privacyPolicyUrl", label: __("Privacy policy URL") }, + { + name: "serviceLevelAgreementUrl", + label: __("Service level agreement URL"), + }, + { + name: "dataProcessingAgreementUrl", + label: __("Data processing agreement URL"), + }, + { name: "securityPageUrl", label: __("Security page URL") }, + { name: "trustPageUrl", label: __("Trust page URL") }, + ] as const, + [] + ); + + usePageTitle(vendor.name + " - " + __("Overview")); + + return ( +
+ {/* Vendor Details */} +
+

{__("Vendor details")}

+ + + + + + + +
+ + {/* Ownership */} +
+

{__("Ownership details`")}

+ + + + +
+ + {/* Links */} +
+

{__("Links")}

+ + {urls.map((url) => ( +
+ + +
+ ))} +
+
+ + {/* Submit */} +
+ {isDirty && ( + + )} +
+
+ ); +} diff --git a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/ComplianceReportListQuery.graphql.ts b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/ComplianceReportListQuery.graphql.ts new file mode 100644 index 000000000..314e7c1c6 --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/ComplianceReportListQuery.graphql.ts @@ -0,0 +1,339 @@ +/** + * @generated SignedSource<<75defbef64a4ee18b514465a44c8eea1>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrderDirection = "ASC" | "DESC"; +export type VendorComplianceReportOrderField = "CREATED_AT" | "REPORT_DATE"; +export type VendorComplianceReportOrder = { + direction: OrderDirection; + field: VendorComplianceReportOrderField; +}; +export type ComplianceReportListQuery$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: VendorComplianceReportOrder | null | undefined; +}; +export type ComplianceReportListQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment">; + }; +}; +export type ComplianceReportListQuery = { + response: ComplianceReportListQuery$data; + variables: ComplianceReportListQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": 50, + "kind": "LocalArgument", + "name": "first" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v5 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" +}, +v6 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v7 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v8 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v9 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v10 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v13 = [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "ComplianceReportListQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "VendorComplianceTabFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v3/*: any*/) + ], + "kind": "Operation", + "name": "ComplianceReportListQuery", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v13/*: any*/), + "concreteType": "VendorComplianceReportConnection", + "kind": "LinkedField", + "name": "complianceReports", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReportEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReport", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "reportDate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validUntil", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "reportName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileSize", + "storageKey": null + }, + (v12/*: any*/), + (v11/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v13/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "VendorComplianceTabFragment_complianceReports", + "kind": "LinkedHandle", + "name": "complianceReports" + } + ], + "type": "Vendor", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "76e0b5dbc01424695b8c642e27c0f4ed", + "id": null, + "metadata": {}, + "name": "ComplianceReportListQuery", + "operationKind": "query", + "text": "query ComplianceReportListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: VendorComplianceReportOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorComplianceTabFragment_16fISc\n id\n }\n}\n\nfragment VendorComplianceTabFragment_16fISc on Vendor {\n complianceReports(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n ...VendorComplianceTabFragment_report\n id\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n" + } +}; +})(); + +(node as any).hash = "7aae49a4b414ed4db5e7b39f8969c02f"; + +export default node; diff --git a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment.graphql.ts b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment.graphql.ts new file mode 100644 index 000000000..02c81fda7 --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment.graphql.ts @@ -0,0 +1,209 @@ +/** + * @generated SignedSource<<079a3066496976bd1238ba194f5a0660>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type VendorComplianceTabFragment$data = { + readonly complianceReports: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment_report">; + }; + }>; + }; + readonly id: string; + readonly " $fragmentType": "VendorComplianceTabFragment"; +}; +export type VendorComplianceTabFragment$key = { + readonly " $data"?: VendorComplianceTabFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment">; +}; + +import ComplianceReportListQuery_graphql from './ComplianceReportListQuery.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "complianceReports" +]; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": 50, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": ComplianceReportListQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "VendorComplianceTabFragment", + "selections": [ + { + "alias": "complianceReports", + "args": [ + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "VendorComplianceReportConnection", + "kind": "LinkedField", + "name": "__VendorComplianceTabFragment_complianceReports_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReportEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "VendorComplianceReport", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "VendorComplianceTabFragment_report" + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "type": "Vendor", + "abstractKey": null +}; +})(); + +(node as any).hash = "7aae49a4b414ed4db5e7b39f8969c02f"; + +export default node; diff --git a/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment_report.graphql.ts b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment_report.graphql.ts new file mode 100644 index 000000000..297116129 --- /dev/null +++ b/apps/console2/src/pages/organizations/vendors/tabs/__generated__/VendorComplianceTabFragment_report.graphql.ts @@ -0,0 +1,74 @@ +/** + * @generated SignedSource<<01d2d791b86ae99330d4ba2ecb01f1ed>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type VendorComplianceTabFragment_report$data = { + readonly fileSize: number; + readonly fileUrl: string; + readonly reportDate: any; + readonly reportName: string; + readonly validUntil: any | null | undefined; + readonly " $fragmentType": "VendorComplianceTabFragment_report"; +}; +export type VendorComplianceTabFragment_report$key = { + readonly " $data"?: VendorComplianceTabFragment_report$data; + readonly " $fragmentSpreads": FragmentRefs<"VendorComplianceTabFragment_report">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "VendorComplianceTabFragment_report", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "reportDate", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "validUntil", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "reportName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fileSize", + "storageKey": null + } + ], + "type": "VendorComplianceReport", + "abstractKey": null +}; + +(node as any).hash = "d39b70dfd0a316b158d7dc7253d0ff5e"; + +export default node; diff --git a/apps/console2/src/routes.tsx b/apps/console2/src/routes.tsx index 5a78359d9..2bd70184a 100644 --- a/apps/console2/src/routes.tsx +++ b/apps/console2/src/routes.tsx @@ -18,6 +18,7 @@ import { useCleanup } from "./hooks/useDelayedEffect.ts"; import { riskRoutes } from "./routes/riskRoutes.ts"; import { measureRoutes } from "./routes/measureRoutes.ts"; import { policiesRoutes } from "./routes/policiesRoutes.ts"; +import { vendorRoutes } from "./routes/vendorRoutes.ts"; import { organizationViewQuery } from "./hooks/graph/OrganizationGraph.ts"; import { peopleRoutes } from "./routes/peopleRoutes.ts"; @@ -79,17 +80,11 @@ const routes = [ }), Component: lazy(() => import("./pages/organizations/SettingsPage")), }, - { - path: "vendors", - fallback: PageSkeleton, - Component: lazy( - () => import("./pages/organizations/vendors/VendorsPage") - ), - }, ...riskRoutes, ...measureRoutes, ...policiesRoutes, ...peopleRoutes, + ...vendorRoutes, ], }, ] satisfies AppRoute[]; diff --git a/apps/console2/src/routes/vendorRoutes.ts b/apps/console2/src/routes/vendorRoutes.ts new file mode 100644 index 000000000..db79d99c6 --- /dev/null +++ b/apps/console2/src/routes/vendorRoutes.ts @@ -0,0 +1,51 @@ +import { lazy } from "react"; +import { loadQuery } from "react-relay"; +import type { AppRoute } from "/routes.tsx"; +import { relayEnvironment } from "/providers/RelayProviders"; +import { PageSkeleton } from "/components/skeletons/PageSkeleton"; +import { vendorNodeQuery, vendorsQuery } from "/hooks/graph/VendorGraph"; + +export const vendorRoutes = [ + { + path: "vendors", + fallback: PageSkeleton, + queryLoader: ({ organizationId }) => + loadQuery(relayEnvironment, vendorsQuery, { organizationId }), + Component: lazy(() => import("/pages/organizations/vendors/VendorsPage")), + }, + { + path: "vendors/:vendorId", + fallback: PageSkeleton, + queryLoader: ({ vendorId }) => + loadQuery(relayEnvironment, vendorNodeQuery, { + vendorId, + }), + Component: lazy( + () => import("../pages/organizations/vendors/VendorDetailPage") + ), + children: [ + { + path: "overview", + Component: lazy( + () => import("../pages/organizations/vendors/tabs/VendorOverviewTab") + ), + }, + { + path: "certifications", + Component: lazy( + () => + import( + "../pages/organizations/vendors/tabs/VendorCertificationsTab" + ) + ), + }, + { + path: "compliance", + Component: lazy( + () => + import("../pages/organizations/vendors/tabs/VendorComplianceTab") + ), + }, + ], + }, +] satisfies AppRoute[]; diff --git a/apps/console2/tsconfig.app.json b/apps/console2/tsconfig.app.json index 6b1da7159..40a8e8c55 100644 --- a/apps/console2/tsconfig.app.json +++ b/apps/console2/tsconfig.app.json @@ -3,7 +3,7 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, diff --git a/packages/helpers/src/certifications.ts b/packages/helpers/src/certifications.ts new file mode 100644 index 000000000..d3c751dc4 --- /dev/null +++ b/packages/helpers/src/certifications.ts @@ -0,0 +1,34 @@ +type Translator = (s: string) => string; + +export const certifications = { + securityStandards: [ + "SOC 2", + "ISO 27001", + "HITRUST", + "NIST", + "SOC 2 Type 2", + "SOC 2 Type 1", + ], + regulatoryLegal: ["HIPAA", "FERPA", "FISMA", "PIPEDA", "GDPR", "CCPA"], + industrySpecific: ["FinTech", "MPAA", "GSMA"], + internationalGov: ["FedRAMP", "ENS High", "IRAP", "CJIS"], + custom: [] as string[], +} as const; + +export const certificationCategoryLabel = ( + __: Translator, + category: keyof typeof certifications, +) => { + switch (category) { + case "securityStandards": + return __("Security Standards"); + case "regulatoryLegal": + return __("Regulatory & Legal"); + case "industrySpecific": + return __("Industry-Specific"); + case "internationalGov": + return __("International & Government"); + default: + return __("Custom certifications"); + } +}; diff --git a/packages/helpers/src/dom.ts b/packages/helpers/src/dom.ts new file mode 100644 index 000000000..ed5f24e7d --- /dev/null +++ b/packages/helpers/src/dom.ts @@ -0,0 +1,7 @@ +export function withViewTransition(fn: () => void) { + if (!document.startViewTransition) { + fn(); + return; + } + document.startViewTransition(fn); +} diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index daa51ce93..3339b6dfa 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -1,4 +1,4 @@ -export { objectKeys } from "./object"; +export { objectKeys, objectEntries } from "./object"; export { sprintf, faviconUrl } from "./string"; export { getTreatment, @@ -6,7 +6,9 @@ export { getRiskLikelihoods, getSeverity, } from "./risk"; +export { withViewTransition } from "./dom"; export { times, groupBy, isEmpty } from "./array"; export { randomInt } from "./number"; export { getMeasureStateLabel, measureStates } from "./measure"; export { getRole, getRoles, peopleRoles } from "./people"; +export { certificationCategoryLabel, certifications } from "./certifications"; diff --git a/packages/helpers/src/object.ts b/packages/helpers/src/object.ts index 1a7846c45..4a991e50b 100644 --- a/packages/helpers/src/object.ts +++ b/packages/helpers/src/object.ts @@ -4,3 +4,7 @@ export function objectKeys>(object: T) { return Object.keys(object) as (keyof T)[]; } + +export function objectEntries>(object: T) { + return Object.entries(object) as [keyof T, T[keyof T]][]; +} diff --git a/packages/hooks/src/usePageTitle.tsx b/packages/hooks/src/usePageTitle.ts similarity index 100% rename from packages/hooks/src/usePageTitle.tsx rename to packages/hooks/src/usePageTitle.ts diff --git a/packages/hooks/src/useToggle.tsx b/packages/hooks/src/useToggle.ts similarity index 100% rename from packages/hooks/src/useToggle.tsx rename to packages/hooks/src/useToggle.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index dd4784581..fe342d573 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-tabs": "^1.1.12", "@tailwindcss/vite": "^4.1.7", "clsx": "^2.1.1", + "react-dropzone": "^14.3.8", "react-markdown": "^10.1.0", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", diff --git a/packages/ui/src/Atoms/Badge/Badge.tsx b/packages/ui/src/Atoms/Badge/Badge.tsx index 8492086a1..8cae95f6a 100644 --- a/packages/ui/src/Atoms/Badge/Badge.tsx +++ b/packages/ui/src/Atoms/Badge/Badge.tsx @@ -1,7 +1,9 @@ import type { HTMLAttributes } from "react"; import { tv } from "tailwind-variants"; +import { Slot } from "../Slot"; type Props = { + asChild?: boolean; variant?: | "success" | "warning" @@ -11,10 +13,10 @@ type Props = { | "outline" | "highlight"; size?: "sm" | "md"; -} & HTMLAttributes; +} & HTMLAttributes; const badge = tv({ - base: "font-medium rounded-lg w-max flex gap-1 items-center", + base: "font-medium rounded-lg w-max flex gap-1 items-center group whitespace-nowrap", variants: { variant: { success: "bg-success text-txt-success", @@ -37,5 +39,7 @@ const badge = tv({ }); export function Badge(props: Props) { - return
; + const Component = props.asChild ? Slot : "div"; + const { asChild, size, variant, ...restProps } = props; + return ; } diff --git a/packages/ui/src/Atoms/Dropzone/Dropzone.stories.tsx b/packages/ui/src/Atoms/Dropzone/Dropzone.stories.tsx new file mode 100644 index 000000000..daf4a4100 --- /dev/null +++ b/packages/ui/src/Atoms/Dropzone/Dropzone.stories.tsx @@ -0,0 +1,12 @@ +import { Dropzone } from "./Dropzone"; +import type { Meta, StoryObj } from "@storybook/react"; + +export default { + title: "Atoms/Dropzone", + component: Dropzone, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/ui/src/Atoms/Dropzone/Dropzone.tsx b/packages/ui/src/Atoms/Dropzone/Dropzone.tsx new file mode 100644 index 000000000..70bccd76b --- /dev/null +++ b/packages/ui/src/Atoms/Dropzone/Dropzone.tsx @@ -0,0 +1,24 @@ +import { useCallback } from "react"; +import { useDropzone } from "react-dropzone"; + +type Props = {}; + +export function Dropzone({}: Props) { + const onDrop = useCallback((acceptedFiles) => { + // Do something with the files + }, []); + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + }); + + return ( +
+ + {isDragActive ? ( +

Drop the files here ...

+ ) : ( +

Drag 'n' drop some files here, or click to select files

+ )} +
+ ); +} diff --git a/packages/ui/src/Atoms/Select/Select.tsx b/packages/ui/src/Atoms/Select/Select.tsx index f577bd2c0..f71095cfe 100644 --- a/packages/ui/src/Atoms/Select/Select.tsx +++ b/packages/ui/src/Atoms/Select/Select.tsx @@ -83,7 +83,10 @@ export function Select({ return ( - + diff --git a/packages/ui/src/Molecules/Combobox/Combobox.tsx b/packages/ui/src/Molecules/Combobox/Combobox.tsx index efa7f3eb7..e2a0e37c1 100644 --- a/packages/ui/src/Molecules/Combobox/Combobox.tsx +++ b/packages/ui/src/Molecules/Combobox/Combobox.tsx @@ -18,13 +18,34 @@ type Props = { loading?: boolean; onSearch: (query: string) => void; placeholder?: string; + autoSelect?: boolean; + onSelect?: (value: string) => void; + resetValueOnHide?: boolean; + value?: string; }; -export function Combobox({ children, onSearch, placeholder }: Props) { +export function Combobox({ + children, + onSearch, + placeholder, + autoSelect, + onSelect, + resetValueOnHide, + value, +}: Props) { const showDropdown = !isEmpty(children); return ( - - + onSelect?.(v as string)} + resetValueOnHide={resetValueOnHide} + > + {showDropdown && ( {children} @@ -39,7 +60,7 @@ export function ComboboxItem({ ...props }: PropsWithChildren>) { return ( - + {children} ); diff --git a/packages/ui/src/Molecules/Field/Field.tsx b/packages/ui/src/Molecules/Field/Field.tsx index b14eeedd3..8d9f31e5f 100644 --- a/packages/ui/src/Molecules/Field/Field.tsx +++ b/packages/ui/src/Molecules/Field/Field.tsx @@ -2,7 +2,7 @@ import { tv } from "tailwind-variants"; import { Label } from "../../Atoms/Label/Label"; import { Input } from "../../Atoms/Input/Input"; import { Textarea } from "../../Atoms/Textarea/Textarea"; -import { type ComponentProps } from "react"; +import { type ComponentProps, type ReactNode } from "react"; import { Select } from "../../Atoms/Select/Select"; type BaseProps = { @@ -10,7 +10,8 @@ type BaseProps = { help?: string; error?: string; onValueChange?: (s: string) => void; - type: T; + type?: T; + children?: ReactNode; } & P; type Props = @@ -33,6 +34,7 @@ const { base: baseClass, label: labelClass, help: helpClass } = field(); export function Field(props: Props) { const showHelp = props.help && !props.error; + const childrenAsInput = !props.type && props.children; return (
{props.label && ( @@ -40,7 +42,7 @@ export function Field(props: Props) { {props.label} )} - {getInput(props)} + {childrenAsInput ? props.children : getInput(props)} {showHelp && {props.help}} {props.error && ( diff --git a/packages/ui/src/theme.css b/packages/ui/src/theme.css index 4de4886ae..d48d91360 100644 --- a/packages/ui/src/theme.css +++ b/packages/ui/src/theme.css @@ -249,6 +249,11 @@ button:focus-visible { } } +/* Allow interpolation : https://developer.chrome.com/docs/css-ui/animate-to-height-auto */ +:root { + interpolate-size: allow-keywords; +} + ::-webkit-scrollbar { width: 7px; height: 7px;