mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
fix: fixes ttc tracking (#6900)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
328
apps/web/lib/response/service.test.ts
Normal file
328
apps/web/lib/response/service.test.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { updateResponse } from "./service";
|
||||
import { calculateTtcTotal } from "./utils";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
response: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/validate", () => ({
|
||||
validateInputs: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./utils", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./utils")>();
|
||||
return {
|
||||
...actual,
|
||||
calculateTtcTotal: vi.fn((ttc) => ({
|
||||
...ttc,
|
||||
_total: Object.values(ttc as Record<string, number>).reduce((a, b) => a + b, 0),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const mockResponseId = "response-123";
|
||||
|
||||
const createMockCurrentResponse = (overrides: Record<string, unknown> = {}) => ({
|
||||
id: mockResponseId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
surveyId: "survey-123",
|
||||
finished: false,
|
||||
endingId: null,
|
||||
data: {},
|
||||
meta: {},
|
||||
ttc: {},
|
||||
variables: {},
|
||||
contactAttributes: {},
|
||||
singleUseId: null,
|
||||
language: "en",
|
||||
displayId: "display-123",
|
||||
contact: null,
|
||||
tags: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockResponseInput = (overrides: Partial<TResponseUpdateInput> = {}): TResponseUpdateInput => ({
|
||||
finished: false,
|
||||
data: {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("updateResponse", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("TTC merging behavior", () => {
|
||||
test("should merge new TTC with existing TTC from previous blocks", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
ttc: { element1: 1000, element2: 2000 },
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
ttc: { element3: 3000 },
|
||||
finished: false,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue({
|
||||
...currentResponse,
|
||||
ttc: { element1: 1000, element2: 2000, element3: 3000 },
|
||||
} as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(prisma.response.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
ttc: { element1: 1000, element2: 2000, element3: 3000 },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should preserve existing TTC when no new TTC is provided", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
ttc: { element1: 1000, element2: 2000 },
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
finished: false,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue(currentResponse as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(prisma.response.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
ttc: { element1: 1000, element2: 2000 },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should calculate total TTC when response is finished", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
ttc: { element1: 1000, element2: 2000 },
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
ttc: { element3: 3000 },
|
||||
finished: true,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue({
|
||||
...currentResponse,
|
||||
finished: true,
|
||||
ttc: { element1: 1000, element2: 2000, element3: 3000, _total: 6000 },
|
||||
} as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(calculateTtcTotal).toHaveBeenCalledWith({
|
||||
element1: 1000,
|
||||
element2: 2000,
|
||||
element3: 3000,
|
||||
});
|
||||
});
|
||||
|
||||
test("should not calculate total TTC when response is not finished", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
ttc: { element1: 1000 },
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
ttc: { element2: 2000 },
|
||||
finished: false,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue({
|
||||
...currentResponse,
|
||||
ttc: { element1: 1000, element2: 2000 },
|
||||
} as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(calculateTtcTotal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle empty existing TTC", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
ttc: {},
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
ttc: { element1: 1000 },
|
||||
finished: false,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue({
|
||||
...currentResponse,
|
||||
ttc: { element1: 1000 },
|
||||
} as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(prisma.response.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
ttc: { element1: 1000 },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle null existing TTC", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
ttc: null,
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
ttc: { element1: 1000 },
|
||||
finished: false,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue({
|
||||
...currentResponse,
|
||||
ttc: { element1: 1000 },
|
||||
} as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(prisma.response.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
ttc: { element1: 1000 },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should overwrite existing element TTC with new value for same element", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
ttc: { element1: 1000 },
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
ttc: { element1: 1500 },
|
||||
finished: false,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue({
|
||||
...currentResponse,
|
||||
ttc: { element1: 1500 },
|
||||
} as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(prisma.response.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
ttc: { element1: 1500 },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("data merging behavior", () => {
|
||||
test("should merge new data with existing data", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
data: { question1: "answer1" },
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
data: { question2: "answer2" },
|
||||
finished: false,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue({
|
||||
...currentResponse,
|
||||
data: { question1: "answer1", question2: "answer2" },
|
||||
} as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(prisma.response.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
data: { question1: "answer1", question2: "answer2" },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("variables merging behavior", () => {
|
||||
test("should merge new variables with existing variables", async () => {
|
||||
const currentResponse = createMockCurrentResponse({
|
||||
variables: { var1: "value1" },
|
||||
});
|
||||
|
||||
const responseInput = createMockResponseInput({
|
||||
variables: { var2: "value2" },
|
||||
finished: false,
|
||||
});
|
||||
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockResolvedValue({
|
||||
...currentResponse,
|
||||
variables: { var1: "value1", var2: "value2" },
|
||||
} as any);
|
||||
|
||||
await updateResponse(mockResponseId, responseInput);
|
||||
|
||||
expect(prisma.response.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
variables: { var1: "value1", var2: "value2" },
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("should throw ResourceNotFoundError when response does not exist", async () => {
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(null);
|
||||
|
||||
const responseInput = createMockResponseInput();
|
||||
|
||||
await expect(updateResponse(mockResponseId, responseInput)).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on Prisma errors", async () => {
|
||||
const currentResponse = createMockCurrentResponse();
|
||||
vi.mocked(prisma.response.findUnique).mockResolvedValue(currentResponse as any);
|
||||
vi.mocked(prisma.response.update).mockRejectedValue(
|
||||
new Prisma.PrismaClientKnownRequestError("Database error", {
|
||||
code: "P2002",
|
||||
clientVersion: "5.0.0",
|
||||
})
|
||||
);
|
||||
|
||||
const responseInput = createMockResponseInput();
|
||||
|
||||
await expect(updateResponse(mockResponseId, responseInput)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -507,11 +507,16 @@ export const updateResponse = async (
|
||||
...currentResponse.data,
|
||||
...responseInput.data,
|
||||
};
|
||||
const ttc = responseInput.ttc
|
||||
? responseInput.finished
|
||||
? calculateTtcTotal(responseInput.ttc)
|
||||
: responseInput.ttc
|
||||
: {};
|
||||
// merge ttc object (similar to data) to preserve TTC from previous blocks
|
||||
const currentTtc = currentResponse.ttc;
|
||||
const mergedTtc = responseInput.ttc
|
||||
? {
|
||||
...currentTtc,
|
||||
...responseInput.ttc,
|
||||
}
|
||||
: currentTtc;
|
||||
// Calculate total only when finished
|
||||
const ttc = responseInput.finished ? calculateTtcTotal(mergedTtc) : mergedTtc;
|
||||
const language = responseInput.language;
|
||||
const variables = {
|
||||
...currentResponse.variables,
|
||||
|
||||
@@ -690,7 +690,9 @@ export const ElementsView = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Set active element to the first element of the first remaining block or ending card
|
||||
setLocalSurvey(result.data);
|
||||
|
||||
// Then set active element to the first element of the first remaining block or ending card
|
||||
const newBlocks = result.data.blocks ?? [];
|
||||
if (newBlocks.length > 0 && newBlocks[0].elements.length > 0) {
|
||||
setActiveElementId(newBlocks[0].elements[0].id);
|
||||
@@ -698,7 +700,6 @@ export const ElementsView = ({
|
||||
setActiveElementId(result.data.endings[0].id);
|
||||
}
|
||||
|
||||
setLocalSurvey(result.data);
|
||||
toast.success(t("environments.surveys.edit.block_deleted"));
|
||||
};
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ export function BlockConditional({
|
||||
// Refs to store form elements for each element so we can trigger their validation
|
||||
const elementFormRefs = useRef<Map<string, HTMLFormElement>>(new Map());
|
||||
|
||||
// Ref to collect TTC values synchronously (state updates are async)
|
||||
const ttcCollectorRef = useRef<TResponseTtc>({});
|
||||
|
||||
// Handle change for an individual element
|
||||
const handleElementChange = (elementId: string, responseData: TResponseData) => {
|
||||
// If user moved to a different element, we should track it
|
||||
@@ -69,6 +72,11 @@ export function BlockConditional({
|
||||
onChange(responseData);
|
||||
};
|
||||
|
||||
// Handler to collect TTC values synchronously (called from element form submissions)
|
||||
const handleTtcCollect = (elementId: string, elementTtc: number) => {
|
||||
ttcCollectorRef.current[elementId] = elementTtc;
|
||||
};
|
||||
|
||||
// Handle skipPrefilled at block level
|
||||
useEffect(() => {
|
||||
if (skipPrefilled && prefilledResponseData) {
|
||||
@@ -171,10 +179,26 @@ export function BlockConditional({
|
||||
return;
|
||||
}
|
||||
|
||||
// All validations passed - collect TTC for all elements in this block
|
||||
// Clear the TTC collector before collecting new values
|
||||
ttcCollectorRef.current = {};
|
||||
|
||||
// Call each form's submit method to trigger TTC calculation
|
||||
// The forms will call handleTtcCollect synchronously with their TTC values
|
||||
block.elements.forEach((element) => {
|
||||
const form = elementFormRefs.current.get(element.id);
|
||||
if (form) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
// Collect TTC from the ref (populated synchronously by form submissions)
|
||||
// Falls back to state for elements that may have TTC from user interactions
|
||||
const blockTtc: TResponseTtc = {};
|
||||
block.elements.forEach((element) => {
|
||||
if (ttc[element.id] !== undefined) {
|
||||
// Prefer freshly calculated TTC from form submission, fall back to state
|
||||
if (ttcCollectorRef.current[element.id] !== undefined) {
|
||||
blockTtc[element.id] = ttcCollectorRef.current[element.id];
|
||||
} else if (ttc[element.id] !== undefined) {
|
||||
blockTtc[element.id] = ttc[element.id];
|
||||
}
|
||||
});
|
||||
@@ -225,6 +249,7 @@ export function BlockConditional({
|
||||
elementFormRefs.current.delete(element.id);
|
||||
}
|
||||
}}
|
||||
onTtcCollect={handleTtcCollect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,6 +42,7 @@ interface ElementConditionalProps {
|
||||
onOpenExternalURL?: (url: string) => void | Promise<void>;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
formRef?: (ref: HTMLFormElement | null) => void; // Callback to expose the form element
|
||||
onTtcCollect?: (elementId: string, ttc: number) => void; // Callback to collect TTC synchronously
|
||||
}
|
||||
|
||||
export function ElementConditional({
|
||||
@@ -60,6 +61,7 @@ export function ElementConditional({
|
||||
onOpenExternalURL,
|
||||
dir,
|
||||
formRef,
|
||||
onTtcCollect,
|
||||
}: ElementConditionalProps) {
|
||||
// Ref to the container div, used to find and expose the form element inside
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -75,6 +77,16 @@ export function ElementConditional({
|
||||
}
|
||||
}, [formRef]);
|
||||
|
||||
// Wrap setTtc to also call onTtcCollect synchronously
|
||||
// This allows the block to collect TTC values without waiting for async state updates
|
||||
const wrappedSetTtc = (newTtc: TResponseTtc) => {
|
||||
setTtc(newTtc);
|
||||
// Extract this element's TTC and call the collector if provided
|
||||
if (onTtcCollect && newTtc[element.id] !== undefined) {
|
||||
onTtcCollect(element.id, newTtc[element.id]);
|
||||
}
|
||||
};
|
||||
|
||||
const getResponseValueForRankingElement = (value: string[], choices: TSurveyElementChoice[]): string[] => {
|
||||
return value
|
||||
.map((entry) => {
|
||||
@@ -122,7 +134,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
dir={dir}
|
||||
@@ -137,7 +149,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
dir={dir}
|
||||
@@ -152,7 +164,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
dir={dir}
|
||||
@@ -167,7 +179,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
dir={dir}
|
||||
@@ -182,7 +194,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
onOpenExternalURL={onOpenExternalURL}
|
||||
@@ -197,7 +209,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
dir={dir}
|
||||
@@ -212,7 +224,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
dir={dir}
|
||||
@@ -227,7 +239,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
/>
|
||||
@@ -241,7 +253,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
dir={dir}
|
||||
@@ -258,7 +270,7 @@ export function ElementConditional({
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
/>
|
||||
@@ -272,7 +284,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
currentElementId={currentElementId}
|
||||
/>
|
||||
);
|
||||
@@ -284,7 +296,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
currentElementId={currentElementId}
|
||||
/>
|
||||
);
|
||||
@@ -296,7 +308,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
currentElementId={currentElementId}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
dir={dir}
|
||||
@@ -310,7 +322,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentElementId={currentElementId}
|
||||
/>
|
||||
@@ -323,7 +335,7 @@ export function ElementConditional({
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
setTtc={wrappedSetTtc}
|
||||
currentElementId={currentElementId}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
dir={dir}
|
||||
|
||||
Reference in New Issue
Block a user