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
This commit is contained in:
Andres Cruciani
2026-01-31 01:41:30 -05:00
parent 9573ae19e6
commit 7d7533b71c
13 changed files with 142 additions and 21 deletions

View File

@@ -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);

View File

@@ -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();

View File

@@ -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 = ({
<div className="space-y-4">
<div>
<Label htmlFor="Surveys">{t("common.questions")}</Label>
<div className="mt-1 max-h-[15vh] overflow-x-hidden overflow-y-auto rounded-lg border border-slate-200">
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{surveyElements.map((question) => (
<div key={question.id} className="my-1 flex items-center space-x-2">

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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"));

View File

@@ -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);

View File

@@ -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({
))}
</>
) : (
<p className="text-sm text-slate-500 italic">
<p className="text-sm italic text-slate-500">
{t("environments.workspace.languages.no_language_found")}
</p>
)}

View File

@@ -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?.();

View File

@@ -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"));
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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) {