mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-01 18:58:46 -06:00
Compare commits
2 Commits
stable
...
fix-lang-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c07e71b47 | ||
|
|
242b003048 |
@@ -577,7 +577,7 @@ checksums:
|
||||
environments/contacts/attribute_created_successfully: e9f90d366d817f2f1c81fb819c0e2f05
|
||||
environments/contacts/attribute_description: e17686a22ffad04cc7bb70524ed4478b
|
||||
environments/contacts/attribute_description_placeholder: 05af83e4cfc6328476ef9719581e47af
|
||||
environments/contacts/attribute_key: 3d1065ab98a1c2f1210507fd5c7bf515
|
||||
environments/contacts/attribute_key: cc92f647873ba9e17cff57d4a59737bd
|
||||
environments/contacts/attribute_key_cannot_be_changed: 0ced703e77a8e620276c1fa21fcc8900
|
||||
environments/contacts/attribute_key_hint: 1a68c6f91e1a5cf9eff811e2e54e92b8
|
||||
environments/contacts/attribute_key_placeholder: 31702e553b3f138a623dbaa42b6f878f
|
||||
|
||||
@@ -167,6 +167,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",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Attribut erfolgreich erstellt",
|
||||
"attribute_description": "Beschreibung",
|
||||
"attribute_description_placeholder": "Kurze Beschreibung",
|
||||
"attribute_key": "Schlüssel",
|
||||
"attribute_key": "Attribut",
|
||||
"attribute_key_cannot_be_changed": "Schlüssel kann nach der Erstellung nicht geändert werden",
|
||||
"attribute_key_hint": "Nur Kleinbuchstaben, Zahlen und Unterstriche. Muss mit einem Buchstaben beginnen.",
|
||||
"attribute_key_placeholder": "z. B. geburtsdatum",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Attribute created successfully",
|
||||
"attribute_description": "Description",
|
||||
"attribute_description_placeholder": "Short description",
|
||||
"attribute_key": "Key",
|
||||
"attribute_key": "Attribute",
|
||||
"attribute_key_cannot_be_changed": "Key cannot be changed after creation",
|
||||
"attribute_key_hint": "Only lowercase letters, numbers, and underscores. Must start with a letter.",
|
||||
"attribute_key_placeholder": "e.g. date_of_birth",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Atributo creado con éxito",
|
||||
"attribute_description": "Descripción",
|
||||
"attribute_description_placeholder": "Descripción breve",
|
||||
"attribute_key": "Clave",
|
||||
"attribute_key": "Atributo",
|
||||
"attribute_key_cannot_be_changed": "La clave no se puede cambiar después de la creación",
|
||||
"attribute_key_hint": "Solo letras minúsculas, números y guiones bajos. Debe empezar con una letra.",
|
||||
"attribute_key_placeholder": "p. ej. fecha_de_nacimiento",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Attribut créé avec succès",
|
||||
"attribute_description": "Description",
|
||||
"attribute_description_placeholder": "Brève description",
|
||||
"attribute_key": "Clé",
|
||||
"attribute_key": "Attribut",
|
||||
"attribute_key_cannot_be_changed": "La clé ne peut pas être modifiée après la création",
|
||||
"attribute_key_hint": "Uniquement des lettres minuscules, des chiffres et des underscores. Doit commencer par une lettre.",
|
||||
"attribute_key_placeholder": "ex. date_de_naissance",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "属性を作成しました",
|
||||
"attribute_description": "説明",
|
||||
"attribute_description_placeholder": "簡単な説明",
|
||||
"attribute_key": "キー",
|
||||
"attribute_key": "属性",
|
||||
"attribute_key_cannot_be_changed": "キーは作成後に変更できません",
|
||||
"attribute_key_hint": "小文字のアルファベット、数字、アンダースコアのみ使用可能です。アルファベットで始める必要があります。",
|
||||
"attribute_key_placeholder": "例: date_of_birth",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Attribuut succesvol aangemaakt",
|
||||
"attribute_description": "Beschrijving",
|
||||
"attribute_description_placeholder": "Korte beschrijving",
|
||||
"attribute_key": "Sleutel",
|
||||
"attribute_key": "Kenmerk",
|
||||
"attribute_key_cannot_be_changed": "Sleutel kan niet worden gewijzigd na aanmaak",
|
||||
"attribute_key_hint": "Alleen kleine letters, cijfers en onderstrepingstekens. Moet beginnen met een letter.",
|
||||
"attribute_key_placeholder": "bijv. geboortedatum",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Atributo criado com sucesso",
|
||||
"attribute_description": "Descrição",
|
||||
"attribute_description_placeholder": "Descrição curta",
|
||||
"attribute_key": "Chave",
|
||||
"attribute_key": "atributo",
|
||||
"attribute_key_cannot_be_changed": "A chave não pode ser alterada após a criação",
|
||||
"attribute_key_hint": "Apenas letras minúsculas, números e underscores. Deve começar com uma letra.",
|
||||
"attribute_key_placeholder": "ex: data_de_nascimento",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Atributo criado com sucesso",
|
||||
"attribute_description": "Descrição",
|
||||
"attribute_description_placeholder": "Descrição breve",
|
||||
"attribute_key": "Chave",
|
||||
"attribute_key": "Atributo",
|
||||
"attribute_key_cannot_be_changed": "A chave não pode ser alterada após a criação",
|
||||
"attribute_key_hint": "Apenas letras minúsculas, números e sublinhados. Deve começar com uma letra.",
|
||||
"attribute_key_placeholder": "ex. data_de_nascimento",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Atribut creat cu succes",
|
||||
"attribute_description": "Descriere",
|
||||
"attribute_description_placeholder": "Descriere scurtă",
|
||||
"attribute_key": "Cheie",
|
||||
"attribute_key": "Atribut",
|
||||
"attribute_key_cannot_be_changed": "Cheia nu poate fi modificată după creare",
|
||||
"attribute_key_hint": "Doar litere mici, cifre și caractere de subliniere. Trebuie să înceapă cu o literă.",
|
||||
"attribute_key_placeholder": "ex: date_of_birth",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Атрибут успешно создан",
|
||||
"attribute_description": "Описание",
|
||||
"attribute_description_placeholder": "Краткое описание",
|
||||
"attribute_key": "Ключ",
|
||||
"attribute_key": "Атрибут",
|
||||
"attribute_key_cannot_be_changed": "Ключ нельзя изменить после создания",
|
||||
"attribute_key_hint": "Только строчные буквы, цифры и символы подчёркивания. Должен начинаться с буквы.",
|
||||
"attribute_key_placeholder": "например, date_of_birth",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "Attributet har skapats",
|
||||
"attribute_description": "Beskrivning",
|
||||
"attribute_description_placeholder": "Kort beskrivning",
|
||||
"attribute_key": "Nyckel",
|
||||
"attribute_key": "Attribut",
|
||||
"attribute_key_cannot_be_changed": "Nyckeln kan inte ändras efter skapande",
|
||||
"attribute_key_hint": "Endast små bokstäver, siffror och understreck. Måste börja med en bokstav.",
|
||||
"attribute_key_placeholder": "t.ex. date_of_birth",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "属性创建成功",
|
||||
"attribute_description": "描述",
|
||||
"attribute_description_placeholder": "简短描述",
|
||||
"attribute_key": "键",
|
||||
"attribute_key": "属性",
|
||||
"attribute_key_cannot_be_changed": "创建后键不可更改",
|
||||
"attribute_key_hint": "仅允许小写字母、数字和下划线,且必须以字母开头。",
|
||||
"attribute_key_placeholder": "例如:date_of_birth",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"attribute_created_successfully": "屬性建立成功",
|
||||
"attribute_description": "描述",
|
||||
"attribute_description_placeholder": "簡短描述",
|
||||
"attribute_key": "金鑰",
|
||||
"attribute_key": "屬性",
|
||||
"attribute_key_cannot_be_changed": "建立後無法變更金鑰",
|
||||
"attribute_key_hint": "僅限小寫字母、數字和底線,且必須以字母開頭。",
|
||||
"attribute_key_placeholder": "例如:date_of_birth",
|
||||
|
||||
@@ -4,6 +4,34 @@ import { getContactAttributes } from "@/modules/ee/contacts/lib/contact-attribut
|
||||
import { getContact } from "@/modules/ee/contacts/lib/contacts";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
|
||||
const DEFAULT_ATTRIBUTE_KEYS = ["email", "language", "userId", "firstName", "lastName"] as const;
|
||||
|
||||
interface AttributeRowProps {
|
||||
label: string;
|
||||
value: string | undefined;
|
||||
notProvidedText: string;
|
||||
isIdBadge?: boolean;
|
||||
}
|
||||
|
||||
const AttributeRow = ({ label, value, notProvidedText, isIdBadge = false }: AttributeRowProps) => {
|
||||
const renderValue = () => {
|
||||
if (!value) {
|
||||
return <span className="text-slate-300">{notProvidedText}</span>;
|
||||
}
|
||||
if (isIdBadge) {
|
||||
return <IdBadge id={value} />;
|
||||
}
|
||||
return <span>{value}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">{label}</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">{renderValue()}</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AttributesSection = async ({ contactId }: { contactId: string }) => {
|
||||
const t = await getTranslate();
|
||||
const [contact, attributes] = await Promise.all([getContact(contactId), getContactAttributes(contactId)]);
|
||||
@@ -14,55 +42,34 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
|
||||
|
||||
const responses = await getResponsesByContactId(contactId);
|
||||
const numberOfResponses = responses?.length || 0;
|
||||
const notProvidedText = t("environments.contacts.not_provided");
|
||||
|
||||
const customAttributes = Object.entries(attributes).filter(
|
||||
([key]) => !DEFAULT_ATTRIBUTE_KEYS.includes(key as (typeof DEFAULT_ATTRIBUTE_KEYS)[number])
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-bold text-slate-700">{t("common.attributes")}</h2>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">email</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
{attributes.email ? (
|
||||
<span>{attributes.email}</span>
|
||||
) : (
|
||||
<span className="text-slate-300">{t("environments.contacts.not_provided")}</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">language</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
{attributes.language ? (
|
||||
<span>{attributes.language}</span>
|
||||
) : (
|
||||
<span className="text-slate-300">{t("environments.contacts.not_provided")}</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">userId</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
{attributes.userId ? (
|
||||
<IdBadge id={attributes.userId} />
|
||||
) : (
|
||||
<span className="text-slate-300">{t("environments.contacts.not_provided")}</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<AttributeRow label="email" value={attributes.email} notProvidedText={notProvidedText} />
|
||||
<AttributeRow label="language" value={attributes.language} notProvidedText={notProvidedText} />
|
||||
<AttributeRow label="userId" value={attributes.userId} notProvidedText={notProvidedText} isIdBadge />
|
||||
<AttributeRow label="firstName" value={attributes.firstName} notProvidedText={notProvidedText} />
|
||||
<AttributeRow label="lastName" value={attributes.lastName} notProvidedText={notProvidedText} />
|
||||
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">contactId</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">{contact.id}</dd>
|
||||
</div>
|
||||
|
||||
{Object.entries(attributes)
|
||||
.filter(([key, _]) => key !== "email" && key !== "userId" && key !== "language")
|
||||
.map(([key, attributeData]) => {
|
||||
return (
|
||||
<div key={key}>
|
||||
<dt className="text-sm font-medium text-slate-500">{key}</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{attributeData}</dd>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{customAttributes.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<dt className="text-sm font-medium text-slate-500">{key}</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
|
||||
@@ -175,6 +175,23 @@ export const EditContactAttributesModal = ({
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
// Memoize email/userId existence checks for delete button logic
|
||||
// A contact MUST have either email or userId, so we prevent deletion if it would violate this
|
||||
const { hasEmail, hasUserId } = useMemo(() => {
|
||||
const emailAttr = watchedAttributes.find((attr) => attr.key === "email");
|
||||
const userIdAttr = watchedAttributes.find((attr) => attr.key === "userId");
|
||||
return {
|
||||
hasEmail: !!emailAttr?.value?.trim(),
|
||||
hasUserId: !!userIdAttr?.value?.trim(),
|
||||
};
|
||||
}, [watchedAttributes]);
|
||||
|
||||
const isDeleteDisabled = (key: string): boolean => {
|
||||
if (key === "email") return !hasUserId;
|
||||
if (key === "userId") return !hasEmail;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent width="default" className="max-h-[90vh]">
|
||||
@@ -235,7 +252,7 @@ export const EditContactAttributesModal = ({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={["email", "userId", "firstName", "lastName"].includes(field.key)}
|
||||
disabled={isDeleteDisabled(field.key)}
|
||||
size="sm"
|
||||
onClick={() => handleRemoveAttribute(index)}
|
||||
className="h-10 w-10 p-0">
|
||||
|
||||
@@ -11,30 +11,23 @@ import {
|
||||
hasUserIdAttribute,
|
||||
} from "@/modules/ee/contacts/lib/contact-attributes";
|
||||
|
||||
// Default/system attributes that should not be deleted even if missing from payload
|
||||
const DEFAULT_ATTRIBUTES = new Set(["email", "userId", "firstName", "lastName"]);
|
||||
|
||||
const deleteAttributes = async (
|
||||
contactId: string,
|
||||
currentAttributes: TContactAttributes,
|
||||
submittedAttributes: TContactAttributes,
|
||||
contactAttributeKeys: TContactAttributeKey[]
|
||||
): Promise<{ success: boolean }> => {
|
||||
): Promise<void> => {
|
||||
const contactAttributeKeyMap = new Map(contactAttributeKeys.map((ack) => [ack.key, ack]));
|
||||
|
||||
// Determine which attributes should be deleted (exist in DB but not in payload, and not default attributes)
|
||||
// Determine which attributes should be deleted (exist in DB but not in payload)
|
||||
const submittedKeys = new Set(Object.keys(submittedAttributes));
|
||||
const currentKeys = new Set(Object.keys(currentAttributes));
|
||||
const keysToDelete = Array.from(currentKeys).filter(
|
||||
(key) => !submittedKeys.has(key) && !DEFAULT_ATTRIBUTES.has(key)
|
||||
);
|
||||
const keysToDelete = Object.keys(currentAttributes).filter((key) => !submittedKeys.has(key));
|
||||
|
||||
// Get attribute key IDs for deletion
|
||||
const attributeKeyIdsToDelete = keysToDelete
|
||||
.map((key) => contactAttributeKeyMap.get(key)?.id)
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
// Delete attributes that were removed from the form (but not default attributes)
|
||||
if (attributeKeyIdsToDelete.length > 0) {
|
||||
await prisma.contactAttribute.deleteMany({
|
||||
where: {
|
||||
@@ -45,10 +38,6 @@ const deleteAttributes = async (
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -161,8 +150,10 @@ export const updateAttributes = async (
|
||||
// Delete attributes that were removed (only when explicitly requested)
|
||||
// This is used by UI forms where all attributes are submitted
|
||||
// For API calls, we want merge behavior by default (only update passed attributes)
|
||||
// We use contactAttributes (not contactAttributesParam) because it includes validation adjustments
|
||||
// (e.g., preserving email/userId when both would be empty)
|
||||
if (deleteRemovedAttributes) {
|
||||
await deleteAttributes(contactId, currentAttributes, contactAttributesParam, contactAttributeKeys);
|
||||
await deleteAttributes(contactId, currentAttributes, contactAttributes, contactAttributeKeys);
|
||||
}
|
||||
|
||||
// Create lookup map for attribute keys
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import type { SurveyElement, ValidationRule } from "./types";
|
||||
|
||||
/**
|
||||
@@ -62,7 +62,7 @@ export function migrateOpenTextCharLimit(element: SurveyElement): void {
|
||||
!hasMatchingRule(existingRules, "minLength", { min: element.charLimit.min })
|
||||
) {
|
||||
const newRule: ValidationRule = {
|
||||
id: uuidv7(),
|
||||
id: createId(),
|
||||
type: "minLength",
|
||||
params: { min: element.charLimit.min },
|
||||
};
|
||||
@@ -78,7 +78,7 @@ export function migrateOpenTextCharLimit(element: SurveyElement): void {
|
||||
})
|
||||
) {
|
||||
const newRule: ValidationRule = {
|
||||
id: uuidv7(),
|
||||
id: createId(),
|
||||
type: "maxLength",
|
||||
params: { max: element.charLimit.max },
|
||||
};
|
||||
@@ -138,7 +138,7 @@ export function migrateFileUploadExtensions(element: SurveyElement): void {
|
||||
if (!hasMatchingExtensionRule) {
|
||||
// Create new fileExtensionIs rule
|
||||
const newRule: ValidationRule = {
|
||||
id: uuidv7(),
|
||||
id: createId(),
|
||||
type: "fileExtensionIs",
|
||||
params: { extensions: [...extensions] },
|
||||
};
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/* 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: "l4n8g7u6a9g2e0k3e1y4a5d6",
|
||||
name: "20260121161018_add_language_attribute_key",
|
||||
run: async ({ tx }) => {
|
||||
const BATCH_SIZE = 10000;
|
||||
let skip = 0;
|
||||
let totalEnvironmentsProcessed = 0;
|
||||
let totalLanguageKeysAdded = 0;
|
||||
|
||||
logger.info("Starting migration to add language attribute key to all 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 (batch starting at ${skip})`);
|
||||
|
||||
// Process each environment
|
||||
for (const env of environments) {
|
||||
try {
|
||||
// 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
|
||||
`;
|
||||
|
||||
totalLanguageKeysAdded++;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to add language attribute key for environment ${env.id}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
skip += environments.length;
|
||||
totalEnvironmentsProcessed += environments.length;
|
||||
}
|
||||
|
||||
// Verify migration
|
||||
const [{ total_language_keys: totalLanguageKeys }] = await tx.$queryRaw<
|
||||
[{ total_language_keys: number }]
|
||||
>`
|
||||
SELECT COUNT(*)::integer AS total_language_keys
|
||||
FROM "ContactAttributeKey"
|
||||
WHERE "key" = 'language'
|
||||
`;
|
||||
|
||||
logger.info(`Migration completed successfully!`);
|
||||
logger.info(`Total environments processed: ${totalEnvironmentsProcessed.toString()}`);
|
||||
logger.info(`Total language attribute keys in database: ${totalLanguageKeys.toString()}`);
|
||||
},
|
||||
};
|
||||
@@ -455,6 +455,7 @@ async function main(): Promise<void> {
|
||||
{ name: "First Name", key: "firstName", isUnique: false, type: "default" },
|
||||
{ name: "Last Name", key: "lastName", isUnique: false, type: "default" },
|
||||
{ name: "userId", key: "userId", isUnique: true, type: "default" },
|
||||
{ name: "Language", key: "language", isUnique: false, type: "default" },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -474,6 +475,7 @@ async function main(): Promise<void> {
|
||||
{ name: "First Name", key: "firstName", isUnique: false, type: "default" },
|
||||
{ name: "Last Name", key: "lastName", isUnique: false, type: "default" },
|
||||
{ name: "userId", key: "userId", isUnique: true, type: "default" },
|
||||
{ name: "Language", key: "language", isUnique: false, type: "default" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user