mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-20 00:55:00 -06:00
Compare commits
4 Commits
poc-surfac
...
fix-lang-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6cbcd8280 | ||
|
|
23ef11d228 | ||
|
|
6c07e71b47 | ||
|
|
242b003048 |
@@ -579,7 +579,7 @@ checksums:
|
|||||||
environments/contacts/attribute_created_successfully: e9f90d366d817f2f1c81fb819c0e2f05
|
environments/contacts/attribute_created_successfully: e9f90d366d817f2f1c81fb819c0e2f05
|
||||||
environments/contacts/attribute_description: e17686a22ffad04cc7bb70524ed4478b
|
environments/contacts/attribute_description: e17686a22ffad04cc7bb70524ed4478b
|
||||||
environments/contacts/attribute_description_placeholder: 05af83e4cfc6328476ef9719581e47af
|
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_cannot_be_changed: 0ced703e77a8e620276c1fa21fcc8900
|
||||||
environments/contacts/attribute_key_hint: 1a68c6f91e1a5cf9eff811e2e54e92b8
|
environments/contacts/attribute_key_hint: 1a68c6f91e1a5cf9eff811e2e54e92b8
|
||||||
environments/contacts/attribute_key_placeholder: 31702e553b3f138a623dbaa42b6f878f
|
environments/contacts/attribute_key_placeholder: 31702e553b3f138a623dbaa42b6f878f
|
||||||
|
|||||||
@@ -167,6 +167,12 @@ export const createEnvironment = async (
|
|||||||
description: "Your contact's last name",
|
description: "Your contact's last name",
|
||||||
type: "default",
|
type: "default",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "language",
|
||||||
|
name: "Language",
|
||||||
|
description: "The language preference of a contact",
|
||||||
|
type: "default",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Attribut erfolgreich erstellt",
|
"attribute_created_successfully": "Attribut erfolgreich erstellt",
|
||||||
"attribute_description": "Beschreibung",
|
"attribute_description": "Beschreibung",
|
||||||
"attribute_description_placeholder": "Kurze 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_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_hint": "Nur Kleinbuchstaben, Zahlen und Unterstriche. Muss mit einem Buchstaben beginnen.",
|
||||||
"attribute_key_placeholder": "z. B. geburtsdatum",
|
"attribute_key_placeholder": "z. B. geburtsdatum",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Attribute created successfully",
|
"attribute_created_successfully": "Attribute created successfully",
|
||||||
"attribute_description": "Description",
|
"attribute_description": "Description",
|
||||||
"attribute_description_placeholder": "Short 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_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_hint": "Only lowercase letters, numbers, and underscores. Must start with a letter.",
|
||||||
"attribute_key_placeholder": "e.g. date_of_birth",
|
"attribute_key_placeholder": "e.g. date_of_birth",
|
||||||
@@ -1267,14 +1267,13 @@
|
|||||||
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
||||||
"date_format": "Date format",
|
"date_format": "Date format",
|
||||||
"days_before_showing_this_survey_again": "or more days to pass between the last shown survey and showing this survey.",
|
"days_before_showing_this_survey_again": "or more days to pass between the last shown survey and showing this survey.",
|
||||||
"display_type": "Display type",
|
|
||||||
"dropdown": "Dropdown",
|
|
||||||
"delete_anyways": "Delete anyways",
|
"delete_anyways": "Delete anyways",
|
||||||
"delete_block": "Delete block",
|
"delete_block": "Delete block",
|
||||||
"delete_choice": "Delete choice",
|
"delete_choice": "Delete choice",
|
||||||
"disable_the_visibility_of_survey_progress": "Disable the visibility of survey progress.",
|
"disable_the_visibility_of_survey_progress": "Disable the visibility of survey progress.",
|
||||||
"display_an_estimate_of_completion_time_for_survey": "Display an estimate of completion time for survey",
|
"display_an_estimate_of_completion_time_for_survey": "Display an estimate of completion time for survey",
|
||||||
"display_number_of_responses_for_survey": "Display number of responses for survey",
|
"display_number_of_responses_for_survey": "Display number of responses for survey",
|
||||||
|
"display_type": "Display type",
|
||||||
"divide": "Divide /",
|
"divide": "Divide /",
|
||||||
"does_not_contain": "Does not contain",
|
"does_not_contain": "Does not contain",
|
||||||
"does_not_end_with": "Does not end with",
|
"does_not_end_with": "Does not end with",
|
||||||
@@ -1282,6 +1281,7 @@
|
|||||||
"does_not_include_all_of": "Does not include all of",
|
"does_not_include_all_of": "Does not include all of",
|
||||||
"does_not_include_one_of": "Does not include one of",
|
"does_not_include_one_of": "Does not include one of",
|
||||||
"does_not_start_with": "Does not start with",
|
"does_not_start_with": "Does not start with",
|
||||||
|
"dropdown": "Dropdown",
|
||||||
"duplicate_block": "Duplicate block",
|
"duplicate_block": "Duplicate block",
|
||||||
"duplicate_question": "Duplicate question",
|
"duplicate_question": "Duplicate question",
|
||||||
"edit_link": "Edit link",
|
"edit_link": "Edit link",
|
||||||
@@ -1414,11 +1414,11 @@
|
|||||||
"limit_the_maximum_file_size": "Limit the maximum file size for uploads.",
|
"limit_the_maximum_file_size": "Limit the maximum file size for uploads.",
|
||||||
"limit_upload_file_size_to": "Limit upload file size to",
|
"limit_upload_file_size_to": "Limit upload file size to",
|
||||||
"link_survey_description": "Share a link to a survey page or embed it in a web page or email.",
|
"link_survey_description": "Share a link to a survey page or embed it in a web page or email.",
|
||||||
|
"list": "List",
|
||||||
"load_segment": "Load segment",
|
"load_segment": "Load segment",
|
||||||
"logic_error_warning": "Changing will cause logic errors",
|
"logic_error_warning": "Changing will cause logic errors",
|
||||||
"logic_error_warning_text": "Changing the question type will remove the logic conditions from this question",
|
"logic_error_warning_text": "Changing the question type will remove the logic conditions from this question",
|
||||||
"logo_settings": "Logo settings",
|
"logo_settings": "Logo settings",
|
||||||
"list": "List",
|
|
||||||
"long_answer": "Long answer",
|
"long_answer": "Long answer",
|
||||||
"long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.",
|
"long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.",
|
||||||
"lower_label": "Lower Label",
|
"lower_label": "Lower Label",
|
||||||
@@ -3094,4 +3094,4 @@
|
|||||||
"usability_rating_description": "Measure perceived usability by asking users to rate their experience with your product using a standardized 10-question survey.",
|
"usability_rating_description": "Measure perceived usability by asking users to rate their experience with your product using a standardized 10-question survey.",
|
||||||
"usability_score_name": "System Usability Score (SUS)"
|
"usability_score_name": "System Usability Score (SUS)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Atributo creado con éxito",
|
"attribute_created_successfully": "Atributo creado con éxito",
|
||||||
"attribute_description": "Descripción",
|
"attribute_description": "Descripción",
|
||||||
"attribute_description_placeholder": "Descripción breve",
|
"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_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_hint": "Solo letras minúsculas, números y guiones bajos. Debe empezar con una letra.",
|
||||||
"attribute_key_placeholder": "p. ej. fecha_de_nacimiento",
|
"attribute_key_placeholder": "p. ej. fecha_de_nacimiento",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Attribut créé avec succès",
|
"attribute_created_successfully": "Attribut créé avec succès",
|
||||||
"attribute_description": "Description",
|
"attribute_description": "Description",
|
||||||
"attribute_description_placeholder": "Brève 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_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_hint": "Uniquement des lettres minuscules, des chiffres et des underscores. Doit commencer par une lettre.",
|
||||||
"attribute_key_placeholder": "ex. date_de_naissance",
|
"attribute_key_placeholder": "ex. date_de_naissance",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "属性を作成しました",
|
"attribute_created_successfully": "属性を作成しました",
|
||||||
"attribute_description": "説明",
|
"attribute_description": "説明",
|
||||||
"attribute_description_placeholder": "簡単な説明",
|
"attribute_description_placeholder": "簡単な説明",
|
||||||
"attribute_key": "キー",
|
"attribute_key": "属性",
|
||||||
"attribute_key_cannot_be_changed": "キーは作成後に変更できません",
|
"attribute_key_cannot_be_changed": "キーは作成後に変更できません",
|
||||||
"attribute_key_hint": "小文字のアルファベット、数字、アンダースコアのみ使用可能です。アルファベットで始める必要があります。",
|
"attribute_key_hint": "小文字のアルファベット、数字、アンダースコアのみ使用可能です。アルファベットで始める必要があります。",
|
||||||
"attribute_key_placeholder": "例: date_of_birth",
|
"attribute_key_placeholder": "例: date_of_birth",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Attribuut succesvol aangemaakt",
|
"attribute_created_successfully": "Attribuut succesvol aangemaakt",
|
||||||
"attribute_description": "Beschrijving",
|
"attribute_description": "Beschrijving",
|
||||||
"attribute_description_placeholder": "Korte 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_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_hint": "Alleen kleine letters, cijfers en onderstrepingstekens. Moet beginnen met een letter.",
|
||||||
"attribute_key_placeholder": "bijv. geboortedatum",
|
"attribute_key_placeholder": "bijv. geboortedatum",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Atributo criado com sucesso",
|
"attribute_created_successfully": "Atributo criado com sucesso",
|
||||||
"attribute_description": "Descrição",
|
"attribute_description": "Descrição",
|
||||||
"attribute_description_placeholder": "Descrição curta",
|
"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_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_hint": "Apenas letras minúsculas, números e underscores. Deve começar com uma letra.",
|
||||||
"attribute_key_placeholder": "ex: data_de_nascimento",
|
"attribute_key_placeholder": "ex: data_de_nascimento",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Atributo criado com sucesso",
|
"attribute_created_successfully": "Atributo criado com sucesso",
|
||||||
"attribute_description": "Descrição",
|
"attribute_description": "Descrição",
|
||||||
"attribute_description_placeholder": "Descrição breve",
|
"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_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_hint": "Apenas letras minúsculas, números e sublinhados. Deve começar com uma letra.",
|
||||||
"attribute_key_placeholder": "ex. data_de_nascimento",
|
"attribute_key_placeholder": "ex. data_de_nascimento",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Atribut creat cu succes",
|
"attribute_created_successfully": "Atribut creat cu succes",
|
||||||
"attribute_description": "Descriere",
|
"attribute_description": "Descriere",
|
||||||
"attribute_description_placeholder": "Descriere scurtă",
|
"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_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_hint": "Doar litere mici, cifre și caractere de subliniere. Trebuie să înceapă cu o literă.",
|
||||||
"attribute_key_placeholder": "ex: date_of_birth",
|
"attribute_key_placeholder": "ex: date_of_birth",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Атрибут успешно создан",
|
"attribute_created_successfully": "Атрибут успешно создан",
|
||||||
"attribute_description": "Описание",
|
"attribute_description": "Описание",
|
||||||
"attribute_description_placeholder": "Краткое описание",
|
"attribute_description_placeholder": "Краткое описание",
|
||||||
"attribute_key": "Ключ",
|
"attribute_key": "Атрибут",
|
||||||
"attribute_key_cannot_be_changed": "Ключ нельзя изменить после создания",
|
"attribute_key_cannot_be_changed": "Ключ нельзя изменить после создания",
|
||||||
"attribute_key_hint": "Только строчные буквы, цифры и символы подчёркивания. Должен начинаться с буквы.",
|
"attribute_key_hint": "Только строчные буквы, цифры и символы подчёркивания. Должен начинаться с буквы.",
|
||||||
"attribute_key_placeholder": "например, date_of_birth",
|
"attribute_key_placeholder": "например, date_of_birth",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "Attributet har skapats",
|
"attribute_created_successfully": "Attributet har skapats",
|
||||||
"attribute_description": "Beskrivning",
|
"attribute_description": "Beskrivning",
|
||||||
"attribute_description_placeholder": "Kort 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_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_hint": "Endast små bokstäver, siffror och understreck. Måste börja med en bokstav.",
|
||||||
"attribute_key_placeholder": "t.ex. date_of_birth",
|
"attribute_key_placeholder": "t.ex. date_of_birth",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "属性创建成功",
|
"attribute_created_successfully": "属性创建成功",
|
||||||
"attribute_description": "描述",
|
"attribute_description": "描述",
|
||||||
"attribute_description_placeholder": "简短描述",
|
"attribute_description_placeholder": "简短描述",
|
||||||
"attribute_key": "键",
|
"attribute_key": "属性",
|
||||||
"attribute_key_cannot_be_changed": "创建后键不可更改",
|
"attribute_key_cannot_be_changed": "创建后键不可更改",
|
||||||
"attribute_key_hint": "仅允许小写字母、数字和下划线,且必须以字母开头。",
|
"attribute_key_hint": "仅允许小写字母、数字和下划线,且必须以字母开头。",
|
||||||
"attribute_key_placeholder": "例如:date_of_birth",
|
"attribute_key_placeholder": "例如:date_of_birth",
|
||||||
|
|||||||
@@ -615,7 +615,7 @@
|
|||||||
"attribute_created_successfully": "屬性建立成功",
|
"attribute_created_successfully": "屬性建立成功",
|
||||||
"attribute_description": "描述",
|
"attribute_description": "描述",
|
||||||
"attribute_description_placeholder": "簡短描述",
|
"attribute_description_placeholder": "簡短描述",
|
||||||
"attribute_key": "金鑰",
|
"attribute_key": "屬性",
|
||||||
"attribute_key_cannot_be_changed": "建立後無法變更金鑰",
|
"attribute_key_cannot_be_changed": "建立後無法變更金鑰",
|
||||||
"attribute_key_hint": "僅限小寫字母、數字和底線,且必須以字母開頭。",
|
"attribute_key_hint": "僅限小寫字母、數字和底線,且必須以字母開頭。",
|
||||||
"attribute_key_placeholder": "例如:date_of_birth",
|
"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 { getContact } from "@/modules/ee/contacts/lib/contacts";
|
||||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
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 }) => {
|
export const AttributesSection = async ({ contactId }: { contactId: string }) => {
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
const [contact, attributes] = await Promise.all([getContact(contactId), getContactAttributes(contactId)]);
|
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 responses = await getResponsesByContactId(contactId);
|
||||||
const numberOfResponses = responses?.length || 0;
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-bold text-slate-700">{t("common.attributes")}</h2>
|
<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>
|
<AttributeRow label="email" value={attributes.email} notProvidedText={notProvidedText} />
|
||||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
<AttributeRow label="language" value={attributes.language} notProvidedText={notProvidedText} />
|
||||||
{attributes.email ? (
|
<AttributeRow label="userId" value={attributes.userId} notProvidedText={notProvidedText} isIdBadge />
|
||||||
<span>{attributes.email}</span>
|
<AttributeRow label="firstName" value={attributes.firstName} notProvidedText={notProvidedText} />
|
||||||
) : (
|
<AttributeRow label="lastName" value={attributes.lastName} notProvidedText={notProvidedText} />
|
||||||
<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>
|
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm font-medium text-slate-500">contactId</dt>
|
<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>
|
<dd className="ph-no-capture mt-1 text-sm text-slate-900">{contact.id}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Object.entries(attributes)
|
{customAttributes.map(([key, value]) => (
|
||||||
.filter(([key, _]) => key !== "email" && key !== "userId" && key !== "language")
|
<div key={key}>
|
||||||
.map(([key, attributeData]) => {
|
<dt className="text-sm font-medium text-slate-500">{key}</dt>
|
||||||
return (
|
<dd className="mt-1 text-sm text-slate-900">{value}</dd>
|
||||||
<div key={key}>
|
</div>
|
||||||
<dt className="text-sm font-medium text-slate-500">{key}</dt>
|
))}
|
||||||
<dd className="mt-1 text-sm text-slate-900">{attributeData}</dd>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -175,6 +175,23 @@ export const EditContactAttributesModal = ({
|
|||||||
setOpen(newOpen);
|
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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent width="default" className="max-h-[90vh]">
|
<DialogContent width="default" className="max-h-[90vh]">
|
||||||
@@ -235,7 +252,7 @@ export const EditContactAttributesModal = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={["email", "userId", "firstName", "lastName"].includes(field.key)}
|
disabled={isDeleteDisabled(field.key)}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleRemoveAttribute(index)}
|
onClick={() => handleRemoveAttribute(index)}
|
||||||
className="h-10 w-10 p-0">
|
className="h-10 w-10 p-0">
|
||||||
|
|||||||
@@ -11,30 +11,27 @@ import {
|
|||||||
hasUserIdAttribute,
|
hasUserIdAttribute,
|
||||||
} from "@/modules/ee/contacts/lib/contact-attributes";
|
} 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 (
|
const deleteAttributes = async (
|
||||||
contactId: string,
|
contactId: string,
|
||||||
currentAttributes: TContactAttributes,
|
currentAttributes: TContactAttributes,
|
||||||
submittedAttributes: TContactAttributes,
|
submittedAttributes: TContactAttributes,
|
||||||
contactAttributeKeys: TContactAttributeKey[]
|
contactAttributeKeys: TContactAttributeKey[]
|
||||||
): Promise<{ success: boolean }> => {
|
): Promise<void> => {
|
||||||
const contactAttributeKeyMap = new Map(contactAttributeKeys.map((ack) => [ack.key, ack]));
|
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 submittedKeys = new Set(Object.keys(submittedAttributes));
|
||||||
const currentKeys = new Set(Object.keys(currentAttributes));
|
const keysToDelete = Object.keys(currentAttributes).filter((key) => !submittedKeys.has(key));
|
||||||
const keysToDelete = Array.from(currentKeys).filter(
|
|
||||||
(key) => !submittedKeys.has(key) && !DEFAULT_ATTRIBUTES.has(key)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get attribute key IDs for deletion
|
// Get attribute key IDs for deletion, but exclude default attributes
|
||||||
const attributeKeyIdsToDelete = keysToDelete
|
const attributeKeyIdsToDelete = keysToDelete
|
||||||
.map((key) => contactAttributeKeyMap.get(key)?.id)
|
.map((key) => {
|
||||||
|
const attributeKey = contactAttributeKeyMap.get(key);
|
||||||
|
// Only include non-default attributes for deletion
|
||||||
|
return attributeKey?.type === "custom" ? attributeKey.id : null;
|
||||||
|
})
|
||||||
.filter((id): id is string => !!id);
|
.filter((id): id is string => !!id);
|
||||||
|
|
||||||
// Delete attributes that were removed from the form (but not default attributes)
|
|
||||||
if (attributeKeyIdsToDelete.length > 0) {
|
if (attributeKeyIdsToDelete.length > 0) {
|
||||||
await prisma.contactAttribute.deleteMany({
|
await prisma.contactAttribute.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -45,10 +42,6 @@ const deleteAttributes = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,8 +154,10 @@ export const updateAttributes = async (
|
|||||||
// Delete attributes that were removed (only when explicitly requested)
|
// Delete attributes that were removed (only when explicitly requested)
|
||||||
// This is used by UI forms where all attributes are submitted
|
// This is used by UI forms where all attributes are submitted
|
||||||
// For API calls, we want merge behavior by default (only update passed attributes)
|
// 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) {
|
if (deleteRemovedAttributes) {
|
||||||
await deleteAttributes(contactId, currentAttributes, contactAttributesParam, contactAttributeKeys);
|
await deleteAttributes(contactId, currentAttributes, contactAttributes, contactAttributeKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create lookup map for attribute keys
|
// Create lookup map for attribute keys
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import toast from "react-hot-toast";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||||
import { TI18nString } from "@formbricks/types/i18n";
|
import { TI18nString } from "@formbricks/types/i18n";
|
||||||
import { TMultipleChoiceOptionDisplayType, TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
|
import {
|
||||||
|
TMultipleChoiceOptionDisplayType,
|
||||||
|
TSurveyElementTypeEnum,
|
||||||
|
TSurveyMultipleChoiceElement,
|
||||||
|
} from "@formbricks/types/surveys/elements";
|
||||||
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
|
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { v7 as uuidv7 } from "uuid";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import type { SurveyElement, ValidationRule } from "./types";
|
import type { SurveyElement, ValidationRule } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,7 +62,7 @@ export function migrateOpenTextCharLimit(element: SurveyElement): void {
|
|||||||
!hasMatchingRule(existingRules, "minLength", { min: element.charLimit.min })
|
!hasMatchingRule(existingRules, "minLength", { min: element.charLimit.min })
|
||||||
) {
|
) {
|
||||||
const newRule: ValidationRule = {
|
const newRule: ValidationRule = {
|
||||||
id: uuidv7(),
|
id: createId(),
|
||||||
type: "minLength",
|
type: "minLength",
|
||||||
params: { min: element.charLimit.min },
|
params: { min: element.charLimit.min },
|
||||||
};
|
};
|
||||||
@@ -78,7 +78,7 @@ export function migrateOpenTextCharLimit(element: SurveyElement): void {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
const newRule: ValidationRule = {
|
const newRule: ValidationRule = {
|
||||||
id: uuidv7(),
|
id: createId(),
|
||||||
type: "maxLength",
|
type: "maxLength",
|
||||||
params: { max: element.charLimit.max },
|
params: { max: element.charLimit.max },
|
||||||
};
|
};
|
||||||
@@ -138,7 +138,7 @@ export function migrateFileUploadExtensions(element: SurveyElement): void {
|
|||||||
if (!hasMatchingExtensionRule) {
|
if (!hasMatchingExtensionRule) {
|
||||||
// Create new fileExtensionIs rule
|
// Create new fileExtensionIs rule
|
||||||
const newRule: ValidationRule = {
|
const newRule: ValidationRule = {
|
||||||
id: uuidv7(),
|
id: createId(),
|
||||||
type: "fileExtensionIs",
|
type: "fileExtensionIs",
|
||||||
params: { extensions: [...extensions] },
|
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: "First Name", key: "firstName", isUnique: false, type: "default" },
|
||||||
{ name: "Last Name", key: "lastName", isUnique: false, type: "default" },
|
{ name: "Last Name", key: "lastName", isUnique: false, type: "default" },
|
||||||
{ name: "userId", key: "userId", isUnique: true, 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: "First Name", key: "firstName", isUnique: false, type: "default" },
|
||||||
{ name: "Last Name", key: "lastName", isUnique: false, type: "default" },
|
{ name: "Last Name", key: "lastName", isUnique: false, type: "default" },
|
||||||
{ name: "userId", key: "userId", isUnique: true, type: "default" },
|
{ name: "userId", key: "userId", isUnique: true, type: "default" },
|
||||||
|
{ name: "Language", key: "language", isUnique: false, type: "default" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -148,15 +148,15 @@ function DropdownVariant({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="rounded-input w-full justify-between bg-option-bg rounded-option border border-option-border my-0 h-input"
|
className="rounded-input bg-option-bg rounded-option border-option-border h-input my-0 w-full justify-between border"
|
||||||
aria-invalid={Boolean(errorMessage)}
|
aria-invalid={Boolean(errorMessage)}
|
||||||
aria-label={headline}>
|
aria-label={headline}>
|
||||||
<span className="truncate font-input font-input-weight text-input-text">{displayText}</span>
|
<span className="font-input font-input-weight text-input-text truncate">{displayText}</span>
|
||||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50 label-headline" />
|
<ChevronDown className="label-headline ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="bg-option-bg w-[var(--radix-dropdown-menu-trigger-width)] max-h-[300px] overflow-y-auto"
|
className="bg-option-bg max-h-[300px] w-[var(--radix-dropdown-menu-trigger-width)] overflow-y-auto"
|
||||||
align="start">
|
align="start">
|
||||||
{options
|
{options
|
||||||
.filter((option) => option.id !== "none")
|
.filter((option) => option.id !== "none")
|
||||||
|
|||||||
@@ -160,15 +160,15 @@ function SingleSelect({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="rounded-input w-full justify-between bg-option-bg rounded-option border border-option-border my-0 h-input"
|
className="rounded-input bg-option-bg rounded-option border-option-border h-input my-0 w-full justify-between border"
|
||||||
aria-invalid={Boolean(errorMessage)}
|
aria-invalid={Boolean(errorMessage)}
|
||||||
aria-label={headline}>
|
aria-label={headline}>
|
||||||
<span className="truncate font-input font-input-weight text-input-text">{displayText}</span>
|
<span className="font-input font-input-weight text-input-text truncate">{displayText}</span>
|
||||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50 label-headline" />
|
<ChevronDown className="label-headline ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="bg-option-bg w-[var(--radix-dropdown-menu-trigger-width)] max-h-[300px] overflow-y-auto"
|
className="bg-option-bg max-h-[300px] w-[var(--radix-dropdown-menu-trigger-width)] overflow-y-auto"
|
||||||
align="start">
|
align="start">
|
||||||
<DropdownMenuRadioGroup value={selectedValue} onValueChange={onChange}>
|
<DropdownMenuRadioGroup value={selectedValue} onValueChange={onChange}>
|
||||||
{options
|
{options
|
||||||
@@ -193,7 +193,9 @@ function SingleSelect({
|
|||||||
id={`${inputId}-${otherOptionId}`}
|
id={`${inputId}-${otherOptionId}`}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
disabled={disabled}>
|
disabled={disabled}>
|
||||||
<span className="font-input font-input-weight text-input-text">{otherValue || otherOptionLabel}</span>
|
<span className="font-input font-input-weight text-input-text">
|
||||||
|
{otherValue || otherOptionLabel}
|
||||||
|
</span>
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
) : null}
|
) : null}
|
||||||
{options
|
{options
|
||||||
@@ -279,7 +281,7 @@ function SingleSelect({
|
|||||||
aria-required={required}
|
aria-required={required}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={cn("mr-3 ml-3 grow", optionLabelClassName)}
|
className={cn("ml-3 mr-3 grow", optionLabelClassName)}
|
||||||
style={{ fontSize: "var(--fb-option-font-size)" }}>
|
style={{ fontSize: "var(--fb-option-font-size)" }}>
|
||||||
{otherOptionLabel}
|
{otherOptionLabel}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ function DropdownMenuContent({
|
|||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
"text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuPrimitive.Portal >
|
</DropdownMenuPrimitive.Portal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ function DropdownMenuItem({
|
|||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -76,12 +76,12 @@ function DropdownMenuCheckboxItem({
|
|||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_*]:cursor-pointer",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_*]:cursor-pointer [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}>
|
{...props}>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center label-headline">
|
<span className="label-headline pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
@@ -104,11 +104,11 @@ function DropdownMenuRadioItem({
|
|||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_*]:cursor-pointer",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_*]:cursor-pointer [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}>
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center label-headline">
|
<span className="label-headline pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckIcon className="size-4" />
|
<CheckIcon className="size-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
@@ -175,12 +175,12 @@ function DropdownMenuSubTrigger({
|
|||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none focus-visible:outline-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus-visible:outline-none data-[inset]:pl-8 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRightIcon className="ml-auto label-headline size-4" />
|
<ChevronRightIcon className="label-headline ml-auto size-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input(
|
|||||||
style={{ fontSize: "var(--fb-input-font-size)" }}
|
style={{ fontSize: "var(--fb-input-font-size)" }}
|
||||||
className={cn(
|
className={cn(
|
||||||
// Layout and behavior
|
// Layout and behavior
|
||||||
"flex min-w-0 border transition-[color,box-shadow] outline-none",
|
"flex min-w-0 border outline-none transition-[color,box-shadow]",
|
||||||
// Customizable styles via CSS variables (using Tailwind theme extensions)
|
// Customizable styles via CSS variables (using Tailwind theme extensions)
|
||||||
"w-input h-input",
|
"w-input h-input",
|
||||||
"bg-input-bg border-input-border rounded-input",
|
"bg-input-bg border-input-border rounded-input",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function Textarea({ className, dir = "auto", ...props }: TextareaProps): React.J
|
|||||||
style={{ fontSize: "var(--fb-input-font-size)" }}
|
style={{ fontSize: "var(--fb-input-font-size)" }}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-input bg-input-bg border-input-border rounded-input font-input font-input-weight px-input-x py-input-y shadow-input placeholder:text-input-placeholder placeholder:opacity-input-placeholder focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 text-input text-input-text flex field-sizing-content min-h-16 border transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"w-input bg-input-bg border-input-border rounded-input font-input font-input-weight px-input-x py-input-y shadow-input placeholder:text-input-placeholder placeholder:opacity-input-placeholder focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 text-input text-input-text field-sizing-content flex min-h-16 border outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"respondents_will_not_see_this_card": "Respondents will not see this card",
|
"respondents_will_not_see_this_card": "Respondents will not see this card",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"retrying": "Retrying...",
|
"retrying": "Retrying...",
|
||||||
|
"select_option": "Select an option",
|
||||||
|
"select_options": "Select options",
|
||||||
"sending_responses": "Sending responses...",
|
"sending_responses": "Sending responses...",
|
||||||
"takes_less_than_x_minutes": "{count, plural, one {Takes less than 1 minute} other {Takes less than {count} minutes}}",
|
"takes_less_than_x_minutes": "{count, plural, one {Takes less than 1 minute} other {Takes less than {count} minutes}}",
|
||||||
"takes_x_minutes": "{count, plural, one {Takes 1 minute} other {Takes {count} minutes}}",
|
"takes_x_minutes": "{count, plural, one {Takes 1 minute} other {Takes {count} minutes}}",
|
||||||
@@ -28,9 +30,7 @@
|
|||||||
"terms_of_service": "Terms of Service",
|
"terms_of_service": "Terms of Service",
|
||||||
"the_servers_cannot_be_reached_at_the_moment": "The servers cannot be reached at the moment.",
|
"the_servers_cannot_be_reached_at_the_moment": "The servers cannot be reached at the moment.",
|
||||||
"they_will_be_redirected_immediately": "They will be redirected immediately",
|
"they_will_be_redirected_immediately": "They will be redirected immediately",
|
||||||
"your_feedback_is_stuck": "Your feedback is stuck :(",
|
"your_feedback_is_stuck": "Your feedback is stuck :("
|
||||||
"select_option": "Select an option",
|
|
||||||
"select_options": "Select options"
|
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"all_options_must_be_ranked": "Please rank all options",
|
"all_options_must_be_ranked": "Please rank all options",
|
||||||
@@ -78,4 +78,4 @@
|
|||||||
"value_must_not_contain": "Value must not contain {value}",
|
"value_must_not_contain": "Value must not contain {value}",
|
||||||
"value_must_not_equal": "Value must not equal {value}"
|
"value_must_not_equal": "Value must not equal {value}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,4 +70,4 @@
|
|||||||
"vite-plugin-dts": "4.5.3",
|
"vite-plugin-dts": "4.5.3",
|
||||||
"vite-tsconfig-paths": "5.1.4"
|
"vite-tsconfig-paths": "5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user