mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-01 11:50:43 -05:00
feat: hit ENTER for new option (#6624)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { debounce } from "lodash";
|
||||
import { ImagePlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyEndScreenCard,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionChoice,
|
||||
TSurveyRedirectUrlCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { useSyncScroll } from "@/lib/utils/hooks/useSyncScroll";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
@@ -10,20 +24,6 @@ import { FileInput } from "@/modules/ui/components/file-input";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { debounce } from "lodash";
|
||||
import { ImagePlusIcon, TrashIcon } from "lucide-react";
|
||||
import { RefObject, useCallback, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyEndScreenCard,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionChoice,
|
||||
TSurveyRedirectUrlCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import {
|
||||
determineImageUploaderVisibility,
|
||||
getChoiceLabel,
|
||||
@@ -50,7 +50,6 @@ interface QuestionFormInputProps {
|
||||
label: string;
|
||||
maxLength?: number;
|
||||
placeholder?: string;
|
||||
ref?: RefObject<HTMLInputElement | null>;
|
||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||
className?: string;
|
||||
locale: TUserLocale;
|
||||
|
||||
@@ -1,335 +1,270 @@
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TLanguage } from "@formbricks/types/project";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLanguage,
|
||||
TSurveyMatrixQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { MatrixQuestionForm } from "./matrix-question-form";
|
||||
|
||||
// Mock cuid2 to track CUID generation
|
||||
let cuidIndex = 0;
|
||||
|
||||
vi.mock("@paralleldrive/cuid2", () => ({
|
||||
default: {
|
||||
createId: vi.fn(() => `cuid${cuidIndex++}`),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock window.matchMedia - required for useAutoAnimate
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock @formkit/auto-animate - simplify implementation
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
vi.mock("@dnd-kit/core", () => ({
|
||||
DndContext: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}));
|
||||
|
||||
// Mock findOptionUsedInLogic
|
||||
vi.mock("@/modules/survey/editor/lib/utils", () => ({
|
||||
findOptionUsedInLogic: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock constants
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
ENCRYPTION_KEY: "test",
|
||||
ENTERPRISE_LICENSE_KEY: "test",
|
||||
GITHUB_ID: "test",
|
||||
GITHUB_SECRET: "test",
|
||||
GOOGLE_CLIENT_ID: "test",
|
||||
GOOGLE_CLIENT_SECRET: "test",
|
||||
AZUREAD_CLIENT_ID: "mock-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
|
||||
AZUREAD_TENANT_ID: "mock-azuread-tenant-id",
|
||||
OIDC_CLIENT_ID: "mock-oidc-client-id",
|
||||
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
|
||||
OIDC_ISSUER: "mock-oidc-issuer",
|
||||
OIDC_DISPLAY_NAME: "mock-oidc-display-name",
|
||||
OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm",
|
||||
WEBAPP_URL: "mock-webapp-url",
|
||||
AI_AZURE_LLM_RESSOURCE_NAME: "mock-azure-llm-resource-name",
|
||||
AI_AZURE_LLM_API_KEY: "mock-azure-llm-api-key",
|
||||
AI_AZURE_LLM_DEPLOYMENT_ID: "mock-azure-llm-deployment-id",
|
||||
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-azure-embeddings-resource-name",
|
||||
AI_AZURE_EMBEDDINGS_API_KEY: "mock-azure-embeddings-api-key",
|
||||
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id",
|
||||
IS_PRODUCTION: true,
|
||||
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
||||
SMTP_HOST: "mock-smtp-host",
|
||||
SMTP_PORT: "mock-smtp-port",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
}));
|
||||
|
||||
// Mock tolgee
|
||||
vi.mock("@tolgee/react", () => ({
|
||||
useTranslate: () => ({
|
||||
t: (key: string) => key,
|
||||
vi.mock("@dnd-kit/sortable", () => ({
|
||||
SortableContext: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
useSortable: () => ({
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
setNodeRef: () => {},
|
||||
transform: null,
|
||||
transition: null,
|
||||
}),
|
||||
verticalListSortingStrategy: () => {},
|
||||
}));
|
||||
|
||||
// Mock QuestionFormInput component
|
||||
// Keep QuestionFormInput simple and forward keydown
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: vi.fn(({ id, updateMatrixLabel, value, updateQuestion, onKeyDown }) => (
|
||||
<div data-testid={`question-input-${id}`}>
|
||||
<input
|
||||
data-testid={`input-${id}`}
|
||||
onChange={(e) => {
|
||||
if (updateMatrixLabel) {
|
||||
const type = id.startsWith("row") ? "row" : "column";
|
||||
const index = parseInt(id.split("-")[1]);
|
||||
updateMatrixLabel(index, type, { default: e.target.value });
|
||||
} else if (updateQuestion) {
|
||||
updateQuestion(0, { [id]: { default: e.target.value } });
|
||||
}
|
||||
}}
|
||||
value={value?.default || ""}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
)),
|
||||
QuestionFormInput: ({ id, value, onKeyDown }: { id: string; value: any; onKeyDown?: any }) => (
|
||||
<input
|
||||
data-testid={`qfi-${id}`}
|
||||
value={value?.en || value?.de || value?.default || ""}
|
||||
onChange={() => {}}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock ShuffleOptionSelect component
|
||||
vi.mock("@/modules/ui/components/shuffle-option-select", () => ({
|
||||
ShuffleOptionSelect: vi.fn(() => <div data-testid="shuffle-option-select" />),
|
||||
}));
|
||||
describe("MatrixQuestionForm - handleKeyDown", () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock TooltipRenderer component
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
TooltipRenderer: vi.fn(({ children }) => (
|
||||
<div data-testid="tooltip-renderer">
|
||||
{children}
|
||||
<button>Delete</button>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock validation
|
||||
vi.mock("../lib/validation", () => ({
|
||||
isLabelValidForAllLanguages: vi.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
// Mock survey languages
|
||||
const mockSurveyLanguages: TSurveyLanguage[] = [
|
||||
{
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: {
|
||||
id: "en",
|
||||
code: "en",
|
||||
alias: "English",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-1",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Mock matrix question
|
||||
const mockMatrixQuestion: TSurveyMatrixQuestion = {
|
||||
id: "matrix-1",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: createI18nString("Matrix Question", ["en"]),
|
||||
subheader: createI18nString("Please rate the following", ["en"]),
|
||||
required: false,
|
||||
logic: [],
|
||||
rows: [
|
||||
{ id: "row-1", label: createI18nString("Row 1", ["en"]) },
|
||||
{ id: "row-2", label: createI18nString("Row 2", ["en"]) },
|
||||
{ id: "row-3", label: createI18nString("Row 3", ["en"]) },
|
||||
],
|
||||
columns: [
|
||||
{ id: "col-1", label: createI18nString("Column 1", ["en"]) },
|
||||
{ id: "col-2", label: createI18nString("Column 2", ["en"]) },
|
||||
{ id: "col-3", label: createI18nString("Column 3", ["en"]) },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
};
|
||||
|
||||
// Mock survey
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "survey-1",
|
||||
name: "Test Survey",
|
||||
questions: [mockMatrixQuestion],
|
||||
languages: mockSurveyLanguages,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const mockUpdateQuestion = vi.fn();
|
||||
|
||||
const defaultProps = {
|
||||
localSurvey: mockSurvey,
|
||||
question: mockMatrixQuestion,
|
||||
questionIdx: 0,
|
||||
updateQuestion: mockUpdateQuestion,
|
||||
selectedLanguageCode: "en",
|
||||
setSelectedLanguageCode: vi.fn(),
|
||||
isInvalid: false,
|
||||
locale: "en-US" as TUserLocale,
|
||||
isStorageConfigured: true,
|
||||
};
|
||||
|
||||
describe("MatrixQuestionForm", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
cuidIndex = 0;
|
||||
});
|
||||
|
||||
test("renders the matrix question form with rows and columns", () => {
|
||||
render(<MatrixQuestionForm {...defaultProps} isStorageConfigured={true} />);
|
||||
const makeSurvey = (languages: Array<Pick<TSurveyLanguage, "language" | "default">>): TSurvey =>
|
||||
({
|
||||
id: "s1",
|
||||
name: "Survey",
|
||||
type: "link",
|
||||
languages: languages as unknown as TSurveyLanguage[],
|
||||
questions: [] as any,
|
||||
endings: [] as any,
|
||||
createdAt: new Date("2024-01-01T00:00:00.000Z"),
|
||||
environmentId: "env1",
|
||||
}) as unknown as TSurvey;
|
||||
|
||||
expect(screen.getByTestId("question-input-headline")).toBeInTheDocument();
|
||||
const langDefault: TSurveyLanguage = {
|
||||
language: { code: "default" } as unknown as TLanguage,
|
||||
default: true,
|
||||
} as unknown as TSurveyLanguage;
|
||||
|
||||
// Check for rows and columns
|
||||
expect(screen.getByTestId("question-input-row-0")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("question-input-row-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("question-input-column-0")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("question-input-column-1")).toBeInTheDocument();
|
||||
|
||||
// Check for shuffle options
|
||||
expect(screen.getByTestId("shuffle-option-select")).toBeInTheDocument();
|
||||
const baseQuestion = (): TSurveyMatrixQuestion => ({
|
||||
id: "q1",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: { default: "Matrix" },
|
||||
required: false,
|
||||
rows: [
|
||||
{ id: "r1", label: { default: "Row 1" } },
|
||||
{ id: "r2", label: { default: "" } },
|
||||
],
|
||||
columns: [
|
||||
{ id: "c1", label: { default: "Col 1" } },
|
||||
{ id: "c2", label: { default: "" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
});
|
||||
|
||||
test("adds description when button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const propsWithoutSubheader = {
|
||||
...defaultProps,
|
||||
question: {
|
||||
...mockMatrixQuestion,
|
||||
subheader: undefined,
|
||||
},
|
||||
};
|
||||
test("Enter on last row adds a new row", async () => {
|
||||
const question = baseQuestion();
|
||||
const localSurvey = makeSurvey([langDefault]);
|
||||
(localSurvey as any).questions = [question];
|
||||
|
||||
const { getByText } = render(
|
||||
<MatrixQuestionForm {...propsWithoutSubheader} isStorageConfigured={true} />
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
render(
|
||||
<MatrixQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestion}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const addDescriptionButton = getByText("environments.surveys.edit.add_description");
|
||||
await user.click(addDescriptionButton);
|
||||
const lastRowInput = screen.getByTestId("qfi-row-1");
|
||||
await userEvent.type(lastRowInput, "{enter}");
|
||||
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, {
|
||||
subheader: expect.any(Object),
|
||||
});
|
||||
expect(updateQuestion).toHaveBeenCalledTimes(1);
|
||||
const [, payload] = updateQuestion.mock.calls[0];
|
||||
expect(payload.rows.length).toBe(3);
|
||||
expect(payload.rows[2]).toEqual(
|
||||
expect.objectContaining({ id: expect.any(String), label: expect.objectContaining({ default: "" }) })
|
||||
);
|
||||
});
|
||||
|
||||
test("renders subheader input when subheader is defined", () => {
|
||||
render(<MatrixQuestionForm {...defaultProps} />);
|
||||
test("Enter on non-last row focuses next row", async () => {
|
||||
const question = baseQuestion();
|
||||
const localSurvey = makeSurvey([langDefault]);
|
||||
(localSurvey as any).questions = [question];
|
||||
|
||||
expect(screen.getByTestId("question-input-subheader")).toBeInTheDocument();
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
render(
|
||||
<MatrixQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestion}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const firstRowInput = screen.getByTestId("qfi-row-0");
|
||||
await userEvent.type(firstRowInput, "{enter}");
|
||||
|
||||
expect(updateQuestion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("deletes a row when delete button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { findAllByTestId } = render(<MatrixQuestionForm {...defaultProps} />);
|
||||
vi.mocked(findOptionUsedInLogic).mockReturnValueOnce(-1);
|
||||
test("Enter on last column adds a new column", async () => {
|
||||
const question = baseQuestion();
|
||||
const localSurvey = makeSurvey([langDefault]);
|
||||
(localSurvey as any).questions = [question];
|
||||
|
||||
const deleteButtons = await findAllByTestId("tooltip-renderer");
|
||||
// First delete button is for the first column
|
||||
await user.click(deleteButtons[0].querySelector("button") as HTMLButtonElement);
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
expect(mockUpdateQuestion).toHaveBeenCalledWith(0, {
|
||||
rows: [mockMatrixQuestion.rows[1], mockMatrixQuestion.rows[2]],
|
||||
});
|
||||
render(
|
||||
<MatrixQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestion}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const lastColInput = screen.getByTestId("qfi-column-1");
|
||||
await userEvent.type(lastColInput, "{enter}");
|
||||
|
||||
expect(updateQuestion).toHaveBeenCalledTimes(1);
|
||||
const [, payload] = updateQuestion.mock.calls[0];
|
||||
expect(payload.columns.length).toBe(3);
|
||||
expect(payload.columns[2]).toEqual(
|
||||
expect.objectContaining({ id: expect.any(String), label: expect.objectContaining({ default: "" }) })
|
||||
);
|
||||
});
|
||||
|
||||
test("doesn't delete a row if it would result in less than 2 rows", async () => {
|
||||
const user = userEvent.setup();
|
||||
const propsWithMinRows = {
|
||||
...defaultProps,
|
||||
question: {
|
||||
...mockMatrixQuestion,
|
||||
rows: [
|
||||
{ id: "row-1", label: createI18nString("Row 1", ["en"]) },
|
||||
{ id: "row-2", label: createI18nString("Row 2", ["en"]) },
|
||||
],
|
||||
},
|
||||
};
|
||||
test("Enter on non-last column focuses next column", async () => {
|
||||
const question = baseQuestion();
|
||||
const localSurvey = makeSurvey([langDefault]);
|
||||
(localSurvey as any).questions = [question];
|
||||
|
||||
const { findAllByTestId } = render(<MatrixQuestionForm {...propsWithMinRows} />);
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
// Try to delete rows until there are only 2 left
|
||||
const deleteButtons = await findAllByTestId("tooltip-renderer");
|
||||
await user.click(deleteButtons[0].querySelector("button") as HTMLButtonElement);
|
||||
render(
|
||||
<MatrixQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestion}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
// Try to delete another row, which should fail
|
||||
vi.mocked(mockUpdateQuestion).mockClear();
|
||||
await user.click(deleteButtons[1].querySelector("button") as HTMLButtonElement);
|
||||
const firstColInput = screen.getByTestId("qfi-column-0");
|
||||
await userEvent.type(firstColInput, "{enter}");
|
||||
|
||||
// The mockUpdateQuestion should not be called again
|
||||
expect(mockUpdateQuestion).not.toHaveBeenCalled();
|
||||
expect(updateQuestion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles row input changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { getByTestId } = render(<MatrixQuestionForm {...defaultProps} />);
|
||||
test("Arrow Down on row focuses next row", async () => {
|
||||
const question = baseQuestion();
|
||||
const localSurvey = makeSurvey([langDefault]);
|
||||
(localSurvey as any).questions = [question];
|
||||
|
||||
const rowInput = getByTestId("input-row-0");
|
||||
await user.clear(rowInput);
|
||||
await user.type(rowInput, "New Row Label");
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
expect(mockUpdateQuestion).toHaveBeenCalled();
|
||||
render(
|
||||
<MatrixQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestion}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const firstRowInput = screen.getByTestId("qfi-row-0");
|
||||
await userEvent.type(firstRowInput, "{arrowdown}");
|
||||
|
||||
expect(updateQuestion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("handles column input changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { getByTestId } = render(<MatrixQuestionForm {...defaultProps} />);
|
||||
test("Arrow Up on row focuses previous row", async () => {
|
||||
const question = baseQuestion();
|
||||
const localSurvey = makeSurvey([langDefault]);
|
||||
(localSurvey as any).questions = [question];
|
||||
|
||||
const columnInput = getByTestId("input-column-0");
|
||||
await user.clear(columnInput);
|
||||
await user.type(columnInput, "New Column Label");
|
||||
const updateQuestion = vi.fn();
|
||||
|
||||
expect(mockUpdateQuestion).toHaveBeenCalled();
|
||||
});
|
||||
render(
|
||||
<MatrixQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={0}
|
||||
updateQuestion={updateQuestion}
|
||||
isInvalid={false}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
test("prevents deletion of a row used in logic", async () => {
|
||||
const { findOptionUsedInLogic } = await import("@/modules/survey/editor/lib/utils");
|
||||
vi.mocked(findOptionUsedInLogic).mockReturnValueOnce(1); // Mock that this row is used in logic
|
||||
const secondRowInput = screen.getByTestId("qfi-row-1");
|
||||
await userEvent.type(secondRowInput, "{arrowup}");
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { findAllByTestId } = render(<MatrixQuestionForm {...defaultProps} />);
|
||||
|
||||
const deleteButtons = await findAllByTestId("tooltip-renderer");
|
||||
await user.click(deleteButtons[0].querySelector("button") as HTMLButtonElement);
|
||||
|
||||
expect(mockUpdateQuestion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("prevents deletion of a column used in logic", async () => {
|
||||
const { findOptionUsedInLogic } = await import("@/modules/survey/editor/lib/utils");
|
||||
vi.mocked(findOptionUsedInLogic).mockReturnValueOnce(1); // Mock that this column is used in logic
|
||||
|
||||
const user = userEvent.setup();
|
||||
const { findAllByTestId } = render(<MatrixQuestionForm {...defaultProps} />);
|
||||
|
||||
// Column delete buttons are after row delete buttons
|
||||
const deleteButtons = await findAllByTestId("tooltip-renderer");
|
||||
// Click the first column delete button (index 2)
|
||||
await user.click(deleteButtons[2].querySelector("button") as HTMLButtonElement);
|
||||
|
||||
expect(mockUpdateQuestion).not.toHaveBeenCalled();
|
||||
expect(updateQuestion).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { MatrixSortableItem } from "@/modules/survey/editor/components/matrix-sortable-item";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
||||
import { DndContext, type DragEndEvent } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
@@ -17,6 +10,13 @@ import { type JSX, useCallback } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { MatrixSortableItem } from "@/modules/survey/editor/components/matrix-sortable-item";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
|
||||
interface MatrixQuestionFormProps {
|
||||
@@ -45,17 +45,24 @@ export const MatrixQuestionForm = ({
|
||||
const languageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
const { t } = useTranslate();
|
||||
|
||||
const focusItem = (targetIdx: number, type: "row" | "column") => {
|
||||
const input = document.querySelector(`input[id="${type}-${targetIdx}"]`) as HTMLInputElement;
|
||||
if (input) input.focus();
|
||||
};
|
||||
|
||||
// Function to add a new Label input field
|
||||
const handleAddLabel = (type: "row" | "column") => {
|
||||
if (type === "row") {
|
||||
const updatedRows = [...question.rows, { id: createId(), label: createI18nString("", languageCodes) }];
|
||||
updateQuestion(questionIdx, { rows: updatedRows });
|
||||
setTimeout(() => focusItem(updatedRows.length - 1, type), 0);
|
||||
} else {
|
||||
const updatedColumns = [
|
||||
...question.columns,
|
||||
{ id: createId(), label: createI18nString("", languageCodes) },
|
||||
];
|
||||
updateQuestion(questionIdx, { columns: updatedColumns });
|
||||
setTimeout(() => focusItem(updatedColumns.length - 1, type), 0);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,10 +119,30 @@ export const MatrixQuestionForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, type: "row" | "column") => {
|
||||
const handleKeyDown = (e: React.KeyboardEvent, type: "row" | "column", currentIndex: number) => {
|
||||
const items = type === "row" ? question.rows : question.columns;
|
||||
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleAddLabel(type);
|
||||
if (currentIndex === items.length - 1) {
|
||||
handleAddLabel(type);
|
||||
} else {
|
||||
focusItem(currentIndex + 1, type);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
if (currentIndex + 1 < items.length) {
|
||||
focusItem(currentIndex + 1, type);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
if (currentIndex > 0) {
|
||||
focusItem(currentIndex - 1, type);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -230,7 +257,7 @@ export const MatrixQuestionForm = ({
|
||||
questionIdx={questionIdx}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
onDelete={(index) => handleDeleteLabel("row", index)}
|
||||
onKeyDown={(e) => handleKeyDown(e, "row")}
|
||||
onKeyDown={(e) => handleKeyDown(e, "row", index)}
|
||||
canDelete={question.rows.length > 2}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
@@ -276,7 +303,7 @@ export const MatrixQuestionForm = ({
|
||||
questionIdx={questionIdx}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
onDelete={(index) => handleDeleteLabel("column", index)}
|
||||
onKeyDown={(e) => handleKeyDown(e, "column")}
|
||||
onKeyDown={(e) => handleKeyDown(e, "column", index)}
|
||||
canDelete={question.columns.length > 2}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
@@ -15,6 +12,9 @@ import {
|
||||
TSurveyMatrixQuestionChoice,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface MatrixSortableItemProps {
|
||||
choice: TSurveyMatrixQuestionChoice;
|
||||
|
||||
@@ -72,7 +72,6 @@ describe("MultipleChoiceQuestionForm", () => {
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
locale="en-US"
|
||||
lastQuestion={false}
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { QuestionOptionChoice } from "@/modules/survey/editor/components/question-option-choice";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
@@ -23,13 +16,19 @@ import {
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { QuestionOptionChoice } from "@/modules/survey/editor/components/question-option-choice";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
||||
|
||||
interface MultipleChoiceQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyMultipleChoiceQuestion;
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceQuestion>) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
@@ -248,28 +247,27 @@ export const MultipleChoiceQuestionForm = ({
|
||||
}}>
|
||||
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
key={choice.id}
|
||||
choice={choice}
|
||||
choiceIdx={choiceIdx}
|
||||
questionIdx={questionIdx}
|
||||
updateChoice={updateChoice}
|
||||
deleteChoice={deleteChoice}
|
||||
addChoice={addChoice}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
surveyLanguages={surveyLanguages}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
))}
|
||||
{question.choices?.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
key={choice.id}
|
||||
choice={choice}
|
||||
choiceIdx={choiceIdx}
|
||||
questionIdx={questionIdx}
|
||||
updateChoice={updateChoice}
|
||||
deleteChoice={deleteChoice}
|
||||
addChoice={addChoice}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
surveyLanguages={surveyLanguages}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { Project } from "@prisma/client";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
@@ -24,22 +40,6 @@ import { getQuestionIconMap, getTSurveyQuestionTypeEnumName } from "@/modules/su
|
||||
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";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { Project } from "@prisma/client";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
interface QuestionCardProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -301,7 +301,6 @@ export const QuestionCard = ({
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
@@ -314,7 +313,6 @@ export const QuestionCard = ({
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
@@ -453,7 +451,6 @@ export const QuestionCard = ({
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
|
||||
@@ -7,7 +7,13 @@ import { QuestionOptionChoice } from "./question-option-choice";
|
||||
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: (props: any) => (
|
||||
<div data-testid="question-form-input" className={props.className}></div>
|
||||
<input
|
||||
data-testid="question-form-input"
|
||||
className={props.className}
|
||||
onKeyDown={props.onKeyDown}
|
||||
value={props.value?.default || props.value?.en || props.value?.de || ""}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -70,6 +76,81 @@ describe("QuestionOptionChoice", () => {
|
||||
expect(addButton).toBeDefined();
|
||||
});
|
||||
|
||||
test("pressing Enter on last choice adds a new choice", async () => {
|
||||
const addChoice = vi.fn();
|
||||
const choice = { id: "choice2", label: { default: "Choice 2" } };
|
||||
const question = {
|
||||
id: "question1",
|
||||
headline: { default: "Question 1" },
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
choices: [{ id: "choice1", label: { default: "Choice 1" } }, choice],
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<QuestionOptionChoice
|
||||
choice={choice}
|
||||
choiceIdx={1}
|
||||
questionIdx={0}
|
||||
updateChoice={vi.fn()}
|
||||
deleteChoice={vi.fn()}
|
||||
addChoice={addChoice}
|
||||
isInvalid={false}
|
||||
localSurvey={{ languages: [{ language: { code: "default" }, default: true }] } as any}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
surveyLanguages={[{ language: { code: "default" } as any, enabled: true, default: true } as any]}
|
||||
question={question}
|
||||
updateQuestion={vi.fn()}
|
||||
surveyLanguageCodes={["default"]}
|
||||
locale="en-US"
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const input = screen.getByTestId("question-form-input");
|
||||
await userEvent.type(input, "{enter}");
|
||||
|
||||
expect(addChoice).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
test("pressing Enter on non-last choice focuses next choice", async () => {
|
||||
const addChoice = vi.fn();
|
||||
const choice = { id: "choice1", label: { default: "Choice 1" } };
|
||||
const question = {
|
||||
id: "question1",
|
||||
headline: { default: "Question 1" },
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
choices: [choice, { id: "choice2", label: { default: "Choice 2" } }],
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<QuestionOptionChoice
|
||||
choice={choice}
|
||||
choiceIdx={0}
|
||||
questionIdx={0}
|
||||
updateChoice={vi.fn()}
|
||||
deleteChoice={vi.fn()}
|
||||
addChoice={addChoice}
|
||||
isInvalid={false}
|
||||
localSurvey={{ languages: [{ language: { code: "default" }, default: true }] } as any}
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={vi.fn()}
|
||||
surveyLanguages={[{ language: { code: "default" } as any, enabled: true, default: true } as any]}
|
||||
question={question}
|
||||
updateQuestion={vi.fn()}
|
||||
surveyLanguageCodes={["default"]}
|
||||
locale="en-US"
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const input = screen.getByTestId("question-form-input");
|
||||
await userEvent.type(input, "{enter}");
|
||||
|
||||
// Should not add a new choice (not the last one)
|
||||
expect(addChoice).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should call deleteChoice when the 'Delete choice' button is clicked for a standard choice", async () => {
|
||||
const choice = { id: "choice1", label: { default: "Choice 1" } };
|
||||
const question = {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
@@ -18,6 +13,11 @@ import {
|
||||
TSurveyRankingQuestion,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
|
||||
interface ChoiceProps {
|
||||
@@ -72,6 +72,17 @@ export const QuestionOptionChoice = ({
|
||||
transform: CSS.Translate.toString(transform),
|
||||
};
|
||||
|
||||
const focusChoiceInput = (targetIdx: number) => {
|
||||
const input = document.querySelector(`input[id="choice-${targetIdx}"]`) as HTMLInputElement;
|
||||
input?.focus();
|
||||
};
|
||||
|
||||
const addChoiceAndFocus = (idx: number) => {
|
||||
addChoice(idx);
|
||||
// Wait for DOM update before focusing the new input
|
||||
setTimeout(() => focusChoiceInput(idx + 1), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2" ref={setNodeRef} style={style}>
|
||||
{/* drag handle */}
|
||||
@@ -101,6 +112,32 @@ export const QuestionOptionChoice = ({
|
||||
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && choice.id !== "other") {
|
||||
e.preventDefault();
|
||||
const lastChoiceIdx = question.choices.findLastIndex((c) => c.id !== "other");
|
||||
|
||||
if (choiceIdx === lastChoiceIdx) {
|
||||
addChoiceAndFocus(choiceIdx);
|
||||
} else {
|
||||
focusChoiceInput(choiceIdx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
if (choiceIdx + 1 < question.choices.length) {
|
||||
focusChoiceInput(choiceIdx + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
if (choiceIdx > 0) {
|
||||
focusChoiceInput(choiceIdx - 1);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{choice.id === "other" && (
|
||||
<QuestionFormInput
|
||||
@@ -110,9 +147,8 @@ export const QuestionOptionChoice = ({
|
||||
label={""}
|
||||
questionIdx={questionIdx}
|
||||
value={
|
||||
question.otherOptionPlaceholder
|
||||
? question.otherOptionPlaceholder
|
||||
: createI18nString(t("environments.surveys.edit.please_specify"), surveyLanguageCodes)
|
||||
question.otherOptionPlaceholder ??
|
||||
createI18nString(t("environments.surveys.edit.please_specify"), surveyLanguageCodes)
|
||||
}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
@@ -127,7 +163,7 @@ export const QuestionOptionChoice = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{question.choices && question.choices.length > 2 && (
|
||||
{question.choices?.length > 2 && (
|
||||
<TooltipRenderer tooltipContent={t("environments.surveys.edit.delete_choice")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -149,7 +185,7 @@ export const QuestionOptionChoice = ({
|
||||
aria-label="Add choice below"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
addChoice(choiceIdx);
|
||||
addChoiceAndFocus(choiceIdx);
|
||||
}}>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
|
||||
@@ -88,7 +88,6 @@ describe("RankingQuestionForm", () => {
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
locale={mockLocale}
|
||||
lastQuestion={false}
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
@@ -135,7 +134,6 @@ describe("RankingQuestionForm", () => {
|
||||
selectedLanguageCode="default"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
locale={mockLocale}
|
||||
lastQuestion={false}
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
@@ -190,7 +188,6 @@ describe("RankingQuestionForm", () => {
|
||||
selectedLanguageCode="en"
|
||||
setSelectedLanguageCode={mockSetSelectedLanguageCode}
|
||||
locale={mockLocale}
|
||||
lastQuestion={false}
|
||||
isStorageConfigured={true}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { QuestionOptionChoice } from "@/modules/survey/editor/components/question-option-choice";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
@@ -15,13 +9,18 @@ import { PlusIcon } from "lucide-react";
|
||||
import { type JSX, useEffect, useRef, useState } from "react";
|
||||
import { TI18nString, TSurvey, TSurveyRankingQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { QuestionOptionChoice } from "@/modules/survey/editor/components/question-option-choice";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
||||
|
||||
interface RankingQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyRankingQuestion;
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyRankingQuestion>) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
@@ -195,28 +194,27 @@ export const RankingQuestionForm = ({
|
||||
}}>
|
||||
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
key={choice.id}
|
||||
choice={choice}
|
||||
choiceIdx={choiceIdx}
|
||||
questionIdx={questionIdx}
|
||||
updateChoice={updateChoice}
|
||||
deleteChoice={deleteChoice}
|
||||
addChoice={addChoice}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
surveyLanguages={surveyLanguages}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
))}
|
||||
{question.choices?.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
key={choice.id}
|
||||
choice={choice}
|
||||
choiceIdx={choiceIdx}
|
||||
questionIdx={questionIdx}
|
||||
updateChoice={updateChoice}
|
||||
deleteChoice={deleteChoice}
|
||||
addChoice={addChoice}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
surveyLanguages={surveyLanguages}
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
Reference in New Issue
Block a user