mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 16:24:08 -06:00
feat: reset survey (#6267)
This commit is contained in:
committed by
GitHub
parent
e83cfa85a4
commit
30fdcff737
@@ -7,6 +7,7 @@ import { TProject } from "@formbricks/types/project";
|
||||
export interface EnvironmentContextType {
|
||||
environment: TEnvironment;
|
||||
project: TProject;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
const EnvironmentContext = createContext<EnvironmentContextType | null>(null);
|
||||
@@ -35,6 +36,7 @@ export const EnvironmentContextWrapper = ({
|
||||
() => ({
|
||||
environment,
|
||||
project,
|
||||
organizationId: project.organizationId,
|
||||
}),
|
||||
[environment, project]
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentI
|
||||
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import Page from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { getDisplayCountBySurveyId } from "@/lib/display/service";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
@@ -73,6 +74,10 @@ vi.mock("@/lib/response/service", () => ({
|
||||
getResponseCountBySurveyId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/display/service", () => ({
|
||||
getDisplayCountBySurveyId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/survey/service", () => ({
|
||||
getSurvey: vi.fn(),
|
||||
}));
|
||||
@@ -178,6 +183,7 @@ describe("ResponsesPage", () => {
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getTagsByEnvironmentId).mockResolvedValue(mockTags);
|
||||
vi.mocked(getResponseCountBySurveyId).mockResolvedValue(10);
|
||||
vi.mocked(getDisplayCountBySurveyId).mockResolvedValue(5);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale);
|
||||
vi.mocked(getPublicDomain).mockReturnValue(mockPublicDomain);
|
||||
});
|
||||
@@ -206,6 +212,8 @@ describe("ResponsesPage", () => {
|
||||
isReadOnly: false,
|
||||
user: mockUser,
|
||||
publicDomain: mockPublicDomain,
|
||||
responseCount: 10,
|
||||
displayCount: 5,
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentI
|
||||
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { IS_FORMBRICKS_CLOUD, RESPONSES_PER_PAGE } from "@/lib/constants";
|
||||
import { getDisplayCountBySurveyId } from "@/lib/display/service";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
@@ -40,6 +41,7 @@ const Page = async (props) => {
|
||||
|
||||
// Get response count for the CTA component
|
||||
const responseCount = await getResponseCountBySurveyId(params.surveyId);
|
||||
const displayCount = await getDisplayCountBySurveyId(params.surveyId);
|
||||
|
||||
const locale = await findMatchingLocale();
|
||||
const publicDomain = getPublicDomain();
|
||||
@@ -56,6 +58,7 @@ const Page = async (props) => {
|
||||
user={user}
|
||||
publicDomain={publicDomain}
|
||||
responseCount={responseCount}
|
||||
displayCount={displayCount}
|
||||
segments={segments}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors";
|
||||
import { deleteResponsesAndDisplaysForSurvey } from "./lib/survey";
|
||||
|
||||
const ZSendEmbedSurveyPreviewEmailAction = z.object({
|
||||
surveyId: ZId,
|
||||
@@ -202,6 +203,61 @@ export const deleteResultShareUrlAction = authenticatedActionClient
|
||||
)
|
||||
);
|
||||
|
||||
const ZResetSurveyAction = z.object({
|
||||
surveyId: ZId,
|
||||
organizationId: ZId,
|
||||
projectId: ZId,
|
||||
});
|
||||
|
||||
export const resetSurveyAction = authenticatedActionClient.schema(ZResetSurveyAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"survey",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZResetSurveyAction>;
|
||||
}) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
minPermission: "readWrite",
|
||||
projectId: parsedInput.projectId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = parsedInput.organizationId;
|
||||
ctx.auditLoggingCtx.surveyId = parsedInput.surveyId;
|
||||
ctx.auditLoggingCtx.oldObject = null;
|
||||
|
||||
const { deletedResponsesCount, deletedDisplaysCount } = await deleteResponsesAndDisplaysForSurvey(
|
||||
parsedInput.surveyId
|
||||
);
|
||||
|
||||
ctx.auditLoggingCtx.newObject = {
|
||||
deletedResponsesCount: deletedResponsesCount,
|
||||
deletedDisplaysCount: deletedDisplaysCount,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedResponsesCount: deletedResponsesCount,
|
||||
deletedDisplaysCount: deletedDisplaysCount,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZGetEmailHtmlAction = z.object({
|
||||
surveyId: ZId,
|
||||
});
|
||||
|
||||
@@ -33,6 +33,18 @@ vi.mock("@tolgee/react", () => ({
|
||||
if (key === "environments.surveys.edit.caution_edit_duplicate") {
|
||||
return "Duplicate & Edit";
|
||||
}
|
||||
if (key === "environments.surveys.summary.reset_survey") {
|
||||
return "Reset survey";
|
||||
}
|
||||
if (key === "environments.surveys.summary.delete_all_existing_responses_and_displays") {
|
||||
return "Delete all existing responses and displays";
|
||||
}
|
||||
if (key === "environments.surveys.summary.reset_survey_warning") {
|
||||
return "Resetting a survey removes all responses and metadata of this survey. This cannot be undone.";
|
||||
}
|
||||
if (key === "environments.surveys.summary.survey_reset_successfully") {
|
||||
return "Survey reset successfully! 5 responses and 3 displays were deleted.";
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
@@ -40,12 +52,14 @@ vi.mock("@tolgee/react", () => ({
|
||||
|
||||
// Mock Next.js hooks
|
||||
const mockPush = vi.fn();
|
||||
const mockRefresh = vi.fn();
|
||||
const mockPathname = "/environments/test-env-id/surveys/test-survey-id/summary";
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
refresh: mockRefresh,
|
||||
}),
|
||||
usePathname: () => mockPathname,
|
||||
useSearchParams: () => mockSearchParams,
|
||||
@@ -69,6 +83,10 @@ vi.mock("@/modules/survey/list/actions", () => ({
|
||||
copySurveyToOtherEnvironmentAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../actions", () => ({
|
||||
resetSurveyAction: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the useSingleUseId hook
|
||||
vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({
|
||||
useSingleUseId: vi.fn(() => ({
|
||||
@@ -147,6 +165,34 @@ vi.mock("@/modules/ui/components/badge", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/confirmation-modal", () => ({
|
||||
ConfirmationModal: ({
|
||||
open,
|
||||
setOpen,
|
||||
title,
|
||||
text,
|
||||
buttonText,
|
||||
onConfirm,
|
||||
buttonVariant,
|
||||
buttonLoading,
|
||||
}: any) => (
|
||||
<div
|
||||
data-testid="confirmation-modal"
|
||||
data-open={open}
|
||||
data-loading={buttonLoading}
|
||||
data-variant={buttonVariant}>
|
||||
<div data-testid="modal-title">{title}</div>
|
||||
<div data-testid="modal-text">{text}</div>
|
||||
<button type="button" onClick={onConfirm} data-testid="confirm-button">
|
||||
{buttonText}
|
||||
</button>
|
||||
<button type="button" onClick={() => setOpen(false)} data-testid="cancel-button">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ui/components/button", () => ({
|
||||
Button: ({ children, onClick, className }: any) => (
|
||||
<button type="button" data-testid="button" onClick={onClick} className={className}>
|
||||
@@ -178,9 +224,17 @@ vi.mock("@/modules/ui/components/iconbar", () => ({
|
||||
vi.mock("lucide-react", () => ({
|
||||
BellRing: () => <svg data-testid="bell-ring-icon" />,
|
||||
Eye: () => <svg data-testid="eye-icon" />,
|
||||
ListRestart: () => <svg data-testid="list-restart-icon" />,
|
||||
SquarePenIcon: () => <svg data-testid="square-pen-icon" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/context/environment-context", () => ({
|
||||
useEnvironment: vi.fn(() => ({
|
||||
organizationId: "test-organization-id",
|
||||
project: { id: "test-project-id" },
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock data
|
||||
const mockEnvironment: TEnvironment = {
|
||||
id: "test-env-id",
|
||||
@@ -270,6 +324,7 @@ const defaultProps = {
|
||||
user: mockUser,
|
||||
publicDomain: "https://example.com",
|
||||
responseCount: 0,
|
||||
displayCount: 0,
|
||||
segments: mockSegments,
|
||||
isContactsEnabled: true,
|
||||
isFormbricksCloud: false,
|
||||
@@ -286,19 +341,19 @@ describe("SurveyAnalysisCTA", () => {
|
||||
});
|
||||
|
||||
test("renders share survey button", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
expect(screen.getByText("Share survey")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders success message component", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
expect(screen.getByTestId("success-message")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders survey status dropdown when app setup is completed", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
expect(screen.getByTestId("survey-status-dropdown")).toBeInTheDocument();
|
||||
});
|
||||
@@ -310,7 +365,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
});
|
||||
|
||||
test("renders icon bar with correct actions", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
expect(screen.getByTestId("icon-bar")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("icon-bar-action-0")).toBeInTheDocument(); // Bell ring
|
||||
@@ -334,7 +389,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
|
||||
test("opens share modal when share button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
await user.click(screen.getByText("Share survey"));
|
||||
|
||||
@@ -344,7 +399,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
|
||||
test("opens share modal when share param is true", () => {
|
||||
mockSearchParams.set("share", "true");
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true");
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-modal-view", "start");
|
||||
@@ -352,7 +407,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
|
||||
test("navigates to edit when edit button is clicked and no responses", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
await user.click(screen.getByTestId("icon-bar-action-1"));
|
||||
|
||||
@@ -363,14 +418,15 @@ describe("SurveyAnalysisCTA", () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
await user.click(screen.getByTestId("icon-bar-action-1"));
|
||||
// With responseCount > 0, the edit button should be at icon-bar-action-2 (after reset button)
|
||||
await user.click(screen.getByTestId("icon-bar-action-2"));
|
||||
|
||||
expect(screen.getByTestId("edit-public-survey-alert-dialog")).toHaveAttribute("data-open", "true");
|
||||
});
|
||||
|
||||
test("navigates to notifications when bell icon is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
await user.click(screen.getByTestId("icon-bar-action-0"));
|
||||
|
||||
@@ -391,7 +447,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
});
|
||||
|
||||
test("does not show icon bar actions when read-only", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} isReadOnly={true} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} isReadOnly={true} />);
|
||||
|
||||
const iconBar = screen.getByTestId("icon-bar");
|
||||
expect(iconBar).toBeInTheDocument();
|
||||
@@ -402,7 +458,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
test("handles modal close correctly", async () => {
|
||||
mockSearchParams.set("share", "true");
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
// Verify modal is open initially
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true");
|
||||
@@ -429,13 +485,13 @@ describe("SurveyAnalysisCTA", () => {
|
||||
|
||||
test("does not show status dropdown when app setup is not completed", () => {
|
||||
const environmentWithoutAppSetup = { ...mockEnvironment, appSetupCompleted: false };
|
||||
render(<SurveyAnalysisCTA {...defaultProps} environment={environmentWithoutAppSetup} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} environment={environmentWithoutAppSetup} />);
|
||||
|
||||
expect(screen.queryByTestId("survey-status-dropdown")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders correctly with all props", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
expect(screen.getByTestId("icon-bar")).toBeInTheDocument();
|
||||
expect(screen.getByText("Share survey")).toBeInTheDocument();
|
||||
@@ -579,8 +635,8 @@ describe("SurveyAnalysisCTA", () => {
|
||||
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
// Click edit button to open dialog
|
||||
await user.click(screen.getByTestId("icon-bar-action-1"));
|
||||
// Click edit button to open dialog (should be icon-bar-action-2 with responses)
|
||||
await user.click(screen.getByTestId("icon-bar-action-2"));
|
||||
expect(screen.getByTestId("edit-public-survey-alert-dialog")).toHaveAttribute("data-open", "true");
|
||||
|
||||
// Click primary button (duplicate & edit)
|
||||
@@ -647,7 +703,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
|
||||
test("opens share modal with correct modal view when share button clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
await user.click(screen.getByText("Share survey"));
|
||||
|
||||
@@ -669,24 +725,28 @@ describe("SurveyAnalysisCTA", () => {
|
||||
});
|
||||
|
||||
test("does not render share modal when user is null", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} user={null as any} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} user={null as any} />);
|
||||
|
||||
expect(screen.queryByTestId("share-survey-modal")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with different isFormbricksCloud values", () => {
|
||||
const { rerender } = render(<SurveyAnalysisCTA {...defaultProps} isFormbricksCloud={true} />);
|
||||
const { rerender } = render(
|
||||
<SurveyAnalysisCTA {...defaultProps} displayCount={0} isFormbricksCloud={true} />
|
||||
);
|
||||
expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument();
|
||||
|
||||
rerender(<SurveyAnalysisCTA {...defaultProps} isFormbricksCloud={false} />);
|
||||
rerender(<SurveyAnalysisCTA {...defaultProps} displayCount={0} isFormbricksCloud={false} />);
|
||||
expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with different isContactsEnabled values", () => {
|
||||
const { rerender } = render(<SurveyAnalysisCTA {...defaultProps} isContactsEnabled={true} />);
|
||||
const { rerender } = render(
|
||||
<SurveyAnalysisCTA {...defaultProps} displayCount={0} isContactsEnabled={true} />
|
||||
);
|
||||
expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument();
|
||||
|
||||
rerender(<SurveyAnalysisCTA {...defaultProps} isContactsEnabled={false} />);
|
||||
rerender(<SurveyAnalysisCTA {...defaultProps} displayCount={0} isContactsEnabled={false} />);
|
||||
expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -701,7 +761,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
|
||||
test("handles modal state changes correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
// Open modal via share button
|
||||
await user.click(screen.getByText("Share survey"));
|
||||
@@ -714,7 +774,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
|
||||
test("opens share modal via share button", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
await user.click(screen.getByText("Share survey"));
|
||||
|
||||
@@ -726,7 +786,7 @@ describe("SurveyAnalysisCTA", () => {
|
||||
test("closes share modal and updates modal state", async () => {
|
||||
mockSearchParams.set("share", "true");
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
// Modal should be open initially due to share param
|
||||
expect(screen.getByTestId("share-survey-modal")).toHaveAttribute("data-open", "true");
|
||||
@@ -738,19 +798,19 @@ describe("SurveyAnalysisCTA", () => {
|
||||
});
|
||||
|
||||
test("handles empty segments array", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} segments={[]} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} segments={[]} />);
|
||||
|
||||
expect(screen.getByTestId("share-survey-modal")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles zero response count", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={0} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} responseCount={0} />);
|
||||
|
||||
expect(screen.queryByTestId("edit-public-survey-alert-dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows all icon actions for non-readonly app survey", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} />);
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={0} />);
|
||||
|
||||
// Should show bell (notifications) and edit actions
|
||||
expect(screen.getByTestId("icon-bar-action-0")).toHaveAttribute("title", "Configure alerts");
|
||||
@@ -766,4 +826,236 @@ describe("SurveyAnalysisCTA", () => {
|
||||
expect(screen.getByTestId("icon-bar-action-1")).toHaveAttribute("title", "Preview");
|
||||
expect(screen.getByTestId("icon-bar-action-2")).toHaveAttribute("title", "Edit");
|
||||
});
|
||||
|
||||
// Reset Survey Feature Tests
|
||||
test("shows reset survey button when responses exist", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows reset survey button when displays exist", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} displayCount={3} />);
|
||||
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides reset survey button when no responses or displays exist", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={0} displayCount={0} />);
|
||||
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeUndefined();
|
||||
});
|
||||
|
||||
test("hides reset survey button for read-only users", () => {
|
||||
render(<SurveyAnalysisCTA {...defaultProps} isReadOnly={true} responseCount={5} displayCount={3} />);
|
||||
|
||||
// For read-only users, there should be no icon bar actions
|
||||
expect(screen.queryAllByTestId(/icon-bar-action-/)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("opens reset confirmation modal when reset button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
|
||||
expect(resetButton).toBeDefined();
|
||||
await user.click(resetButton!);
|
||||
|
||||
expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "true");
|
||||
expect(screen.getByTestId("modal-title")).toHaveTextContent("Delete all existing responses and displays");
|
||||
expect(screen.getByTestId("modal-text")).toHaveTextContent(
|
||||
"Resetting a survey removes all responses and metadata of this survey. This cannot be undone."
|
||||
);
|
||||
});
|
||||
|
||||
test("executes reset survey action when confirmed", async () => {
|
||||
const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction;
|
||||
mockResetSurveyAction.mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
deletedResponsesCount: 5,
|
||||
deletedDisplaysCount: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const toast = await import("react-hot-toast");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
// Open reset modal
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeDefined();
|
||||
await user.click(resetButton!);
|
||||
|
||||
// Confirm reset
|
||||
await user.click(screen.getByTestId("confirm-button"));
|
||||
|
||||
expect(mockResetSurveyAction).toHaveBeenCalledWith({
|
||||
surveyId: "test-survey-id",
|
||||
organizationId: "test-organization-id",
|
||||
projectId: "test-project-id",
|
||||
});
|
||||
expect(toast.default.success).toHaveBeenCalledWith(
|
||||
"Survey reset successfully! 5 responses and 3 displays were deleted."
|
||||
);
|
||||
});
|
||||
|
||||
test("handles reset survey action error", async () => {
|
||||
const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction;
|
||||
mockResetSurveyAction.mockResolvedValue({
|
||||
data: undefined,
|
||||
serverError: "Reset failed",
|
||||
validationErrors: undefined,
|
||||
bindArgsValidationErrors: [],
|
||||
});
|
||||
|
||||
const toast = await import("react-hot-toast");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
// Open reset modal
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeDefined();
|
||||
await user.click(resetButton!);
|
||||
|
||||
// Confirm reset
|
||||
await user.click(screen.getByTestId("confirm-button"));
|
||||
|
||||
expect(toast.default.error).toHaveBeenCalledWith("Error message");
|
||||
});
|
||||
|
||||
test("shows loading state during reset operation", async () => {
|
||||
const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction;
|
||||
|
||||
// Mock a delayed response
|
||||
mockResetSurveyAction.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
data: {
|
||||
success: true,
|
||||
deletedResponsesCount: 5,
|
||||
deletedDisplaysCount: 3,
|
||||
},
|
||||
}),
|
||||
100
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
// Open reset modal
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeDefined();
|
||||
await user.click(resetButton!);
|
||||
|
||||
// Confirm reset
|
||||
await user.click(screen.getByTestId("confirm-button"));
|
||||
|
||||
// Check loading state
|
||||
expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-loading", "true");
|
||||
});
|
||||
|
||||
test("closes reset modal after successful reset", async () => {
|
||||
const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction;
|
||||
mockResetSurveyAction.mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
deletedResponsesCount: 5,
|
||||
deletedDisplaysCount: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
// Open reset modal
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeDefined();
|
||||
await user.click(resetButton!);
|
||||
expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "true");
|
||||
|
||||
// Confirm reset - wait for the action to complete
|
||||
await user.click(screen.getByTestId("confirm-button"));
|
||||
|
||||
// Wait for the action to complete and the modal to close
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "false");
|
||||
});
|
||||
});
|
||||
|
||||
test("cancels reset operation when cancel button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
// Open reset modal
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeDefined();
|
||||
await user.click(resetButton!);
|
||||
expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "true");
|
||||
|
||||
// Cancel reset
|
||||
await user.click(screen.getByTestId("cancel-button"));
|
||||
|
||||
// Modal should be closed
|
||||
expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-open", "false");
|
||||
});
|
||||
|
||||
test("shows destructive button variant for reset confirmation", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
// Open reset modal
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeDefined();
|
||||
await user.click(resetButton!);
|
||||
|
||||
expect(screen.getByTestId("confirmation-modal")).toHaveAttribute("data-variant", "destructive");
|
||||
});
|
||||
|
||||
test("refreshes page after successful reset", async () => {
|
||||
const mockResetSurveyAction = vi.mocked(await import("../actions")).resetSurveyAction;
|
||||
|
||||
mockResetSurveyAction.mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
deletedResponsesCount: 5,
|
||||
deletedDisplaysCount: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<SurveyAnalysisCTA {...defaultProps} responseCount={5} />);
|
||||
|
||||
// Open reset modal
|
||||
const iconActions = screen.getAllByTestId(/icon-bar-action-/);
|
||||
const resetButton = iconActions.find((button) => button.getAttribute("title") === "Reset survey");
|
||||
expect(resetButton).toBeDefined();
|
||||
await user.click(resetButton!);
|
||||
|
||||
// Confirm reset
|
||||
await user.click(screen.getByTestId("confirm-button"));
|
||||
|
||||
expect(mockRefresh).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEnvironment } from "@/app/(app)/environments/[environmentId]/context/environment-context";
|
||||
import { SuccessMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
|
||||
import { ShareSurveyModal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/share-survey-modal";
|
||||
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||
@@ -9,9 +10,10 @@ import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
|
||||
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
||||
import { IconBar } from "@/modules/ui/components/iconbar";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { BellRing, Eye, SquarePenIcon } from "lucide-react";
|
||||
import { BellRing, Eye, ListRestart, SquarePenIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -19,6 +21,7 @@ import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { resetSurveyAction } from "../actions";
|
||||
|
||||
interface SurveyAnalysisCTAProps {
|
||||
survey: TSurvey;
|
||||
@@ -27,6 +30,7 @@ interface SurveyAnalysisCTAProps {
|
||||
user: TUser;
|
||||
publicDomain: string;
|
||||
responseCount: number;
|
||||
displayCount: number;
|
||||
segments: TSegment[];
|
||||
isContactsEnabled: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -44,6 +48,7 @@ export const SurveyAnalysisCTA = ({
|
||||
user,
|
||||
publicDomain,
|
||||
responseCount,
|
||||
displayCount,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
isFormbricksCloud,
|
||||
@@ -57,7 +62,10 @@ export const SurveyAnalysisCTA = ({
|
||||
start: searchParams.get("share") === "true",
|
||||
share: false,
|
||||
});
|
||||
const [isResetModalOpen, setIsResetModalOpen] = useState(false);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const { organizationId, project } = useEnvironment();
|
||||
const { refreshSingleUseId } = useSingleUseId(survey);
|
||||
|
||||
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||
@@ -118,6 +126,29 @@ export const SurveyAnalysisCTA = ({
|
||||
|
||||
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
|
||||
|
||||
const handleResetSurvey = async () => {
|
||||
setIsResetting(true);
|
||||
const result = await resetSurveyAction({
|
||||
surveyId: survey.id,
|
||||
organizationId: organizationId,
|
||||
projectId: project.id,
|
||||
});
|
||||
if (result?.data) {
|
||||
toast.success(
|
||||
t("environments.surveys.summary.survey_reset_successfully", {
|
||||
responseCount: result.data.deletedResponsesCount,
|
||||
displayCount: result.data.deletedDisplaysCount,
|
||||
})
|
||||
);
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(result);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
setIsResetting(false);
|
||||
setIsResetModalOpen(false);
|
||||
};
|
||||
|
||||
const iconActions = [
|
||||
{
|
||||
icon: BellRing,
|
||||
@@ -134,6 +165,12 @@ export const SurveyAnalysisCTA = ({
|
||||
},
|
||||
isVisible: survey.type === "link",
|
||||
},
|
||||
{
|
||||
icon: ListRestart,
|
||||
tooltip: t("environments.surveys.summary.reset_survey"),
|
||||
onClick: () => setIsResetModalOpen(true),
|
||||
isVisible: !isReadOnly && (responseCount > 0 || displayCount > 0),
|
||||
},
|
||||
{
|
||||
icon: SquarePenIcon,
|
||||
tooltip: t("common.edit"),
|
||||
@@ -202,6 +239,17 @@ export const SurveyAnalysisCTA = ({
|
||||
secondaryButtonText={t("common.edit")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmationModal
|
||||
open={isResetModalOpen}
|
||||
setOpen={setIsResetModalOpen}
|
||||
title={t("environments.surveys.summary.delete_all_existing_responses_and_displays")}
|
||||
text={t("environments.surveys.summary.reset_survey_warning")}
|
||||
buttonText={t("environments.surveys.summary.reset_survey")}
|
||||
onConfirm={handleResetSurvey}
|
||||
buttonVariant="destructive"
|
||||
buttonLoading={isResetting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { deleteResponsesAndDisplaysForSurvey } from "./survey";
|
||||
|
||||
// Mock prisma
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
response: {
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
display: {
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const surveyId = "clq5n7p1q0000m7z0h5p6g3r2";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("Tests for deleteResponsesAndDisplaysForSurvey service", () => {
|
||||
describe("Happy Path", () => {
|
||||
test("Deletes all responses and displays for a survey", async () => {
|
||||
const { prisma } = await import("@formbricks/database");
|
||||
|
||||
// Mock $transaction to return the results directly
|
||||
vi.mocked(prisma.$transaction).mockResolvedValue([{ count: 5 }, { count: 3 }]);
|
||||
|
||||
const result = await deleteResponsesAndDisplaysForSurvey(surveyId);
|
||||
|
||||
expect(prisma.$transaction).toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
deletedResponsesCount: 5,
|
||||
deletedDisplaysCount: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test("Handles case with no responses or displays to delete", async () => {
|
||||
const { prisma } = await import("@formbricks/database");
|
||||
|
||||
// Mock $transaction to return zero counts
|
||||
vi.mocked(prisma.$transaction).mockResolvedValue([{ count: 0 }, { count: 0 }]);
|
||||
|
||||
const result = await deleteResponsesAndDisplaysForSurvey(surveyId);
|
||||
|
||||
expect(result).toEqual({
|
||||
deletedResponsesCount: 0,
|
||||
deletedDisplaysCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
test("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => {
|
||||
const { prisma } = await import("@formbricks/database");
|
||||
|
||||
const mockErrorMessage = "Mock error message";
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
|
||||
code: PrismaErrorType.UniqueConstraintViolation,
|
||||
clientVersion: "0.0.1",
|
||||
});
|
||||
|
||||
vi.mocked(prisma.$transaction).mockRejectedValue(errToThrow);
|
||||
|
||||
await expect(deleteResponsesAndDisplaysForSurvey(surveyId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("Throws a generic Error for other exceptions", async () => {
|
||||
const { prisma } = await import("@formbricks/database");
|
||||
|
||||
const mockErrorMessage = "Mock error message";
|
||||
vi.mocked(prisma.$transaction).mockRejectedValue(new Error(mockErrorMessage));
|
||||
|
||||
await expect(deleteResponsesAndDisplaysForSurvey(surveyId)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
|
||||
export const deleteResponsesAndDisplaysForSurvey = async (
|
||||
surveyId: string
|
||||
): Promise<{ deletedResponsesCount: number; deletedDisplaysCount: number }> => {
|
||||
try {
|
||||
// Delete all responses for this survey
|
||||
|
||||
const [deletedResponsesCount, deletedDisplaysCount] = await prisma.$transaction([
|
||||
prisma.response.deleteMany({
|
||||
where: {
|
||||
surveyId: surveyId,
|
||||
},
|
||||
}),
|
||||
prisma.display.deleteMany({
|
||||
where: {
|
||||
surveyId: surveyId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
deletedResponsesCount: deletedResponsesCount.count,
|
||||
deletedDisplaysCount: deletedDisplaysCount.count,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -58,6 +58,7 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
||||
user={user}
|
||||
publicDomain={publicDomain}
|
||||
responseCount={initialSurveySummary?.meta.totalResponses ?? 0}
|
||||
displayCount={initialSurveySummary?.meta.displayCount ?? 0}
|
||||
segments={segments}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
|
||||
@@ -1779,6 +1779,7 @@
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Verbinde deine Website oder App mit Formbricks, um loszulegen.",
|
||||
"copy_link_to_public_results": "Link zu öffentlichen Ergebnissen kopieren",
|
||||
"custom_range": "Benutzerdefinierter Bereich...",
|
||||
"delete_all_existing_responses_and_displays": "Alle bestehenden Antworten und Anzeigen löschen",
|
||||
"download_qr_code": "QR Code herunterladen",
|
||||
"drop_offs": "Drop-Off Rate",
|
||||
"drop_offs_tooltip": "So oft wurde die Umfrage gestartet, aber nicht abgeschlossen.",
|
||||
@@ -1843,6 +1844,8 @@
|
||||
"qr_code_download_failed": "QR-Code-Download fehlgeschlagen",
|
||||
"qr_code_download_with_start_soon": "QR Code-Download startet bald",
|
||||
"qr_code_generation_failed": "Es gab ein Problem beim Laden des QR-Codes für die Umfrage. Bitte versuchen Sie es erneut.",
|
||||
"reset_survey": "Umfrage zurücksetzen",
|
||||
"reset_survey_warning": "Das Zurücksetzen einer Umfrage entfernt alle Antworten und Anzeigen, die mit dieser Umfrage verbunden sind. Dies kann nicht rückgängig gemacht werden.",
|
||||
"results_are_public": "Ergebnisse sind öffentlich",
|
||||
"selected_responses_csv": "Ausgewählte Antworten (CSV)",
|
||||
"selected_responses_excel": "Ausgewählte Antworten (Excel)",
|
||||
@@ -1853,6 +1856,7 @@
|
||||
"show_all_responses_where": "Zeige alle Antworten, bei denen...",
|
||||
"starts": "Startet",
|
||||
"starts_tooltip": "So oft wurde die Umfrage gestartet.",
|
||||
"survey_reset_successfully": "Umfrage erfolgreich zurückgesetzt! {responseCount} Antworten und {displayCount} Anzeigen wurden gelöscht.",
|
||||
"survey_results_are_public": "Deine Umfrageergebnisse sind öffentlich",
|
||||
"survey_results_are_shared_with_anyone_who_has_the_link": "Deine Umfrageergebnisse stehen allen zur Verfügung, die den Link haben. Die Ergebnisse werden nicht von Suchmaschinen indexiert.",
|
||||
"this_month": "Dieser Monat",
|
||||
|
||||
@@ -1779,6 +1779,7 @@
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Connect your website or app with Formbricks to get started.",
|
||||
"copy_link_to_public_results": "Copy link to public results",
|
||||
"custom_range": "Custom range...",
|
||||
"delete_all_existing_responses_and_displays": "Delete all existing responses and displays",
|
||||
"download_qr_code": "Download QR code",
|
||||
"drop_offs": "Drop-Offs",
|
||||
"drop_offs_tooltip": "Number of times the survey has been started but not completed.",
|
||||
@@ -1843,6 +1844,8 @@
|
||||
"qr_code_download_failed": "QR code download failed",
|
||||
"qr_code_download_with_start_soon": "QR code download will start soon",
|
||||
"qr_code_generation_failed": "There was a problem, loading the survey QR Code. Please try again.",
|
||||
"reset_survey": "Reset survey",
|
||||
"reset_survey_warning": "Resetting a survey removes all responses and displays associated with this survey. This cannot be undone.",
|
||||
"results_are_public": "Results are public",
|
||||
"selected_responses_csv": "Selected responses (CSV)",
|
||||
"selected_responses_excel": "Selected responses (Excel)",
|
||||
@@ -1853,6 +1856,7 @@
|
||||
"show_all_responses_where": "Show all responses where...",
|
||||
"starts": "Starts",
|
||||
"starts_tooltip": "Number of times the survey has been started.",
|
||||
"survey_reset_successfully": "Survey reset successfully! {responseCount} responses and {displayCount} displays were deleted.",
|
||||
"survey_results_are_public": "Your survey results are public!",
|
||||
"survey_results_are_shared_with_anyone_who_has_the_link": "Your survey results are shared with anyone who has the link. The results will not be indexed by search engines.",
|
||||
"this_month": "This month",
|
||||
|
||||
@@ -1779,6 +1779,7 @@
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Connectez votre site web ou votre application à Formbricks pour commencer.",
|
||||
"copy_link_to_public_results": "Copier le lien vers les résultats publics",
|
||||
"custom_range": "Plage personnalisée...",
|
||||
"delete_all_existing_responses_and_displays": "Supprimer toutes les réponses existantes et les affichages",
|
||||
"download_qr_code": "Télécharger code QR",
|
||||
"drop_offs": "Dépôts",
|
||||
"drop_offs_tooltip": "Nombre de fois que l'enquête a été commencée mais non terminée.",
|
||||
@@ -1843,6 +1844,8 @@
|
||||
"qr_code_download_failed": "Échec du téléchargement du code QR",
|
||||
"qr_code_download_with_start_soon": "Le téléchargement du code QR débutera bientôt",
|
||||
"qr_code_generation_failed": "\"Un problème est survenu lors du chargement du code QR du sondage. Veuillez réessayer.\"",
|
||||
"reset_survey": "Réinitialiser l'enquête",
|
||||
"reset_survey_warning": "Réinitialiser un sondage supprime toutes les réponses et les affichages associés à ce sondage. Cela ne peut pas être annulé.",
|
||||
"results_are_public": "Les résultats sont publics.",
|
||||
"selected_responses_csv": "Réponses sélectionnées (CSV)",
|
||||
"selected_responses_excel": "Réponses sélectionnées (Excel)",
|
||||
@@ -1853,6 +1856,7 @@
|
||||
"show_all_responses_where": "Afficher toutes les réponses où...",
|
||||
"starts": "Commence",
|
||||
"starts_tooltip": "Nombre de fois que l'enquête a été commencée.",
|
||||
"survey_reset_successfully": "Réinitialisation du sondage réussie ! {responseCount} réponses et {displayCount} affichages ont été supprimés.",
|
||||
"survey_results_are_public": "Les résultats de votre enquête sont publics !",
|
||||
"survey_results_are_shared_with_anyone_who_has_the_link": "Les résultats de votre enquête sont partagés avec quiconque possède le lien. Les résultats ne seront pas indexés par les moteurs de recherche.",
|
||||
"this_month": "Ce mois-ci",
|
||||
|
||||
@@ -1779,6 +1779,7 @@
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Conecte seu site ou app com o Formbricks para começar.",
|
||||
"copy_link_to_public_results": "Copiar link para resultados públicos",
|
||||
"custom_range": "Intervalo personalizado...",
|
||||
"delete_all_existing_responses_and_displays": "Excluir todas as respostas e exibições existentes",
|
||||
"download_qr_code": "baixar código QR",
|
||||
"drop_offs": "Pontos de Entrega",
|
||||
"drop_offs_tooltip": "Número de vezes que a pesquisa foi iniciada mas não concluída.",
|
||||
@@ -1843,6 +1844,8 @@
|
||||
"qr_code_download_failed": "falha no download do código QR",
|
||||
"qr_code_download_with_start_soon": "O download do código QR começará em breve",
|
||||
"qr_code_generation_failed": "Houve um problema ao carregar o Código QR do questionário. Por favor, tente novamente.",
|
||||
"reset_survey": "Redefinir pesquisa",
|
||||
"reset_survey_warning": "Redefinir uma pesquisa remove todas as respostas e exibições associadas a esta pesquisa. Isto não pode ser desfeito.",
|
||||
"results_are_public": "Os resultados são públicos",
|
||||
"selected_responses_csv": "Respostas selecionadas (CSV)",
|
||||
"selected_responses_excel": "Respostas selecionadas (Excel)",
|
||||
@@ -1853,6 +1856,7 @@
|
||||
"show_all_responses_where": "Mostre todas as respostas onde...",
|
||||
"starts": "começa",
|
||||
"starts_tooltip": "Número de vezes que a pesquisa foi iniciada.",
|
||||
"survey_reset_successfully": "Pesquisa redefinida com sucesso! {responseCount} respostas e {displayCount} exibições foram deletadas.",
|
||||
"survey_results_are_public": "Os resultados da sua pesquisa são públicos!",
|
||||
"survey_results_are_shared_with_anyone_who_has_the_link": "Os resultados da sua pesquisa são compartilhados com quem tiver o link. Os resultados não serão indexados por motores de busca.",
|
||||
"this_month": "Este mês",
|
||||
|
||||
@@ -1779,6 +1779,7 @@
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "Ligue o seu website ou aplicação ao Formbricks para começar.",
|
||||
"copy_link_to_public_results": "Copiar link para resultados públicos",
|
||||
"custom_range": "Intervalo personalizado...",
|
||||
"delete_all_existing_responses_and_displays": "Excluir todas as respostas existentes e exibições",
|
||||
"download_qr_code": "Transferir código QR",
|
||||
"drop_offs": "Desistências",
|
||||
"drop_offs_tooltip": "Número de vezes que o inquérito foi iniciado mas não concluído.",
|
||||
@@ -1843,6 +1844,8 @@
|
||||
"qr_code_download_failed": "Falha ao transferir o código QR",
|
||||
"qr_code_download_with_start_soon": "O download do código QR começará em breve",
|
||||
"qr_code_generation_failed": "Ocorreu um problema ao carregar o Código QR do questionário. Por favor, tente novamente.",
|
||||
"reset_survey": "Reiniciar inquérito",
|
||||
"reset_survey_warning": "Repor um inquérito remove todas as respostas e visualizações associadas a este inquérito. Isto não pode ser desfeito.",
|
||||
"results_are_public": "Os resultados são públicos",
|
||||
"selected_responses_csv": "Respostas selecionadas (CSV)",
|
||||
"selected_responses_excel": "Respostas selecionadas (Excel)",
|
||||
@@ -1853,6 +1856,7 @@
|
||||
"show_all_responses_where": "Mostrar todas as respostas onde...",
|
||||
"starts": "Começa",
|
||||
"starts_tooltip": "Número de vezes que o inquérito foi iniciado.",
|
||||
"survey_reset_successfully": "Inquérito reiniciado com sucesso! {responseCount} respostas e {displayCount} exibições foram eliminadas.",
|
||||
"survey_results_are_public": "Os resultados do seu inquérito são públicos!",
|
||||
"survey_results_are_shared_with_anyone_who_has_the_link": "Os resultados do seu inquérito são partilhados com qualquer pessoa que tenha o link. Os resultados não serão indexados pelos motores de busca.",
|
||||
"this_month": "Este mês",
|
||||
|
||||
@@ -1779,6 +1779,7 @@
|
||||
"connect_your_website_or_app_with_formbricks_to_get_started": "將您的網站或應用程式與 Formbricks 連線以開始使用。",
|
||||
"copy_link_to_public_results": "複製公開結果的連結",
|
||||
"custom_range": "自訂範圍...",
|
||||
"delete_all_existing_responses_and_displays": "刪除 所有 現有 回應 和 顯示",
|
||||
"download_qr_code": "下載 QR code",
|
||||
"drop_offs": "放棄",
|
||||
"drop_offs_tooltip": "問卷已開始但未完成的次數。",
|
||||
@@ -1843,6 +1844,8 @@
|
||||
"qr_code_download_failed": "QR code 下載失敗",
|
||||
"qr_code_download_with_start_soon": "QR code 下載即將開始",
|
||||
"qr_code_generation_failed": "載入調查 QR Code 時發生問題。請再試一次。",
|
||||
"reset_survey": "重設問卷",
|
||||
"reset_survey_warning": "重置 調查 會 移除 與 此 調查 相關 的 所有 回應 和 顯示 。 這 是 不可 撤銷 的 。",
|
||||
"results_are_public": "結果是公開的",
|
||||
"selected_responses_csv": "選擇的回應 (CSV)",
|
||||
"selected_responses_excel": "選擇的回應 (Excel)",
|
||||
@@ -1853,6 +1856,7 @@
|
||||
"show_all_responses_where": "顯示所有回應,其中...",
|
||||
"starts": "開始次數",
|
||||
"starts_tooltip": "問卷已開始的次數。",
|
||||
"survey_reset_successfully": "調查 重置 成功!{responseCount} 條回應和 {displayCount} 個顯示被刪除。",
|
||||
"survey_results_are_public": "您的問卷結果是公開的!",
|
||||
"survey_results_are_shared_with_anyone_who_has_the_link": "您的問卷結果與任何擁有連結的人員分享。這些結果將不會被搜尋引擎編入索引。",
|
||||
"this_month": "本月",
|
||||
|
||||
Reference in New Issue
Block a user