mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-23 22:50:35 -06:00
Compare commits
6 Commits
personaliz
...
devin/1735
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
272846a7ad | ||
|
|
64ed3e231c | ||
|
|
baa58b766d | ||
|
|
1bb09dbd94 | ||
|
|
6d441874c6 | ||
|
|
9dd94467a9 |
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@ vi.mock("../../../ee/license-check/lib/utils", () => ({
|
||||
getIsAuditLogsEnabled: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: { audit: vi.fn(), error: vi.fn() },
|
||||
logger: { info: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
const validEvent = {
|
||||
@@ -37,7 +37,7 @@ describe("logAuditEvent", () => {
|
||||
test("logs event if access is granted and event is valid", async () => {
|
||||
getIsAuditLogsEnabled.mockResolvedValue(true);
|
||||
await logAuditEvent(validEvent);
|
||||
expect(logger.audit).toHaveBeenCalledWith(validEvent);
|
||||
expect(logger.info).toHaveBeenCalledWith(validEvent, "Audit event logged");
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("logAuditEvent", () => {
|
||||
getIsAuditLogsEnabled.mockResolvedValue(true);
|
||||
const invalidEvent = { ...validEvent, action: "invalid.action" };
|
||||
await logAuditEvent(invalidEvent as any);
|
||||
expect(logger.audit).not.toHaveBeenCalled();
|
||||
expect(logger.info).not.toHaveBeenCalled();
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -53,12 +53,12 @@ describe("logAuditEvent", () => {
|
||||
getIsAuditLogsEnabled.mockResolvedValue(true);
|
||||
const event = { ...validEvent, organizationId: UNKNOWN_DATA };
|
||||
await logAuditEvent(event);
|
||||
expect(logger.audit).toHaveBeenCalledWith(event);
|
||||
expect(logger.info).toHaveBeenCalledWith(event, "Audit event logged");
|
||||
});
|
||||
|
||||
test("does not throw if logger.audit throws", async () => {
|
||||
test("does not throw if logger.info throws", async () => {
|
||||
getIsAuditLogsEnabled.mockResolvedValue(true);
|
||||
logger.audit.mockImplementation(() => {
|
||||
logger.info.mockImplementation(() => {
|
||||
throw new Error("fail");
|
||||
});
|
||||
await logAuditEvent(validEvent);
|
||||
|
||||
@@ -11,7 +11,7 @@ const validateEvent = (event: TAuditLogEvent): void => {
|
||||
export const logAuditEvent = async (event: TAuditLogEvent): Promise<void> => {
|
||||
try {
|
||||
validateEvent(event);
|
||||
logger.audit(event);
|
||||
logger.info(event, "Audit event logged");
|
||||
} catch (error) {
|
||||
// Log error to application logger but don't throw
|
||||
// This ensures audit logging failures don't break the application
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
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 { MatrixLabelChoice } from "./matrix-label-choice";
|
||||
|
||||
// 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, params?: any) => {
|
||||
if (key === "environments.surveys.edit.row_idx") {
|
||||
return `Row ${params?.rowIndex}`;
|
||||
}
|
||||
if (key === "environments.surveys.edit.column_idx") {
|
||||
return `Column ${params?.columnIndex}`;
|
||||
}
|
||||
if (key === "environments.surveys.edit.delete_row") {
|
||||
return "Delete row";
|
||||
}
|
||||
if (key === "environments.surveys.edit.delete_column") {
|
||||
return "Delete column";
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock QuestionFormInput component
|
||||
vi.mock("@/modules/survey/components/question-form-input", () => ({
|
||||
QuestionFormInput: vi.fn(({ id, updateMatrixLabel, value, 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 });
|
||||
}
|
||||
}}
|
||||
value={value?.default || ""}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
// Mock TooltipRenderer component
|
||||
vi.mock("@/modules/ui/components/tooltip", () => ({
|
||||
TooltipRenderer: vi.fn(({ children }) => <div data-testid="tooltip-renderer">{children}</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 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"]),
|
||||
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 = {
|
||||
labelIdx: 0,
|
||||
type: "row" as const,
|
||||
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={[`${finalProps.type}-${finalProps.labelIdx}`]}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<MatrixLabelChoice {...finalProps} />
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
describe("MatrixLabelChoice", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Row type", () => {
|
||||
test("renders the row choice with drag handle and input", () => {
|
||||
renderWithDndContext({ type: "row" });
|
||||
|
||||
expect(screen.getByDisplayValue("Row 1")).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows delete button when there are more than 2 rows", () => {
|
||||
renderWithDndContext({ type: "row" });
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const deleteButton = buttons.find((button) => button.querySelector('svg[class*="lucide-trash"]'));
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides delete button when there are only 2 rows", () => {
|
||||
const questionWith2Rows = {
|
||||
...mockQuestion,
|
||||
rows: [createI18nString("Row 1", ["en"]), createI18nString("Row 2", ["en"])],
|
||||
};
|
||||
|
||||
renderWithDndContext({ type: "row", question: questionWith2Rows });
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const deleteButton = buttons.find((button) => button.querySelector('svg[class*="lucide-trash"]'));
|
||||
expect(deleteButton).toBeUndefined();
|
||||
});
|
||||
|
||||
test("calls handleDeleteLabel when delete button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleDeleteLabel = vi.fn();
|
||||
|
||||
renderWithDndContext({ type: "row", handleDeleteLabel });
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const deleteButton = buttons.find((button) => button.querySelector('svg[class*="lucide-trash"]'));
|
||||
expect(deleteButton).toBeDefined();
|
||||
|
||||
await user.click(deleteButton!);
|
||||
|
||||
expect(handleDeleteLabel).toHaveBeenCalledWith("row", 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Column type", () => {
|
||||
test("renders the column choice with drag handle and input", () => {
|
||||
renderWithDndContext({ type: "column" });
|
||||
|
||||
expect(screen.getByDisplayValue("Column 1")).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows delete button when there are more than 2 columns", () => {
|
||||
renderWithDndContext({ type: "column" });
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const deleteButton = buttons.find((button) => button.querySelector('svg[class*="lucide-trash"]'));
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides delete button when there are only 2 columns", () => {
|
||||
const questionWith2Columns = {
|
||||
...mockQuestion,
|
||||
columns: [createI18nString("Column 1", ["en"]), createI18nString("Column 2", ["en"])],
|
||||
};
|
||||
|
||||
renderWithDndContext({ type: "column", question: questionWith2Columns });
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const deleteButton = buttons.find((button) => button.querySelector('svg[class*="lucide-trash"]'));
|
||||
expect(deleteButton).toBeUndefined();
|
||||
});
|
||||
|
||||
test("calls handleDeleteLabel when delete button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleDeleteLabel = vi.fn();
|
||||
|
||||
renderWithDndContext({ type: "column", handleDeleteLabel });
|
||||
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const deleteButton = buttons.find((button) => button.querySelector('svg[class*="lucide-trash"]'));
|
||||
expect(deleteButton).toBeDefined();
|
||||
|
||||
await user.click(deleteButton!);
|
||||
|
||||
expect(handleDeleteLabel).toHaveBeenCalledWith("column", 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Common functionality", () => {
|
||||
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).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
"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 MatrixLabelChoiceProps {
|
||||
labelIdx: number;
|
||||
type: "row" | "column";
|
||||
questionIdx: number;
|
||||
updateMatrixLabel: (index: number, type: "row" | "column", data: 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 MatrixLabelChoice = ({
|
||||
labelIdx,
|
||||
type,
|
||||
questionIdx,
|
||||
updateMatrixLabel,
|
||||
handleDeleteLabel,
|
||||
handleKeyDown,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
question,
|
||||
locale,
|
||||
}: MatrixLabelChoiceProps) => {
|
||||
const { t } = useTranslate();
|
||||
const labels = type === "row" ? question.rows : question.columns;
|
||||
const surveyLanguages = localSurvey.languages ?? [];
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
id: `${type}-${labelIdx}`,
|
||||
});
|
||||
|
||||
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}>
|
||||
{/* drag handle */}
|
||||
<div {...listeners} {...attributes}>
|
||||
<GripVerticalIcon className="h-4 w-4 cursor-move text-slate-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full space-x-2">
|
||||
<QuestionFormInput
|
||||
key={`${type}-${labelIdx}`}
|
||||
id={`${type}-${labelIdx}`}
|
||||
placeholder={t(`environments.surveys.edit.${type}_idx`, {
|
||||
[`${type}Index`]: labelIdx + 1,
|
||||
})}
|
||||
label=""
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={labels[labelIdx]}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid && !isLabelValidForAllLanguages(labels[labelIdx], surveyLanguages)}
|
||||
onKeyDown={(e) => handleKeyDown(e, type)}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{labels.length > 2 && (
|
||||
<TooltipRenderer tooltipContent={t(`environments.surveys.edit.delete_${type}`)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
aria-label={`Delete ${type}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDeleteLabel(type, labelIdx);
|
||||
}}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { MatrixLabelChoice } from "@/modules/survey/editor/components/matrix-label-choice";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
interface MatrixLabelSectionProps {
|
||||
type: "row" | "column";
|
||||
labels: TI18nString[];
|
||||
question: TSurveyMatrixQuestion;
|
||||
questionIdx: number;
|
||||
updateMatrixLabel: (index: number, type: "row" | "column", data: TI18nString) => void;
|
||||
handleDeleteLabel: (type: "row" | "column", index: number) => void;
|
||||
handleKeyDown: (e: React.KeyboardEvent, type: "row" | "column") => void;
|
||||
handleAddLabel: (type: "row" | "column") => void;
|
||||
onDragEnd: (event: any) => void;
|
||||
isInvalid: boolean;
|
||||
localSurvey: TSurvey;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
locale: TUserLocale;
|
||||
parent: any;
|
||||
}
|
||||
|
||||
export const MatrixLabelSection = ({
|
||||
type,
|
||||
labels,
|
||||
question,
|
||||
questionIdx,
|
||||
updateMatrixLabel,
|
||||
handleDeleteLabel,
|
||||
handleKeyDown,
|
||||
handleAddLabel,
|
||||
onDragEnd,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
locale,
|
||||
parent,
|
||||
}: MatrixLabelSectionProps) => {
|
||||
const { t } = useTranslate();
|
||||
const labelKey = type === "row" ? "rows" : "columns";
|
||||
const addKey = type === "row" ? "add_row" : "add_column";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label htmlFor={labelKey}>{t(`environments.surveys.edit.${labelKey}`)}</Label>
|
||||
<div className="mt-2" id={labelKey}>
|
||||
<DndContext id={`matrix-${labelKey}`} onDragEnd={onDragEnd}>
|
||||
<SortableContext
|
||||
items={labels.map((_, idx) => `${type}-${idx}`)}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
{labels.map((_, index) => (
|
||||
<MatrixLabelChoice
|
||||
key={`${type}-${index}`}
|
||||
labelIdx={index}
|
||||
type={type}
|
||||
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="mt-2 w-fit"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddLabel(type);
|
||||
}}>
|
||||
<PlusIcon />
|
||||
{t(`environments.surveys.edit.${addKey}`)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,19 +2,17 @@
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { MatrixLabelSection } from "@/modules/survey/editor/components/matrix-label-section";
|
||||
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 { 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 +107,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",
|
||||
@@ -178,105 +212,41 @@ export const MatrixQuestionForm = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-4">
|
||||
<MatrixLabelSection
|
||||
type="row"
|
||||
labels={question.rows}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
handleDeleteLabel={handleDeleteLabel}
|
||||
handleKeyDown={handleKeyDown}
|
||||
handleAddLabel={handleAddLabel}
|
||||
onDragEnd={handleRowDragEnd}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
parent={parent}
|
||||
/>
|
||||
<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>
|
||||
))}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddLabel("row");
|
||||
}}>
|
||||
<PlusIcon />
|
||||
{t("environments.surveys.edit.add_row")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
))}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddLabel("column");
|
||||
}}>
|
||||
<PlusIcon />
|
||||
{t("environments.surveys.edit.add_column")}
|
||||
</Button>
|
||||
</div>
|
||||
<MatrixLabelSection
|
||||
type="column"
|
||||
labels={question.columns}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateMatrixLabel={updateMatrixLabel}
|
||||
handleDeleteLabel={handleDeleteLabel}
|
||||
handleKeyDown={handleKeyDown}
|
||||
handleAddLabel={handleAddLabel}
|
||||
onDragEnd={handleColumnDragEnd}
|
||||
isInvalid={isInvalid}
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
parent={parent}
|
||||
/>
|
||||
<div className="mt-3 flex flex-1 items-center justify-end gap-2">
|
||||
<ShuffleOptionSelect
|
||||
shuffleOptionsTypes={shuffleOptionsTypes}
|
||||
|
||||
Reference in New Issue
Block a user