Compare commits

...

2 Commits

Author SHA1 Message Date
pandeymangg 5e78b20fe2 Merge remote-tracking branch 'origin/main' into fix/server-error-toast-3302 2026-02-06 10:56:18 +05:30
Andres Cruciani 7d7533b71c 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
2026-01-31 10:23:58 -05:00
13 changed files with 142 additions and 21 deletions
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { TOrganization } from "@formbricks/types/organizations"; import { TOrganization } from "@formbricks/types/organizations";
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions"; import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage"; import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertDescription } from "@/modules/ui/components/alert"; import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog"; import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
@@ -32,7 +33,12 @@ export const DeleteOrganization = ({
setIsDeleting(true); setIsDeleting(true);
try { 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")); toast.success(t("environments.settings.general.organization_deleted_successfully"));
if (typeof localStorage !== "undefined") { if (typeof localStorage !== "undefined") {
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS); localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
@@ -21,6 +21,7 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/BaseSelectDropdown"; 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 { fetchTables } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/lib/airtable";
import AirtableLogo from "@/images/airtableLogo.svg"; import AirtableLogo from "@/images/airtableLogo.svg";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { recallToHeadline } from "@/lib/utils/recall"; import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings"; import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
@@ -268,7 +269,14 @@ export const AddIntegrationModal = ({
airtableIntegrationData.config?.data.push(integrationData); 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) { if (isEditMode) {
toast.success(t("environments.integrations.integration_updated_successfully")); toast.success(t("environments.integrations.integration_updated_successfully"));
} else { } else {
@@ -304,7 +312,11 @@ export const AddIntegrationModal = ({
const integrationData = structuredClone(airtableIntegrationData); const integrationData = structuredClone(airtableIntegrationData);
integrationData.config.data.splice(index, 1); 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(); handleClose();
router.refresh(); router.refresh();
@@ -165,7 +165,14 @@ export const AddIntegrationModal = ({
// create action // create action
googleSheetIntegrationData.config.data.push(integrationData); 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) { if (selectedIntegration) {
toast.success(t("environments.integrations.integration_updated_successfully")); toast.success(t("environments.integrations.integration_updated_successfully"));
} else { } else {
@@ -205,7 +212,14 @@ export const AddIntegrationModal = ({
googleSheetIntegrationData.config.data.splice(selectedIntegration!.index, 1); googleSheetIntegrationData.config.data.splice(selectedIntegration!.index, 1);
try { try {
setIsDeleting(true); 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")); toast.success(t("environments.integrations.integration_removed_successfully"));
setOpen(false); setOpen(false);
} catch (error) { } catch (error) {
@@ -266,7 +280,7 @@ export const AddIntegrationModal = ({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Label htmlFor="Surveys">{t("common.questions")}</Label> <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"> <div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{surveyElements.map((question) => ( {surveyElements.map((question) => (
<div key={question.id} className="my-1 flex items-center space-x-2"> <div key={question.id} className="my-1 flex items-center space-x-2">
@@ -22,6 +22,7 @@ import {
createEmptyMapping, createEmptyMapping,
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/MappingRow"; } from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/MappingRow";
import NotionLogo from "@/images/notion.png"; import NotionLogo from "@/images/notion.png";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { recallToHeadline } from "@/lib/utils/recall"; import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
@@ -217,7 +218,14 @@ export const AddIntegrationModal = ({
notionIntegrationData.config.data.push(integrationData); 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) { if (selectedIntegration) {
toast.success(t("environments.integrations.integration_updated_successfully")); toast.success(t("environments.integrations.integration_updated_successfully"));
} else { } else {
@@ -236,7 +244,14 @@ export const AddIntegrationModal = ({
notionIntegrationData.config.data.splice(selectedIntegration!.index, 1); notionIntegrationData.config.data.splice(selectedIntegration!.index, 1);
try { try {
setIsDeleting(true); 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")); toast.success(t("environments.integrations.integration_removed_successfully"));
setOpen(false); setOpen(false);
} catch (error) { } catch (error) {
@@ -17,6 +17,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation"; import { getTextContent } from "@formbricks/types/surveys/validation";
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions"; import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
import SlackLogo from "@/images/slacklogo.png"; import SlackLogo from "@/images/slacklogo.png";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { recallToHeadline } from "@/lib/utils/recall"; import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings"; import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
@@ -144,7 +145,14 @@ export const AddChannelMappingModal = ({
// create action // create action
slackIntegrationData.config.data.push(integrationData); 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) { if (selectedIntegration) {
toast.success(t("environments.integrations.integration_updated_successfully")); toast.success(t("environments.integrations.integration_updated_successfully"));
} else { } else {
@@ -181,7 +189,14 @@ export const AddChannelMappingModal = ({
slackIntegrationData.config.data.splice(selectedIntegration!.index, 1); slackIntegrationData.config.data.splice(selectedIntegration!.index, 1);
try { try {
setIsDeleting(true); 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")); toast.success(t("environments.integrations.integration_removed_successfully"));
setOpen(false); setOpen(false);
} catch (error) { } catch (error) {
@@ -109,7 +109,13 @@ export function SegmentSettings({
const handleDeleteSegment = async () => { const handleDeleteSegment = async () => {
try { try {
setIsDeletingSegment(true); 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); setIsDeletingSegment(false);
toast.success(t("environments.segments.segment_deleted_successfully")); toast.success(t("environments.segments.segment_deleted_successfully"));
@@ -17,6 +17,7 @@ import type {
import type { TSurvey } from "@formbricks/types/surveys/types"; import type { TSurvey } from "@formbricks/types/surveys/types";
import { cn } from "@/lib/cn"; import { cn } from "@/lib/cn";
import { structuredClone } from "@/lib/pollyfills/structuredClone"; import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { import {
cloneSegmentAction, cloneSegmentAction,
createSegmentAction, createSegmentAction,
@@ -135,7 +136,11 @@ export function TargetingCard({
const handleSaveSegment = async (data: TSegmentUpdateInput) => { const handleSaveSegment = async (data: TSegmentUpdateInput) => {
try { try {
if (!segment) throw new Error(t("environments.segments.invalid_segment")); 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")); toast.success(t("environments.segments.segment_saved_successfully"));
setIsSegmentEditorOpen(false); setIsSegmentEditorOpen(false);
@@ -154,7 +154,12 @@ export function EditLanguage({
const performLanguageDeletion = async (languageId: string) => { const performLanguageDeletion = async (languageId: string) => {
try { 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)); setLanguages((prev) => prev.filter((lang) => lang.id !== languageId));
toast.success(t("environments.workspace.languages.language_deleted_successfully")); toast.success(t("environments.workspace.languages.language_deleted_successfully"));
// Close the modal after deletion // Close the modal after deletion
@@ -187,7 +192,7 @@ export function EditLanguage({
const handleSaveChanges = async () => { const handleSaveChanges = async () => {
if (!validateLanguages(languages, t)) return; if (!validateLanguages(languages, t)) return;
await Promise.all( const results = await Promise.all(
languages.map((lang) => { languages.map((lang) => {
return lang.id === "new" return lang.id === "new"
? createLanguageAction({ ? 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")); toast.success(t("environments.workspace.languages.languages_updated_successfully"));
router.refresh(); router.refresh();
setIsEditing(false); 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")} {t("environments.workspace.languages.no_language_found")}
</p> </p>
)} )}
@@ -4,6 +4,7 @@ import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { deleteTeamAction } from "@/modules/ee/teams/team-list/actions"; import { deleteTeamAction } from "@/modules/ee/teams/team-list/actions";
import { TTeam } from "@/modules/ee/teams/team-list/types/team"; import { TTeam } from "@/modules/ee/teams/team-list/types/team";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
@@ -27,6 +28,12 @@ export const DeleteTeam = ({ teamId, onDelete, isOwnerOrManager }: DeleteTeamPro
setIsDeleting(true); setIsDeleting(true);
const deleteTeamActionResponse = await deleteTeamAction({ teamId }); const deleteTeamActionResponse = await deleteTeamAction({ teamId });
if (deleteTeamActionResponse?.serverError) {
toast.error(getFormattedErrorMessage(deleteTeamActionResponse));
setIsDeleteDialogOpen(false);
setIsDeleting(false);
return;
}
if (deleteTeamActionResponse?.data) { if (deleteTeamActionResponse?.data) {
toast.success(t("environments.settings.teams.team_deleted_successfully")); toast.success(t("environments.settings.teams.team_deleted_successfully"));
onDelete?.(); onDelete?.();
@@ -42,14 +42,27 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
if (!member && invite) { if (!member && invite) {
// This is an 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")); toast.success(t("environments.settings.general.invite_deleted_successfully"));
} }
if (member && !invite) { if (member && !invite) {
// This is a member // 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")); toast.success(t("environments.settings.general.member_deleted_successfully"));
} }
@@ -71,7 +71,12 @@ export const OrganizationActions = ({
const handleLeaveOrganization = async () => { const handleLeaveOrganization = async () => {
setLoading(true); setLoading(true);
try { 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")); toast.success(t("environments.settings.general.member_deleted_successfully"));
router.refresh(); router.refresh();
setLoading(false); setLoading(false);
@@ -8,6 +8,7 @@ import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TActionClass, TActionClassInput } from "@formbricks/types/action-classes"; import { TActionClass, TActionClassInput } from "@formbricks/types/action-classes";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { import {
deleteActionClassAction, deleteActionClassAction,
updateActionClassAction, updateActionClassAction,
@@ -92,10 +93,14 @@ export const ActionSettingsTab = ({
validatePermissions(isReadOnly, t); validatePermissions(isReadOnly, t);
const updatedAction = buildActionObject(data, actionClass.environmentId, t); const updatedAction = buildActionObject(data, actionClass.environmentId, t);
await updateActionClassAction({ const result = await updateActionClassAction({
actionClassId: actionClass.id, actionClassId: actionClass.id,
updatedAction: updatedAction, updatedAction: updatedAction,
}); });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
setOpen(false); setOpen(false);
router.refresh(); router.refresh();
toast.success(t("environments.actions.action_updated_successfully")); toast.success(t("environments.actions.action_updated_successfully"));
@@ -109,7 +114,11 @@ export const ActionSettingsTab = ({
const handleDeleteAction = async () => { const handleDeleteAction = async () => {
try { try {
setIsDeletingAction(true); setIsDeletingAction(true);
await deleteActionClassAction({ actionClassId: actionClass.id }); const result = await deleteActionClassAction({ actionClassId: actionClass.id });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
router.refresh(); router.refresh();
toast.success(t("environments.actions.action_deleted_successfully")); toast.success(t("environments.actions.action_deleted_successfully"));
setOpen(false); setOpen(false);
@@ -69,7 +69,11 @@ export const SurveyDropDownMenu = ({
const handleDeleteSurvey = async (surveyId: string) => { const handleDeleteSurvey = async (surveyId: string) => {
setLoading(true); setLoading(true);
try { try {
await deleteSurveyAction({ surveyId }); const result = await deleteSurveyAction({ surveyId });
if (result?.serverError) {
toast.error(getFormattedErrorMessage(result));
return;
}
deleteSurvey(surveyId); deleteSurvey(surveyId);
toast.success(t("environments.surveys.survey_deleted_successfully")); toast.success(t("environments.surveys.survey_deleted_successfully"));
} catch (error) { } catch (error) {