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 (
+
{__("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