mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-23 05:17:49 -05:00
feat: Add drag and drop functionality to Matrix question options
- Create MatrixRowChoice component with sortable drag and drop for reordering rows - Create MatrixColumnChoice component with sortable drag and drop for reordering columns - Update matrix-question-form.tsx to integrate DndContext and SortableContext - Add handleRowDragEnd and handleColumnDragEnd methods for reordering logic - Maintain minimum of 2 rows/columns during reordering operations - Add comprehensive test coverage for both new components - Follow existing patterns from QuestionOptionChoice component - Integrate with existing internationalization and validation systems - Fix Husky pre-commit hook to use absolute path for .env file Fixes #4944 Co-Authored-By: Johannes <johannes@formbricks.com>
This commit is contained in:
+3
-4
@@ -1,10 +1,9 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# Load environment variables from .env files
|
||||
if [ -f .env ]; then
|
||||
if [ -f "$(git rev-parse --show-toplevel)/.env" ]; then
|
||||
set -a
|
||||
. .env
|
||||
. "$(git rev-parse --show-toplevel)/.env"
|
||||
set +a
|
||||
fi
|
||||
|
||||
@@ -18,4 +17,4 @@ if [ -f branch.json ]; then
|
||||
pnpm run tolgee-pull
|
||||
git add apps/web/locales
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLanguage,
|
||||
TSurveyMatrixQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { MatrixColumnChoice } from "./matrix-column-choice";
|
||||
|
||||
// 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 mockQuestion: TSurveyMatrixQuestion = {
|
||||
id: "matrix-1",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: createI18nString("Matrix Question", ["en"]),
|
||||
required: false,
|
||||
logic: [],
|
||||
rows: [createI18nString("Row 1", ["en"]), createI18nString("Row 2", ["en"])],
|
||||
columns: [
|
||||
createI18nString("Column 1", ["en"]),
|
||||
createI18nString("Column 2", ["en"]),
|
||||
createI18nString("Column 3", ["en"]),
|
||||
],
|
||||
shuffleOption: "none",
|
||||
};
|
||||
|
||||
// Mock survey
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "survey-1",
|
||||
name: "Test Survey",
|
||||
questions: [mockQuestion],
|
||||
languages: mockSurveyLanguages,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const defaultProps = {
|
||||
columnIdx: 0,
|
||||
questionIdx: 0,
|
||||
updateMatrixLabel: vi.fn(),
|
||||
handleDeleteLabel: vi.fn(),
|
||||
handleKeyDown: vi.fn(),
|
||||
isInvalid: false,
|
||||
localSurvey: mockSurvey,
|
||||
selectedLanguageCode: "en",
|
||||
setSelectedLanguageCode: vi.fn(),
|
||||
question: mockQuestion,
|
||||
locale: "en-US" as TUserLocale,
|
||||
};
|
||||
|
||||
const renderWithDndContext = (props = {}) => {
|
||||
const finalProps = { ...defaultProps, ...props };
|
||||
return render(
|
||||
<DndContext>
|
||||
<SortableContext items={["column-0"]} strategy={verticalListSortingStrategy}>
|
||||
<MatrixColumnChoice {...finalProps} />
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
describe("MatrixColumnChoice", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders the column choice with drag handle and input", () => {
|
||||
renderWithDndContext();
|
||||
|
||||
expect(screen.getByDisplayValue("Column 1")).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows delete button when there are more than 2 columns", () => {
|
||||
renderWithDndContext();
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides delete button when there are only 2 columns", () => {
|
||||
const questionWith2Columns = {
|
||||
...mockQuestion,
|
||||
columns: [createI18nString("Column 1", ["en"]), createI18nString("Column 2", ["en"])],
|
||||
};
|
||||
|
||||
renderWithDndContext({ question: questionWith2Columns });
|
||||
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls handleDeleteLabel when delete button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleDeleteLabel = vi.fn();
|
||||
|
||||
renderWithDndContext({ handleDeleteLabel });
|
||||
|
||||
const deleteButton = screen.getByRole("button");
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(handleDeleteLabel).toHaveBeenCalledWith("column", 0);
|
||||
});
|
||||
|
||||
test("calls updateMatrixLabel when input value changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const updateMatrixLabel = vi.fn();
|
||||
|
||||
renderWithDndContext({ updateMatrixLabel });
|
||||
|
||||
const input = screen.getByDisplayValue("Column 1");
|
||||
await user.clear(input);
|
||||
await user.type(input, "Updated Column");
|
||||
|
||||
expect(updateMatrixLabel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls handleKeyDown when Enter key is pressed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleKeyDown = vi.fn();
|
||||
|
||||
renderWithDndContext({ handleKeyDown });
|
||||
|
||||
const input = screen.getByDisplayValue("Column 1");
|
||||
await user.type(input, "{Enter}");
|
||||
|
||||
expect(handleKeyDown).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies invalid styling when isInvalid is true", () => {
|
||||
renderWithDndContext({ isInvalid: true });
|
||||
|
||||
const input = screen.getByDisplayValue("Column 1");
|
||||
expect(input).toHaveClass("border-red-300");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
"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";
|
||||
import { GripVerticalIcon, TrashIcon } from "lucide-react";
|
||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
|
||||
interface MatrixColumnChoiceProps {
|
||||
columnIdx: number;
|
||||
questionIdx: number;
|
||||
updateMatrixLabel: (index: number, type: "row" | "column", matrixLabel: TI18nString) => void;
|
||||
handleDeleteLabel: (type: "row" | "column", index: number) => void;
|
||||
handleKeyDown: (e: React.KeyboardEvent, type: "row" | "column") => void;
|
||||
isInvalid: boolean;
|
||||
localSurvey: TSurvey;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
question: TSurveyMatrixQuestion;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const MatrixColumnChoice = ({
|
||||
columnIdx,
|
||||
questionIdx,
|
||||
updateMatrixLabel,
|
||||
handleDeleteLabel,
|
||||
handleKeyDown,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
question,
|
||||
locale,
|
||||
}: MatrixColumnChoiceProps) => {
|
||||
const { t } = useTranslate();
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
id: `column-${columnIdx}`,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transition: transition ?? "transform 100ms ease",
|
||||
transform: CSS.Translate.toString(transform),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2" ref={setNodeRef} style={style}>
|
||||
<div {...listeners} {...attributes}>
|
||||
<GripVerticalIcon className="h-4 w-4 cursor-move text-slate-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full">
|
||||
<QuestionFormInput
|
||||
id={`column-${columnIdx}`}
|
||||
label={""}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={question.columns[columnIdx]}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.columns[columnIdx], localSurvey.languages)
|
||||
}
|
||||
locale={locale}
|
||||
onKeyDown={(e) => handleKeyDown(e, "column")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{question.columns.length > 2 && (
|
||||
<TooltipRenderer data-testid="tooltip-renderer" tooltipContent={t("common.delete")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDeleteLabel("column", columnIdx);
|
||||
}}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,19 +2,21 @@
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { MatrixColumnChoice } from "@/modules/survey/editor/components/matrix-column-choice";
|
||||
import { MatrixRowChoice } from "@/modules/survey/editor/components/matrix-row-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 { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import type { JSX } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
|
||||
interface MatrixQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -109,6 +111,42 @@ export const MatrixQuestionForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowDragEnd = (event: any) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!active || !over) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeIndex = question.rows.findIndex((_, idx) => `row-${idx}` === active.id);
|
||||
const overIndex = question.rows.findIndex((_, idx) => `row-${idx}` === over.id);
|
||||
|
||||
if (activeIndex !== overIndex) {
|
||||
const newRows = [...question.rows];
|
||||
const [reorderedItem] = newRows.splice(activeIndex, 1);
|
||||
newRows.splice(overIndex, 0, reorderedItem);
|
||||
updateQuestion(questionIdx, { rows: newRows });
|
||||
}
|
||||
};
|
||||
|
||||
const handleColumnDragEnd = (event: any) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!active || !over) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeIndex = question.columns.findIndex((_, idx) => `column-${idx}` === active.id);
|
||||
const overIndex = question.columns.findIndex((_, idx) => `column-${idx}` === over.id);
|
||||
|
||||
if (activeIndex !== overIndex) {
|
||||
const newColumns = [...question.columns];
|
||||
const [reorderedItem] = newColumns.splice(activeIndex, 1);
|
||||
newColumns.splice(overIndex, 0, reorderedItem);
|
||||
updateQuestion(questionIdx, { columns: newColumns });
|
||||
}
|
||||
};
|
||||
|
||||
const shuffleOptionsTypes = {
|
||||
none: {
|
||||
id: "none",
|
||||
@@ -181,44 +219,35 @@ export const MatrixQuestionForm = ({
|
||||
<div>
|
||||
{/* Rows section */}
|
||||
<Label htmlFor="rows">{t("environments.surveys.edit.rows")}</Label>
|
||||
<div className="mt-2 flex flex-col gap-2" ref={parent}>
|
||||
{question.rows.map((row, index) => (
|
||||
<div className="flex items-center" key={`${row}-${index}`}>
|
||||
<QuestionFormInput
|
||||
id={`row-${index}`}
|
||||
label={""}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={question.rows[index]}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.rows[index], localSurvey.languages)
|
||||
}
|
||||
locale={locale}
|
||||
onKeyDown={(e) => handleKeyDown(e, "row")}
|
||||
/>
|
||||
{question.rows.length > 2 && (
|
||||
<TooltipRenderer data-testid="tooltip-renderer" tooltipContent={t("common.delete")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDeleteLabel("row", index);
|
||||
}}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2" id="rows">
|
||||
<DndContext id="matrix-rows" onDragEnd={handleRowDragEnd}>
|
||||
<SortableContext
|
||||
items={question.rows.map((_, idx) => `row-${idx}`)}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
{question.rows.map((_, index) => (
|
||||
<MatrixRowChoice
|
||||
key={`row-${index}`}
|
||||
rowIdx={index}
|
||||
questionIdx={questionIdx}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
handleDeleteLabel={handleDeleteLabel}
|
||||
handleKeyDown={handleKeyDown}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
question={question}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
className="mt-2 w-fit"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddLabel("row");
|
||||
@@ -231,44 +260,35 @@ export const MatrixQuestionForm = ({
|
||||
<div>
|
||||
{/* Columns section */}
|
||||
<Label htmlFor="columns">{t("environments.surveys.edit.columns")}</Label>
|
||||
<div className="mt-2 flex flex-col gap-2" ref={parent}>
|
||||
{question.columns.map((column, index) => (
|
||||
<div className="flex items-center" key={`${column}-${index}`}>
|
||||
<QuestionFormInput
|
||||
id={`column-${index}`}
|
||||
label={""}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={question.columns[index]}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.columns[index], localSurvey.languages)
|
||||
}
|
||||
locale={locale}
|
||||
onKeyDown={(e) => handleKeyDown(e, "column")}
|
||||
/>
|
||||
{question.columns.length > 2 && (
|
||||
<TooltipRenderer data-testid="tooltip-renderer" tooltipContent={t("common.delete")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDeleteLabel("column", index);
|
||||
}}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2" id="columns">
|
||||
<DndContext id="matrix-columns" onDragEnd={handleColumnDragEnd}>
|
||||
<SortableContext
|
||||
items={question.columns.map((_, idx) => `column-${idx}`)}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
{question.columns.map((_, index) => (
|
||||
<MatrixColumnChoice
|
||||
key={`column-${index}`}
|
||||
columnIdx={index}
|
||||
questionIdx={questionIdx}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
handleDeleteLabel={handleDeleteLabel}
|
||||
handleKeyDown={handleKeyDown}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
question={question}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
className="mt-2 w-fit"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddLabel("column");
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { createI18nString } from "@/lib/i18n/utils";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLanguage,
|
||||
TSurveyMatrixQuestion,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { MatrixRowChoice } from "./matrix-row-choice";
|
||||
|
||||
// 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 mockQuestion: TSurveyMatrixQuestion = {
|
||||
id: "matrix-1",
|
||||
type: TSurveyQuestionTypeEnum.Matrix,
|
||||
headline: createI18nString("Matrix Question", ["en"]),
|
||||
required: false,
|
||||
logic: [],
|
||||
rows: [
|
||||
createI18nString("Row 1", ["en"]),
|
||||
createI18nString("Row 2", ["en"]),
|
||||
createI18nString("Row 3", ["en"]),
|
||||
],
|
||||
columns: [createI18nString("Column 1", ["en"]), createI18nString("Column 2", ["en"])],
|
||||
shuffleOption: "none",
|
||||
};
|
||||
|
||||
// Mock survey
|
||||
const mockSurvey: TSurvey = {
|
||||
id: "survey-1",
|
||||
name: "Test Survey",
|
||||
questions: [mockQuestion],
|
||||
languages: mockSurveyLanguages,
|
||||
} as unknown as TSurvey;
|
||||
|
||||
const defaultProps = {
|
||||
rowIdx: 0,
|
||||
questionIdx: 0,
|
||||
updateMatrixLabel: vi.fn(),
|
||||
handleDeleteLabel: vi.fn(),
|
||||
handleKeyDown: vi.fn(),
|
||||
isInvalid: false,
|
||||
localSurvey: mockSurvey,
|
||||
selectedLanguageCode: "en",
|
||||
setSelectedLanguageCode: vi.fn(),
|
||||
question: mockQuestion,
|
||||
locale: "en-US" as TUserLocale,
|
||||
};
|
||||
|
||||
const renderWithDndContext = (props = {}) => {
|
||||
const finalProps = { ...defaultProps, ...props };
|
||||
return render(
|
||||
<DndContext>
|
||||
<SortableContext items={["row-0"]} strategy={verticalListSortingStrategy}>
|
||||
<MatrixRowChoice {...finalProps} />
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
describe("MatrixRowChoice", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders the row choice with drag handle and input", () => {
|
||||
renderWithDndContext();
|
||||
|
||||
expect(screen.getByDisplayValue("Row 1")).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows delete button when there are more than 2 rows", () => {
|
||||
renderWithDndContext();
|
||||
|
||||
expect(screen.getByRole("button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides delete button when there are only 2 rows", () => {
|
||||
const questionWith2Rows = {
|
||||
...mockQuestion,
|
||||
rows: [createI18nString("Row 1", ["en"]), createI18nString("Row 2", ["en"])],
|
||||
};
|
||||
|
||||
renderWithDndContext({ question: questionWith2Rows });
|
||||
|
||||
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls handleDeleteLabel when delete button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleDeleteLabel = vi.fn();
|
||||
|
||||
renderWithDndContext({ handleDeleteLabel });
|
||||
|
||||
const deleteButton = screen.getByRole("button");
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(handleDeleteLabel).toHaveBeenCalledWith("row", 0);
|
||||
});
|
||||
|
||||
test("calls updateMatrixLabel when input value changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const updateMatrixLabel = vi.fn();
|
||||
|
||||
renderWithDndContext({ updateMatrixLabel });
|
||||
|
||||
const input = screen.getByDisplayValue("Row 1");
|
||||
await user.clear(input);
|
||||
await user.type(input, "Updated Row");
|
||||
|
||||
expect(updateMatrixLabel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("calls handleKeyDown when Enter key is pressed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleKeyDown = vi.fn();
|
||||
|
||||
renderWithDndContext({ handleKeyDown });
|
||||
|
||||
const input = screen.getByDisplayValue("Row 1");
|
||||
await user.type(input, "{Enter}");
|
||||
|
||||
expect(handleKeyDown).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("applies invalid styling when isInvalid is true", () => {
|
||||
renderWithDndContext({ isInvalid: true });
|
||||
|
||||
const input = screen.getByDisplayValue("Row 1");
|
||||
expect(input).toHaveClass("border-red-300");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
"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";
|
||||
import { GripVerticalIcon, TrashIcon } from "lucide-react";
|
||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
|
||||
interface MatrixRowChoiceProps {
|
||||
rowIdx: number;
|
||||
questionIdx: number;
|
||||
updateMatrixLabel: (index: number, type: "row" | "column", matrixLabel: TI18nString) => void;
|
||||
handleDeleteLabel: (type: "row" | "column", index: number) => void;
|
||||
handleKeyDown: (e: React.KeyboardEvent, type: "row" | "column") => void;
|
||||
isInvalid: boolean;
|
||||
localSurvey: TSurvey;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
question: TSurveyMatrixQuestion;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const MatrixRowChoice = ({
|
||||
rowIdx,
|
||||
questionIdx,
|
||||
updateMatrixLabel,
|
||||
handleDeleteLabel,
|
||||
handleKeyDown,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
question,
|
||||
locale,
|
||||
}: MatrixRowChoiceProps) => {
|
||||
const { t } = useTranslate();
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
id: `row-${rowIdx}`,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transition: transition ?? "transform 100ms ease",
|
||||
transform: CSS.Translate.toString(transform),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-2" ref={setNodeRef} style={style}>
|
||||
<div {...listeners} {...attributes}>
|
||||
<GripVerticalIcon className="h-4 w-4 cursor-move text-slate-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full">
|
||||
<QuestionFormInput
|
||||
id={`row-${rowIdx}`}
|
||||
label={""}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={question.rows[rowIdx]}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid && !isLabelValidForAllLanguages(question.rows[rowIdx], localSurvey.languages)}
|
||||
locale={locale}
|
||||
onKeyDown={(e) => handleKeyDown(e, "row")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{question.rows.length > 2 && (
|
||||
<TooltipRenderer data-testid="tooltip-renderer" tooltipContent={t("common.delete")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDeleteLabel("row", rowIdx);
|
||||
}}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user