diff --git a/apps/console2/public/logos/gdpr.png b/apps/console2/public/logos/gdpr.png new file mode 100644 index 000000000..72510b40a Binary files /dev/null and b/apps/console2/public/logos/gdpr.png differ diff --git a/apps/console2/public/logos/iso27001.png b/apps/console2/public/logos/iso27001.png new file mode 100644 index 000000000..00dff487e Binary files /dev/null and b/apps/console2/public/logos/iso27001.png differ diff --git a/apps/console2/public/logos/soc2.png b/apps/console2/public/logos/soc2.png new file mode 100644 index 000000000..f3b9be6c8 Binary files /dev/null and b/apps/console2/public/logos/soc2.png differ diff --git a/apps/console2/public/logos/sox.png b/apps/console2/public/logos/sox.png new file mode 100644 index 000000000..7daf0bef5 Binary files /dev/null and b/apps/console2/public/logos/sox.png differ diff --git a/apps/console2/src/hooks/graph/FrameworkGraph.ts b/apps/console2/src/hooks/graph/FrameworkGraph.ts new file mode 100644 index 000000000..0b1a5f0f4 --- /dev/null +++ b/apps/console2/src/hooks/graph/FrameworkGraph.ts @@ -0,0 +1,75 @@ +import { graphql } from "relay-runtime"; +import { useMutationWithToasts } from "../useMutationWithToasts"; +import { useCallback } from "react"; +import { sprintf } from "@probo/helpers"; +import { useTranslate } from "@probo/i18n"; +import { useConfirm } from "@probo/ui"; + +export const frameworksQuery = graphql` + query FrameworkGraphListQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + ... on Organization { + id + frameworks(first: 100) + @connection(key: "FrameworksListQuery_frameworks") { + __id + edges { + node { + id + ...FrameworksPageCardFragment + } + } + } + } + } + } +`; + +const deleteFrameworkMutation = graphql` + mutation FrameworkGraphDeleteMutation( + $input: DeleteFrameworkInput! + $connections: [ID!]! + ) { + deleteFramework(input: $input) { + deletedFrameworkId @deleteEdge(connections: $connections) + } + } +`; + +export const useDeleteFrameworkMutation = ( + framework: { id: string; name: string }, + connectionId: string +) => { + const [commitDelete, isDeleting] = useMutationWithToasts( + deleteFrameworkMutation, + { + errorMessage: "Failed to delete framework", + successMessage: "Framework deleted successfully", + } + ); + const confirm = useConfirm(); + const { __ } = useTranslate(); + + return useCallback(() => { + confirm( + () => { + return commitDelete({ + variables: { + input: { + frameworkId: framework.id!, + }, + connections: [connectionId], + }, + }); + }, + { + message: sprintf( + __( + 'This will permanently delete framework "%s". This action cannot be undone.' + ), + framework.name + ), + } + ); + }, [framework, connectionId, commitDelete]); +}; diff --git a/apps/console2/src/hooks/graph/__generated__/FrameworkGraphDeleteMutation.graphql.ts b/apps/console2/src/hooks/graph/__generated__/FrameworkGraphDeleteMutation.graphql.ts new file mode 100644 index 000000000..040bd7711 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/FrameworkGraphDeleteMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<05d197ce59adad67cfb89dff4c502c58>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteFrameworkInput = { + frameworkId: string; +}; +export type FrameworkGraphDeleteMutation$variables = { + connections: ReadonlyArray; + input: DeleteFrameworkInput; +}; +export type FrameworkGraphDeleteMutation$data = { + readonly deleteFramework: { + readonly deletedFrameworkId: string; + }; +}; +export type FrameworkGraphDeleteMutation = { + response: FrameworkGraphDeleteMutation$data; + variables: FrameworkGraphDeleteMutation$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": "deletedFrameworkId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "FrameworkGraphDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteFrameworkPayload", + "kind": "LinkedField", + "name": "deleteFramework", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "FrameworkGraphDeleteMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteFrameworkPayload", + "kind": "LinkedField", + "name": "deleteFramework", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedFrameworkId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "761a391f497aa0e2e03d0db958089712", + "id": null, + "metadata": {}, + "name": "FrameworkGraphDeleteMutation", + "operationKind": "mutation", + "text": "mutation FrameworkGraphDeleteMutation(\n $input: DeleteFrameworkInput!\n) {\n deleteFramework(input: $input) {\n deletedFrameworkId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "6ebefeb003ce3a27bd134651e1b5c94b"; + +export default node; diff --git a/apps/console2/src/hooks/graph/__generated__/FrameworkGraphListQuery.graphql.ts b/apps/console2/src/hooks/graph/__generated__/FrameworkGraphListQuery.graphql.ts new file mode 100644 index 000000000..0d4e9fd07 --- /dev/null +++ b/apps/console2/src/hooks/graph/__generated__/FrameworkGraphListQuery.graphql.ts @@ -0,0 +1,302 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type FrameworkGraphListQuery$variables = { + organizationId: string; +}; +export type FrameworkGraphListQuery$data = { + readonly organization: { + readonly frameworks?: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"FrameworksPageCardFragment">; + }; + }>; + }; + readonly id?: string; + }; +}; +export type FrameworkGraphListQuery = { + response: FrameworkGraphListQuery$data; + variables: FrameworkGraphListQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v4 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null +}, +v5 = { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null +}, +v6 = { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] +}, +v7 = [ + { + "kind": "Literal", + "name": "first", + "value": 100 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FrameworkGraphListQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + (v2/*: any*/), + { + "alias": "frameworks", + "args": null, + "concreteType": "FrameworkConnection", + "kind": "LinkedField", + "name": "__FrameworksListQuery_frameworks_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FrameworkEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "FrameworksPageCardFragment" + }, + (v3/*: any*/) + ], + "storageKey": null + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v5/*: any*/), + (v6/*: any*/) + ], + "storageKey": null + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FrameworkGraphListQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v7/*: any*/), + "concreteType": "FrameworkConnection", + "kind": "LinkedField", + "name": "frameworks", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FrameworkEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + }, + (v4/*: any*/) + ], + "storageKey": null + }, + (v5/*: any*/), + (v6/*: any*/) + ], + "storageKey": "frameworks(first:100)" + }, + { + "alias": null, + "args": (v7/*: any*/), + "filters": null, + "handle": "connection", + "key": "FrameworksListQuery_frameworks", + "kind": "LinkedHandle", + "name": "frameworks" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "b31afd68395ae800a18885bca3abfa1c", + "id": null, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "organization", + "frameworks" + ] + } + ] + }, + "name": "FrameworkGraphListQuery", + "operationKind": "query", + "text": "query FrameworkGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n frameworks(first: 100) {\n edges {\n node {\n id\n ...FrameworksPageCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n" + } +}; +})(); + +(node as any).hash = "4aaead183328e669aba942992db6f89c"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx b/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx new file mode 100644 index 000000000..d21b3ad49 --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx @@ -0,0 +1,219 @@ +import { usePageTitle } from "@probo/hooks"; +import { useTranslate } from "@probo/i18n"; +import { + ActionDropdown, + Avatar, + Button, + Card, + Dropdown, + DropdownItem, + FileButton, + FrameworkSelector, + IconChevronDown, + IconPlusLarge, + IconTrashCan, + IconUpload, + PageHeader, + useDialogRef, +} from "@probo/ui"; +import { + graphql, + useFragment, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; +import type { FrameworkGraphListQuery } from "/hooks/graph/__generated__/FrameworkGraphListQuery.graphql"; +import { + frameworksQuery, + useDeleteFrameworkMutation, +} from "/hooks/graph/FrameworkGraph"; +import { Link } from "react-router"; +import type { FrameworksPageCardFragment$key } from "./__generated__/FrameworksPageCardFragment.graphql"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { useState, type ChangeEventHandler } from "react"; +import { availableFrameworks } from "@probo/helpers"; +import { CreateFrameworkDialog } from "./dialogs/CreateFrameworkDialog"; + +type Props = { + queryRef: PreloadedQuery; +}; + +const importFrameworkMutation = graphql` + mutation FrameworksPageImportMutation( + $input: ImportFrameworkInput! + $connections: [ID!]! + ) { + importFramework(input: $input) { + frameworkEdge @prependEdge(connections: $connections) { + node { + id + ...FrameworksPageCardFragment + } + } + } + } +`; + +const availableLogos = new Map( + availableFrameworks.map((framework) => [framework.name, framework.logo]) +); + +export default function FrameworksPage(props: Props) { + const { __ } = useTranslate(); + usePageTitle(__("Frameworks")); + const data = usePreloadedQuery(frameworksQuery, props.queryRef); + const connectionId = data.organization.frameworks!.__id; + const frameworks = + data.organization.frameworks?.edges.map((edge) => edge.node) ?? []; + const [commitImport, isImporting] = useMutationWithToasts( + importFrameworkMutation, + { + successMessage: __("Framework imported successfully"), + errorMessage: __("Failed to import framework"), + } + ); + const [isUploading, setUploading] = useState(false); + const dialogRef = useDialogRef(); + + const importNamedFramework = async (name: string) => { + // For custom framework, open the form + if (name === "custom") { + console.log(name, dialogRef); + dialogRef.current?.open(); + return; + } + // Otherwise load the JSON and send the file to the server + try { + setUploading(true); + const fileName = `${name}.json`; + const json = await fetch(`/data/frameworks/${fileName}`).then((res) => + res.text() + ); + const file = new File([json], fileName, { + type: "application/json", + }); + await importFile(file); + } finally { + setUploading(false); + } + }; + + const importFile = (file: File) => { + return commitImport({ + variables: { + input: { + organizationId: data.organization.id!, + file: null, + }, + connections: [connectionId], + }, + uploadables: { + "input.file": file, + }, + }); + }; + + const handleUpload: ChangeEventHandler = (event) => { + const input = event.currentTarget; + const file = input.files?.[0]; + if (!file) { + return; + } + importFile(file).finally(() => { + input.value = ""; + }); + }; + + const isLoading = isUploading || isImporting; + + return ( +
+ + + + {__("Import")} + + + + +
+ {frameworks.map((framework) => ( + + ))} +
+
+ ); +} + +const frameworkCardFragment = graphql` + fragment FrameworksPageCardFragment on Framework { + id + name + description + } +`; + +type FrameworkCardProps = { + organizationId: string; + connectionId: string; + framework: FrameworksPageCardFragment$key; +}; + +function FrameworkCard(props: FrameworkCardProps) { + const framework = useFragment(frameworkCardFragment, props.framework); + const deleteFramework = useDeleteFrameworkMutation( + framework, + props.connectionId + ); + const { __ } = useTranslate(); + const logo = availableLogos.get(framework.name); + return ( + +
+ {logo ? ( + + ) : ( + + )} + + + {__("Delete")} + + +
+

+ + {framework.name} + +

+

{framework.description}

+
+ ); +} diff --git a/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageCardFragment.graphql.ts b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageCardFragment.graphql.ts new file mode 100644 index 000000000..1ecc063c9 --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageCardFragment.graphql.ts @@ -0,0 +1,58 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type FrameworksPageCardFragment$data = { + readonly description: string; + readonly id: string; + readonly name: string; + readonly " $fragmentType": "FrameworksPageCardFragment"; +}; +export type FrameworksPageCardFragment$key = { + readonly " $data"?: FrameworksPageCardFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"FrameworksPageCardFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "FrameworksPageCardFragment", + "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 + } + ], + "type": "Framework", + "abstractKey": null +}; + +(node as any).hash = "4481f380673963e72f88071031e37d14"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageImportMutation.graphql.ts b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageImportMutation.graphql.ts new file mode 100644 index 000000000..5546d50c5 --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/__generated__/FrameworksPageImportMutation.graphql.ts @@ -0,0 +1,202 @@ +/** + * @generated SignedSource<<9436ef052ea63a9785d82ec810e810f1>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type ImportFrameworkInput = { + file: any; + organizationId: string; +}; +export type FrameworksPageImportMutation$variables = { + connections: ReadonlyArray; + input: ImportFrameworkInput; +}; +export type FrameworksPageImportMutation$data = { + readonly importFramework: { + readonly frameworkEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"FrameworksPageCardFragment">; + }; + }; + }; +}; +export type FrameworksPageImportMutation = { + response: FrameworksPageImportMutation$data; + variables: FrameworksPageImportMutation$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": "FrameworksPageImportMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "ImportFrameworkPayload", + "kind": "LinkedField", + "name": "importFramework", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FrameworkEdge", + "kind": "LinkedField", + "name": "frameworkEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "FrameworksPageCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "FrameworksPageImportMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "ImportFrameworkPayload", + "kind": "LinkedField", + "name": "importFramework", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FrameworkEdge", + "kind": "LinkedField", + "name": "frameworkEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Framework", + "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": "description", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "frameworkEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "87a38788cb2e060657b552af69acb707", + "id": null, + "metadata": {}, + "name": "FrameworksPageImportMutation", + "operationKind": "mutation", + "text": "mutation FrameworksPageImportMutation(\n $input: ImportFrameworkInput!\n) {\n importFramework(input: $input) {\n frameworkEdge {\n node {\n id\n ...FrameworksPageCardFragment\n }\n }\n }\n}\n\nfragment FrameworksPageCardFragment on Framework {\n id\n name\n description\n}\n" + } +}; +})(); + +(node as any).hash = "e2ff0f6d6780cf967a021a093542dab9"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx b/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx new file mode 100644 index 000000000..cc87995ca --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx @@ -0,0 +1,105 @@ +import { graphql } from "relay-runtime"; +import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; +import { useTranslate } from "@probo/i18n"; +import { + Breadcrumb, + Button, + Dialog, + DialogContent, + DialogFooter, + Field, + Input, + Textarea, + useDialogRef, + type DialogRef, +} from "@probo/ui"; +import type { RefObject } from "react"; +import { z } from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; + +const createFrameworkMutation = graphql` + mutation CreateFrameworkDialogMutation( + $input: CreateFrameworkInput! + $connections: [ID!]! + ) { + createFramework(input: $input) { + frameworkEdge @prependEdge(connections: $connections) { + node { + id + ...FrameworksPageCardFragment + } + } + } + } +`; + +type Props = { + connectionId: string; + organizationId: string; + ref: DialogRef; +}; + +const schema = z.object({ + name: z.string().min(1).max(255), + description: z.string().max(255).optional(), +}); + +export function CreateFrameworkDialog(props: Props) { + const { __ } = useTranslate(); + const { + register, + handleSubmit, + formState: { errors }, + reset, + } = useFormWithSchema(schema, {}); + const [commitCreate, isCreating] = useMutationWithToasts( + createFrameworkMutation, + { + successMessage: __("Framework created successfully"), + errorMessage: __("Failed to create framework"), + } + ); + const onSubmit = handleSubmit(async (data) => { + await commitCreate({ + variables: { + input: { + ...data, + organizationId: props.organizationId, + }, + connections: [props.connectionId], + }, + }); + reset(); + props.ref.current?.close(); + }); + + return ( + } + > +
+ + +