mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 02:10:12 -06:00
feat: Add language as default contact attribute for case-insensitive CSV matching
- Add language as a default attribute key in environment creation - Create data migration to add language attribute key to existing environments - Update tests to verify language is treated like other default attributes - Fixes issue where CSV columns with 'Language' (capital L) would create duplicate custom attributes The existing isStringMatch() function already handles case-insensitive matching, so this change ensures language is properly matched alongside userId, email, firstName, and lastName without any hardcoding in the UI layer.
This commit is contained in:
@@ -168,6 +168,12 @@ export const createEnvironment = async (
|
||||
description: "Your contact's last name",
|
||||
type: "default",
|
||||
},
|
||||
{
|
||||
key: "language",
|
||||
name: "Language",
|
||||
description: "The language preference of a contact",
|
||||
type: "default",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { parse } from "csv-parse/sync";
|
||||
import { ArrowUpFromLineIcon, FileUpIcon, PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { isStringMatch } from "@/lib/utils/helper";
|
||||
import { createContactsFromCSVAction } from "@/modules/ee/contacts/actions";
|
||||
@@ -18,12 +24,6 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
import { StylingTabs } from "@/modules/ui/components/styling-tabs";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { parse } from "csv-parse/sync";
|
||||
import { ArrowUpFromLineIcon, FileUpIcon, PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
|
||||
interface UploadContactsCSVButtonProps {
|
||||
environmentId: string;
|
||||
|
||||
@@ -319,6 +319,51 @@ describe("createContactsFromCSV", () => {
|
||||
createContactsFromCSV(csvData, environmentId, "skip", { email: "email", name: "name" })
|
||||
).rejects.toThrow(genericError);
|
||||
});
|
||||
|
||||
test("handles language attribute key like other default attributes", async () => {
|
||||
vi.mocked(prisma.contact.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]);
|
||||
vi.mocked(prisma.contactAttributeKey.findMany).mockResolvedValue([
|
||||
{ key: "email", id: "id-email" },
|
||||
{ key: "userId", id: "id-userId" },
|
||||
{ key: "firstName", id: "id-firstName" },
|
||||
{ key: "lastName", id: "id-lastName" },
|
||||
{ key: "language", id: "id-language" },
|
||||
] as any);
|
||||
vi.mocked(prisma.contact.create).mockResolvedValue({
|
||||
id: "c1",
|
||||
environmentId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
attributes: [
|
||||
{ attributeKey: { key: "email" }, value: "john@example.com" },
|
||||
{ attributeKey: { key: "userId" }, value: "user123" },
|
||||
{ attributeKey: { key: "firstName" }, value: "John" },
|
||||
{ attributeKey: { key: "lastName" }, value: "Doe" },
|
||||
{ attributeKey: { key: "language" }, value: "en" },
|
||||
],
|
||||
} as any);
|
||||
const csvData = [
|
||||
{
|
||||
email: "john@example.com",
|
||||
userId: "user123",
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
language: "en",
|
||||
},
|
||||
];
|
||||
const result = await createContactsFromCSV(csvData, environmentId, "skip", {
|
||||
email: "email",
|
||||
userId: "userId",
|
||||
firstName: "firstName",
|
||||
lastName: "lastName",
|
||||
language: "language",
|
||||
});
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result[0].id).toBe("c1");
|
||||
// language attribute key should already exist, no need to create it
|
||||
expect(prisma.contactAttributeKey.createMany).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildContactWhereClause", () => {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable no-constant-condition -- Required for the while loop */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import type { MigrationScript } from "../../src/scripts/migration-runner";
|
||||
|
||||
export const addLanguageAttributeKey: MigrationScript = {
|
||||
type: "data",
|
||||
id: "add_language_attribute_key_v1",
|
||||
name: "20251016000000_add_language_attribute_key",
|
||||
run: async ({ tx }) => {
|
||||
const BATCH_SIZE = 1000;
|
||||
let skip = 0;
|
||||
let totalProcessed = 0;
|
||||
|
||||
logger.info("Starting migration to add language attribute key to environments");
|
||||
|
||||
while (true) {
|
||||
// Fetch environments in batches
|
||||
const environments = await tx.$queryRaw<{ id: string }[]>`
|
||||
SELECT id FROM "Environment"
|
||||
LIMIT ${BATCH_SIZE} OFFSET ${skip}
|
||||
`;
|
||||
|
||||
if (environments.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
logger.info(`Processing ${environments.length.toString()} environments`);
|
||||
|
||||
// Process each environment
|
||||
for (const env of environments) {
|
||||
// Insert language attribute key if it doesn't exist
|
||||
await tx.$executeRaw`
|
||||
INSERT INTO "ContactAttributeKey" (
|
||||
"id", "created_at", "updated_at", "key", "name", "description", "type", "isUnique", "environmentId"
|
||||
) VALUES (
|
||||
${createId()},
|
||||
NOW(),
|
||||
NOW(),
|
||||
'language',
|
||||
'Language',
|
||||
'The language preference of a contact',
|
||||
'default',
|
||||
false,
|
||||
${env.id}
|
||||
)
|
||||
ON CONFLICT ("key", "environmentId") DO NOTHING
|
||||
`;
|
||||
}
|
||||
|
||||
totalProcessed += environments.length;
|
||||
skip += BATCH_SIZE;
|
||||
|
||||
logger.info(`Processed ${totalProcessed.toString()} environments so far`);
|
||||
}
|
||||
|
||||
logger.info(`Migration completed. Total environments processed: ${totalProcessed.toString()}`);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user