diff --git a/apps/console2/src/hooks/graph/FrameworkGraph.ts b/apps/console2/src/hooks/graph/FrameworkGraph.ts index c4c95c4f8..e525b30bf 100644 --- a/apps/console2/src/hooks/graph/FrameworkGraph.ts +++ b/apps/console2/src/hooks/graph/FrameworkGraph.ts @@ -5,6 +5,8 @@ import { sprintf } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; import { useConfirm } from "@probo/ui"; +export const connectionListKey = "FrameworksListQuery_frameworks"; + export const frameworksQuery = graphql` query FrameworkGraphListQuery($organizationId: ID!) { organization: node(id: $organizationId) { @@ -47,28 +49,32 @@ export const useDeleteFrameworkMutation = ( const confirm = useConfirm(); const { __ } = useTranslate(); - return useCallback(() => { - confirm( - () => { - return commitDelete({ - variables: { - input: { - frameworkId: framework.id!, + return useCallback( + (options?: { onSuccess?: () => void }) => { + return confirm( + () => { + return commitDelete({ + variables: { + input: { + frameworkId: framework.id!, + }, + connections: [connectionId], }, - connections: [connectionId], - }, - }); - }, - { - message: sprintf( - __( - 'This will permanently delete framework "%s". This action cannot be undone.' + ...options, + }); + }, + { + message: sprintf( + __( + 'This will permanently delete framework "%s". This action cannot be undone.' + ), + framework.name ), - framework.name - ), - } - ); - }, [framework, connectionId, commitDelete]); + } + ); + }, + [framework, connectionId, commitDelete] + ); }; export const frameworkNodeQuery = graphql` diff --git a/apps/console2/src/hooks/useMutationWithToasts.ts b/apps/console2/src/hooks/useMutationWithToasts.ts index 4ab71cfed..9cd7dc7cb 100644 --- a/apps/console2/src/hooks/useMutationWithToasts.ts +++ b/apps/console2/src/hooks/useMutationWithToasts.ts @@ -28,7 +28,7 @@ export function useMutationWithToasts( const options = { ...baseOptions, ...queryOptions }; return new Promise((resolve, reject) => mutate({ - ...options, + ...queryOptions, onCompleted: (response, error) => { options.onCompleted?.(response, error); if (error) { diff --git a/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx b/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx index b916c3034..362e7e7cf 100644 --- a/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx +++ b/apps/console2/src/pages/organizations/frameworks/FrameworkDetailPage.tsx @@ -5,17 +5,30 @@ import { type PreloadedQuery, } from "react-relay"; import { usePageTitle } from "@probo/hooks"; -import { graphql } from "relay-runtime"; -import { ControlItem, PageHeader } from "@probo/ui"; +import { ConnectionHandler, graphql } from "relay-runtime"; +import { + ActionDropdown, + Button, + ControlItem, + DropdownItem, + IconPencil, + IconTrashCan, + PageHeader, +} from "@probo/ui"; import { FrameworkLogo } from "/components/FrameworkLogo"; -import { frameworkNodeQuery } from "/hooks/graph/FrameworkGraph"; +import { + connectionListKey, + frameworkNodeQuery, + useDeleteFrameworkMutation, +} from "/hooks/graph/FrameworkGraph"; import { useTranslate } from "@probo/i18n"; import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard"; -import { useParams } from "react-router"; +import { useNavigate, useParams } from "react-router"; import { useOrganizationId } from "/hooks/useOrganizationId"; import type { FrameworkGraphNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphNodeQuery.graphql"; import type { FrameworkDetailPageFragment$key } from "./__generated__/FrameworkDetailPageFragment.graphql"; import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard"; +import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; const frameworkDetailFragment = graphql` fragment FrameworkDetailPageFragment on Framework { @@ -122,7 +135,14 @@ export default function FrameworkDetailPage(props: Props) { frameworkDetailFragment, data.node ); + const navigate = useNavigate(); + const controls = framework.controls.edges.map((edge) => edge.node); + const selectedControl = controlId + ? controls.find((control) => control.id === controlId) + : controls[0]; + usePageTitle(`${framework.name} | ${selectedControl?.referenceId}`); + // Mutations const [detachMeasure, isDetachingMeasure] = useMutation( detachMeasureMutation ); @@ -135,13 +155,18 @@ export default function FrameworkDetailPage(props: Props) { const [attachDocument, isAttachingDocument] = useMutation( attachDocumentMutation ); + const deleteFramework = useDeleteFrameworkMutation( + framework, + ConnectionHandler.getConnectionID(organizationId, connectionListKey)! + ); - const controls = framework.controls.edges.map((edge) => edge.node); - - const selectedControl = controlId - ? controls.find((control) => control.id === controlId) - : controls[0]; - usePageTitle(`${framework.name} | ${selectedControl?.referenceId}`); + const onDelete = () => { + deleteFramework({ + onSuccess: () => { + navigate(`/organizations/${organizationId}/frameworks`); + }, + }); + }; return (
@@ -152,7 +177,21 @@ export default function FrameworkDetailPage(props: Props) { {framework.name} } - /> + > + + + + + + {__("Delete")} + + +
{__("Requirement categories")}
diff --git a/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx b/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx index 47bac620e..8d20b027b 100644 --- a/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx +++ b/apps/console2/src/pages/organizations/frameworks/FrameworksPage.tsx @@ -6,6 +6,7 @@ import { DropdownItem, FileButton, FrameworkSelector, + IconPencil, IconTrashCan, IconUpload, PageHeader, @@ -26,8 +27,8 @@ 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 { CreateFrameworkDialog } from "./dialogs/CreateFrameworkDialog"; import { FrameworkLogo } from "/components/FrameworkLogo"; +import { FrameworkFormDialog } from "./dialogs/FrameworkFormDialog"; type Props = { queryRef: PreloadedQuery; @@ -119,7 +120,7 @@ export default function FrameworksPage(props: Props) { return (
- +
+ { + dialogRef.current?.open(); + }} + > + {__("Edit")} + deleteFramework()} variant="danger" > {__("Delete")} diff --git a/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx b/apps/console2/src/pages/organizations/frameworks/dialogs/FrameworkFormDialog.tsx similarity index 52% rename from apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx rename to apps/console2/src/pages/organizations/frameworks/dialogs/FrameworkFormDialog.tsx index d39ab1bde..5eda28c6b 100644 --- a/apps/console2/src/pages/organizations/frameworks/dialogs/CreateFrameworkDialog.tsx +++ b/apps/console2/src/pages/organizations/frameworks/dialogs/FrameworkFormDialog.tsx @@ -9,13 +9,14 @@ import { DialogFooter, Input, Textarea, + useDialogRef, type DialogRef, } from "@probo/ui"; import { z } from "zod"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; const createFrameworkMutation = graphql` - mutation CreateFrameworkDialogMutation( + mutation FrameworkFormDialogMutation( $input: CreateFrameworkInput! $connections: [ID!]! ) { @@ -30,10 +31,28 @@ const createFrameworkMutation = graphql` } `; +const updateFrameworkMutation = graphql` + mutation FrameworkFormDialogUpdateMutation($input: UpdateFrameworkInput!) { + updateFramework(input: $input) { + framework { + id + name + description + } + } + } +`; + type Props = { - connectionId: string; + connectionId?: string; organizationId: string; - ref: DialogRef; + framework?: { + id: string; + name: string; + description: string; + }; + ref?: DialogRef; + children?: React.ReactNode; }; const schema = z.object({ @@ -41,18 +60,41 @@ const schema = z.object({ description: z.string().max(255).optional(), }); -export function CreateFrameworkDialog(props: Props) { +/** + * Form to update or create a new framework + */ +export function FrameworkFormDialog(props: Props) { const { __ } = useTranslate(); - const { register, handleSubmit, reset } = useFormWithSchema(schema, {}); - const [commitCreate, isCreating] = useMutationWithToasts( - createFrameworkMutation, - { - successMessage: __("Framework created successfully"), - errorMessage: __("Failed to create framework"), - } - ); + const dialogRef = props.ref ?? useDialogRef(); + const { register, handleSubmit, reset } = useFormWithSchema(schema, { + defaultValues: { + name: props.framework?.name ?? "", + description: props.framework?.description ?? "", + }, + }); + const [create, isCreating] = useMutationWithToasts(createFrameworkMutation, { + successMessage: __("Framework created successfully"), + errorMessage: __("Failed to create framework"), + }); + const [update, isUpdating] = useMutationWithToasts(updateFrameworkMutation, { + successMessage: __("Framework updated successfully"), + errorMessage: __("Failed to update framework"), + }); const onSubmit = handleSubmit(async (data) => { - await commitCreate({ + if (props.framework) { + await update({ + variables: { + input: { + id: props.framework.id, + ...data, + }, + }, + }); + reset(data); + dialogRef.current?.close(); + return; + } + await create({ variables: { input: { ...data, @@ -62,12 +104,13 @@ export function CreateFrameworkDialog(props: Props) { }, }); reset(); - props.ref.current?.close(); + dialogRef.current?.close(); }); return ( } >
@@ -86,8 +129,8 @@ export function CreateFrameworkDialog(props: Props) { /> -
diff --git a/apps/console2/src/pages/organizations/frameworks/dialogs/__generated__/FrameworkFormDialogMutation.graphql.ts b/apps/console2/src/pages/organizations/frameworks/dialogs/__generated__/FrameworkFormDialogMutation.graphql.ts new file mode 100644 index 000000000..62fc725d0 --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/dialogs/__generated__/FrameworkFormDialogMutation.graphql.ts @@ -0,0 +1,203 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type CreateFrameworkInput = { + description: string; + name: string; + organizationId: string; +}; +export type FrameworkFormDialogMutation$variables = { + connections: ReadonlyArray; + input: CreateFrameworkInput; +}; +export type FrameworkFormDialogMutation$data = { + readonly createFramework: { + readonly frameworkEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"FrameworksPageCardFragment">; + }; + }; + }; +}; +export type FrameworkFormDialogMutation = { + response: FrameworkFormDialogMutation$data; + variables: FrameworkFormDialogMutation$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": "FrameworkFormDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateFrameworkPayload", + "kind": "LinkedField", + "name": "createFramework", + "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": "FrameworkFormDialogMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateFrameworkPayload", + "kind": "LinkedField", + "name": "createFramework", + "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": "931895b4bbb7caacd8ee0ee69de76cd6", + "id": null, + "metadata": {}, + "name": "FrameworkFormDialogMutation", + "operationKind": "mutation", + "text": "mutation FrameworkFormDialogMutation(\n $input: CreateFrameworkInput!\n) {\n createFramework(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 = "efed7b4ef49eea43e0bf9c0a8839c4ee"; + +export default node; diff --git a/apps/console2/src/pages/organizations/frameworks/dialogs/__generated__/FrameworkFormDialogUpdateMutation.graphql.ts b/apps/console2/src/pages/organizations/frameworks/dialogs/__generated__/FrameworkFormDialogUpdateMutation.graphql.ts new file mode 100644 index 000000000..28f92eadf --- /dev/null +++ b/apps/console2/src/pages/organizations/frameworks/dialogs/__generated__/FrameworkFormDialogUpdateMutation.graphql.ts @@ -0,0 +1,123 @@ +/** + * @generated SignedSource<<530c955a326f52f3c668e9f43788a54f>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type UpdateFrameworkInput = { + description?: string | null | undefined; + id: string; + name?: string | null | undefined; +}; +export type FrameworkFormDialogUpdateMutation$variables = { + input: UpdateFrameworkInput; +}; +export type FrameworkFormDialogUpdateMutation$data = { + readonly updateFramework: { + readonly framework: { + readonly description: string; + readonly id: string; + readonly name: string; + }; + }; +}; +export type FrameworkFormDialogUpdateMutation = { + response: FrameworkFormDialogUpdateMutation$data; + variables: FrameworkFormDialogUpdateMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateFrameworkPayload", + "kind": "LinkedField", + "name": "updateFramework", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Framework", + "kind": "LinkedField", + "name": "framework", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "FrameworkFormDialogUpdateMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "FrameworkFormDialogUpdateMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "54eaea672c9f7a7ddc3c6ba2b0655fc7", + "id": null, + "metadata": {}, + "name": "FrameworkFormDialogUpdateMutation", + "operationKind": "mutation", + "text": "mutation FrameworkFormDialogUpdateMutation(\n $input: UpdateFrameworkInput!\n) {\n updateFramework(input: $input) {\n framework {\n id\n name\n description\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "b8e9b44f6fa1cb437d7fae09133d97f0"; + +export default node;