From 7d7533b71cabf9afd4882be28b493c79aed998de Mon Sep 17 00:00:00 2001 From: Andres Cruciani Date: Sat, 31 Jan 2026 01:41:30 -0500 Subject: [PATCH] fix: check serverError before showing success toast Server actions return HTTP 200 with serverError field on failure. Frontend was only checking HTTP status, not the response body, causing success toasts to show when operations actually failed. Fixed handlers: - handleDeleteSurvey (survey-dropdown-menu.tsx) - handleLeaveOrganization (organization-actions.tsx) - handleDeleteMember (member-actions.tsx) - handleDeleteTeam (delete-team.tsx) - handleDeleteSegment (segment-settings.tsx) - performLanguageDeletion, handleSaveChanges (edit-language.tsx) - onSubmit, handleDeleteAction (ActionSettingsTab.tsx) - handleSaveSegment (targeting-card.tsx) Each now checks result?.serverError before showing success toast. Fixes #3302 --- .../general/components/DeleteOrganization.tsx | 8 +++++++- .../components/AddIntegrationModal.tsx | 16 +++++++++++++-- .../components/AddIntegrationModal.tsx | 20 ++++++++++++++++--- .../notion/components/AddIntegrationModal.tsx | 19 ++++++++++++++++-- .../components/AddChannelMappingModal.tsx | 19 ++++++++++++++++-- .../segments/components/segment-settings.tsx | 8 +++++++- .../segments/components/targeting-card.tsx | 7 ++++++- .../components/edit-language.tsx | 16 ++++++++++++--- .../components/team-settings/delete-team.tsx | 7 +++++++ .../edit-memberships/member-actions.tsx | 17 ++++++++++++++-- .../edit-memberships/organization-actions.tsx | 7 ++++++- .../(setup)/components/ActionSettingsTab.tsx | 13 ++++++++++-- .../list/components/survey-dropdown-menu.tsx | 6 +++++- 13 files changed, 142 insertions(+), 21 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/DeleteOrganization.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/DeleteOrganization.tsx index dc3cc2e4bb..14df58d4a9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/DeleteOrganization.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/components/DeleteOrganization.tsx @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next"; import { TOrganization } from "@formbricks/types/organizations"; import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions"; import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Alert, AlertDescription } from "@/modules/ui/components/alert"; import { Button } from "@/modules/ui/components/button"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; @@ -32,7 +33,12 @@ export const DeleteOrganization = ({ setIsDeleting(true); try { - await deleteOrganizationAction({ organizationId: organization.id }); + const result = await deleteOrganizationAction({ organizationId: organization.id }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + setIsDeleting(false); + return; + } toast.success(t("environments.settings.general.organization_deleted_successfully")); if (typeof localStorage !== "undefined") { localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS); diff --git a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AddIntegrationModal.tsx index ef75bfd445..a26091bb1b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/AddIntegrationModal.tsx @@ -21,6 +21,7 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/BaseSelectDropdown"; import { fetchTables } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/lib/airtable"; import AirtableLogo from "@/images/airtableLogo.svg"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { recallToHeadline } from "@/lib/utils/recall"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings"; @@ -268,7 +269,14 @@ export const AddIntegrationModal = ({ airtableIntegrationData.config?.data.push(integrationData); } - await createOrUpdateIntegrationAction({ environmentId, integrationData: airtableIntegrationData }); + const result = await createOrUpdateIntegrationAction({ + environmentId, + integrationData: airtableIntegrationData, + }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } if (isEditMode) { toast.success(t("environments.integrations.integration_updated_successfully")); } else { @@ -304,7 +312,11 @@ export const AddIntegrationModal = ({ const integrationData = structuredClone(airtableIntegrationData); integrationData.config.data.splice(index, 1); - await createOrUpdateIntegrationAction({ environmentId, integrationData }); + const result = await createOrUpdateIntegrationAction({ environmentId, integrationData }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } handleClose(); router.refresh(); diff --git a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/AddIntegrationModal.tsx index 1cdb9b19d0..0b16be342f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/google-sheets/components/AddIntegrationModal.tsx @@ -165,7 +165,14 @@ export const AddIntegrationModal = ({ // create action googleSheetIntegrationData.config.data.push(integrationData); } - await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData }); + const result = await createOrUpdateIntegrationAction({ + environmentId, + integrationData: googleSheetIntegrationData, + }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } if (selectedIntegration) { toast.success(t("environments.integrations.integration_updated_successfully")); } else { @@ -205,7 +212,14 @@ export const AddIntegrationModal = ({ googleSheetIntegrationData.config.data.splice(selectedIntegration!.index, 1); try { setIsDeleting(true); - await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData }); + const result = await createOrUpdateIntegrationAction({ + environmentId, + integrationData: googleSheetIntegrationData, + }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } toast.success(t("environments.integrations.integration_removed_successfully")); setOpen(false); } catch (error) { @@ -266,7 +280,7 @@ export const AddIntegrationModal = ({
-
+
{surveyElements.map((question) => (
diff --git a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/AddIntegrationModal.tsx index 55003f64b0..c3409375e5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/AddIntegrationModal.tsx @@ -22,6 +22,7 @@ import { createEmptyMapping, } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/MappingRow"; import NotionLogo from "@/images/notion.png"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { recallToHeadline } from "@/lib/utils/recall"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { Button } from "@/modules/ui/components/button"; @@ -217,7 +218,14 @@ export const AddIntegrationModal = ({ notionIntegrationData.config.data.push(integrationData); } - await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData }); + const result = await createOrUpdateIntegrationAction({ + environmentId, + integrationData: notionIntegrationData, + }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } if (selectedIntegration) { toast.success(t("environments.integrations.integration_updated_successfully")); } else { @@ -236,7 +244,14 @@ export const AddIntegrationModal = ({ notionIntegrationData.config.data.splice(selectedIntegration!.index, 1); try { setIsDeleting(true); - await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData }); + const result = await createOrUpdateIntegrationAction({ + environmentId, + integrationData: notionIntegrationData, + }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } toast.success(t("environments.integrations.integration_removed_successfully")); setOpen(false); } catch (error) { diff --git a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/AddChannelMappingModal.tsx b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/AddChannelMappingModal.tsx index 88e501bbae..13bc7af4d5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/AddChannelMappingModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/workspace/integrations/slack/components/AddChannelMappingModal.tsx @@ -17,6 +17,7 @@ import { TSurvey } from "@formbricks/types/surveys/types"; import { getTextContent } from "@formbricks/types/surveys/validation"; import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions"; import SlackLogo from "@/images/slacklogo.png"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { recallToHeadline } from "@/lib/utils/recall"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings"; @@ -144,7 +145,14 @@ export const AddChannelMappingModal = ({ // create action slackIntegrationData.config.data.push(integrationData); } - await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData }); + const result = await createOrUpdateIntegrationAction({ + environmentId, + integrationData: slackIntegrationData, + }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } if (selectedIntegration) { toast.success(t("environments.integrations.integration_updated_successfully")); } else { @@ -181,7 +189,14 @@ export const AddChannelMappingModal = ({ slackIntegrationData.config.data.splice(selectedIntegration!.index, 1); try { setIsDeleting(true); - await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData }); + const result = await createOrUpdateIntegrationAction({ + environmentId, + integrationData: slackIntegrationData, + }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } toast.success(t("environments.integrations.integration_removed_successfully")); setOpen(false); } catch (error) { diff --git a/apps/web/modules/ee/contacts/segments/components/segment-settings.tsx b/apps/web/modules/ee/contacts/segments/components/segment-settings.tsx index a087442ca6..cc56f9a4e6 100644 --- a/apps/web/modules/ee/contacts/segments/components/segment-settings.tsx +++ b/apps/web/modules/ee/contacts/segments/components/segment-settings.tsx @@ -109,7 +109,13 @@ export function SegmentSettings({ const handleDeleteSegment = async () => { try { setIsDeletingSegment(true); - await deleteSegmentAction({ segmentId: segment.id }); + const result = await deleteSegmentAction({ segmentId: segment.id }); + + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + setIsDeletingSegment(false); + return; + } setIsDeletingSegment(false); toast.success(t("environments.segments.segment_deleted_successfully")); diff --git a/apps/web/modules/ee/contacts/segments/components/targeting-card.tsx b/apps/web/modules/ee/contacts/segments/components/targeting-card.tsx index 549de03e36..c0207abc4b 100644 --- a/apps/web/modules/ee/contacts/segments/components/targeting-card.tsx +++ b/apps/web/modules/ee/contacts/segments/components/targeting-card.tsx @@ -17,6 +17,7 @@ import type { import type { TSurvey } from "@formbricks/types/surveys/types"; import { cn } from "@/lib/cn"; import { structuredClone } from "@/lib/pollyfills/structuredClone"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { cloneSegmentAction, createSegmentAction, @@ -135,7 +136,11 @@ export function TargetingCard({ const handleSaveSegment = async (data: TSegmentUpdateInput) => { try { if (!segment) throw new Error(t("environments.segments.invalid_segment")); - await updateSegmentAction({ segmentId: segment.id, environmentId, data }); + const result = await updateSegmentAction({ segmentId: segment.id, environmentId, data }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } toast.success(t("environments.segments.segment_saved_successfully")); setIsSegmentEditorOpen(false); diff --git a/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx b/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx index cf37396e09..8895229487 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/edit-language.tsx @@ -154,7 +154,12 @@ export function EditLanguage({ const performLanguageDeletion = async (languageId: string) => { try { - await deleteLanguageAction({ languageId, projectId: project.id }); + const result = await deleteLanguageAction({ languageId, projectId: project.id }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + setConfirmationModal((prev) => ({ ...prev, isOpen: false })); + return; + } setLanguages((prev) => prev.filter((lang) => lang.id !== languageId)); toast.success(t("environments.workspace.languages.language_deleted_successfully")); // Close the modal after deletion @@ -187,7 +192,7 @@ export function EditLanguage({ const handleSaveChanges = async () => { if (!validateLanguages(languages, t)) return; - await Promise.all( + const results = await Promise.all( languages.map((lang) => { return lang.id === "new" ? createLanguageAction({ @@ -201,6 +206,11 @@ export function EditLanguage({ }); }) ); + const errorResult = results.find((result) => result?.serverError); + if (errorResult) { + toast.error(getFormattedErrorMessage(errorResult)); + return; + } toast.success(t("environments.workspace.languages.languages_updated_successfully")); router.refresh(); setIsEditing(false); @@ -239,7 +249,7 @@ export function EditLanguage({ ))} ) : ( -

+

{t("environments.workspace.languages.no_language_found")}

)} diff --git a/apps/web/modules/ee/teams/team-list/components/team-settings/delete-team.tsx b/apps/web/modules/ee/teams/team-list/components/team-settings/delete-team.tsx index a34d33382e..efb37bae81 100644 --- a/apps/web/modules/ee/teams/team-list/components/team-settings/delete-team.tsx +++ b/apps/web/modules/ee/teams/team-list/components/team-settings/delete-team.tsx @@ -4,6 +4,7 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { deleteTeamAction } from "@/modules/ee/teams/team-list/actions"; import { TTeam } from "@/modules/ee/teams/team-list/types/team"; import { Button } from "@/modules/ui/components/button"; @@ -27,6 +28,12 @@ export const DeleteTeam = ({ teamId, onDelete, isOwnerOrManager }: DeleteTeamPro setIsDeleting(true); const deleteTeamActionResponse = await deleteTeamAction({ teamId }); + if (deleteTeamActionResponse?.serverError) { + toast.error(getFormattedErrorMessage(deleteTeamActionResponse)); + setIsDeleteDialogOpen(false); + setIsDeleting(false); + return; + } if (deleteTeamActionResponse?.data) { toast.success(t("environments.settings.teams.team_deleted_successfully")); onDelete?.(); diff --git a/apps/web/modules/organization/settings/teams/components/edit-memberships/member-actions.tsx b/apps/web/modules/organization/settings/teams/components/edit-memberships/member-actions.tsx index 3477b7bf15..41d7266c17 100644 --- a/apps/web/modules/organization/settings/teams/components/edit-memberships/member-actions.tsx +++ b/apps/web/modules/organization/settings/teams/components/edit-memberships/member-actions.tsx @@ -42,14 +42,27 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton } if (!member && invite) { // This is an invite - await deleteInviteAction({ inviteId: invite?.id, organizationId: organization.id }); + const result = await deleteInviteAction({ inviteId: invite?.id, organizationId: organization.id }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + setIsDeleting(false); + return; + } toast.success(t("environments.settings.general.invite_deleted_successfully")); } if (member && !invite) { // This is a member - await deleteMembershipAction({ userId: member.userId, organizationId: organization.id }); + const result = await deleteMembershipAction({ + userId: member.userId, + organizationId: organization.id, + }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + setIsDeleting(false); + return; + } toast.success(t("environments.settings.general.member_deleted_successfully")); } diff --git a/apps/web/modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx b/apps/web/modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx index d919af059a..6d1d19cb00 100644 --- a/apps/web/modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx +++ b/apps/web/modules/organization/settings/teams/components/edit-memberships/organization-actions.tsx @@ -71,7 +71,12 @@ export const OrganizationActions = ({ const handleLeaveOrganization = async () => { setLoading(true); try { - await leaveOrganizationAction({ organizationId: organization.id }); + const result = await leaveOrganizationAction({ organizationId: organization.id }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + setLoading(false); + return; + } toast.success(t("environments.settings.general.member_deleted_successfully")); router.refresh(); setLoading(false); diff --git a/apps/web/modules/projects/settings/(setup)/components/ActionSettingsTab.tsx b/apps/web/modules/projects/settings/(setup)/components/ActionSettingsTab.tsx index 51e3953da8..fb00d33cdf 100644 --- a/apps/web/modules/projects/settings/(setup)/components/ActionSettingsTab.tsx +++ b/apps/web/modules/projects/settings/(setup)/components/ActionSettingsTab.tsx @@ -8,6 +8,7 @@ import { FormProvider, useForm } from "react-hook-form"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { TActionClass, TActionClassInput } from "@formbricks/types/action-classes"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { deleteActionClassAction, updateActionClassAction, @@ -92,10 +93,14 @@ export const ActionSettingsTab = ({ validatePermissions(isReadOnly, t); const updatedAction = buildActionObject(data, actionClass.environmentId, t); - await updateActionClassAction({ + const result = await updateActionClassAction({ actionClassId: actionClass.id, updatedAction: updatedAction, }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } setOpen(false); router.refresh(); toast.success(t("environments.actions.action_updated_successfully")); @@ -109,7 +114,11 @@ export const ActionSettingsTab = ({ const handleDeleteAction = async () => { try { setIsDeletingAction(true); - await deleteActionClassAction({ actionClassId: actionClass.id }); + const result = await deleteActionClassAction({ actionClassId: actionClass.id }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } router.refresh(); toast.success(t("environments.actions.action_deleted_successfully")); setOpen(false); diff --git a/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx index 0502306bfd..614546e380 100644 --- a/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx +++ b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx @@ -69,7 +69,11 @@ export const SurveyDropDownMenu = ({ const handleDeleteSurvey = async (surveyId: string) => { setLoading(true); try { - await deleteSurveyAction({ surveyId }); + const result = await deleteSurveyAction({ surveyId }); + if (result?.serverError) { + toast.error(getFormattedErrorMessage(result)); + return; + } deleteSurvey(surveyId); toast.success(t("environments.surveys.survey_deleted_successfully")); } catch (error) {