Compare commits

...

3 Commits

Author SHA1 Message Date
Cursor Agent 1a6b612ba7 test: cover survey status list update 2026-05-18 10:35:46 +00:00
Cursor Agent 80d3d88532 refactor: scope survey status action to list 2026-05-18 10:26:11 +00:00
Cursor Agent 778a02d3b3 feat: add survey status menu action 2026-05-18 07:29:44 +00:00
9 changed files with 569 additions and 9 deletions
@@ -0,0 +1,118 @@
import { revalidatePath } from "next/cache";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getOrganizationIdFromSurveyId, getWorkspaceIdFromSurveyId } from "@/lib/utils/helper";
import { updateSurvey } from "@/modules/survey/editor/lib/survey";
import { getSurvey } from "@/modules/survey/lib/survey";
import { updateSurveyStatusAction } from "./actions";
vi.mock("next/cache", () => ({
revalidatePath: vi.fn(),
}));
vi.mock("@/lib/utils/action-client", () => ({
authenticatedActionClient: {
inputSchema: vi.fn(() => ({
action: vi.fn((fn) => fn),
})),
},
}));
vi.mock("@/lib/utils/action-client/action-client-middleware", () => ({
checkAuthorizationUpdated: vi.fn(),
}));
vi.mock("@/lib/utils/helper", () => ({
getOrganizationIdFromSurveyId: vi.fn(),
getOrganizationIdFromWorkspaceId: vi.fn(),
getWorkspaceIdFromSurveyId: vi.fn(),
}));
vi.mock("@/lib/utils/single-use-surveys", () => ({
generateSurveySingleUseLinkParams: vi.fn(),
generateSurveySingleUseLinkParamsList: vi.fn(),
}));
vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
withAuditLogging: vi.fn((_eventName, _targetType, fn) => fn),
}));
vi.mock("@/modules/survey/editor/lib/survey", () => ({
updateSurvey: vi.fn(),
}));
vi.mock("@/modules/survey/lib/survey", () => ({
getSurvey: vi.fn(),
}));
vi.mock("@/modules/survey/list/lib/survey", () => ({
copySurveyToOtherWorkspace: vi.fn(),
}));
const baseSurvey = {
id: "survey_1",
workspaceId: "workspace_1",
status: "inProgress",
};
const ctx = {
user: { id: "user_1" },
auditLoggingCtx: {},
};
describe("updateSurveyStatusAction", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(getOrganizationIdFromSurveyId).mockResolvedValue("organization_1");
vi.mocked(getWorkspaceIdFromSurveyId).mockResolvedValue("workspace_1");
vi.mocked(checkAuthorizationUpdated).mockResolvedValue(undefined);
vi.mocked(getSurvey).mockResolvedValue(baseSurvey as never);
vi.mocked(updateSurvey).mockResolvedValue({ ...baseSurvey, status: "completed" } as never);
});
test("updates a non-draft survey status with read-write access", async () => {
const result = await updateSurveyStatusAction({
ctx,
parsedInput: { surveyId: "survey_1", status: "completed" },
} as never);
expect(checkAuthorizationUpdated).toHaveBeenCalledWith({
userId: "user_1",
organizationId: "organization_1",
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
workspaceId: "workspace_1",
minPermission: "readWrite",
},
],
});
expect(updateSurvey).toHaveBeenCalledWith({ ...baseSurvey, status: "completed" });
expect(ctx.auditLoggingCtx).toEqual({
organizationId: "organization_1",
surveyId: "survey_1",
oldObject: baseSurvey,
newObject: { ...baseSurvey, status: "completed" },
});
expect(revalidatePath).toHaveBeenCalledWith("/workspaces/workspace_1/surveys");
expect(revalidatePath).toHaveBeenCalledWith("/workspaces/workspace_1/surveys/survey_1");
expect(result).toEqual({ ...baseSurvey, status: "completed" });
});
test("rejects draft survey status changes from the list", async () => {
vi.mocked(getSurvey).mockResolvedValue({ ...baseSurvey, status: "draft" } as never);
await expect(
updateSurveyStatusAction({
ctx: { user: { id: "user_1" }, auditLoggingCtx: {} },
parsedInput: { surveyId: "survey_1", status: "completed" },
} as never)
).rejects.toThrow(OperationNotAllowedError);
expect(updateSurvey).not.toHaveBeenCalled();
});
});
+53
View File
@@ -1,6 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
@@ -14,6 +16,8 @@ import {
generateSurveySingleUseLinkParamsList,
} from "@/lib/utils/single-use-surveys";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { updateSurvey } from "@/modules/survey/editor/lib/survey";
import { getSurvey } from "@/modules/survey/lib/survey";
import { copySurveyToOtherWorkspace } from "@/modules/survey/list/lib/survey";
const ZCopySurveyToOtherWorkspaceAction = z.object({
@@ -81,6 +85,55 @@ export const copySurveyToOtherWorkspaceAction = authenticatedActionClient
})
);
const ZUpdateSurveyStatusAction = z.object({
surveyId: ZId,
status: z.enum(["inProgress", "paused", "completed"]),
});
export const updateSurveyStatusAction = authenticatedActionClient
.inputSchema(ZUpdateSurveyStatusAction)
.action(
withAuditLogging("updated", "survey", async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
const workspaceId = await getWorkspaceIdFromSurveyId(parsedInput.surveyId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "workspaceTeam",
workspaceId,
minPermission: "readWrite",
},
],
});
const survey = await getSurvey(parsedInput.surveyId);
if (survey.status === "draft") {
throw new OperationNotAllowedError("Draft surveys must be published from the editor.");
}
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.surveyId = parsedInput.surveyId;
ctx.auditLoggingCtx.oldObject = survey;
const updatedSurvey = await updateSurvey({ ...survey, status: parsedInput.status });
ctx.auditLoggingCtx.newObject = updatedSurvey;
revalidatePath(`/workspaces/${updatedSurvey.workspaceId}/surveys`);
revalidatePath(`/workspaces/${updatedSurvey.workspaceId}/surveys/${updatedSurvey.id}`);
return updatedSurvey;
})
);
const ZGenerateSingleUseIdAction = z
.object({
surveyId: z.cuid2(),
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { useMemo } from "react";
import { type ComponentProps, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { TUserLocale } from "@formbricks/types/user";
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
@@ -18,9 +18,17 @@ interface SurveyCardProps {
publicDomain: string;
isReadOnly: boolean;
deleteSurvey: (surveyId: string) => Promise<void>;
updateSurveyStatus: ComponentProps<typeof SurveyDropDownMenu>["updateSurveyStatus"];
locale: TUserLocale;
}
export const SurveyCard = ({ survey, publicDomain, isReadOnly, deleteSurvey, locale }: SurveyCardProps) => {
export const SurveyCard = ({
survey,
publicDomain,
isReadOnly,
deleteSurvey,
updateSurveyStatus,
locale,
}: Readonly<SurveyCardProps>) => {
const { t } = useTranslation();
const { workspace } = useWorkspace();
const workspaceBasePath = `/workspaces/${workspace?.id}`;
@@ -104,6 +112,7 @@ export const SurveyCard = ({ survey, publicDomain, isReadOnly, deleteSurvey, loc
disabled={isDraftAndReadOnly}
isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled}
deleteSurvey={deleteSurvey}
updateSurveyStatus={updateSurveyStatus}
/>
</div>
</div>
@@ -9,18 +9,35 @@ import { useTranslation } from "react-i18next";
import { logger } from "@formbricks/logger";
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
import { cn } from "@/lib/cn";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { getV3ApiErrorMessage } from "@/modules/api/lib/v3-client";
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
import { copySurveyLink } from "@/modules/survey/lib/client-utils";
import { TSurveyListItem } from "@/modules/survey/list/types/survey-overview";
import type { TSurveyListItem } from "@/modules/survey/list/types/survey-overview";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
type TSurveyStatusUpdate = Exclude<TSurveyListItem["status"], "draft">;
type TUpdateSurveyStatusResponse = {
data?: {
status: TSurveyListItem["status"];
publishOn: Date | null;
};
serverError?: string;
validationErrors?: unknown;
};
interface SurveyDropDownMenuProps {
survey: TSurveyListItem;
@@ -28,6 +45,10 @@ interface SurveyDropDownMenuProps {
disabled?: boolean;
isSurveyCreationDeletionDisabled?: boolean;
deleteSurvey: (surveyId: string) => Promise<void>;
updateSurveyStatus: (variables: {
surveyId: string;
status: TSurveyStatusUpdate;
}) => Promise<TUpdateSurveyStatusResponse>;
}
export const SurveyDropDownMenu = ({
@@ -36,7 +57,8 @@ export const SurveyDropDownMenu = ({
disabled,
isSurveyCreationDeletionDisabled,
deleteSurvey,
}: SurveyDropDownMenuProps) => {
updateSurveyStatus,
}: Readonly<SurveyDropDownMenuProps>) => {
const { workspace } = useWorkspace();
const { t } = useTranslation();
@@ -49,11 +71,26 @@ export const SurveyDropDownMenu = ({
const editHref = `/workspaces/${workspace?.id}/surveys/${survey.id}/edit`;
const surveyLink = useMemo(() => `${publicDomain}/s/${survey.id}`, [publicDomain, survey.id]);
const isScheduled = survey.status === "paused" && survey.publishOn !== null;
const isSingleUseEnabled = survey.singleUse?.enabled ?? false;
const canManageSurvey = !isSurveyCreationDeletionDisabled;
const canUpdateSurveyStatus = canManageSurvey && survey.status !== "draft";
const canPreviewOrCopyLink = survey.type === "link" && survey.status !== "draft";
const hasVisibleActions = canManageSurvey || canPreviewOrCopyLink;
const getSurveyStatusLabel = (status: TSurveyListItem["status"], isScheduledStatus = isScheduled) => {
switch (status) {
case "inProgress":
return t("common.in_progress");
case "completed":
return t("common.completed");
case "draft":
return t("common.draft");
case "paused":
return isScheduledStatus ? t("common.scheduled") : t("common.paused");
}
};
const handleDeleteSurvey = async (surveyId: string) => {
setLoading(true);
@@ -85,6 +122,40 @@ export const SurveyDropDownMenu = ({
setIsCautionDialogOpen(true);
};
const handleStatusChange = async (status: TSurveyStatusUpdate) => {
if (status === survey.status) {
return;
}
setIsDropDownOpen(false);
const toastId = toast.loading(t("common.saving"));
try {
const updateSurveyStatusResponse = await updateSurveyStatus({ surveyId: survey.id, status });
if (updateSurveyStatusResponse?.data) {
const { publishOn, status: resultingStatus } = updateSurveyStatusResponse.data;
const isResultScheduled = resultingStatus === "paused" && publishOn !== null;
const statusToToastMessage: Record<TSurveyStatusUpdate, string> = {
inProgress: t("common.survey_live"),
paused: isResultScheduled ? t("common.survey_scheduled") : t("common.survey_paused"),
completed: t("common.survey_completed"),
};
if (resultingStatus !== "draft") {
toast.success(statusToToastMessage[resultingStatus], { id: toastId });
} else {
toast.success(t("workspace.surveys.edit.changes_saved"), { id: toastId });
}
} else {
toast.error(getFormattedErrorMessage(updateSurveyStatusResponse), { id: toastId });
}
} catch (error) {
logger.error(error);
toast.error(t("common.something_went_wrong_please_try_again"), { id: toastId });
}
};
if (!hasVisibleActions) {
return null;
}
@@ -120,6 +191,39 @@ export const SurveyDropDownMenu = ({
</Link>
</DropdownMenuItem>
)}
{canUpdateSurveyStatus && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<div className="flex min-w-36 flex-1 items-center gap-2">
<SurveyStatusIndicator status={survey.status} isScheduled={isScheduled} />
<span>{t("common.status")}</span>
<span className="ml-auto pl-4 text-xs font-normal text-slate-500">
{getSurveyStatusLabel(survey.status)}
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={survey.status}
onValueChange={(value) => {
void handleStatusChange(value as TSurveyStatusUpdate);
}}>
<DropdownMenuRadioItem value="inProgress">
<SurveyStatusIndicator status="inProgress" />
{getSurveyStatusLabel("inProgress", false)}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="paused">
<SurveyStatusIndicator status="paused" isScheduled={isScheduled} />
{getSurveyStatusLabel("paused")}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="completed">
<SurveyStatusIndicator status="completed" />
{getSurveyStatusLabel("completed", false)}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
{canPreviewOrCopyLink && (
<DropdownMenuItem>
<button
@@ -11,6 +11,7 @@ import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
import { getV3ApiErrorMessage } from "@/modules/api/lib/v3-client";
import { useDeleteSurvey } from "@/modules/survey/list/hooks/use-delete-survey";
import { useSurveys } from "@/modules/survey/list/hooks/use-surveys";
import { useUpdateSurveyStatus } from "@/modules/survey/list/hooks/use-update-survey-status";
import { initialFilters } from "@/modules/survey/list/lib/constants";
import {
hasActiveSurveyFilters,
@@ -44,7 +45,7 @@ export const SurveysList = ({
surveysPerPage,
currentWorkspaceChannel,
locale,
}: SurveysListProps) => {
}: Readonly<SurveysListProps>) => {
const { t } = useTranslation();
const [surveyFilters, setSurveyFilters] = useState<TSurveyOverviewFilters>(initialFilters);
const [isFilterInitialized, setIsFilterInitialized] = useState(false);
@@ -103,6 +104,7 @@ export const SurveysList = ({
});
const deleteSurveyMutation = useDeleteSurvey({ queryKey });
const updateSurveyStatusMutation = useUpdateSurveyStatus({ queryKey });
const hasAppliedFilters = hasActiveSurveyFilters(normalizedFilters);
const showInitialLoading = !isFilterInitialized || (isLoading && surveys.length === 0);
@@ -113,6 +115,10 @@ export const SurveysList = ({
await deleteSurveyMutation.mutateAsync({ surveyId });
};
const handleUpdateSurveyStatus = async (
variables: Parameters<typeof updateSurveyStatusMutation.mutateAsync>[0]
) => updateSurveyStatusMutation.mutateAsync(variables);
const createSurveyButton = (
<Button size="sm" asChild>
<Link href={`/workspaces/${workspace.id}/surveys/templates`}>
@@ -203,6 +209,7 @@ export const SurveysList = ({
survey={survey}
isReadOnly={isReadOnly}
deleteSurvey={handleDeleteSurvey}
updateSurveyStatus={handleUpdateSurveyStatus}
publicDomain={publicDomain}
locale={locale}
/>
@@ -0,0 +1,142 @@
/**
* @vitest-environment jsdom
*/
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import { type ReactNode, createElement } from "react";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { updateSurveyStatusAction } from "@/modules/survey/list/actions";
import { surveyKeys } from "@/modules/survey/list/lib/query";
import type { TSurveyListPage } from "@/modules/survey/list/lib/v3-surveys-client";
import { useUpdateSurveyStatus } from "./use-update-survey-status";
vi.mock("@/modules/survey/list/actions", () => ({
updateSurveyStatusAction: vi.fn(),
}));
const queryKey = surveyKeys.list({
workspaceId: "workspace_1",
limit: 20,
filters: {
name: "",
status: [],
type: [],
sortBy: "relevance",
},
});
const queryData = {
pages: [
{
data: [
{
id: "survey_1",
name: "Survey 1",
workspaceId: "workspace_1",
type: "link",
status: "inProgress",
publishOn: null,
createdAt: new Date("2026-04-15T10:00:00.000Z"),
updatedAt: new Date("2026-04-15T10:00:00.000Z"),
responseCount: 0,
creator: { name: "Alice" },
singleUse: null,
},
],
meta: {
limit: 20,
nextCursor: null,
totalCount: 1,
},
},
],
pageParams: [null],
} satisfies { pages: TSurveyListPage[]; pageParams: (string | null)[] };
const createWrapper = (queryClient: QueryClient) => {
const Wrapper = ({ children }: Readonly<{ children: ReactNode }>) =>
createElement(QueryClientProvider, { client: queryClient }, children);
Wrapper.displayName = "UseUpdateSurveyStatusTestWrapper";
return Wrapper;
};
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
mutations: { retry: false },
queries: { retry: false },
},
});
describe("useUpdateSurveyStatus", () => {
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
});
afterEach(() => {
vi.clearAllMocks();
});
test("updates cached list data from the status action result", async () => {
const updatedAt = new Date("2026-04-16T10:00:00.000Z");
vi.mocked(updateSurveyStatusAction).mockResolvedValue({
data: {
id: "survey_1",
status: "completed",
publishOn: null,
updatedAt,
},
});
const queryClient = createQueryClient();
queryClient.setQueryData(queryKey, queryData);
const { result } = renderHook(() => useUpdateSurveyStatus({ queryKey }), {
wrapper: createWrapper(queryClient),
});
result.current.mutate({ surveyId: "survey_1", status: "completed" });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(queryClient.getQueryData<{ pages: TSurveyListPage[] }>(queryKey)?.pages[0]?.data[0]).toEqual(
expect.objectContaining({
status: "completed",
publishOn: null,
updatedAt,
})
);
});
test("rolls cached list data back when the action throws", async () => {
vi.mocked(updateSurveyStatusAction).mockRejectedValue(new Error("Unable to update"));
const queryClient = createQueryClient();
queryClient.setQueryData(queryKey, queryData);
const { result } = renderHook(() => useUpdateSurveyStatus({ queryKey }), {
wrapper: createWrapper(queryClient),
});
result.current.mutate({ surveyId: "survey_1", status: "completed" });
await waitFor(() => expect(result.current.isError).toBe(true));
expect(queryClient.getQueryData(queryKey)).toEqual(queryData);
});
test("rolls cached list data back when the action returns no data", async () => {
vi.mocked(updateSurveyStatusAction).mockResolvedValue({ serverError: "Unable to update" });
const queryClient = createQueryClient();
queryClient.setQueryData(queryKey, queryData);
const { result } = renderHook(() => useUpdateSurveyStatus({ queryKey }), {
wrapper: createWrapper(queryClient),
});
result.current.mutate({ surveyId: "survey_1", status: "completed" });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(queryClient.getQueryData(queryKey)).toEqual(queryData);
});
});
@@ -0,0 +1,67 @@
"use client";
import { InfiniteData, useMutation, useQueryClient } from "@tanstack/react-query";
import type { TSurveyStatus } from "@formbricks/types/surveys/types";
import { updateSurveyStatusAction } from "@/modules/survey/list/actions";
import { surveyKeys, updateSurveyInInfiniteData } from "@/modules/survey/list/lib/query";
import type { TSurveyListPage } from "@/modules/survey/list/lib/v3-surveys-client";
type TUpdateSurveyStatusInput = {
surveyId: string;
status: Exclude<TSurveyStatus, "draft">;
};
export const useUpdateSurveyStatus = ({ queryKey }: { queryKey: ReturnType<typeof surveyKeys.list> }) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ surveyId, status }: TUpdateSurveyStatusInput) =>
updateSurveyStatusAction({ surveyId, status }),
onMutate: async ({ surveyId, status }) => {
await queryClient.cancelQueries({ queryKey });
const previousData = queryClient.getQueryData<InfiniteData<TSurveyListPage>>(queryKey);
queryClient.setQueryData<InfiniteData<TSurveyListPage> | undefined>(queryKey, (currentData) =>
updateSurveyInInfiniteData(currentData, {
id: surveyId,
status,
updatedAt: new Date(),
...(status === "paused" ? {} : { publishOn: null }),
})
);
return {
previousData,
};
},
onError: (_error, _variables, context) => {
if (context?.previousData) {
queryClient.setQueryData(queryKey, context.previousData);
}
},
onSuccess: (response, _variables, context) => {
if (!response?.data) {
if (context?.previousData) {
queryClient.setQueryData(queryKey, context.previousData);
}
return;
}
const { id, publishOn, status, updatedAt } = response.data;
queryClient.setQueryData<InfiniteData<TSurveyListPage> | undefined>(queryKey, (currentData) =>
updateSurveyInInfiniteData(currentData, {
id,
publishOn,
status,
updatedAt,
})
);
},
onSettled: async () => {
await queryClient.invalidateQueries({ queryKey: surveyKeys.lists() });
},
});
};
+27 -2
View File
@@ -1,7 +1,7 @@
import type { InfiniteData } from "@tanstack/react-query";
import { describe, expect, test } from "vitest";
import { flattenSurveyPages, removeSurveyFromInfiniteData } from "./query";
import { TSurveyListPage } from "./v3-surveys-client";
import { flattenSurveyPages, removeSurveyFromInfiniteData, updateSurveyInInfiniteData } from "./query";
import type { TSurveyListPage } from "./v3-surveys-client";
const surveyA = {
id: "survey_a",
@@ -9,6 +9,7 @@ const surveyA = {
workspaceId: "env_1",
type: "link" as const,
status: "draft" as const,
publishOn: null,
createdAt: new Date("2026-04-15T10:00:00.000Z"),
updatedAt: new Date("2026-04-15T10:00:00.000Z"),
responseCount: 0,
@@ -50,6 +51,30 @@ describe("flattenSurveyPages", () => {
});
});
describe("updateSurveyInInfiniteData", () => {
test("updates the matching survey across cached pages", () => {
const updatedAt = new Date("2026-04-16T10:00:00.000Z");
const nextData = updateSurveyInInfiniteData(baseData, {
id: "survey_b",
status: "completed",
publishOn: null,
updatedAt,
});
expect(nextData?.pages[0]?.data).toEqual([surveyA]);
expect(nextData?.pages[1]?.data[0]).toEqual({
...surveyB,
status: "completed",
publishOn: null,
updatedAt,
});
});
test("returns the original cache when the survey is not present", () => {
expect(updateSurveyInInfiniteData(baseData, { id: "missing_survey", status: "paused" })).toBe(baseData);
});
});
describe("removeSurveyFromInfiniteData", () => {
test("removes the survey from cached pages and decrements each page total", () => {
const nextData = removeSurveyFromInfiniteData(baseData, "survey_a");
+37 -2
View File
@@ -1,6 +1,6 @@
import type { InfiniteData } from "@tanstack/react-query";
import { TSurveyListItem, TSurveyOverviewFilters } from "@/modules/survey/list/types/survey-overview";
import { TSurveyListPage } from "./v3-surveys-client";
import type { TSurveyListItem, TSurveyOverviewFilters } from "@/modules/survey/list/types/survey-overview";
import type { TSurveyListPage } from "./v3-surveys-client";
type TSurveyListKeyInput = {
workspaceId: string;
@@ -55,3 +55,38 @@ export function removeSurveyFromInfiniteData(
})),
};
}
export function updateSurveyInInfiniteData(
data: InfiniteData<TSurveyListPage> | undefined,
updatedSurvey: Pick<TSurveyListItem, "id"> & Partial<TSurveyListItem>
): InfiniteData<TSurveyListPage> | undefined {
if (!data) {
return data;
}
let surveyWasUpdated = false;
const pages = data.pages.map((page) => ({
...page,
data: page.data.map((survey) => {
if (survey.id !== updatedSurvey.id) {
return survey;
}
surveyWasUpdated = true;
return {
...survey,
...updatedSurvey,
};
}),
}));
if (!surveyWasUpdated) {
return data;
}
return {
...data,
pages,
};
}