mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-23 08:10:19 -05:00
fix: Editing active surveys (#5015)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
+1
@@ -57,6 +57,7 @@ const Page = async (props) => {
|
||||
isReadOnly={isReadOnly}
|
||||
user={user}
|
||||
surveyDomain={surveyDomain}
|
||||
responseCount={totalResponseCount}
|
||||
/>
|
||||
}>
|
||||
{isAIEnabled && shouldGenerateInsights && (
|
||||
|
||||
+45
-1
@@ -3,8 +3,11 @@
|
||||
import { ShareEmbedSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey";
|
||||
import { SuccessMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
|
||||
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
||||
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
|
||||
import { copySurveyLink } from "@/modules/survey/lib/client-utils";
|
||||
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { IconBar } from "@/modules/ui/components/iconbar";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
@@ -22,6 +25,7 @@ interface SurveyAnalysisCTAProps {
|
||||
isReadOnly: boolean;
|
||||
user: TUser;
|
||||
surveyDomain: string;
|
||||
responseCount: number;
|
||||
}
|
||||
|
||||
interface ModalState {
|
||||
@@ -37,11 +41,13 @@ export const SurveyAnalysisCTA = ({
|
||||
isReadOnly,
|
||||
user,
|
||||
surveyDomain,
|
||||
responseCount,
|
||||
}: SurveyAnalysisCTAProps) => {
|
||||
const { t } = useTranslate();
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [modalState, setModalState] = useState<ModalState>({
|
||||
share: searchParams.get("share") === "true",
|
||||
@@ -89,6 +95,24 @@ export const SurveyAnalysisCTA = ({
|
||||
setModalState((prev) => ({ ...prev, dropdown: false }));
|
||||
};
|
||||
|
||||
const duplicateSurveyAndRoute = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
|
||||
environmentId: environment.id,
|
||||
surveyId: surveyId,
|
||||
targetEnvironmentId: environment.id,
|
||||
});
|
||||
if (duplicatedSurveyResponse?.data) {
|
||||
toast.success(t("environments.surveys.survey_duplicated_successfully"));
|
||||
router.push(`/environments/${environment.id}/surveys/${duplicatedSurveyResponse.data.id}/edit`);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(duplicatedSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
setIsCautionDialogOpen(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getPreviewUrl = () => {
|
||||
const separator = surveyUrl.includes("?") ? "&" : "?";
|
||||
return `${surveyUrl}${separator}preview=true`;
|
||||
@@ -107,6 +131,8 @@ export const SurveyAnalysisCTA = ({
|
||||
{ key: "panel", modalView: "panel" as const, setOpen: handleModalState("panel") },
|
||||
];
|
||||
|
||||
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
|
||||
|
||||
const iconActions = [
|
||||
{
|
||||
icon: Eye,
|
||||
@@ -144,7 +170,11 @@ export const SurveyAnalysisCTA = ({
|
||||
{
|
||||
icon: SquarePenIcon,
|
||||
tooltip: t("common.edit"),
|
||||
onClick: () => router.push(`/environments/${environment.id}/surveys/${survey.id}/edit`),
|
||||
onClick: () => {
|
||||
responseCount && responseCount > 0
|
||||
? setIsCautionDialogOpen(true)
|
||||
: router.push(`/environments/${environment.id}/surveys/${survey.id}/edit`);
|
||||
},
|
||||
isVisible: !isReadOnly,
|
||||
},
|
||||
];
|
||||
@@ -182,6 +212,20 @@ export const SurveyAnalysisCTA = ({
|
||||
<SuccessMessage environment={environment} survey={survey} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{responseCount > 0 && (
|
||||
<EditPublicSurveyAlertDialog
|
||||
open={isCautionDialogOpen}
|
||||
setOpen={setIsCautionDialogOpen}
|
||||
isLoading={loading}
|
||||
primaryButtonAction={() => duplicateSurveyAndRoute(survey.id)}
|
||||
primaryButtonText={t("environments.surveys.edit.caution_edit_duplicate")}
|
||||
secondaryButtonAction={() =>
|
||||
router.push(`/environments/${environment.id}/surveys/${survey.id}/edit`)
|
||||
}
|
||||
secondaryButtonText={t("common.edit")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+247
-7
@@ -1,7 +1,7 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
@@ -49,10 +49,12 @@ vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({
|
||||
}));
|
||||
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
const mockPush = vi.fn();
|
||||
|
||||
// Mock next/navigation
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
useSearchParams: () => mockSearchParams, // Reuse the same object
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
useSearchParams: () => mockSearchParams,
|
||||
usePathname: () => "/current",
|
||||
}));
|
||||
|
||||
@@ -61,13 +63,27 @@ vi.mock("@/modules/survey/lib/client-utils", () => ({
|
||||
copySurveyLink: vi.fn((url: string, id: string) => `${url}?id=${id}`),
|
||||
}));
|
||||
|
||||
// Mock the copy survey action
|
||||
const mockCopySurveyToOtherEnvironmentAction = vi.fn();
|
||||
vi.mock("@/modules/survey/list/actions", () => ({
|
||||
copySurveyToOtherEnvironmentAction: (args: any) => mockCopySurveyToOtherEnvironmentAction(args),
|
||||
}));
|
||||
|
||||
// Mock getFormattedErrorMessage function
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getFormattedErrorMessage: vi.fn((response) => response?.error || "Unknown error"),
|
||||
}));
|
||||
|
||||
vi.spyOn(toast, "success");
|
||||
vi.spyOn(toast, "error");
|
||||
|
||||
// Set up a fake clipboard
|
||||
const writeTextMock = vi.fn(() => Promise.resolve());
|
||||
Object.assign(navigator, {
|
||||
clipboard: { writeText: writeTextMock },
|
||||
// Mock clipboard API
|
||||
const writeTextMock = vi.fn().mockImplementation(() => Promise.resolve());
|
||||
|
||||
// Define it at the global level
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
value: { writeText: writeTextMock },
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const dummySurvey = {
|
||||
@@ -93,6 +109,7 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
isReadOnly={false}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -117,6 +134,7 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
isReadOnly={false}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -130,3 +148,225 @@ describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// New tests for squarePenIcon and edit functionality
|
||||
describe("SurveyAnalysisCTA - Edit functionality", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("opens EditPublicSurveyAlertDialog when edit icon is clicked and response count > 0", async () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the edit button
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButton);
|
||||
|
||||
// Check if dialog is shown
|
||||
const dialogTitle = screen.getByText("environments.surveys.edit.caution_edit_published_survey");
|
||||
expect(dialogTitle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("navigates directly to edit page when response count = 0", async () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={0}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find the edit button
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButton);
|
||||
|
||||
// Should navigate directly to edit page
|
||||
expect(mockPush).toHaveBeenCalledWith(
|
||||
`/environments/${dummyEnvironment.id}/surveys/${dummySurvey.id}/edit`
|
||||
);
|
||||
});
|
||||
|
||||
test("doesn't show edit button when isReadOnly is true", () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={true}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
/>
|
||||
);
|
||||
|
||||
// Try to find the edit button (it shouldn't exist)
|
||||
const editButton = screen.queryByRole("button", { name: "common.edit" });
|
||||
expect(editButton).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Updated test description to mention EditPublicSurveyAlertDialog
|
||||
describe("SurveyAnalysisCTA - duplicateSurveyAndRoute and EditPublicSurveyAlertDialog", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test("duplicates survey successfully and navigates to edit page", async () => {
|
||||
// Mock the API response
|
||||
mockCopySurveyToOtherEnvironmentAction.mockResolvedValueOnce({
|
||||
data: { id: "duplicated-survey-456" },
|
||||
});
|
||||
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
/>
|
||||
);
|
||||
|
||||
// Find and click the edit button to show dialog
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButton);
|
||||
|
||||
// Find and click the duplicate button in dialog
|
||||
const duplicateButton = screen.getByRole("button", {
|
||||
name: "environments.surveys.edit.caution_edit_duplicate",
|
||||
});
|
||||
await fireEvent.click(duplicateButton);
|
||||
|
||||
// Verify the API was called with correct parameters
|
||||
expect(mockCopySurveyToOtherEnvironmentAction).toHaveBeenCalledWith({
|
||||
environmentId: dummyEnvironment.id,
|
||||
surveyId: dummySurvey.id,
|
||||
targetEnvironmentId: dummyEnvironment.id,
|
||||
});
|
||||
|
||||
// Verify success toast was shown
|
||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.survey_duplicated_successfully");
|
||||
|
||||
// Verify navigation to edit page
|
||||
expect(mockPush).toHaveBeenCalledWith(
|
||||
`/environments/${dummyEnvironment.id}/surveys/duplicated-survey-456/edit`
|
||||
);
|
||||
});
|
||||
|
||||
test("shows error toast when duplication fails with error object", async () => {
|
||||
// Mock API failure with error object
|
||||
mockCopySurveyToOtherEnvironmentAction.mockResolvedValueOnce({
|
||||
error: "Test error message",
|
||||
});
|
||||
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
/>
|
||||
);
|
||||
|
||||
// Open dialog
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButton);
|
||||
|
||||
// Click duplicate
|
||||
const duplicateButton = screen.getByRole("button", {
|
||||
name: "environments.surveys.edit.caution_edit_duplicate",
|
||||
});
|
||||
await fireEvent.click(duplicateButton);
|
||||
|
||||
// Verify error toast
|
||||
expect(toast.error).toHaveBeenCalledWith("Test error message");
|
||||
});
|
||||
|
||||
test("navigates to edit page when cancel button is clicked in dialog", async () => {
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
/>
|
||||
);
|
||||
|
||||
// Open dialog
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButton);
|
||||
|
||||
// Click edit (cancel) button
|
||||
const editButtonInDialog = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButtonInDialog);
|
||||
|
||||
// Verify navigation
|
||||
expect(mockPush).toHaveBeenCalledWith(
|
||||
`/environments/${dummyEnvironment.id}/surveys/${dummySurvey.id}/edit`
|
||||
);
|
||||
});
|
||||
|
||||
test("shows loading state when duplicating survey", async () => {
|
||||
// Create a promise that we can resolve manually
|
||||
let resolvePromise: (value: any) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
|
||||
mockCopySurveyToOtherEnvironmentAction.mockImplementation(() => promise);
|
||||
|
||||
render(
|
||||
<SurveyAnalysisCTA
|
||||
survey={dummySurvey}
|
||||
environment={dummyEnvironment}
|
||||
isReadOnly={false}
|
||||
surveyDomain={surveyDomain}
|
||||
user={dummyUser}
|
||||
responseCount={5}
|
||||
/>
|
||||
);
|
||||
|
||||
// Open dialog
|
||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||
await fireEvent.click(editButton);
|
||||
|
||||
// Click duplicate
|
||||
const duplicateButton = screen.getByRole("button", {
|
||||
name: "environments.surveys.edit.caution_edit_duplicate",
|
||||
});
|
||||
await fireEvent.click(duplicateButton);
|
||||
|
||||
// Button should now be in loading state
|
||||
// expect(duplicateButton).toHaveAttribute("data-state", "loading");
|
||||
|
||||
// Resolve the promise
|
||||
resolvePromise!({
|
||||
data: { id: "duplicated-survey-456" },
|
||||
});
|
||||
|
||||
// Wait for the promise to resolve
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
@@ -68,6 +68,7 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
||||
isReadOnly={isReadOnly}
|
||||
user={user}
|
||||
surveyDomain={surveyDomain}
|
||||
responseCount={totalResponseCount}
|
||||
/>
|
||||
}>
|
||||
{isAIEnabled && shouldGenerateInsights && (
|
||||
|
||||
@@ -204,7 +204,7 @@ export const LoginForm = ({
|
||||
aria-label="password"
|
||||
aria-required="true"
|
||||
required
|
||||
className="focus:border-brand-dark focus:ring-brand-dark block w-full pr-8 rounded-md border-slate-300 shadow-sm sm:text-sm"
|
||||
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 pr-8 shadow-sm sm:text-sm"
|
||||
value={field.value}
|
||||
onChange={(password) => field.onChange(password)}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { EditPublicSurveyAlertDialog } from "./index";
|
||||
|
||||
// Mock translation to return keys as text
|
||||
vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ t: (key: string) => key }) }));
|
||||
|
||||
describe("EditPublicSurveyAlertDialog", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders with all expected content", () => {
|
||||
const setOpen = vi.fn();
|
||||
render(<EditPublicSurveyAlertDialog open={true} setOpen={setOpen} />);
|
||||
|
||||
// Title
|
||||
expect(screen.getByText("environments.surveys.edit.caution_edit_published_survey")).toBeInTheDocument();
|
||||
|
||||
// Paragraphs
|
||||
expect(screen.getByText("environments.surveys.edit.caution_recommendation")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.surveys.edit.caution_explanation_intro")).toBeInTheDocument();
|
||||
|
||||
// List items
|
||||
expect(
|
||||
screen.getByText("environments.surveys.edit.caution_explanation_responses_are_safe")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.edit.caution_explanation_new_responses_separated")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.edit.caution_explanation_only_new_responses_in_summary")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("environments.surveys.edit.caution_explanation_all_data_as_download")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders default close button and calls setOpen when clicked", () => {
|
||||
const setOpen = vi.fn();
|
||||
render(<EditPublicSurveyAlertDialog open={true} setOpen={setOpen} />);
|
||||
|
||||
// Find the close button in the UI
|
||||
const closeButton = screen.getByRole("button", { name: "common.close" });
|
||||
expect(closeButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(closeButton);
|
||||
expect(setOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
test("renders primary button and calls action when clicked", async () => {
|
||||
const setOpen = vi.fn();
|
||||
const primaryAction = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<EditPublicSurveyAlertDialog
|
||||
open={true}
|
||||
setOpen={setOpen}
|
||||
primaryButtonAction={primaryAction}
|
||||
primaryButtonText="Primary Text"
|
||||
/>
|
||||
);
|
||||
|
||||
const primaryButton = screen.getByRole("button", { name: "Primary Text" });
|
||||
expect(primaryButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(primaryButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(primaryAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("renders secondary button and calls action when clicked", () => {
|
||||
const setOpen = vi.fn();
|
||||
const secondaryAction = vi.fn();
|
||||
|
||||
render(
|
||||
<EditPublicSurveyAlertDialog
|
||||
open={true}
|
||||
setOpen={setOpen}
|
||||
secondaryButtonAction={secondaryAction}
|
||||
secondaryButtonText="Secondary Text"
|
||||
/>
|
||||
);
|
||||
|
||||
const secondaryButton = screen.getByRole("button", { name: "Secondary Text" });
|
||||
expect(secondaryButton).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(secondaryButton);
|
||||
expect(secondaryAction).toHaveBeenCalledTimes(1);
|
||||
expect(setOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("renders both buttons when both actions are provided", async () => {
|
||||
const setOpen = vi.fn();
|
||||
const primaryAction = vi.fn().mockResolvedValue(undefined);
|
||||
const secondaryAction = vi.fn();
|
||||
|
||||
render(
|
||||
<EditPublicSurveyAlertDialog
|
||||
open={true}
|
||||
setOpen={setOpen}
|
||||
primaryButtonAction={primaryAction}
|
||||
primaryButtonText="Primary Text"
|
||||
secondaryButtonAction={secondaryAction}
|
||||
secondaryButtonText="Secondary Text"
|
||||
/>
|
||||
);
|
||||
|
||||
// Modal has a close button by default plus our two action buttons
|
||||
const allButtons = screen.getAllByRole("button");
|
||||
// Verify our two action buttons by their text content
|
||||
const actionButtons = allButtons.filter(
|
||||
(button) => button.textContent === "Secondary Text" || button.textContent === "Primary Text"
|
||||
);
|
||||
expect(actionButtons).toHaveLength(2);
|
||||
expect(actionButtons[0]).toHaveTextContent("Secondary Text");
|
||||
expect(actionButtons[1]).toHaveTextContent("Primary Text");
|
||||
|
||||
fireEvent.click(actionButtons[0]);
|
||||
expect(secondaryAction).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.click(actionButtons[1]);
|
||||
await waitFor(() => {
|
||||
expect(primaryAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("shows loading state in primary button when isLoading is true", () => {
|
||||
const setOpen = vi.fn();
|
||||
const primaryAction = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<EditPublicSurveyAlertDialog
|
||||
open={true}
|
||||
setOpen={setOpen}
|
||||
isLoading={true}
|
||||
primaryButtonAction={primaryAction}
|
||||
primaryButtonText="Primary Text"
|
||||
/>
|
||||
);
|
||||
|
||||
const primaryButton = screen.getByRole("button", { name: "Primary Text" });
|
||||
// Check if button has loading class or attribute
|
||||
expect(
|
||||
primaryButton.classList.contains("loading") ||
|
||||
primaryButton.innerHTML.includes("loader") ||
|
||||
primaryButton.getAttribute("aria-busy") === "true"
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Modal } from "@/modules/ui/components/modal";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
|
||||
interface EditPublicSurveyAlertDialogProps {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
isLoading?: boolean;
|
||||
primaryButtonAction?: () => Promise<void>;
|
||||
secondaryButtonAction?: () => void;
|
||||
primaryButtonText?: string;
|
||||
secondaryButtonText?: string;
|
||||
}
|
||||
|
||||
export const EditPublicSurveyAlertDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
isLoading = false,
|
||||
primaryButtonAction,
|
||||
secondaryButtonAction,
|
||||
primaryButtonText,
|
||||
secondaryButtonText,
|
||||
}: EditPublicSurveyAlertDialogProps) => {
|
||||
const { t } = useTranslate();
|
||||
const actions = [] as Array<{
|
||||
label?: string;
|
||||
onClick: () => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
variant: React.ComponentProps<typeof Button>["variant"];
|
||||
}>;
|
||||
if (secondaryButtonAction) {
|
||||
actions.push({
|
||||
label: secondaryButtonText,
|
||||
onClick: secondaryButtonAction,
|
||||
disabled: isLoading,
|
||||
variant: "outline",
|
||||
});
|
||||
}
|
||||
if (primaryButtonAction) {
|
||||
actions.push({
|
||||
label: primaryButtonText,
|
||||
onClick: primaryButtonAction,
|
||||
loading: isLoading,
|
||||
variant: "default",
|
||||
});
|
||||
}
|
||||
if (actions.length === 0) {
|
||||
actions.push({
|
||||
label: secondaryButtonText ?? t("common.close"),
|
||||
onClick: () => setOpen(false),
|
||||
variant: "default",
|
||||
});
|
||||
}
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} title={t("environments.surveys.edit.caution_edit_published_survey")}>
|
||||
<p>{t("environments.surveys.edit.caution_recommendation")}</p>
|
||||
<p className="mt-3">{t("environments.surveys.edit.caution_explanation_intro")}</p>
|
||||
<ul className="mt-3 list-disc space-y-0.5 pl-5">
|
||||
<li>{t("environments.surveys.edit.caution_explanation_responses_are_safe")}</li>
|
||||
<li>{t("environments.surveys.edit.caution_explanation_new_responses_separated")}</li>
|
||||
<li>{t("environments.surveys.edit.caution_explanation_only_new_responses_in_summary")}</li>
|
||||
<li>{t("environments.surveys.edit.caution_explanation_all_data_as_download")}</li>
|
||||
</ul>
|
||||
<div className="my-4 space-x-2 text-right">
|
||||
{actions.map(({ label, onClick, loading, variant, disabled }) => (
|
||||
<Button key={label} variant={variant} onClick={onClick} loading={loading} disabled={disabled}>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import { RankingQuestionForm } from "@/modules/survey/editor/components/ranking-
|
||||
import { RatingQuestionForm } from "@/modules/survey/editor/components/rating-question-form";
|
||||
import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
|
||||
import { getQuestionIconMap, getTSurveyQuestionTypeEnumName } from "@/modules/survey/lib/questions";
|
||||
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
@@ -59,6 +60,8 @@ interface QuestionCardProps {
|
||||
isFormbricksCloud: boolean;
|
||||
isCxMode: boolean;
|
||||
locale: TUserLocale;
|
||||
responseCount: number;
|
||||
onAlertTrigger: () => void;
|
||||
}
|
||||
|
||||
export const QuestionCard = ({
|
||||
@@ -80,6 +83,8 @@ export const QuestionCard = ({
|
||||
isFormbricksCloud,
|
||||
isCxMode,
|
||||
locale,
|
||||
responseCount,
|
||||
onAlertTrigger,
|
||||
}: QuestionCardProps) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: question.id,
|
||||
@@ -257,6 +262,21 @@ export const QuestionCard = ({
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-4"}`}>
|
||||
{responseCount > 0 &&
|
||||
[
|
||||
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
TSurveyQuestionTypeEnum.MultipleChoiceMulti,
|
||||
TSurveyQuestionTypeEnum.PictureSelection,
|
||||
TSurveyQuestionTypeEnum.Rating,
|
||||
TSurveyQuestionTypeEnum.NPS,
|
||||
TSurveyQuestionTypeEnum.Ranking,
|
||||
TSurveyQuestionTypeEnum.Matrix,
|
||||
].includes(question.type) ? (
|
||||
<Alert variant="warning" size="small" className="w-fill">
|
||||
<AlertTitle>{t("environments.surveys.edit.caution_text")}</AlertTitle>
|
||||
<AlertButton onClick={() => onAlertTrigger()}>{t("common.learn_more")}</AlertButton>
|
||||
</Alert>
|
||||
) : null}
|
||||
{question.type === TSurveyQuestionTypeEnum.OpenText ? (
|
||||
<OpenQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
|
||||
@@ -21,6 +21,8 @@ interface QuestionsDraggableProps {
|
||||
isFormbricksCloud: boolean;
|
||||
isCxMode: boolean;
|
||||
locale: TUserLocale;
|
||||
responseCount: number;
|
||||
onAlertTrigger: () => void;
|
||||
}
|
||||
|
||||
export const QuestionsDroppable = ({
|
||||
@@ -39,6 +41,8 @@ export const QuestionsDroppable = ({
|
||||
isFormbricksCloud,
|
||||
isCxMode,
|
||||
locale,
|
||||
responseCount,
|
||||
onAlertTrigger,
|
||||
}: QuestionsDraggableProps) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
@@ -66,6 +70,8 @@ export const QuestionsDroppable = ({
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isCxMode={isCxMode}
|
||||
locale={locale}
|
||||
responseCount={responseCount}
|
||||
onAlertTrigger={onAlertTrigger}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
||||
@@ -63,6 +63,8 @@ interface QuestionsViewProps {
|
||||
plan: TOrganizationBillingPlan;
|
||||
isCxMode: boolean;
|
||||
locale: TUserLocale;
|
||||
responseCount: number;
|
||||
setIsCautionDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const QuestionsView = ({
|
||||
@@ -81,6 +83,8 @@ export const QuestionsView = ({
|
||||
plan,
|
||||
isCxMode,
|
||||
locale,
|
||||
responseCount,
|
||||
setIsCautionDialogOpen,
|
||||
}: QuestionsViewProps) => {
|
||||
const { t } = useTranslate();
|
||||
const internalQuestionIdMap = useMemo(() => {
|
||||
@@ -460,6 +464,8 @@ export const QuestionsView = ({
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isCxMode={isCxMode}
|
||||
locale={locale}
|
||||
responseCount={responseCount}
|
||||
onAlertTrigger={() => setIsCautionDialogOpen(true)}
|
||||
/>
|
||||
</DndContext>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { extractLanguageCodes, getEnabledLanguages } from "@/lib/i18n/utils";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
import { useDocumentVisibility } from "@/lib/useDocumentVisibility";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
||||
import { LoadingSkeleton } from "@/modules/survey/editor/components/loading-skeleton";
|
||||
import { QuestionsView } from "@/modules/survey/editor/components/questions-view";
|
||||
import { SettingsView } from "@/modules/survey/editor/components/settings-view";
|
||||
@@ -89,6 +90,8 @@ export const SurveyEditor = ({
|
||||
}
|
||||
}, [localProject.id]);
|
||||
|
||||
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
|
||||
|
||||
useDocumentVisibility(fetchLatestProject);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -160,6 +163,7 @@ export const SurveyEditor = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isCxMode={isCxMode}
|
||||
locale={locale}
|
||||
setIsCautionDialogOpen={setIsCautionDialogOpen}
|
||||
/>
|
||||
<div className="relative z-0 flex flex-1 overflow-hidden">
|
||||
<main
|
||||
@@ -190,6 +194,8 @@ export const SurveyEditor = ({
|
||||
plan={plan}
|
||||
isCxMode={isCxMode}
|
||||
locale={locale}
|
||||
responseCount={responseCount}
|
||||
setIsCautionDialogOpen={setIsCautionDialogOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -250,6 +256,7 @@ export const SurveyEditor = ({
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
<EditPublicSurveyAlertDialog open={isCautionDialogOpen} setOpen={setIsCautionDialogOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { createSegmentAction } from "@/modules/ee/contacts/segments/actions";
|
||||
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
import { Project } from "@prisma/client";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { isEqual } from "lodash";
|
||||
import { AlertTriangleIcon, ArrowLeftIcon, SettingsIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, SettingsIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -40,6 +40,7 @@ interface SurveyMenuBarProps {
|
||||
setSelectedLanguageCode: (selectedLanguage: string) => void;
|
||||
isCxMode: boolean;
|
||||
locale: string;
|
||||
setIsCautionDialogOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const SurveyMenuBar = ({
|
||||
@@ -55,6 +56,7 @@ export const SurveyMenuBar = ({
|
||||
selectedLanguageCode,
|
||||
isCxMode,
|
||||
locale,
|
||||
setIsCautionDialogOpen,
|
||||
}: SurveyMenuBarProps) => {
|
||||
const { t } = useTranslate();
|
||||
const router = useRouter();
|
||||
@@ -63,7 +65,6 @@ export const SurveyMenuBar = ({
|
||||
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [isSurveyPublishing, setIsSurveyPublishing] = useState(false);
|
||||
const [isSurveySaving, setIsSurveySaving] = useState(false);
|
||||
const cautionText = t("environments.surveys.edit.caution_text");
|
||||
|
||||
useEffect(() => {
|
||||
if (audiencePrompt && activeId === "settings") {
|
||||
@@ -303,111 +304,100 @@ export const SurveyMenuBar = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-slate-200 bg-white px-5 py-2.5 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex h-full items-center space-x-2 whitespace-nowrap">
|
||||
{!isCxMode && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-full"
|
||||
onClick={() => {
|
||||
handleBack();
|
||||
}}>
|
||||
<ArrowLeftIcon />
|
||||
{t("common.back")}
|
||||
</Button>
|
||||
)}
|
||||
<p className="hidden pl-4 font-semibold md:block">{project.name} / </p>
|
||||
<Input
|
||||
defaultValue={localSurvey.name}
|
||||
onChange={(e) => {
|
||||
const updatedSurvey = { ...localSurvey, name: e.target.value };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
}}
|
||||
className="h-8 w-72 border-white py-0 hover:border-slate-200"
|
||||
/>
|
||||
</div>
|
||||
{responseCount > 0 && (
|
||||
<div className="flex items-center rounded-lg border border-amber-200 bg-amber-100 p-1.5 text-amber-800 shadow-sm lg:mx-auto">
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<AlertTriangleIcon className="h-5 w-5 text-amber-400" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={"top"} className="lg:hidden">
|
||||
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">{cautionText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<p className="hidden pl-1.5 text-xs text-ellipsis whitespace-nowrap md:text-sm lg:block">
|
||||
{cautionText}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-b border-slate-200 bg-white px-5 py-2.5 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex h-full items-center space-x-2 whitespace-nowrap">
|
||||
{!isCxMode && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-full"
|
||||
onClick={() => {
|
||||
handleBack();
|
||||
}}>
|
||||
<ArrowLeftIcon />
|
||||
{t("common.back")}
|
||||
</Button>
|
||||
)}
|
||||
<div className="mt-3 flex sm:mt-0 sm:ml-4">
|
||||
{!isCxMode && (
|
||||
<Button
|
||||
disabled={disableSave}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="mr-3"
|
||||
loading={isSurveySaving}
|
||||
onClick={() => handleSurveySave()}
|
||||
type="submit">
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{localSurvey.status !== "draft" && (
|
||||
<Button
|
||||
disabled={disableSave}
|
||||
className="mr-3"
|
||||
size="sm"
|
||||
loading={isSurveySaving}
|
||||
onClick={() => handleSaveAndGoBack()}>
|
||||
{t("environments.surveys.edit.save_and_close")}
|
||||
</Button>
|
||||
)}
|
||||
{localSurvey.status === "draft" && audiencePrompt && !isLinkSurvey && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAudiencePrompt(false);
|
||||
setActiveId("settings");
|
||||
}}>
|
||||
{t("environments.surveys.edit.continue_to_settings")}
|
||||
<SettingsIcon />
|
||||
</Button>
|
||||
)}
|
||||
{/* Always display Publish button for link surveys for better CR */}
|
||||
{localSurvey.status === "draft" && (!audiencePrompt || isLinkSurvey) && (
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={isSurveySaving || containsEmptyTriggers}
|
||||
loading={isSurveyPublishing}
|
||||
onClick={handleSurveyPublish}>
|
||||
{isCxMode
|
||||
? t("environments.surveys.edit.save_and_close")
|
||||
: t("environments.surveys.edit.publish")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialog
|
||||
headerText={t("environments.surveys.edit.confirm_survey_changes")}
|
||||
open={isConfirmDialogOpen}
|
||||
setOpen={setConfirmDialogOpen}
|
||||
mainText={t("environments.surveys.edit.unsaved_changes_warning")}
|
||||
confirmBtnLabel={t("common.save")}
|
||||
declineBtnLabel={t("common.discard")}
|
||||
declineBtnVariant="destructive"
|
||||
onDecline={() => {
|
||||
setConfirmDialogOpen(false);
|
||||
router.back();
|
||||
<p className="hidden pl-4 font-semibold md:block">{project.name} / </p>
|
||||
<Input
|
||||
defaultValue={localSurvey.name}
|
||||
onChange={(e) => {
|
||||
const updatedSurvey = { ...localSurvey, name: e.target.value };
|
||||
setLocalSurvey(updatedSurvey);
|
||||
}}
|
||||
onConfirm={() => handleSaveAndGoBack()}
|
||||
className="h-8 w-72 border-white py-0 hover:border-slate-200"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2 sm:mt-0 sm:ml-4">
|
||||
{responseCount > 0 && (
|
||||
<div>
|
||||
<Alert variant="warning" size="small">
|
||||
<AlertTitle>{t("environments.surveys.edit.caution_text")}</AlertTitle>
|
||||
<AlertButton onClick={() => setIsCautionDialogOpen(true)}>{t("common.learn_more")}</AlertButton>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
{!isCxMode && (
|
||||
<Button
|
||||
disabled={disableSave}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
loading={isSurveySaving}
|
||||
onClick={() => handleSurveySave()}
|
||||
type="submit">
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{localSurvey.status !== "draft" && (
|
||||
<Button
|
||||
disabled={disableSave}
|
||||
className="mr-3"
|
||||
size="sm"
|
||||
loading={isSurveySaving}
|
||||
onClick={() => handleSaveAndGoBack()}>
|
||||
{t("environments.surveys.edit.save_and_close")}
|
||||
</Button>
|
||||
)}
|
||||
{localSurvey.status === "draft" && audiencePrompt && !isLinkSurvey && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAudiencePrompt(false);
|
||||
setActiveId("settings");
|
||||
}}>
|
||||
{t("environments.surveys.edit.continue_to_settings")}
|
||||
<SettingsIcon />
|
||||
</Button>
|
||||
)}
|
||||
{/* Always display Publish button for link surveys for better CR */}
|
||||
{localSurvey.status === "draft" && (!audiencePrompt || isLinkSurvey) && (
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={isSurveySaving || containsEmptyTriggers}
|
||||
loading={isSurveyPublishing}
|
||||
onClick={handleSurveyPublish}>
|
||||
{isCxMode
|
||||
? t("environments.surveys.edit.save_and_close")
|
||||
: t("environments.surveys.edit.publish")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialog
|
||||
headerText={t("environments.surveys.edit.confirm_survey_changes")}
|
||||
open={isConfirmDialogOpen}
|
||||
setOpen={setConfirmDialogOpen}
|
||||
mainText={t("environments.surveys.edit.unsaved_changes_warning")}
|
||||
confirmBtnLabel={t("common.save")}
|
||||
declineBtnLabel={t("common.discard")}
|
||||
declineBtnVariant="destructive"
|
||||
onDecline={() => {
|
||||
setConfirmDialogOpen(false);
|
||||
router.back();
|
||||
}}
|
||||
onConfirm={handleSaveAndGoBack}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
||||
import { copySurveyLink } from "@/modules/survey/lib/client-utils";
|
||||
import {
|
||||
copySurveyToOtherEnvironmentAction,
|
||||
@@ -59,6 +60,8 @@ export const SurveyDropDownMenu = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
|
||||
const [isCopyFormOpen, setIsCopyFormOpen] = useState(false);
|
||||
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const surveyLink = useMemo(() => surveyDomain + "/s/" + survey.id, [survey.id, surveyDomain]);
|
||||
@@ -117,6 +120,12 @@ export const SurveyDropDownMenu = ({
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleEditforActiveSurvey = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDropDownOpen(false);
|
||||
setIsCautionDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`${survey.name.toLowerCase().split(" ").join("-")}-survey-actions`}
|
||||
@@ -140,8 +149,9 @@ export const SurveyDropDownMenu = ({
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
className="flex w-full items-center"
|
||||
href={`/environments/${environmentId}/surveys/${survey.id}/edit`}>
|
||||
<SquarePenIcon className="mr-2 h-4 w-4" />
|
||||
href={`/environments/${environmentId}/surveys/${survey.id}/edit`}
|
||||
onClick={survey.responseCount > 0 ? handleEditforActiveSurvey : undefined}>
|
||||
<SquarePenIcon className="mr-2 size-4" />
|
||||
{t("common.edit")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
@@ -238,6 +248,23 @@ export const SurveyDropDownMenu = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{survey.responseCount > 0 && (
|
||||
<EditPublicSurveyAlertDialog
|
||||
open={isCautionDialogOpen}
|
||||
setOpen={setIsCautionDialogOpen}
|
||||
isLoading={loading}
|
||||
primaryButtonAction={async () => {
|
||||
await duplicateSurveyAndRefresh(survey.id);
|
||||
setIsCautionDialogOpen(false);
|
||||
}}
|
||||
primaryButtonText={t("common.duplicate")}
|
||||
secondaryButtonAction={() =>
|
||||
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`)
|
||||
}
|
||||
secondaryButtonText={t("common.edit")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isCopyFormOpen && (
|
||||
<CopySurveyModal open={isCopyFormOpen} setOpen={setIsCopyFormOpen} survey={survey} />
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { TSurvey } from "@/modules/survey/list/types/surveys";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { SurveyDropDownMenu } from "../survey-dropdown-menu";
|
||||
|
||||
// Mock translation
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
// Mock constants
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
@@ -39,13 +44,12 @@ vi.mock("@/modules/survey/lib/client-utils", () => ({
|
||||
copySurveyLink: vi.fn((url: string, suId?: string) => (suId ? `${url}?suId=${suId}` : url)),
|
||||
}));
|
||||
|
||||
const fakeSurvey = {
|
||||
id: "testSurvey",
|
||||
name: "Test Survey",
|
||||
status: "inProgress",
|
||||
type: "link",
|
||||
creator: { name: "Test User" },
|
||||
} as unknown as TSurvey;
|
||||
vi.mock("@/modules/survey/list/actions", () => ({
|
||||
copySurveyToOtherEnvironmentAction: vi.fn(() => Promise.resolve({ data: { id: "duplicatedSurveyId" } })),
|
||||
getSurveyAction: vi.fn(() =>
|
||||
Promise.resolve({ data: { id: "duplicatedSurveyId", name: "Duplicated Survey" } })
|
||||
),
|
||||
}));
|
||||
|
||||
describe("SurveyDropDownMenu", () => {
|
||||
afterEach(() => {
|
||||
@@ -119,4 +123,127 @@ describe("SurveyDropDownMenu", () => {
|
||||
expect(editItem).toBeInTheDocument();
|
||||
expect(deleteItem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const fakeSurvey = {
|
||||
id: "testSurvey",
|
||||
name: "Test Survey",
|
||||
status: "inProgress",
|
||||
type: "link",
|
||||
responseCount: 5,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
test("handleEditforActiveSurvey opens EditPublicSurveyAlertDialog for active surveys", async () => {
|
||||
render(
|
||||
<SurveyDropDownMenu
|
||||
environmentId="env123"
|
||||
survey={fakeSurvey}
|
||||
surveyDomain="http://survey.test"
|
||||
refreshSingleUseId={vi.fn()}
|
||||
duplicateSurvey={vi.fn()}
|
||||
deleteSurvey={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
|
||||
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
|
||||
expect(triggerElement).toBeInTheDocument();
|
||||
await userEvent.click(triggerElement);
|
||||
|
||||
const editButton = screen.getByText("common.edit");
|
||||
await userEvent.click(editButton);
|
||||
|
||||
expect(screen.getByText("environments.surveys.edit.caution_edit_published_survey")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handleEditforActiveSurvey does not open caution dialog for surveys with 0 response count", async () => {
|
||||
render(
|
||||
<SurveyDropDownMenu
|
||||
environmentId="env123"
|
||||
survey={{ ...fakeSurvey, responseCount: 0 }}
|
||||
surveyDomain="http://survey.test"
|
||||
refreshSingleUseId={vi.fn()}
|
||||
duplicateSurvey={vi.fn()}
|
||||
deleteSurvey={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
|
||||
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
|
||||
expect(triggerElement).toBeInTheDocument();
|
||||
await userEvent.click(triggerElement);
|
||||
|
||||
const editButton = screen.getByText("common.edit");
|
||||
await userEvent.click(editButton);
|
||||
|
||||
expect(
|
||||
screen.queryByText("environments.surveys.edit.caution_edit_published_survey")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("<DropdownMenuItem> renders and triggers actions correctly", async () => {
|
||||
const mockDuplicateSurvey = vi.fn();
|
||||
render(
|
||||
<SurveyDropDownMenu
|
||||
environmentId="env123"
|
||||
survey={fakeSurvey}
|
||||
surveyDomain="http://survey.test"
|
||||
refreshSingleUseId={vi.fn()}
|
||||
duplicateSurvey={mockDuplicateSurvey}
|
||||
deleteSurvey={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
|
||||
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
|
||||
expect(triggerElement).toBeInTheDocument();
|
||||
await userEvent.click(triggerElement);
|
||||
|
||||
const duplicateButton = screen.getByText("common.duplicate");
|
||||
expect(duplicateButton).toBeInTheDocument();
|
||||
await userEvent.click(duplicateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDuplicateSurvey).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test("<EditPublicSurveyAlertDialog> displays and handles actions correctly", async () => {
|
||||
const mockDuplicateSurvey = vi.fn();
|
||||
render(
|
||||
<SurveyDropDownMenu
|
||||
environmentId="env123"
|
||||
survey={{ ...fakeSurvey, responseCount: 5 }}
|
||||
surveyDomain="http://survey.test"
|
||||
refreshSingleUseId={vi.fn()}
|
||||
duplicateSurvey={mockDuplicateSurvey}
|
||||
deleteSurvey={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const menuWrapper = screen.getByTestId("survey-dropdown-menu");
|
||||
const triggerElement = menuWrapper.querySelector("[class*='p-2']") as HTMLElement;
|
||||
expect(triggerElement).toBeInTheDocument();
|
||||
await userEvent.click(triggerElement);
|
||||
|
||||
const editButton = screen.getByText("common.edit");
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
|
||||
// Test that the dialog is shown
|
||||
const dialogTitle = screen.getByText("environments.surveys.edit.caution_edit_published_survey");
|
||||
expect(dialogTitle).toBeInTheDocument();
|
||||
|
||||
// Test that the dialog buttons work
|
||||
const editButtonInDialog = screen.getByRole("button", { name: "common.edit" });
|
||||
expect(editButtonInDialog).toBeInTheDocument();
|
||||
await userEvent.click(editButtonInDialog);
|
||||
|
||||
const duplicateButton = screen.getByRole("button", { name: "common.duplicate" });
|
||||
expect(duplicateButton).toBeInTheDocument();
|
||||
await userEvent.click(duplicateButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDuplicateSurvey).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,8 +11,8 @@ interface AlertDialogProps {
|
||||
mainText: string;
|
||||
confirmBtnLabel: string;
|
||||
declineBtnLabel?: string;
|
||||
declineBtnVariant?: "destructive" | "ghost";
|
||||
onDecline: () => void;
|
||||
declineBtnVariant?: "destructive" | "ghost" | "outline";
|
||||
onDecline?: () => void;
|
||||
onConfirm?: () => void;
|
||||
}
|
||||
|
||||
@@ -34,9 +34,11 @@ export const AlertDialog = ({
|
||||
{mainText ?? t("common.are_you_sure_this_action_cannot_be_undone")}
|
||||
</p>
|
||||
<div className="space-x-2 text-right">
|
||||
<Button variant={declineBtnVariant} onClick={onDecline}>
|
||||
{declineBtnLabel || "Discard"}
|
||||
</Button>
|
||||
{declineBtnLabel && onDecline && (
|
||||
<Button variant={declineBtnVariant} onClick={onDecline}>
|
||||
{declineBtnLabel}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (onConfirm) {
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { TolgeeNextProvider } from "@/tolgee/client";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { AlertDialog } from "./index";
|
||||
|
||||
const meta: Meta<typeof AlertDialog> = {
|
||||
title: "UI/AlertDialog",
|
||||
component: AlertDialog,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
open: {
|
||||
control: "boolean",
|
||||
description: "Controls the open state of the dialog",
|
||||
},
|
||||
setOpen: {
|
||||
description: "Function to set the open state",
|
||||
},
|
||||
headerText: {
|
||||
control: "text",
|
||||
description: "Heading text for the dialog",
|
||||
},
|
||||
mainText: {
|
||||
control: "text",
|
||||
description: "Main content text for the dialog",
|
||||
},
|
||||
confirmBtnLabel: {
|
||||
control: "text",
|
||||
description: "Label for the confirmation button",
|
||||
},
|
||||
declineBtnLabel: {
|
||||
control: "text",
|
||||
description: "Optional label for the decline button",
|
||||
},
|
||||
declineBtnVariant: {
|
||||
control: "select",
|
||||
options: ["destructive", "ghost"],
|
||||
description: "Style variant for the decline button",
|
||||
},
|
||||
onConfirm: {
|
||||
description: "Function called when confirm button is clicked",
|
||||
},
|
||||
onDecline: {
|
||||
description: "Function called when decline button is clicked",
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<TolgeeNextProvider language="en" staticData={{}}>
|
||||
<Story />
|
||||
</TolgeeNextProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof AlertDialog>;
|
||||
|
||||
// Basic example
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
open: true,
|
||||
setOpen: () => {},
|
||||
headerText: "Confirm Action",
|
||||
mainText: "Are you sure you want to proceed with this action?",
|
||||
confirmBtnLabel: "Confirm",
|
||||
declineBtnLabel: "Cancel",
|
||||
onDecline: () => console.log("Declined"),
|
||||
onConfirm: () => console.log("Confirmed"),
|
||||
},
|
||||
};
|
||||
|
||||
// Example with destructive action
|
||||
export const Destructive: Story = {
|
||||
args: {
|
||||
open: true,
|
||||
setOpen: () => {},
|
||||
headerText: "Delete Item",
|
||||
mainText: "This action cannot be undone. Are you sure you want to delete this item?",
|
||||
confirmBtnLabel: "Delete",
|
||||
declineBtnLabel: "Cancel",
|
||||
declineBtnVariant: "ghost",
|
||||
onDecline: () => console.log("Declined"),
|
||||
onConfirm: () => console.log("Confirmed delete"),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Used for destructive actions that require user confirmation.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Example with warning
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
open: true,
|
||||
setOpen: () => {},
|
||||
headerText: "Warning",
|
||||
mainText: "You are about to make changes that will affect multiple records.",
|
||||
confirmBtnLabel: "Proceed",
|
||||
declineBtnLabel: "Go Back",
|
||||
onDecline: () => console.log("Declined"),
|
||||
onConfirm: () => console.log("Confirmed proceed"),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Used for warning users about consequential actions.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Example with success confirmation
|
||||
export const SuccessConfirmation: Story = {
|
||||
args: {
|
||||
open: true,
|
||||
setOpen: () => {},
|
||||
headerText: "Success",
|
||||
mainText: "Your changes have been saved successfully. Would you like to continue editing?",
|
||||
confirmBtnLabel: "Continue Editing",
|
||||
declineBtnLabel: "Close",
|
||||
onDecline: () => console.log("Closed"),
|
||||
onConfirm: () => console.log("Continue editing"),
|
||||
},
|
||||
};
|
||||
|
||||
// Example with destructive decline button
|
||||
export const DestructiveDecline: Story = {
|
||||
args: {
|
||||
open: true,
|
||||
setOpen: () => {},
|
||||
headerText: "Discard Changes",
|
||||
mainText: "You have unsaved changes. Are you sure you want to discard them?",
|
||||
confirmBtnLabel: "Keep Editing",
|
||||
declineBtnLabel: "Discard Changes",
|
||||
declineBtnVariant: "destructive",
|
||||
onDecline: () => console.log("Discarded changes"),
|
||||
onConfirm: () => console.log("Keep editing"),
|
||||
},
|
||||
};
|
||||
@@ -74,6 +74,7 @@ export default defineConfig({
|
||||
"apps/web/app/(app)/environments/**/integrations/notion/components/ManageIntegration.tsx",
|
||||
"app/(app)/environments/**/integrations/slack/components/ManageIntegration.tsx",
|
||||
"app/(app)/environments/**/surveys/**/(analysis)/responses/components/ResponseTableCell.tsx",
|
||||
"app/(app)/environments/**/surveys/**/(analysis)/summary/components/SurveyAnalysisCTA.tsx",
|
||||
"modules/ee/sso/lib/**/*.ts",
|
||||
"app/lib/**/*.ts",
|
||||
"app/api/(internal)/insights/lib/**/*.ts",
|
||||
@@ -88,6 +89,7 @@ export default defineConfig({
|
||||
"modules/survey/components/question-form-input/index.tsx",
|
||||
"modules/survey/components/template-list/components/template-tags.tsx",
|
||||
"modules/survey/lib/client-utils.ts",
|
||||
"modules/survey/components/edit-public-survey-alert-dialog/index.tsx",
|
||||
"modules/survey/list/components/survey-card.tsx",
|
||||
"modules/survey/list/components/survey-dropdown-menu.tsx",
|
||||
"modules/survey/follow-ups/components/follow-up-item.tsx",
|
||||
|
||||
@@ -1332,6 +1332,14 @@
|
||||
"card_shadow_color": "Farbton des Kartenschattens",
|
||||
"card_styling": "Kartenstil",
|
||||
"casual": "Lässig",
|
||||
"caution_edit_duplicate": "Duplizieren & bearbeiten",
|
||||
"caution_edit_published_survey": "Eine veröffentlichte Umfrage bearbeiten?",
|
||||
"caution_explanation_all_data_as_download": "Alle Daten, einschließlich früherer Antworten, stehen als Download zur Verfügung.",
|
||||
"caution_explanation_intro": "Wir verstehen, dass du vielleicht noch Änderungen vornehmen möchtest. Hier erfährst du, was passiert, wenn du das tust:",
|
||||
"caution_explanation_new_responses_separated": "Neue Antworten werden separat gesammelt.",
|
||||
"caution_explanation_only_new_responses_in_summary": "Nur neue Antworten erscheinen in der Umfragezusammenfassung.",
|
||||
"caution_explanation_responses_are_safe": "Vorhandene Antworten bleiben sicher.",
|
||||
"caution_recommendation": "Das Bearbeiten deiner Umfrage kann zu Dateninkonsistenzen in der Umfragezusammenfassung führen. Wir empfehlen stattdessen, die Umfrage zu duplizieren.",
|
||||
"caution_text": "Änderungen werden zu Inkonsistenzen führen",
|
||||
"centered_modal_overlay_color": "Zentrierte modale Überlagerungsfarbe",
|
||||
"change_anyway": "Trotzdem ändern",
|
||||
@@ -1791,6 +1799,10 @@
|
||||
"quickstart_mobile_apps_description": "Um mit Umfragen in mobilen Apps zu beginnen, folge bitte der Schnellstartanleitung:",
|
||||
"quickstart_web_apps": "Schnellstart: Web-Apps",
|
||||
"quickstart_web_apps_description": "Bitte folge der Schnellstartanleitung, um loszulegen:",
|
||||
"response_inconsistencies_available_as_download": "Deine Daten sind sicher — frühere Antworten stehen zum Download bereit",
|
||||
"response_inconsistencies_edited_while_public": "Bearbeitet, während öffentlich.",
|
||||
"response_inconsistencies_explanation": "Die Online-Zusammenfassung zeigt Antworten, die nach der letzten Bearbeitung der Umfrage eingereicht wurden. Dies verhindert Dateninkonsistenzen in deiner Zusammenfassung. Wir empfehlen, veröffentlichte Umfragen zu duplizieren, anstatt sie zu bearbeiten.",
|
||||
"response_inconsistencies_missing_responses_dont_worry": "Fehlende Antworten? Keine Sorge!",
|
||||
"results_are_public": "Ergebnisse sind öffentlich",
|
||||
"send_preview": "Vorschau senden",
|
||||
"send_to_panel": "An das Panel senden",
|
||||
|
||||
@@ -1332,6 +1332,14 @@
|
||||
"card_shadow_color": "Card shadow color",
|
||||
"card_styling": "Card Styling",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicate & edit",
|
||||
"caution_edit_published_survey": "Edit a published survey?",
|
||||
"caution_explanation_all_data_as_download": "All data, including past responses are available as download.",
|
||||
"caution_explanation_intro": "We understand you might still want to make changes. Here’s what happens if you do: ",
|
||||
"caution_explanation_new_responses_separated": "New responses are collected separately.",
|
||||
"caution_explanation_only_new_responses_in_summary": "Only new responses appear in the survey summary.",
|
||||
"caution_explanation_responses_are_safe": "Existing responses remain safe.",
|
||||
"caution_recommendation": "Editing your survey may cause data inconsistencies in the survey summary. We recommend duplicating the survey instead.",
|
||||
"caution_text": "Changes will lead to inconsistencies",
|
||||
"centered_modal_overlay_color": "Centered modal overlay color",
|
||||
"change_anyway": "Change anyway",
|
||||
@@ -1791,6 +1799,10 @@
|
||||
"quickstart_mobile_apps_description": "To get started with surveys in mobile apps, please follow the Quickstart guide:",
|
||||
"quickstart_web_apps": "Quickstart: Web apps",
|
||||
"quickstart_web_apps_description": "Please follow the Quickstart guide to get started:",
|
||||
"response_inconsistencies_available_as_download": "Your data is safe — earlier responses are available for download. ",
|
||||
"response_inconsistencies_edited_while_public": "Edited while public.",
|
||||
"response_inconsistencies_explanation": "The online summary shows responses, that were submitted after the latest edit of the survey. This prevents data inconsistencies in your summary. We recommend duplicating published surveys, instead of editing them.",
|
||||
"response_inconsistencies_missing_responses_dont_worry": "Missing responses? Don’t worry!",
|
||||
"results_are_public": "Results are public",
|
||||
"send_preview": "Send preview",
|
||||
"send_to_panel": "Send to panel",
|
||||
|
||||
@@ -1332,6 +1332,14 @@
|
||||
"card_shadow_color": "Couleur de l'ombre de la carte",
|
||||
"card_styling": "Style de carte",
|
||||
"casual": "Décontracté",
|
||||
"caution_edit_duplicate": "Dupliquer et modifier",
|
||||
"caution_edit_published_survey": "Modifier un sondage publié ?",
|
||||
"caution_explanation_all_data_as_download": "Toutes les données, y compris les réponses passées, sont disponibles en téléchargement.",
|
||||
"caution_explanation_intro": "Nous comprenons que vous souhaitiez encore apporter des modifications. Voici ce qui se passe si vous le faites : ",
|
||||
"caution_explanation_new_responses_separated": "Les nouvelles réponses sont collectées séparément.",
|
||||
"caution_explanation_only_new_responses_in_summary": "Seules les nouvelles réponses apparaissent dans le résumé de l'enquête.",
|
||||
"caution_explanation_responses_are_safe": "Les réponses existantes restent en sécurité.",
|
||||
"caution_recommendation": "Modifier votre enquête peut entraîner des incohérences dans le résumé de l'enquête. Nous vous recommandons de dupliquer l'enquête à la place.",
|
||||
"caution_text": "Les changements entraîneront des incohérences.",
|
||||
"centered_modal_overlay_color": "Couleur de superposition modale centrée",
|
||||
"change_anyway": "Changer de toute façon",
|
||||
@@ -1791,6 +1799,10 @@
|
||||
"quickstart_mobile_apps_description": "Pour commencer avec les enquêtes dans les applications mobiles, veuillez suivre le guide de démarrage rapide :",
|
||||
"quickstart_web_apps": "Démarrage rapide : Applications web",
|
||||
"quickstart_web_apps_description": "Veuillez suivre le guide de démarrage rapide pour commencer :",
|
||||
"response_inconsistencies_available_as_download": "Vos données sont en sécurité — les réponses précédentes sont disponibles en téléchargement",
|
||||
"response_inconsistencies_edited_while_public": "Modifié en public.",
|
||||
"response_inconsistencies_explanation": "Le résumé en ligne affiche les réponses soumises après la dernière modification du sondage. Cela évite les incohérences de données dans votre résumé. Nous vous recommandons de dupliquer les sondages publiés, au lieu de les modifier.",
|
||||
"response_inconsistencies_missing_responses_dont_worry": "Réponses manquantes ? Ne vous inquiétez pas !",
|
||||
"results_are_public": "Les résultats sont publics.",
|
||||
"send_preview": "Envoyer un aperçu",
|
||||
"send_to_panel": "Envoyer au panneau",
|
||||
|
||||
@@ -1332,6 +1332,14 @@
|
||||
"card_shadow_color": "cor da sombra do cartão",
|
||||
"card_styling": "Estilização de Cartão",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicar e editar",
|
||||
"caution_edit_published_survey": "Editar uma pesquisa publicada?",
|
||||
"caution_explanation_all_data_as_download": "Todos os dados, incluindo respostas anteriores, estão disponíveis para download.",
|
||||
"caution_explanation_intro": "Entendemos que você ainda pode querer fazer alterações. Aqui está o que acontece se você fizer:",
|
||||
"caution_explanation_new_responses_separated": "Novas respostas são coletadas separadamente.",
|
||||
"caution_explanation_only_new_responses_in_summary": "Apenas novas respostas aparecem no resumo da pesquisa.",
|
||||
"caution_explanation_responses_are_safe": "As respostas existentes permanecem seguras.",
|
||||
"caution_recommendation": "Editar sua pesquisa pode causar inconsistências de dados no resumo da pesquisa. Recomendamos duplicar a pesquisa em vez disso.",
|
||||
"caution_text": "Mudanças vão levar a inconsistências",
|
||||
"centered_modal_overlay_color": "cor de sobreposição modal centralizada",
|
||||
"change_anyway": "Mudar mesmo assim",
|
||||
@@ -1791,6 +1799,10 @@
|
||||
"quickstart_mobile_apps_description": "Para começar com pesquisas em aplicativos móveis, por favor, siga o guia de início rápido:",
|
||||
"quickstart_web_apps": "Início rápido: Aplicativos web",
|
||||
"quickstart_web_apps_description": "Por favor, siga o guia de início rápido para começar:",
|
||||
"response_inconsistencies_available_as_download": "Seus dados estão seguros — respostas anteriores estão disponíveis para download",
|
||||
"response_inconsistencies_edited_while_public": "Editado enquanto público.",
|
||||
"response_inconsistencies_explanation": "O resumo online mostra respostas que foram enviadas após a última edição da pesquisa. Isso evita inconsistências de dados no seu resumo. Recomendamos duplicar pesquisas publicadas, em vez de editá-las.",
|
||||
"response_inconsistencies_missing_responses_dont_worry": "Respostas faltando? Não se preocupe!",
|
||||
"results_are_public": "Os resultados são públicos",
|
||||
"send_preview": "Enviar prévia",
|
||||
"send_to_panel": "Enviar para o painel",
|
||||
|
||||
@@ -1332,6 +1332,14 @@
|
||||
"card_shadow_color": "Cor da sombra do cartão",
|
||||
"card_styling": "Estilo do cartão",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicar e editar",
|
||||
"caution_edit_published_survey": "Editar um inquérito publicado?",
|
||||
"caution_explanation_all_data_as_download": "Todos os dados, incluindo respostas anteriores, estão disponíveis para download.",
|
||||
"caution_explanation_intro": "Entendemos que ainda pode querer fazer alterações. Eis o que acontece se o fizer:",
|
||||
"caution_explanation_new_responses_separated": "As novas respostas são recolhidas separadamente.",
|
||||
"caution_explanation_only_new_responses_in_summary": "Apenas novas respostas aparecem no resumo do inquérito.",
|
||||
"caution_explanation_responses_are_safe": "As respostas existentes permanecem seguras.",
|
||||
"caution_recommendation": "Editar o seu inquérito pode causar inconsistências de dados no resumo do inquérito. Recomendamos duplicar o inquérito em vez disso.",
|
||||
"caution_text": "As alterações levarão a inconsistências",
|
||||
"centered_modal_overlay_color": "Cor da sobreposição modal centralizada",
|
||||
"change_anyway": "Alterar mesmo assim",
|
||||
@@ -1791,6 +1799,10 @@
|
||||
"quickstart_mobile_apps_description": "Para começar com inquéritos em aplicações móveis, por favor, siga o guia de início rápido:",
|
||||
"quickstart_web_apps": "Início rápido: Aplicações web",
|
||||
"quickstart_web_apps_description": "Por favor, siga o guia de início rápido para começar:",
|
||||
"response_inconsistencies_available_as_download": "Os seus dados estão seguros — as respostas anteriores estão disponíveis para download",
|
||||
"response_inconsistencies_edited_while_public": "Editado enquanto público.",
|
||||
"response_inconsistencies_explanation": "O resumo online mostra respostas, que foram submetidas após a última edição do inquérito. Isto previne inconsistências de dados no seu resumo. Recomendamos duplicar inquéritos publicados, em vez de os editar.",
|
||||
"response_inconsistencies_missing_responses_dont_worry": "Respostas em falta? Não se preocupe!",
|
||||
"results_are_public": "Os resultados são públicos",
|
||||
"send_preview": "Enviar pré-visualização",
|
||||
"send_to_panel": "Enviar para painel",
|
||||
|
||||
@@ -1332,6 +1332,14 @@
|
||||
"card_shadow_color": "卡片陰影顏色",
|
||||
"card_styling": "卡片樣式設定",
|
||||
"casual": "隨意",
|
||||
"caution_edit_duplicate": "複製 & 編輯",
|
||||
"caution_edit_published_survey": "編輯已發佈的調查?",
|
||||
"caution_explanation_all_data_as_download": "所有數據,包括過去的回應,都可以下載。",
|
||||
"caution_explanation_intro": "我們了解您可能仍然想要進行更改。如果您這樣做,將會發生以下情況:",
|
||||
"caution_explanation_new_responses_separated": "新回應會分開收集。",
|
||||
"caution_explanation_only_new_responses_in_summary": "只有新的回應會出現在調查摘要中。",
|
||||
"caution_explanation_responses_are_safe": "現有回應仍然安全。",
|
||||
"caution_recommendation": "編輯您的調查可能會導致調查摘要中的數據不一致。我們建議複製調查。",
|
||||
"caution_text": "變更會導致不一致",
|
||||
"centered_modal_overlay_color": "置中彈窗覆蓋顏色",
|
||||
"change_anyway": "仍然變更",
|
||||
@@ -1791,6 +1799,10 @@
|
||||
"quickstart_mobile_apps_description": "要開始使用行動應用程式中的調查,請按照 Quickstart 指南:",
|
||||
"quickstart_web_apps": "快速入門:Web apps",
|
||||
"quickstart_web_apps_description": "請按照 Quickstart 指南開始:",
|
||||
"response_inconsistencies_available_as_download": "您的資料是安全的 — 先前的回應可供下載。",
|
||||
"response_inconsistencies_edited_while_public": "公開時已編輯。",
|
||||
"response_inconsistencies_explanation": "線上摘要顯示在調查最新編輯後提交的回應。這可以防止摘要中的數據不一致。我們建議複製已發布的調查,而不是編輯它們。",
|
||||
"response_inconsistencies_missing_responses_dont_worry": "缺少回應?別擔心!",
|
||||
"results_are_public": "結果是公開的",
|
||||
"send_preview": "發送預覽",
|
||||
"send_to_panel": "發送到小組",
|
||||
|
||||
@@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Coverage
|
||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/stories.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts
|
||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/*.stories.*,**/stories.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts
|
||||
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/types/**,**/stories.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts
|
||||
|
||||
Reference in New Issue
Block a user