From 9a33f7b771ce8c8758d09b734753127b82812abe Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Wed, 29 Oct 2025 14:03:50 +0100 Subject: [PATCH] Manage errors Signed-off-by: Sacha Al Himdani --- apps/console/src/components/PageError.tsx | 14 + .../src/components/tasks/TaskFormDialog.tsx | 41 +- .../src/hooks/forms/useDocumentForm.tsx | 6 +- apps/console/src/hooks/forms/useRiskForm.tsx | 8 +- .../console/src/hooks/forms/useVendorForm.tsx | 26 +- apps/console/src/hooks/graph/DocumentGraph.ts | 10 +- apps/console/src/hooks/graph/MeasureGraph.ts | 4 +- .../src/hooks/graph/OrganizationGraph.ts | 2 +- apps/console/src/hooks/graph/PeopleGraph.ts | 11 +- apps/console/src/hooks/graph/RiskGraph.ts | 2 +- .../src/hooks/graph/SAMLConfigurationGraph.ts | 2 +- .../src/hooks/graph/TrustCenterAuditGraph.ts | 2 +- .../hooks/graph/TrustCenterDocumentGraph.ts | 2 +- .../src/hooks/graph/TrustCenterVendorGraph.ts | 2 +- apps/console/src/hooks/graph/VendorGraph.ts | 2 +- .../src/hooks/useMutationWithIncrement.ts | 26 +- .../src/hooks/useMutationWithToasts.ts | 10 +- .../src/pages/auth/ConfirmEmailPage.tsx | 4 +- .../organizations/NewOrganizationPage.tsx | 6 +- .../organizations/audits/AuditDetailsPage.tsx | 4 +- .../audits/dialogs/CreateAuditDialog.tsx | 4 +- .../ContinualImprovementDetailsPage.tsx | 3 +- .../CreateContinualImprovementDialog.tsx | 3 +- .../documents/DocumentDetailPage.tsx | 24 +- .../dialogs/CreateDocumentDialog.tsx | 2 +- .../documents/dialogs/UpdateVersionDialog.tsx | 2 +- .../documents/tabs/DocumentControlsTab.tsx | 2 + .../frameworks/FrameworkControlPage.tsx | 67 +- .../dialogs/FrameworkControlDialog.tsx | 4 +- .../organizations/measures/MeasuresPage.tsx | 2 +- .../measures/dialog/MeasureFormDialog.tsx | 33 +- .../NonconformityDetailsPage.tsx | 4 +- .../dialogs/CreateNonconformityDialog.tsx | 3 +- .../obligations/ObligationDetailsPage.tsx | 5 +- .../dialogs/CreateObligationDialog.tsx | 5 +- .../people/dialogs/CreatePeopleDialog.tsx | 2 +- .../people/tabs/PeopleProfileTab.tsx | 2 +- .../people/tabs/PeopleRoleTab.tsx | 2 +- .../ProcessingActivityDetailsPage.tsx | 3 +- .../CreateProcessingActivityDialog.tsx | 3 +- .../organizations/risks/FormRiskDialog.tsx | 4 +- .../snapshots/dialog/SnapshotFormDialog.tsx | 2 +- .../trustCenter/TrustCenterAccessTab.tsx | 6 +- .../vendors/dialogs/CreateContactDialog.tsx | 6 +- .../dialogs/CreateRiskAssessmentDialog.tsx | 2 +- .../vendors/dialogs/CreateServiceDialog.tsx | 2 +- .../vendors/dialogs/EditContactDialog.tsx | 6 +- .../vendors/dialogs/EditServiceDialog.tsx | 2 +- .../dialogs/ImportAssessmentDialog.tsx | 2 +- apps/console/src/providers/RelayProviders.tsx | 22 +- apps/console/src/routes.tsx | 5 + packages/helpers/src/error.ts | 26 + packages/helpers/src/index.ts | 1 + .../ui/src/Layouts/ErrorLayout.stories.tsx | 2 +- .../ui/src/Molecules/Dialog/ConfirmDialog.tsx | 4 +- pkg/authz/service.go | 8 + pkg/coredata/asset.go | 25 + pkg/coredata/audit.go | 25 + pkg/coredata/control.go | 53 +- pkg/coredata/custom_domain.go | 26 + pkg/coredata/document.go | 21 + pkg/coredata/document_version.go | 41 ++ pkg/coredata/document_version_signature.go | 26 + pkg/coredata/evidence.go | 33 +- pkg/coredata/file.go | 37 +- pkg/coredata/framework.go | 26 +- pkg/coredata/measure.go | 37 +- pkg/coredata/organization.go | 25 + pkg/coredata/people.go | 12 + pkg/coredata/risk.go | 21 + pkg/coredata/session.go | 37 +- pkg/coredata/task.go | 37 +- pkg/coredata/trust_center.go | 26 + pkg/coredata/trust_center_access.go | 17 + pkg/coredata/trust_center_document_access.go | 28 + pkg/coredata/trust_center_reference.go | 24 +- pkg/coredata/vendor.go | 21 + .../vendor_business_associate_agreement.go | 31 +- pkg/coredata/vendor_data_privacy_agreement.go | 31 +- pkg/probo/document_service.go | 4 +- pkg/server/api/console/v1/resolver.go | 4 +- pkg/server/api/console/v1/v1_resolver.go | 582 ++++++++++++++---- pkg/server/graphql/errors.go | 69 +++ pkg/server/graphql/recovery.go | 36 +- 84 files changed, 1483 insertions(+), 331 deletions(-) create mode 100644 packages/helpers/src/error.ts create mode 100644 pkg/server/graphql/errors.go diff --git a/apps/console/src/components/PageError.tsx b/apps/console/src/components/PageError.tsx index a315db75d..a27b8d42a 100644 --- a/apps/console/src/components/PageError.tsx +++ b/apps/console/src/components/PageError.tsx @@ -68,6 +68,20 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) { ); } + if (error && error.toString().includes("UNAUTHORIZED")) { + return ( +
+

+ + {__("Access denied")} +

+

+ {__("You don't have permission to access this organization")} +

+
+ ); + } + return (

{__("Unexpected error :(")}

diff --git a/apps/console/src/components/tasks/TaskFormDialog.tsx b/apps/console/src/components/tasks/TaskFormDialog.tsx index 14100f845..117eb2a01 100644 --- a/apps/console/src/components/tasks/TaskFormDialog.tsx +++ b/apps/console/src/components/tasks/TaskFormDialog.tsx @@ -69,13 +69,28 @@ export const taskUpdateMutation = graphql` } `; -const schema = z.object({ - name: z.string(), +const createTaskSchema = z.object({ + name: z.string().min(1), description: z.string(), - timeEstimate: z.string().nullable(), - assignedToId: z.string(), - measureId: z.string(), - deadline: z.string().optional(), + timeEstimate: z.string().optional().nullable(), + assignedToId: z.preprocess( + (val) => (val === "" || val == null ? undefined : val), + z.string({ required_error: "Assigned to is required" }).min(1, "Assigned to is required") + ), + measureId: z.preprocess( + (val) => (val === "" || val == null ? undefined : val), + z.string({ required_error: "Measure is required" }).min(1, "Measure is required") + ), + deadline: z.string().optional().nullable(), +}); + +const updateTaskSchema = z.object({ + name: z.string().min(1), + description: z.string(), + timeEstimate: z.string().optional().nullable(), + assignedToId: z.string().optional(), + measureId: z.string().optional(), + deadline: z.string().optional().nullable(), }); type Props = { @@ -95,15 +110,17 @@ export default function TaskFormDialog(props: Props) { const [mutate] = task ? useMutationWithToasts(taskUpdateMutation, { successMessage: __("Task updated successfully."), - errorMessage: __("Failed to update task. Please try again."), + errorMessage: __("Failed to update task"), }) : useMutationWithToasts(taskCreateMutation, { successMessage: __("Task created successfully."), - errorMessage: __("Failed to create task. Please try again."), + errorMessage: __("Failed to create task"), }); + const isUpdating = !!task; + const { control, handleSubmit, register, formState, reset } = - useFormWithSchema(schema, { + useFormWithSchema(isUpdating ? updateTaskSchema : createTaskSchema, { defaultValues: { name: task?.name ?? "", description: task?.description ?? "", @@ -142,7 +159,7 @@ export default function TaskFormDialog(props: Props) { connections: [props.connection!], }, onCompleted: (_response, errors) => { - if (!errors) { + if (!errors && data.measureId) { updateStoreCounter(relayEnv, data.measureId, "tasks(first:0)", 1); } }, @@ -151,7 +168,6 @@ export default function TaskFormDialog(props: Props) { } dialogRef.current?.close(); }); - const isUpdating = !!task; const showMeasure = !props.measureId && !isUpdating; const isCreating = !isUpdating; @@ -217,9 +233,10 @@ export default function TaskFormDialog(props: Props) { ( + render={({ field: { onChange, value, ...field } }) => ( onChange(value)} /> )} diff --git a/apps/console/src/hooks/forms/useDocumentForm.tsx b/apps/console/src/hooks/forms/useDocumentForm.tsx index 82e7ab2e6..9ab6bf427 100644 --- a/apps/console/src/hooks/forms/useDocumentForm.tsx +++ b/apps/console/src/hooks/forms/useDocumentForm.tsx @@ -2,9 +2,9 @@ import { z } from "zod"; import { useFormWithSchema } from "../useFormWithSchema"; export const documentSchema = z.object({ - title: z.string(), - content: z.string(), - ownerId: z.string(), + title: z.string().min(1, "Title is required"), + content: z.string().min(1, "Content is required"), + ownerId: z.string().min(1, "Owner is required"), documentType: z.enum(["OTHER", "ISMS", "POLICY", "PROCEDURE"]), classification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]), }); diff --git a/apps/console/src/hooks/forms/useRiskForm.tsx b/apps/console/src/hooks/forms/useRiskForm.tsx index 50791095e..262e0d4f1 100644 --- a/apps/console/src/hooks/forms/useRiskForm.tsx +++ b/apps/console/src/hooks/forms/useRiskForm.tsx @@ -32,10 +32,10 @@ export type RiskKey = useRiskFormFragment$key & { id: string }; // Export the schema so it can be used elsewhere export const riskSchema = z.object({ - category: z.string(), - name: z.string(), - description: z.string(), - ownerId: z.string(), + category: z.string().min(1, "Category is required"), + name: z.string().min(1, "Name is required"), + description: z.string().min(1, "Description is required"), + ownerId: z.string().min(1, "Owner is required"), treatment: z.enum(["AVOIDED", "MITIGATED", "TRANSFERRED", "ACCEPTED"]), inherentLikelihood: z.number({ coerce: true }).min(1).max(5), inherentImpact: z.number({ coerce: true }).min(1).max(5), diff --git a/apps/console/src/hooks/forms/useVendorForm.tsx b/apps/console/src/hooks/forms/useVendorForm.tsx index 1c11fa516..16b2aeba2 100644 --- a/apps/console/src/hooks/forms/useVendorForm.tsx +++ b/apps/console/src/hooks/forms/useVendorForm.tsx @@ -8,21 +8,21 @@ import { useTranslate } from "@probo/i18n"; import { useEffect, useMemo } from "react"; const schema = z.object({ - name: z.string(), - description: z.string(), + name: z.string().min(1, "Name is required"), + description: z.string().min(1, "Description is required"), category: z.string().nullish(), - statusPageUrl: z.string(), - termsOfServiceUrl: z.string(), - privacyPolicyUrl: z.string(), - serviceLevelAgreementUrl: z.string(), - dataProcessingAgreementUrl: z.string(), - websiteUrl: z.string(), - legalName: z.string(), - headquarterAddress: z.string(), + statusPageUrl: z.string().optional(), + termsOfServiceUrl: z.string().optional(), + privacyPolicyUrl: z.string().optional(), + serviceLevelAgreementUrl: z.string().optional(), + dataProcessingAgreementUrl: z.string().optional(), + websiteUrl: z.string().optional(), + legalName: z.string().optional(), + headquarterAddress: z.string().optional(), certifications: z.array(z.string()), countries: z.array(z.string()), - securityPageUrl: z.string(), - trustPageUrl: z.string(), + securityPageUrl: z.string().optional(), + trustPageUrl: z.string().optional(), businessOwnerId: z.string().nullish(), securityOwnerId: z.string().nullish(), }); @@ -70,7 +70,7 @@ export function useVendorForm(vendorKey: useVendorFormFragment$key) { const [mutate] = useMutationWithToasts(vendorUpdateQuery, { successMessage: __("Vendor updated successfully."), - errorMessage: __("Failed to update vendor. Please try again."), + errorMessage: __("Failed to update vendor"), }); const defaultValues = useMemo( diff --git a/apps/console/src/hooks/graph/DocumentGraph.ts b/apps/console/src/hooks/graph/DocumentGraph.ts index 19953adcb..49b180cf8 100644 --- a/apps/console/src/hooks/graph/DocumentGraph.ts +++ b/apps/console/src/hooks/graph/DocumentGraph.ts @@ -34,7 +34,7 @@ export function useDeleteDocumentMutation() { deleteDocumentMutation, { successMessage: __("Document deleted successfully."), - errorMessage: __("Failed to delete document. Please try again."), + errorMessage: __("Failed to delete document"), } ); } @@ -57,7 +57,7 @@ export function useDeleteDraftDocumentVersionMutation() { deleteDraftDocumentVersionMutation, { successMessage: __("Draft deleted successfully."), - errorMessage: __("Failed to delete draft. Please try again."), + errorMessage: __("Failed to delete draft"), } ); } @@ -79,7 +79,7 @@ export function useBulkDeleteDocumentsMutation() { bulkDeleteDocumentsMutation, { successMessage: __("Documents deleted successfully."), - errorMessage: __("Failed to delete documents. Please try again."), + errorMessage: __("Failed to delete documents"), } ); } @@ -101,7 +101,7 @@ export function useSendSigningNotificationsMutation() { sendSigningNotificationsMutation, { successMessage: __("Signing notifications sent successfully."), - errorMessage: __("Failed to send signing notifications. Please try again."), + errorMessage: __("Failed to send signing notifications"), } ); } @@ -123,7 +123,7 @@ export function useBulkExportDocumentsMutation() { bulkExportDocumentsMutation, { successMessage: __("Document export started successfully. You will receive an email when the export is ready."), - errorMessage: __("Failed to start document export. Please try again."), + errorMessage: __("Failed to start document export"), } ); } diff --git a/apps/console/src/hooks/graph/MeasureGraph.ts b/apps/console/src/hooks/graph/MeasureGraph.ts index 235d64985..cf4277b27 100644 --- a/apps/console/src/hooks/graph/MeasureGraph.ts +++ b/apps/console/src/hooks/graph/MeasureGraph.ts @@ -32,7 +32,7 @@ export function useDeleteMeasureMutation() { deleteMeasureMutation, { successMessage: __("Measure deleted successfully."), - errorMessage: __("Failed to delete measure. Please try again."), + errorMessage: __("Failed to delete measure"), } ); } @@ -83,6 +83,6 @@ export const useUpdateMeasure = () => { return useMutationWithToasts(measureUpdateMutation, { successMessage: __("Measure updated successfully."), - errorMessage: __("Failed to update measure. Please try again."), + errorMessage: __("Failed to update measure"), }); }; diff --git a/apps/console/src/hooks/graph/OrganizationGraph.ts b/apps/console/src/hooks/graph/OrganizationGraph.ts index 2d4330cb5..8df814d97 100644 --- a/apps/console/src/hooks/graph/OrganizationGraph.ts +++ b/apps/console/src/hooks/graph/OrganizationGraph.ts @@ -33,7 +33,7 @@ export function useDeleteOrganizationMutation() { deleteOrganizationMutation, { successMessage: __("Organization deleted successfully."), - errorMessage: __("Failed to delete organization. Please try again."), + errorMessage: __("Failed to delete organization"), } ); } diff --git a/apps/console/src/hooks/graph/PeopleGraph.ts b/apps/console/src/hooks/graph/PeopleGraph.ts index ba2e571ff..dec15875f 100644 --- a/apps/console/src/hooks/graph/PeopleGraph.ts +++ b/apps/console/src/hooks/graph/PeopleGraph.ts @@ -10,9 +10,9 @@ import { 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 { useConfirm, useToast } from "@probo/ui"; import type { PeopleGraphDeleteMutation } from "./__generated__/PeopleGraphDeleteMutation.graphql"; -import { promisifyMutation, sprintf } from "@probo/helpers"; +import { promisifyMutation, sprintf, formatError, type GraphQLError } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; const peopleQuery = graphql` @@ -132,6 +132,7 @@ export const useDeletePeople = ( ) => { const [mutate] = useMutation(deletePeopleMutation); const confirm = useConfirm(); + const { toast } = useToast(); const { __ } = useTranslate(); return () => { @@ -147,6 +148,12 @@ export const useDeletePeople = ( }, connections: [connectionId], }, + }).catch((error) => { + toast({ + title: __("Error"), + description: formatError(__("Failed to delete people"), error as GraphQLError), + variant: "error", + }); }), { message: sprintf( diff --git a/apps/console/src/hooks/graph/RiskGraph.ts b/apps/console/src/hooks/graph/RiskGraph.ts index 17ad0f038..8dc288c25 100644 --- a/apps/console/src/hooks/graph/RiskGraph.ts +++ b/apps/console/src/hooks/graph/RiskGraph.ts @@ -26,7 +26,7 @@ export function useDeleteRiskMutation() { return useMutationWithToasts(deleteRiskMutation, { successMessage: __("Risk deleted successfully."), - errorMessage: __("Failed to delete risk. Please try again."), + errorMessage: __("Failed to delete risk"), }); } diff --git a/apps/console/src/hooks/graph/SAMLConfigurationGraph.ts b/apps/console/src/hooks/graph/SAMLConfigurationGraph.ts index 5df137a64..841ab9ee2 100644 --- a/apps/console/src/hooks/graph/SAMLConfigurationGraph.ts +++ b/apps/console/src/hooks/graph/SAMLConfigurationGraph.ts @@ -137,7 +137,7 @@ export function useCreateSAMLConfigurationMutation() { createSAMLConfigurationMutation, { successMessage: "SAML configuration created successfully.", - errorMessage: "Failed to create SAML configuration. Please try again.", + errorMessage: "Failed to create SAML configuration", } ); } diff --git a/apps/console/src/hooks/graph/TrustCenterAuditGraph.ts b/apps/console/src/hooks/graph/TrustCenterAuditGraph.ts index 6d8d4cf5b..324f711d6 100644 --- a/apps/console/src/hooks/graph/TrustCenterAuditGraph.ts +++ b/apps/console/src/hooks/graph/TrustCenterAuditGraph.ts @@ -22,7 +22,7 @@ export function useTrustCenterAuditUpdate() { trustCenterAuditUpdateMutation, { successMessage: __("Audit visibility updated successfully."), - errorMessage: __("Failed to update audit visibility. Please try again."), + errorMessage: __("Failed to update audit visibility"), } ); } diff --git a/apps/console/src/hooks/graph/TrustCenterDocumentGraph.ts b/apps/console/src/hooks/graph/TrustCenterDocumentGraph.ts index 2780650b9..5bead06e9 100644 --- a/apps/console/src/hooks/graph/TrustCenterDocumentGraph.ts +++ b/apps/console/src/hooks/graph/TrustCenterDocumentGraph.ts @@ -40,7 +40,7 @@ export function useUpdateDocumentVisibilityMutation() { updateDocumentVisibilityMutation, { successMessage: __("Document visibility updated successfully."), - errorMessage: __("Failed to update document visibility. Please try again."), + errorMessage: __("Failed to update document visibility"), } ); } diff --git a/apps/console/src/hooks/graph/TrustCenterVendorGraph.ts b/apps/console/src/hooks/graph/TrustCenterVendorGraph.ts index 85a38402d..b94e94308 100644 --- a/apps/console/src/hooks/graph/TrustCenterVendorGraph.ts +++ b/apps/console/src/hooks/graph/TrustCenterVendorGraph.ts @@ -22,7 +22,7 @@ export function useTrustCenterVendorUpdate() { trustCenterVendorUpdateMutation, { successMessage: __("Vendor visibility updated successfully."), - errorMessage: __("Failed to update vendor visibility. Please try again."), + errorMessage: __("Failed to update vendor visibility"), } ); } diff --git a/apps/console/src/hooks/graph/VendorGraph.ts b/apps/console/src/hooks/graph/VendorGraph.ts index 31e879039..6139e5bd4 100644 --- a/apps/console/src/hooks/graph/VendorGraph.ts +++ b/apps/console/src/hooks/graph/VendorGraph.ts @@ -36,7 +36,7 @@ export function useCreateVendorMutation() { createVendorMutation, { successMessage: __("Vendor created successfully."), - errorMessage: __("Failed to create vendor. Please try again."), + errorMessage: __("Failed to create vendor"), } ); } diff --git a/apps/console/src/hooks/useMutationWithIncrement.ts b/apps/console/src/hooks/useMutationWithIncrement.ts index b6fab88c7..8af8b6c86 100644 --- a/apps/console/src/hooks/useMutationWithIncrement.ts +++ b/apps/console/src/hooks/useMutationWithIncrement.ts @@ -10,6 +10,9 @@ import { type GraphQLTaggedNode, type MutationParameters, } from "relay-runtime"; +import { useToast } from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { formatError, type GraphQLError } from "@probo/helpers"; const defaultOptions = { field: "totalCount", @@ -26,17 +29,27 @@ export function useMutationWithIncrement( node: string; field?: string; value?: 1 | -1; + errorMessage?: string; }, ) { const [mutate, isLoading] = useMutation(query); const relayEnv = useRelayEnvironment(); + const { toast } = useToast(); + const { __ } = useTranslate(); const options = { ...defaultOptions, ...baseOptions }; const mutateAndIncrement = useCallback( (queryOptions: UseMutationConfig) => { return mutate({ ...queryOptions, onCompleted: (response, error) => { - if (!error) { + if (error) { + const errorTitle = options.errorMessage ?? __("Failed to commit this operation"); + toast({ + title: __("Error"), + description: formatError(errorTitle, error as GraphQLError[]), + variant: "error", + }); + } else { updateStoreCounter( relayEnv, options.id, @@ -47,9 +60,18 @@ export function useMutationWithIncrement( } queryOptions.onCompleted?.(response, error); }, + onError: (error) => { + const errorTitle = options.errorMessage ?? __("Failed to commit this operation"); + toast({ + title: __("Error"), + description: formatError(errorTitle, error as GraphQLError), + variant: "error", + }); + queryOptions.onError?.(error); + }, }); }, - [mutate, options.id, options.node, options.field, options.value, relayEnv], + [mutate, options.id, options.node, options.field, options.value, options.errorMessage, relayEnv, toast, __], ); return [mutateAndIncrement, isLoading] as const; diff --git a/apps/console/src/hooks/useMutationWithToasts.ts b/apps/console/src/hooks/useMutationWithToasts.ts index e6e42fbad..3d18b6a36 100644 --- a/apps/console/src/hooks/useMutationWithToasts.ts +++ b/apps/console/src/hooks/useMutationWithToasts.ts @@ -3,6 +3,7 @@ import { useMutation, type UseMutationConfig } from "react-relay"; import { useToast } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import type { MutationParameters, GraphQLTaggedNode } from "relay-runtime"; +import { formatError, type GraphQLError } from "@probo/helpers"; /** * A decorated useMutation hook that emits toast notifications on success or error. @@ -32,11 +33,10 @@ export function useMutationWithToasts( onCompleted: (response, error) => { options.onCompleted?.(response, error); if (error) { + const errorTitle = options.errorMessage ?? __("Failed to commit this operation"); toast({ title: __("Error"), - description: - options.errorMessage ?? - __("Failed to commit this operation."), + description: formatError(errorTitle, error as GraphQLError[]), variant: "error", }); reject(error); @@ -57,10 +57,10 @@ export function useMutationWithToasts( resolve(); }, onError: (error) => { + const errorTitle = options.errorMessage ?? __("Failed to commit this operation"); toast({ title: __("Error"), - description: - options.errorMessage ?? __("Failed to commit this operation."), + description: formatError(errorTitle, error as GraphQLError), variant: "error", }); reject(error); diff --git a/apps/console/src/pages/auth/ConfirmEmailPage.tsx b/apps/console/src/pages/auth/ConfirmEmailPage.tsx index f788e79a5..c35a11658 100644 --- a/apps/console/src/pages/auth/ConfirmEmailPage.tsx +++ b/apps/console/src/pages/auth/ConfirmEmailPage.tsx @@ -81,7 +81,7 @@ export default function ConfirmEmailPage() { onError: (err) => { toast({ title: __("Error"), - description: err.message || __("Failed to confirm email. Please try again."), + description: err.message || __("Failed to confirm email"), variant: "error", }); setIsLoading(false); @@ -90,7 +90,7 @@ export default function ConfirmEmailPage() { } catch (error) { toast({ title: __("Error"), - description: error instanceof Error ? error.message : __("Failed to confirm email. Please try again."), + description: error instanceof Error ? error.message : __("Failed to confirm email"), variant: "error", }); setIsLoading(false); diff --git a/apps/console/src/pages/organizations/NewOrganizationPage.tsx b/apps/console/src/pages/organizations/NewOrganizationPage.tsx index 80adeb9ac..868dc24d9 100644 --- a/apps/console/src/pages/organizations/NewOrganizationPage.tsx +++ b/apps/console/src/pages/organizations/NewOrganizationPage.tsx @@ -6,6 +6,7 @@ import type { NewOrganizationPageQuery as NewOrganizationPageQueryType } from ". import type { NewOrganizationPageMutation as NewOrganizationPageMutationType } from "./__generated__/NewOrganizationPageMutation.graphql"; import { useState, type FormEventHandler } from "react"; import { useNavigate } from "react-router"; +import { formatError, type GraphQLError } from "@probo/helpers"; const createOrganizationMutation = graphql` mutation NewOrganizationPageMutation( @@ -75,11 +76,12 @@ export default function NewOrganizationPage() { variant: "success", }); }, - onError: (e) => { + onError: (e: GraphQLError) => { setIsFetching(false); + toast({ title: __("Error"), - description: e.message ?? __("Failed to create organization"), + description: formatError(__("Failed to create organization"), e), variant: "error", }); }, diff --git a/apps/console/src/pages/organizations/audits/AuditDetailsPage.tsx b/apps/console/src/pages/organizations/audits/AuditDetailsPage.tsx index 8da206fe4..dd8180905 100644 --- a/apps/console/src/pages/organizations/audits/AuditDetailsPage.tsx +++ b/apps/console/src/pages/organizations/audits/AuditDetailsPage.tsx @@ -32,7 +32,7 @@ import { FrameworkLogo } from "/components/FrameworkLogo"; import { ControlledField } from "/components/form/ControlledField"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import z from "zod"; -import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatDate } from "@probo/helpers"; +import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime, formatError, formatDate, type GraphQLError } from "@probo/helpers"; import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql"; const updateAuditSchema = z.object({ @@ -96,7 +96,7 @@ export default function AuditDetailsPage(props: Props) { } catch (error) { toast({ title: __("Error"), - description: error instanceof Error ? error.message : __("Failed to update audit"), + description: formatError(__("Failed to update audit"), error as GraphQLError), variant: "error", }); } diff --git a/apps/console/src/pages/organizations/audits/dialogs/CreateAuditDialog.tsx b/apps/console/src/pages/organizations/audits/dialogs/CreateAuditDialog.tsx index 9f7fc8353..81ba3fa6e 100644 --- a/apps/console/src/pages/organizations/audits/dialogs/CreateAuditDialog.tsx +++ b/apps/console/src/pages/organizations/audits/dialogs/CreateAuditDialog.tsx @@ -16,7 +16,7 @@ import z from "zod"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { ControlledField } from "/components/form/ControlledField"; import { useCreateAudit } from "/hooks/graph/AuditGraph"; -import { auditStates, getAuditStateLabel, formatDatetime } from "@probo/helpers"; +import { auditStates, getAuditStateLabel, formatDatetime, formatError, type GraphQLError } from "@probo/helpers"; import { useLazyLoadQuery } from "react-relay"; import { graphql } from "relay-runtime"; import { Suspense } from "react"; @@ -94,7 +94,7 @@ export function CreateAuditDialog({ } catch (error) { toast({ title: __("Error"), - description: error instanceof Error ? error.message : __("Failed to create audit"), + description: formatError(__("Failed to create audit"), error as GraphQLError), variant: "error", }); } diff --git a/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx b/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx index b8c1667b1..0a6b95e52 100644 --- a/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx +++ b/apps/console/src/pages/organizations/continualImprovements/ContinualImprovementDetailsPage.tsx @@ -30,6 +30,7 @@ import { useOrganizationId } from "/hooks/useOrganizationId"; import { PeopleSelectField } from "/components/form/PeopleSelectField"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { Controller } from "react-hook-form"; +import { formatError, type GraphQLError } from "@probo/helpers"; import z from "zod"; import { getStatusVariant, getStatusLabel, formatDatetime, validateSnapshotConsistency } from "@probo/helpers"; import { SnapshotBanner } from "/components/SnapshotBanner"; @@ -113,7 +114,7 @@ export default function ContinualImprovementDetailsPage(props: Props) { } catch (error) { toast({ title: __("Error"), - description: __("Failed to update continual improvement entry"), + description: formatError(__("Failed to update continual improvement"), error as GraphQLError), variant: "error", }); } diff --git a/apps/console/src/pages/organizations/continualImprovements/dialogs/CreateContinualImprovementDialog.tsx b/apps/console/src/pages/organizations/continualImprovements/dialogs/CreateContinualImprovementDialog.tsx index a33a2c3cc..a5a6b5af3 100644 --- a/apps/console/src/pages/organizations/continualImprovements/dialogs/CreateContinualImprovementDialog.tsx +++ b/apps/console/src/pages/organizations/continualImprovements/dialogs/CreateContinualImprovementDialog.tsx @@ -20,6 +20,7 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useCreateContinualImprovement } from "../../../../hooks/graph/ContinualImprovementGraph"; import { PeopleSelectField } from "/components/form/PeopleSelectField"; import { Controller } from "react-hook-form"; +import { formatError, type GraphQLError } from "@probo/helpers"; import { formatDatetime } from "@probo/helpers"; const schema = z.object({ @@ -87,7 +88,7 @@ export function CreateContinualImprovementDialog({ } catch (error) { toast({ title: __("Error"), - description: __("Failed to create continual improvement entry"), + description: formatError(__("Failed to create continual improvement"), error as GraphQLError), variant: "error", }); } diff --git a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx index 3e5966367..0b8461536 100644 --- a/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx +++ b/apps/console/src/pages/organizations/documents/DocumentDetailPage.tsx @@ -245,7 +245,7 @@ export default function DocumentDetailPage(props: Props) { publishDocumentVersionMutation, { successMessage: __("Document published successfully."), - errorMessage: __("Failed to publish document. Please try again."), + errorMessage: __("Failed to publish document"), } ); const [deleteDocument, isDeleting] = useDeleteDocumentMutation(); @@ -256,23 +256,19 @@ export default function DocumentDetailPage(props: Props) { exportDocumentVersionPDFMutation, { successMessage: __("PDF download started."), - errorMessage: __("Failed to generate PDF. Please try again."), + errorMessage: __("Failed to generate PDF"), } ); - const userEmailData = useLazyLoadQuery( - UserEmailQuery, - {} - ); + const userEmailData = useLazyLoadQuery(UserEmailQuery, {}); const defaultEmail = userEmailData.viewer.user.email; - const [updateDocument, isUpdatingDocument] = - useMutationWithToasts( - updateDocumentMutation, - { - successMessage: __("Document updated successfully."), - errorMessage: __("Failed to update document. Please try again."), - } - ); + const [updateDocument, isUpdatingDocument] = useMutationWithToasts( + updateDocumentMutation, + { + successMessage: __("Document updated successfully."), + errorMessage: __("Failed to update document"), + } + ); const versionConnectionId = document.versions.__id; const { register, control, handleSubmit, reset } = useFormWithSchema( diff --git a/apps/console/src/pages/organizations/documents/dialogs/CreateDocumentDialog.tsx b/apps/console/src/pages/organizations/documents/dialogs/CreateDocumentDialog.tsx index 460c95acf..6b5c0955d 100644 --- a/apps/console/src/pages/organizations/documents/dialogs/CreateDocumentDialog.tsx +++ b/apps/console/src/pages/organizations/documents/dialogs/CreateDocumentDialog.tsx @@ -67,7 +67,7 @@ export function CreateDocumentDialog({ trigger, connection }: Props) { connections: [connection!], }, successMessage: __("Document created successfully."), - errorMessage: __("Failed to create document. Please try again."), + errorMessage: __("Failed to create document"), onSuccess: () => { dialogRef.current?.close(); reset(); diff --git a/apps/console/src/pages/organizations/documents/dialogs/UpdateVersionDialog.tsx b/apps/console/src/pages/organizations/documents/dialogs/UpdateVersionDialog.tsx index 1b208b8c9..8fa5fe2b6 100644 --- a/apps/console/src/pages/organizations/documents/dialogs/UpdateVersionDialog.tsx +++ b/apps/console/src/pages/organizations/documents/dialogs/UpdateVersionDialog.tsx @@ -89,7 +89,7 @@ export default function UpdateVersionDialog({ UpdateDocumentMutation, { successMessage: __("Document updated successfully."), - errorMessage: __("Failed to update document. Please try again."), + errorMessage: __("Failed to update document"), } ); const { handleSubmit, register } = useFormWithSchema(versionSchema, { diff --git a/apps/console/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx b/apps/console/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx index d8e22ff28..007d0fe28 100644 --- a/apps/console/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx +++ b/apps/console/src/pages/organizations/documents/tabs/DocumentControlsTab.tsx @@ -78,6 +78,7 @@ export default function DocumentControlsTab() { { ...incrementOptions, value: -1, + errorMessage: "Failed to unlink control", }, ); const [attachControl, isAttaching] = useMutationWithIncrement( @@ -85,6 +86,7 @@ export default function DocumentControlsTab() { { ...incrementOptions, value: 1, + errorMessage: "Failed to link control", }, ); const isLoading = isDetaching || isAttaching; diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx index 94d7badaa..3f919092a 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx @@ -2,8 +2,9 @@ import { useMutation, usePreloadedQuery, type PreloadedQuery, + type UseMutationConfig, } from "react-relay"; -import { graphql } from "relay-runtime"; +import { graphql, type MutationParameters } from "relay-runtime"; import { ActionDropdown, Button, @@ -11,8 +12,10 @@ import { IconPencil, IconTrashCan, useConfirm, + useToast, } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; +import { formatError, type GraphQLError } from "@probo/helpers"; import { LinkedMeasuresCard } from "/components/measures/LinkedMeasuresCard"; import { useNavigate, useOutletContext } from "react-router"; import { useOrganizationId } from "/hooks/useOrganizationId"; @@ -153,6 +156,7 @@ type Props = { */ export default function FrameworkControlPage({ queryRef }: Props) { const { __ } = useTranslate(); + const { toast } = useToast(); const { framework } = useOutletContext<{ framework: FrameworkDetailPageFragment$data; }>(); @@ -161,25 +165,44 @@ export default function FrameworkControlPage({ queryRef }: Props) { const organizationId = useOrganizationId(); const confirm = useConfirm(); const navigate = useNavigate(); - // Mutations - const [detachMeasure, isDetachingMeasure] = useMutation( - detachMeasureMutation - ); - const [attachMeasure, isAttachingMeasure] = useMutation( - attachMeasureMutation - ); - const [detachDocument, isDetachingDocument] = useMutation( - detachDocumentMutation - ); - const [attachDocument, isAttachingDocument] = useMutation( - attachDocumentMutation - ); + + const [detachMeasure, isDetachingMeasure] = useMutation(detachMeasureMutation); + const [attachMeasure, isAttachingMeasure] = useMutation(attachMeasureMutation); + const [detachDocument, isDetachingDocument] = useMutation(detachDocumentMutation); + const [attachDocument, isAttachingDocument] = useMutation(attachDocumentMutation); const [detachAudit, isDetachingAudit] = useMutation(detachAuditMutation); const [attachAudit, isAttachingAudit] = useMutation(attachAuditMutation); const [detachSnapshot, isDetachingSnapshot] = useMutation(detachSnapshotMutation); const [attachSnapshot, isAttachingSnapshot] = useMutation(attachSnapshotMutation); const [deleteControl] = useMutation(deleteControlMutation); + const withErrorHandling = ( + mutationFn: (config: UseMutationConfig) => void, + errorMessage: string + ) => (options: UseMutationConfig) => { + mutationFn({ + ...options, + onCompleted: (response, error) => { + if (error) { + toast({ + title: __("Error"), + description: formatError(errorMessage, error as GraphQLError), + variant: "error", + }); + } + options.onCompleted?.(response, error); + }, + onError: (error) => { + toast({ + title: __("Error"), + description: formatError(errorMessage, error as GraphQLError), + variant: "error", + }); + options.onError?.(error); + }, + }); + }; + const onDelete = () => { confirm( () => { @@ -253,8 +276,8 @@ export default function FrameworkControlPage({ queryRef }: Props) { measures={control.measures?.edges.map((edge) => edge.node) ?? []} params={{ controlId: control.id }} connectionId={control.measures?.__id!} - onAttach={attachMeasure} - onDetach={detachMeasure} + onAttach={withErrorHandling(attachMeasure, __("Failed to link measure"))} + onDetach={withErrorHandling(detachMeasure, __("Failed to unlink measure"))} disabled={isAttachingMeasure || isDetachingMeasure} />
@@ -264,8 +287,8 @@ export default function FrameworkControlPage({ queryRef }: Props) { documents={control.documents?.edges.map((edge) => edge.node) ?? []} params={{ controlId: control.id }} connectionId={control.documents?.__id!} - onAttach={attachDocument} - onDetach={detachDocument} + onAttach={withErrorHandling(attachDocument, __("Failed to link document"))} + onDetach={withErrorHandling(detachDocument, __("Failed to unlink document"))} disabled={isAttachingDocument || isDetachingDocument} /> @@ -275,8 +298,8 @@ export default function FrameworkControlPage({ queryRef }: Props) { audits={control.audits?.edges.map((edge) => edge.node) ?? []} params={{ controlId: control.id }} connectionId={control.audits?.__id!} - onAttach={attachAudit} - onDetach={detachAudit} + onAttach={withErrorHandling(attachAudit, __("Failed to link audit"))} + onDetach={withErrorHandling(detachAudit, __("Failed to unlink audit"))} disabled={isAttachingAudit || isDetachingAudit} /> @@ -286,8 +309,8 @@ export default function FrameworkControlPage({ queryRef }: Props) { snapshots={control.snapshots?.edges.map((edge) => edge.node) ?? []} params={{ controlId: control.id }} connectionId={control.snapshots?.__id!} - onAttach={attachSnapshot} - onDetach={detachSnapshot} + onAttach={withErrorHandling(attachSnapshot, __("Failed to link snapshot"))} + onDetach={withErrorHandling(detachSnapshot, __("Failed to unlink snapshot"))} disabled={isAttachingSnapshot || isDetachingSnapshot} /> diff --git a/apps/console/src/pages/organizations/frameworks/dialogs/FrameworkControlDialog.tsx b/apps/console/src/pages/organizations/frameworks/dialogs/FrameworkControlDialog.tsx index e806d6656..9aa3ba8b0 100644 --- a/apps/console/src/pages/organizations/frameworks/dialogs/FrameworkControlDialog.tsx +++ b/apps/console/src/pages/organizations/frameworks/dialogs/FrameworkControlDialog.tsx @@ -86,11 +86,11 @@ export function FrameworkControlDialog(props: Props) { const [mutate, isMutating] = props.control ? useMutationWithToasts(updateMutation, { successMessage: __("Control updated successfully."), - errorMessage: __("Failed to update control. Please try again."), + errorMessage: __("Failed to update control"), }) : useMutationWithToasts(createMutation, { successMessage: __("Control created successfully."), - errorMessage: __("Failed to create control. Please try again."), + errorMessage: __("Failed to create control"), }); const defaultValues = useMemo(() => ({ diff --git a/apps/console/src/pages/organizations/measures/MeasuresPage.tsx b/apps/console/src/pages/organizations/measures/MeasuresPage.tsx index 2ebe638a6..67eac2912 100644 --- a/apps/console/src/pages/organizations/measures/MeasuresPage.tsx +++ b/apps/console/src/pages/organizations/measures/MeasuresPage.tsx @@ -107,7 +107,7 @@ export default function MeasuresPage(props: Props) { importMeasuresMutation, { successMessage: __("Measures imported successfully."), - errorMessage: __("Failed to import measures. Please try again."), + errorMessage: __("Failed to import measures"), } ); const importFileRef = useRef(null); diff --git a/apps/console/src/pages/organizations/measures/dialog/MeasureFormDialog.tsx b/apps/console/src/pages/organizations/measures/dialog/MeasureFormDialog.tsx index 811c9db94..87e4b34f1 100644 --- a/apps/console/src/pages/organizations/measures/dialog/MeasureFormDialog.tsx +++ b/apps/console/src/pages/organizations/measures/dialog/MeasureFormDialog.tsx @@ -3,11 +3,11 @@ import { Dialog, DialogContent, DialogFooter, + Field, Input, Label, Option, PropertyRow, - Textarea, useDialogRef, type DialogRef, } from "@probo/ui"; @@ -51,9 +51,9 @@ const measureCreateMutation = graphql` `; const measureSchema = z.object({ - name: z.string(), - description: z.string(), - category: z.string(), + name: z.string().min(1, "Name is required"), + description: z.string().min(1, "Description is required"), + category: z.string().min(1, "Category is required"), state: z.enum(measureStates), }); @@ -73,7 +73,7 @@ export default function MeasureFormDialog(props: Props) { ? useUpdateMeasure() : useMutationWithToasts(measureCreateMutation, { successMessage: __("Measure created successfully."), - errorMessage: __("Failed to create measure. Please try again."), + errorMessage: __("Failed to create measure"), }); const { control, handleSubmit, register, formState, reset } = @@ -131,20 +131,21 @@ export default function MeasureFormDialog(props: Props) { >
-
- + -