fix: duplicate response and contact deletion calls (#6489)

This commit is contained in:
Dhruwang Jariwala
2025-09-02 11:19:20 +05:30
committed by GitHub
parent bd48139a4f
commit 3a4e2a9f85
15 changed files with 59 additions and 67 deletions

View File

@@ -145,7 +145,7 @@ const mockLocale: TUserLocale = "en-US";
const mockSetSelectedResponseId = vi.fn();
const mockUpdateResponse = vi.fn();
const mockDeleteResponses = vi.fn();
const mockUpdateResponseList = vi.fn();
const mockSetOpen = vi.fn();
const defaultProps = {
@@ -157,7 +157,7 @@ const defaultProps = {
user: mockUser,
environmentTags: mockEnvironmentTags,
updateResponse: mockUpdateResponse,
deleteResponses: mockDeleteResponses,
updateResponseList: mockUpdateResponseList,
isReadOnly: false,
open: true,
setOpen: mockSetOpen,

View File

@@ -18,7 +18,7 @@ interface ResponseCardModalProps {
user?: TUser;
environmentTags: TTag[];
updateResponse: (responseId: string, updatedResponse: TResponse) => void;
deleteResponses: (responseIds: string[]) => void;
updateResponseList: (responseIds: string[]) => void;
isReadOnly: boolean;
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
@@ -34,7 +34,7 @@ export const ResponseCardModal = ({
user,
environmentTags,
updateResponse,
deleteResponses,
updateResponseList,
isReadOnly,
open,
setOpen,
@@ -86,7 +86,7 @@ export const ResponseCardModal = ({
environmentTags={environmentTags}
isReadOnly={isReadOnly}
updateResponse={updateResponse}
deleteResponses={deleteResponses}
updateResponseList={updateResponseList}
setSelectedResponseId={setSelectedResponseId}
locale={locale}
/>

View File

@@ -176,7 +176,7 @@ const defaultProps = {
isReadOnly: false,
fetchNextPage: vi.fn(),
hasMore: true,
deleteResponses: vi.fn(),
updateResponseList: vi.fn(),
updateResponse: vi.fn(),
isFetchingFirstPage: false,
locale: mockLocale,
@@ -264,7 +264,7 @@ describe("ResponseDataView", () => {
expect(responseTableMock.mock.calls[0][0].environment).toEqual(mockEnvironment);
expect(responseTableMock.mock.calls[0][0].fetchNextPage).toBe(defaultProps.fetchNextPage);
expect(responseTableMock.mock.calls[0][0].hasMore).toBe(true);
expect(responseTableMock.mock.calls[0][0].deleteResponses).toBe(defaultProps.deleteResponses);
expect(responseTableMock.mock.calls[0][0].updateResponseList).toBe(defaultProps.updateResponseList);
expect(responseTableMock.mock.calls[0][0].updateResponse).toBe(defaultProps.updateResponse);
expect(responseTableMock.mock.calls[0][0].isFetchingFirstPage).toBe(false);
expect(responseTableMock.mock.calls[0][0].locale).toBe(mockLocale);

View File

@@ -18,7 +18,7 @@ interface ResponseDataViewProps {
isReadOnly: boolean;
fetchNextPage: () => void;
hasMore: boolean;
deleteResponses: (responseIds: string[]) => void;
updateResponseList: (responseIds: string[]) => void;
updateResponse: (responseId: string, updatedResponse: TResponse) => void;
isFetchingFirstPage: boolean;
locale: TUserLocale;
@@ -113,7 +113,7 @@ export const ResponseDataView: React.FC<ResponseDataViewProps> = ({
isReadOnly,
fetchNextPage,
hasMore,
deleteResponses,
updateResponseList,
updateResponse,
isFetchingFirstPage,
locale,
@@ -133,7 +133,7 @@ export const ResponseDataView: React.FC<ResponseDataViewProps> = ({
environment={environment}
fetchNextPage={fetchNextPage}
hasMore={hasMore}
deleteResponses={deleteResponses}
updateResponseList={updateResponseList}
updateResponse={updateResponse}
isFetchingFirstPage={isFetchingFirstPage}
locale={locale}

View File

@@ -187,7 +187,7 @@ describe("ResponsePage", () => {
).mock.calls[0][0];
act(() => {
responseDataViewProps.deleteResponses(["response1"]);
responseDataViewProps.updateResponseList(["response1"]);
});
// Check if ResponseDataView is re-rendered with updated responses

View File

@@ -70,14 +70,12 @@ export const ResponsePage = ({
setPage(newPage);
}, [filters, page, responses, responsesPerPage, surveyId]);
const deleteResponses = (responseIds: string[]) => {
setResponses(responses.filter((response) => !responseIds.includes(response.id)));
const updateResponseList = (responseIds: string[]) => {
setResponses((prev) => prev.filter((r) => !responseIds.includes(r.id)));
};
const updateResponse = (responseId: string, updatedResponse: TResponse) => {
if (responses) {
setResponses(responses.map((response) => (response.id === responseId ? updatedResponse : response)));
}
setResponses((prev) => prev.map((r) => (r.id === responseId ? updatedResponse : r)));
};
const surveyMemoized = useMemo(() => {
@@ -136,7 +134,7 @@ export const ResponsePage = ({
isReadOnly={isReadOnly}
fetchNextPage={fetchNextPage}
hasMore={hasMore}
deleteResponses={deleteResponses}
updateResponseList={updateResponseList}
updateResponse={updateResponse}
isFetchingFirstPage={isFetchingFirstPage}
locale={locale}

View File

@@ -47,7 +47,7 @@ interface ResponseTableProps {
isReadOnly: boolean;
fetchNextPage: () => void;
hasMore: boolean;
deleteResponses: (responseIds: string[]) => void;
updateResponseList: (responseIds: string[]) => void;
updateResponse: (responseId: string, updatedResponse: TResponse) => void;
isFetchingFirstPage: boolean;
locale: TUserLocale;
@@ -63,7 +63,7 @@ export const ResponseTable = ({
isReadOnly,
fetchNextPage,
hasMore,
deleteResponses,
updateResponseList,
updateResponse,
isFetchingFirstPage,
locale,
@@ -221,7 +221,7 @@ export const ResponseTable = ({
setIsTableSettingsModalOpen={setIsTableSettingsModalOpen}
isExpanded={isExpanded ?? false}
table={table}
deleteRowsAction={deleteResponses}
updateRowList={updateResponseList}
type="response"
deleteAction={deleteResponse}
downloadRowsAction={downloadSelectedRows}
@@ -299,7 +299,7 @@ export const ResponseTable = ({
environmentTags={environmentTags}
isReadOnly={isReadOnly}
updateResponse={updateResponse}
deleteResponses={deleteResponses}
updateResponseList={updateResponseList}
setSelectedResponseId={setSelectedResponseId}
selectedResponseId={selectedResponseId}
open={selectedResponse !== null}

View File

@@ -31,7 +31,7 @@ const dummyEnvironment = { id: "env1" } as TEnvironment;
const dummyUser = { id: "user1", email: "user1@example.com", name: "User One" } as TUser;
const dummyLocale = "en-US";
const dummyDeleteResponses = vi.fn();
const dummyUpdateResponseList = vi.fn();
const dummyUpdateResponse = vi.fn();
const dummySetSelectedResponseId = vi.fn();
@@ -85,7 +85,7 @@ describe("SingleResponseCard", () => {
environmentTags={[]}
environment={dummyEnvironment}
updateResponse={dummyUpdateResponse}
deleteResponses={dummyDeleteResponses}
updateResponseList={dummyUpdateResponseList}
isReadOnly={true}
setSelectedResponseId={dummySetSelectedResponseId}
locale={dummyLocale}
@@ -105,7 +105,7 @@ describe("SingleResponseCard", () => {
environmentTags={[]}
environment={dummyEnvironment}
updateResponse={dummyUpdateResponse}
deleteResponses={dummyDeleteResponses}
updateResponseList={dummyUpdateResponseList}
isReadOnly={false}
setSelectedResponseId={dummySetSelectedResponseId}
locale={dummyLocale}
@@ -120,7 +120,7 @@ describe("SingleResponseCard", () => {
expect(deleteResponseAction).toHaveBeenCalledWith({ responseId: dummyResponse.id });
});
expect(dummyDeleteResponses).toHaveBeenCalledWith([dummyResponse.id]);
expect(dummyUpdateResponseList).toHaveBeenCalledWith([dummyResponse.id]);
});
test("calls toast.error when deleteResponseAction throws error", async () => {
@@ -133,7 +133,7 @@ describe("SingleResponseCard", () => {
environmentTags={[]}
environment={dummyEnvironment}
updateResponse={dummyUpdateResponse}
deleteResponses={dummyDeleteResponses}
updateResponseList={dummyUpdateResponseList}
isReadOnly={false}
setSelectedResponseId={dummySetSelectedResponseId}
locale={dummyLocale}
@@ -158,7 +158,7 @@ describe("SingleResponseCard", () => {
environmentTags={[]}
environment={dummyEnvironment}
updateResponse={dummyUpdateResponse}
deleteResponses={dummyDeleteResponses}
updateResponseList={dummyUpdateResponseList}
isReadOnly={false}
setSelectedResponseId={dummySetSelectedResponseId}
locale={dummyLocale}

View File

@@ -23,7 +23,7 @@ interface SingleResponseCardProps {
environmentTags: TTag[];
environment: TEnvironment;
updateResponse?: (responseId: string, responses: TResponse) => void;
deleteResponses?: (responseIds: string[]) => void;
updateResponseList?: (responseIds: string[]) => void;
isReadOnly: boolean;
setSelectedResponseId?: (responseId: string | null) => void;
locale: TUserLocale;
@@ -36,7 +36,7 @@ export const SingleResponseCard = ({
environmentTags,
environment,
updateResponse,
deleteResponses,
updateResponseList,
isReadOnly,
setSelectedResponseId,
locale,
@@ -87,7 +87,7 @@ export const SingleResponseCard = ({
throw new Error(t("common.not_authorized"));
}
await deleteResponseAction({ responseId: response.id });
deleteResponses?.([response.id]);
updateResponseList?.([response.id]);
router.refresh();
if (setSelectedResponseId) setSelectedResponseId(null);
toast.success(t("environments.surveys.responses.response_deleted_successfully"));

View File

@@ -39,14 +39,12 @@ export const ResponseFeed = ({
setFetchedResponses(responses);
}, [responses]);
const deleteResponses = (responseIds: string[]) => {
setFetchedResponses(responses.filter((response) => !responseIds.includes(response.id)));
const updateResponseList = (responseIds: string[]) => {
setFetchedResponses((prev) => prev.filter((r) => !responseIds.includes(r.id)));
};
const updateResponse = (responseId: string, updatedResponse: TResponse) => {
setFetchedResponses(
responses.map((response) => (response.id === responseId ? updatedResponse : response))
);
setFetchedResponses((prev) => prev.map((r) => (r.id === responseId ? updatedResponse : r)));
};
return (
@@ -62,7 +60,7 @@ export const ResponseFeed = ({
user={user}
environmentTags={environmentTags}
environment={environment}
deleteResponses={deleteResponses}
updateResponseList={updateResponseList}
updateResponse={updateResponse}
locale={locale}
projectPermission={projectPermission}
@@ -79,7 +77,7 @@ const ResponseSurveyCard = ({
user,
environmentTags,
environment,
deleteResponses,
updateResponseList,
updateResponse,
locale,
projectPermission,
@@ -89,7 +87,7 @@ const ResponseSurveyCard = ({
user: TUser;
environmentTags: TTag[];
environment: TEnvironment;
deleteResponses: (responseIds: string[]) => void;
updateResponseList: (responseIds: string[]) => void;
updateResponse: (responseId: string, response: TResponse) => void;
locale: TUserLocale;
projectPermission: TTeamPermission | null;
@@ -114,7 +112,7 @@ const ResponseSurveyCard = ({
user={user}
environmentTags={environmentTags}
environment={environment}
deleteResponses={deleteResponses}
updateResponseList={updateResponseList}
updateResponse={updateResponse}
isReadOnly={isReadOnly}
locale={locale}

View File

@@ -3,12 +3,11 @@
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
import { debounce } from "lodash";
import dynamic from "next/dynamic";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import toast from "react-hot-toast";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import { deleteContactAction, getContactsAction } from "../actions";
import { getContactsAction } from "../actions";
import { TContactTableData, TContactWithAttributes } from "../types/contact";
const ContactsTableDynamic = dynamic(() => import("./contacts-table").then((mod) => mod.ContactsTable), {
@@ -33,7 +32,6 @@ export const ContactDataView = ({
hasMore: initialHasMore,
initialContacts,
}: ContactDataViewProps) => {
const router = useRouter();
const [contacts, setContacts] = useState<TContactWithAttributes[]>([...initialContacts]);
const [hasMore, setHasMore] = useState<boolean>(initialHasMore);
const [loadingNextPage, setLoadingNextPage] = useState<boolean>(false);
@@ -115,11 +113,8 @@ export const ContactDataView = ({
};
// Delete selected contacts
const deleteContacts = async (contactIds: string[]) => {
await Promise.all(contactIds.map((contactId) => deleteContactAction({ contactId })));
const updateContactList = (contactIds: string[]) => {
setContacts((prevContacts) => prevContacts.filter((contact) => !contactIds.includes(contact.id)));
router.refresh();
};
// Prepare data for the ContactTable component
@@ -144,7 +139,7 @@ export const ContactDataView = ({
fetchNextPage={fetchNextPage}
hasMore={hasMore}
isDataLoaded={isFirstRender.current ? true : isDataLoaded}
deleteContacts={deleteContacts}
updateContactList={updateContactList}
environmentId={environment.id}
searchValue={searchValue}
setSearchValue={setSearchValue}

View File

@@ -36,7 +36,7 @@ interface ContactsTableProps {
data: TContactTableData[];
fetchNextPage: () => void;
hasMore: boolean;
deleteContacts: (contactIds: string[]) => void;
updateContactList: (contactIds: string[]) => void;
isDataLoaded: boolean;
environmentId: string;
searchValue: string;
@@ -48,7 +48,7 @@ export const ContactsTable = ({
data,
fetchNextPage,
hasMore,
deleteContacts,
updateContactList,
isDataLoaded,
environmentId,
searchValue,
@@ -234,7 +234,7 @@ export const ContactsTable = ({
setIsTableSettingsModalOpen={setIsTableSettingsModalOpen}
isExpanded={isExpanded ?? false}
table={table}
deleteRowsAction={deleteContacts}
updateRowList={updateContactList}
type="contact"
deleteAction={deleteContact}
/>

View File

@@ -14,7 +14,7 @@ interface DataTableToolbarProps<T> {
setIsExpanded: (isExpanded: boolean) => void;
isExpanded: boolean;
table: Table<T>;
deleteRowsAction: (rowIds: string[]) => void;
updateRowList: (rowIds: string[]) => void;
type: "response" | "contact";
deleteAction: (id: string) => Promise<void>;
downloadRowsAction?: (rowIds: string[], format: string) => Promise<void>;
@@ -25,7 +25,7 @@ export const DataTableToolbar = <T,>({
setIsTableSettingsModalOpen,
isExpanded,
table,
deleteRowsAction,
updateRowList,
type,
deleteAction,
downloadRowsAction,
@@ -38,7 +38,7 @@ export const DataTableToolbar = <T,>({
{table.getFilteredSelectedRowModel().rows.length > 0 ? (
<SelectedRowSettings
table={table}
deleteRowsAction={deleteRowsAction}
updateRowList={updateRowList}
type={type}
deleteAction={deleteAction}
downloadRowsAction={downloadRowsAction}

View File

@@ -30,7 +30,7 @@ vi.mock("@/modules/ui/components/button", () => ({
describe("SelectedRowSettings", () => {
const rows = [{ id: "r1" }, { id: "r2" }];
let table: any;
let deleteRowsAction: ReturnType<typeof vi.fn>;
let updateRowList: ReturnType<typeof vi.fn>;
let deleteAction: ReturnType<typeof vi.fn>;
let downloadRowsAction: ReturnType<typeof vi.fn>;
@@ -39,7 +39,7 @@ describe("SelectedRowSettings", () => {
getFilteredSelectedRowModel: () => ({ rows }),
toggleAllPageRowsSelected: vi.fn(),
};
deleteRowsAction = vi.fn();
updateRowList = vi.fn();
deleteAction = vi.fn(() => Promise.resolve());
downloadRowsAction = vi.fn();
@@ -57,7 +57,7 @@ describe("SelectedRowSettings", () => {
render(
<SelectedRowSettings
table={table}
deleteRowsAction={deleteRowsAction}
updateRowList={updateRowList}
deleteAction={deleteAction}
downloadRowsAction={downloadRowsAction}
type="contact"
@@ -76,7 +76,7 @@ describe("SelectedRowSettings", () => {
render(
<SelectedRowSettings
table={table}
deleteRowsAction={deleteRowsAction}
updateRowList={updateRowList}
deleteAction={deleteAction}
type="response"
/>
@@ -88,7 +88,7 @@ describe("SelectedRowSettings", () => {
render(
<SelectedRowSettings
table={table}
deleteRowsAction={deleteRowsAction}
updateRowList={updateRowList}
deleteAction={deleteAction}
downloadRowsAction={downloadRowsAction}
type="response"
@@ -108,7 +108,7 @@ describe("SelectedRowSettings", () => {
render(
<SelectedRowSettings
table={table}
deleteRowsAction={deleteRowsAction}
updateRowList={updateRowList}
deleteAction={deleteAction}
downloadRowsAction={downloadRowsAction}
type="contact"
@@ -119,7 +119,7 @@ describe("SelectedRowSettings", () => {
fireEvent.click(screen.getByText("Confirm Delete"));
await waitFor(() => {
expect(deleteAction).toHaveBeenCalledTimes(2);
expect(deleteRowsAction).toHaveBeenCalledWith(["r1", "r2"]);
expect(updateRowList).toHaveBeenCalledWith(["r1", "r2"]);
expect(toast.success).toHaveBeenCalledWith("common.table_items_deleted_successfully");
});
});
@@ -129,7 +129,7 @@ describe("SelectedRowSettings", () => {
render(
<SelectedRowSettings
table={table}
deleteRowsAction={deleteRowsAction}
updateRowList={updateRowList}
deleteAction={deleteAction}
downloadRowsAction={downloadRowsAction}
type="response"
@@ -149,7 +149,7 @@ describe("SelectedRowSettings", () => {
render(
<SelectedRowSettings
table={table}
deleteRowsAction={deleteRowsAction}
updateRowList={updateRowList}
deleteAction={deleteAction}
downloadRowsAction={downloadRowsAction}
type="response"
@@ -160,7 +160,7 @@ describe("SelectedRowSettings", () => {
fireEvent.click(screen.getByText("Confirm Delete"));
await waitFor(() => {
expect(deleteAction).toHaveBeenCalledTimes(2);
expect(deleteRowsAction).toHaveBeenCalledWith(["r1", "r2"]);
expect(updateRowList).toHaveBeenCalledWith(["r1", "r2"]);
expect(toast.success).toHaveBeenCalledWith("common.table_items_deleted_successfully");
});
});
@@ -170,7 +170,7 @@ describe("SelectedRowSettings", () => {
render(
<SelectedRowSettings
table={table}
deleteRowsAction={deleteRowsAction}
updateRowList={updateRowList}
deleteAction={deleteAction}
downloadRowsAction={downloadRowsAction}
type="contact"

View File

@@ -18,7 +18,7 @@ import { toast } from "react-hot-toast";
interface SelectedRowSettingsProps<T> {
table: Table<T>;
deleteRowsAction: (rowId: string[]) => void;
updateRowList: (rowId: string[]) => void;
type: "response" | "contact";
deleteAction: (id: string) => Promise<void>;
downloadRowsAction?: (rowIds: string[], format: string) => Promise<void>;
@@ -26,7 +26,7 @@ interface SelectedRowSettingsProps<T> {
export const SelectedRowSettings = <T,>({
table,
deleteRowsAction,
updateRowList,
type,
deleteAction,
downloadRowsAction,
@@ -55,7 +55,8 @@ export const SelectedRowSettings = <T,>({
await Promise.all(rowsToBeDeleted.map((rowId) => deleteAction(rowId)));
}
deleteRowsAction(rowsToBeDeleted);
// Update the row list UI
updateRowList(rowsToBeDeleted);
toast.success(t("common.table_items_deleted_successfully", { type: capitalizeFirstLetter(type) }));
} catch (error) {
if (error instanceof Error) {