diff --git a/apps/web/app/(app)/environments/[environmentId]/(contacts)/attributes/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(contacts)/attributes/page.tsx new file mode 100644 index 0000000000..939c3f2b6d --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(contacts)/attributes/page.tsx @@ -0,0 +1 @@ +export { AttributesPage as default } from "@/modules/ee/contacts/attributes/page"; diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index 2dec53aee9..94c601b146 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -573,7 +573,6 @@ checksums: environments/contacts/attribute_description: e17686a22ffad04cc7bb70524ed4478b environments/contacts/attribute_description_placeholder: 05af83e4cfc6328476ef9719581e47af environments/contacts/attribute_key: 3d1065ab98a1c2f1210507fd5c7bf515 - environments/contacts/attribute_key_already_exists: c1ac75f3324781bdc165cf4567ff6497 environments/contacts/attribute_key_cannot_be_changed: 0ced703e77a8e620276c1fa21fcc8900 environments/contacts/attribute_key_hint: 1a68c6f91e1a5cf9eff811e2e54e92b8 environments/contacts/attribute_key_placeholder: 31702e553b3f138a623dbaa42b6f878f diff --git a/apps/web/lingodotdev/language.ts b/apps/web/lingodotdev/language.ts index d0f114fd71..1def6106d7 100644 --- a/apps/web/lingodotdev/language.ts +++ b/apps/web/lingodotdev/language.ts @@ -1,12 +1,13 @@ import { getServerSession } from "next-auth"; +import { TUserLocale } from "@formbricks/types/user"; import { DEFAULT_LOCALE } from "@/lib/constants"; import { getUserLocale } from "@/lib/user/service"; import { findMatchingLocale } from "@/lib/utils/locale"; import { authOptions } from "@/modules/auth/lib/authOptions"; -export const getLocale = async (): Promise => { +export const getLocale = async (): Promise => { const session = await getServerSession(authOptions); - let locale: string | undefined; + let locale: TUserLocale | undefined; if (session?.user?.id) { locale = await getUserLocale(session.user.id); } else { diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index ad54e2de09..829a87465f 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -609,7 +609,6 @@ "attribute_description": "Beschreibung", "attribute_description_placeholder": "Kurze Beschreibung", "attribute_key": "Schlüssel", - "attribute_key_already_exists": "Attributschlüssel existiert bereits", "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", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 6626412b0e..4c2eff7e04 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -171,9 +171,9 @@ "copy": "Copy", "copy_code": "Copy code", "copy_link": "Copy Link", + "count_attributes": "{value, plural, one {{value} attribute} other {{value} attributes}}", "count_contacts": "{value, plural, one {{value} contact} other {{value} contacts}}", "count_responses": "{value, plural, one {{value} response} other {{value} responses}}", - "count_attributes": "{value, plural, one {{value} attribute} other {{value} attributes}}", "create_new_organization": "Create new organization", "create_project": "Create project", "create_segment": "Create segment", @@ -276,7 +276,6 @@ "move_up": "Move up", "multiple_languages": "Multiple languages", "name": "Name", - "optional": "Optional", "new": "New", "new_version_available": "Formbricks {version} is here. Upgrade now!", "next": "Next", @@ -300,6 +299,7 @@ "only_owners_managers_and_manage_access_members_can_perform_this_action": "Only owners and managers can perform this action.", "option_id": "Option ID", "option_ids": "Option IDs", + "optional": "Optional", "or": "or", "organization": "Organization", "organization_id": "Organization ID", @@ -605,12 +605,31 @@ "waiting_for_your_signal": "Waiting for your signal..." }, "contacts": { + "attribute_created_successfully": "Attribute created successfully", + "attribute_description": "Description", + "attribute_description_placeholder": "Short description", + "attribute_key": "Key", + "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", + "attribute_key_required": "Key is required", + "attribute_key_safe_identifier_required": "Key must be a safe identifier: only lowercase letters, numbers, and underscores, and must start with a letter", + "attribute_label": "Label", + "attribute_label_placeholder": "e.g. Date of Birth", + "attribute_updated_successfully": "Attribute updated successfully", "contact_deleted_successfully": "Contact deleted successfully", "contact_not_found": "No such contact found", "contacts_table_refresh": "Refresh contacts", "contacts_table_refresh_success": "Contacts refreshed successfully", + "create_attribute": "Create attribute", + "create_key": "Create Key", + "create_new_attribute": "Create new attribute", + "create_new_attribute_description": "Create a new attribute for segmentation purposes.", + "delete_attribute_confirmation": "{value, plural, one {This will delete the selected attribute. Any contact data associated with this attribute will be lost.} other {This will delete the selected attributes. Any contact data associated with these attributes will be lost.}}", "delete_contact_confirmation": "This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contact's data will be lost.", "delete_contact_confirmation_with_quotas": "{value, plural, one {This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contact's data will be lost. If this contact has responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.} other {This will delete all survey responses and contact attributes associated with these contacts. Any targeting and personalization based on these contacts' data will be lost. If these contacts have responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.}}", + "edit_attribute": "Edit Attribute", + "edit_attribute_description": "Update the label and description for this attribute.", "generate_personal_link": "Generate Personal Link", "generate_personal_link_description": "Select a published survey to generate a personalized link for this contact.", "no_published_link_surveys_available": "No published link surveys available. Please publish a link survey first.", @@ -621,30 +640,10 @@ "personal_link_generated_but_clipboard_failed": "Personal link generated but failed to copy to clipboard: {url}", "personal_survey_link": "Personal Survey Link", "please_select_a_survey": "Please select a survey", - "search_contact": "Search contact", "search_attribute_keys": "Search attribute keys...", + "search_contact": "Search contact", "select_a_survey": "Select a survey", "select_attribute": "Select Attribute", - "create_attribute": "Create attribute", - "create_new_attribute": "Create new attribute", - "create_new_attribute_description": "Create a new attribute for segmentation purposes.", - "create_key": "Create Key", - "edit_attribute": "Edit Attribute", - "edit_attribute_description": "Update the label and description for this attribute.", - "attribute_key": "Key", - "attribute_key_placeholder": "e.g. date_of_birth", - "attribute_key_hint": "Only lowercase letters, numbers, and underscores. Must start with a letter.", - "attribute_key_required": "Key is required", - "attribute_key_safe_identifier_required": "Key must be a safe identifier: only lowercase letters, numbers, and underscores, and must start with a letter", - "attribute_key_already_exists": "Attribute key already exists", - "attribute_key_cannot_be_changed": "Key cannot be changed after creation", - "attribute_label": "Label", - "attribute_label_placeholder": "e.g. Date of Birth", - "attribute_description": "Description", - "attribute_description_placeholder": "Short description", - "attribute_created_successfully": "Attribute created successfully", - "attribute_updated_successfully": "Attribute updated successfully", - "delete_attribute_confirmation": "{value, plural, one {This will delete the selected attribute. Any contact data associated with this attribute will be lost.} other {This will delete the selected attributes. Any contact data associated with these attributes will be lost.}}", "unlock_contacts_description": "Manage contacts and send out targeted surveys", "unlock_contacts_title": "Unlock contacts with a higher plan", "upload_contacts_modal_attributes_description": "Map the columns in your CSV to the attributes in Formbricks.", @@ -2996,4 +2995,4 @@ "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)" } -} \ No newline at end of file +} diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index c5fa40ac61..76cf6079bc 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -609,7 +609,6 @@ "attribute_description": "Descripción", "attribute_description_placeholder": "Descripción breve", "attribute_key": "Clave", - "attribute_key_already_exists": "La clave del atributo ya existe", "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", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index d92f300e39..55495b4862 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -609,7 +609,6 @@ "attribute_description": "Description", "attribute_description_placeholder": "Brève description", "attribute_key": "Clé", - "attribute_key_already_exists": "La clé d'attribut existe déjà", "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", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index 4f513e9062..baa7c7aaa5 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -609,7 +609,6 @@ "attribute_description": "説明", "attribute_description_placeholder": "簡単な説明", "attribute_key": "キー", - "attribute_key_already_exists": "属性キーは既に存在します", "attribute_key_cannot_be_changed": "キーは作成後に変更できません", "attribute_key_hint": "小文字のアルファベット、数字、アンダースコアのみ使用可能です。アルファベットで始める必要があります。", "attribute_key_placeholder": "例: date_of_birth", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index d8c6d8c323..62c20caf3b 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -609,7 +609,6 @@ "attribute_description": "Beschrijving", "attribute_description_placeholder": "Korte beschrijving", "attribute_key": "Sleutel", - "attribute_key_already_exists": "Attribuutsleutel bestaat al", "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", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 3bcc757633..6dc5672d8a 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -609,7 +609,6 @@ "attribute_description": "Descrição", "attribute_description_placeholder": "Descrição curta", "attribute_key": "Chave", - "attribute_key_already_exists": "A chave do atributo já existe", "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", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index c862537f32..1bb0956c54 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -609,7 +609,6 @@ "attribute_description": "Descrição", "attribute_description_placeholder": "Descrição breve", "attribute_key": "Chave", - "attribute_key_already_exists": "A chave do atributo já existe", "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", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 62c9b1084d..44ec7ecf79 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -609,7 +609,6 @@ "attribute_description": "Descriere", "attribute_description_placeholder": "Descriere scurtă", "attribute_key": "Cheie", - "attribute_key_already_exists": "Cheia atributului există deja", "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", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index ea9aa54e27..82166ae5ae 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -609,7 +609,6 @@ "attribute_description": "Описание", "attribute_description_placeholder": "Краткое описание", "attribute_key": "Ключ", - "attribute_key_already_exists": "Ключ атрибута уже существует", "attribute_key_cannot_be_changed": "Ключ нельзя изменить после создания", "attribute_key_hint": "Только строчные буквы, цифры и символы подчёркивания. Должен начинаться с буквы.", "attribute_key_placeholder": "например, date_of_birth", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index 43800c945a..3ce4df02c7 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -609,7 +609,6 @@ "attribute_description": "Beskrivning", "attribute_description_placeholder": "Kort beskrivning", "attribute_key": "Nyckel", - "attribute_key_already_exists": "Attributnyckeln finns redan", "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", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 4b2cb1fa89..3a757594fd 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -609,7 +609,6 @@ "attribute_description": "描述", "attribute_description_placeholder": "简短描述", "attribute_key": "键", - "attribute_key_already_exists": "属性键已存在", "attribute_key_cannot_be_changed": "创建后键不可更改", "attribute_key_hint": "仅允许小写字母、数字和下划线,且必须以字母开头。", "attribute_key_placeholder": "例如:date_of_birth", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 0ceec443d3..bb57444b62 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -609,7 +609,6 @@ "attribute_description": "描述", "attribute_description_placeholder": "簡短描述", "attribute_key": "金鑰", - "attribute_key_already_exists": "屬性金鑰已存在", "attribute_key_cannot_be_changed": "建立後無法變更金鑰", "attribute_key_hint": "僅限小寫字母、數字和底線,且必須以字母開頭。", "attribute_key_placeholder": "例如:date_of_birth", diff --git a/apps/web/modules/api/v2/management/contact-attribute-keys/route.ts b/apps/web/modules/api/v2/management/contact-attribute-keys/route.ts index 4a8e521073..6d25d2b5c0 100644 --- a/apps/web/modules/api/v2/management/contact-attribute-keys/route.ts +++ b/apps/web/modules/api/v2/management/contact-attribute-keys/route.ts @@ -49,7 +49,7 @@ export const POST = async (request: NextRequest) => authenticatedApiClient({ request, schemas: { - body: ZContactAttributeKeyInput, + body: ZContactAttributeKeyInput.sourceType(), }, handler: async ({ authentication, parsedInput, auditLog }) => { const { body } = parsedInput; diff --git a/apps/web/modules/ee/contacts/attributes/components/attribute-table-column.tsx b/apps/web/modules/ee/contacts/attributes/components/attribute-table-column.tsx index a022a1fe49..b7d971e81d 100644 --- a/apps/web/modules/ee/contacts/attributes/components/attribute-table-column.tsx +++ b/apps/web/modules/ee/contacts/attributes/components/attribute-table-column.tsx @@ -1,83 +1,79 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { format, formatDistanceToNow } from "date-fns"; +import { format } from "date-fns"; +import { TFunction } from "i18next"; +import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; +import { TUserLocale } from "@formbricks/types/user"; +import { timeSince } from "@/lib/time"; import { getSelectionColumn } from "@/modules/ui/components/data-table"; import { HighlightedText } from "@/modules/ui/components/highlighted-text"; import { IdBadge } from "@/modules/ui/components/id-badge"; -import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { TFunction } from "i18next"; export const generateAttributeTableColumns = ( - searchValue: string, - isReadOnly: boolean, - isExpanded: boolean, - t: TFunction + searchValue: string, + isReadOnly: boolean, + isExpanded: boolean, + t: TFunction, + locale: TUserLocale ): ColumnDef[] => { - const labelColumn: ColumnDef = { - id: "name", - accessorKey: "name", - header: t("common.label"), - cell: ({ row }) => { - const name = row.original.name ?? row.original.key; - return ; - }, - }; + const labelColumn: ColumnDef = { + id: "name", + accessorKey: "name", + header: t("common.label"), + cell: ({ row }) => { + const name = row.original.name ?? row.original.key; + return ; + }, + }; - const keyColumn: ColumnDef = { - id: "key", - accessorKey: "key", - header: t("common.key"), - cell: ({ row }) => { - const key = row.original.key; - return ; - }, - }; + const keyColumn: ColumnDef = { + id: "key", + accessorKey: "key", + header: t("common.key"), + cell: ({ row }) => { + const key = row.original.key; + return ; + }, + }; - const descriptionColumn: ColumnDef = { - id: "description", - accessorKey: "description", - header: t("common.description"), - cell: ({ row }) => { - const description = row.original.description; - return description ? ( -
- -
- ) : ( - - - ); - }, - }; + const descriptionColumn: ColumnDef = { + id: "description", + accessorKey: "description", + header: t("common.description"), + cell: ({ row }) => { + const description = row.original.description; + return description ? ( +
+ +
+ ) : ( + - + ); + }, + }; - const createdAtColumn: ColumnDef = { - id: "createdAt", - accessorKey: "createdAt", - header: t("common.created_at"), - cell: ({ row }) => { - const createdAt = row.original.createdAt; - return {format(createdAt, "do 'of' MMMM, yyyy")}; - }, - }; + const createdAtColumn: ColumnDef = { + id: "createdAt", + accessorKey: "createdAt", + header: t("common.created_at"), + cell: ({ row }) => { + const createdAt = row.original.createdAt; + return {format(createdAt, "do 'of' MMMM, yyyy")}; + }, + }; - const updatedAtColumn: ColumnDef = { - id: "updatedAt", - accessorKey: "updatedAt", - header: t("common.updated_at"), - cell: ({ row }) => { - const updatedAt = row.original.updatedAt; - return ( - - {formatDistanceToNow(updatedAt, { - addSuffix: true, - }).replace("about", "")} - - ); - }, - }; + const updatedAtColumn: ColumnDef = { + id: "updatedAt", + accessorKey: "updatedAt", + header: t("common.updated_at"), + cell: ({ row }) => { + const updatedAt = row.original.updatedAt; + return {timeSince(updatedAt.toISOString(), locale)}; + }, + }; - const baseColumns = [labelColumn, keyColumn, descriptionColumn, createdAtColumn, updatedAtColumn]; + const baseColumns = [labelColumn, keyColumn, descriptionColumn, createdAtColumn, updatedAtColumn]; - return isReadOnly ? baseColumns : [getSelectionColumn(), ...baseColumns]; + return isReadOnly ? baseColumns : [getSelectionColumn(), ...baseColumns]; }; - diff --git a/apps/web/modules/ee/contacts/attributes/components/attributes-table.tsx b/apps/web/modules/ee/contacts/attributes/components/attributes-table.tsx index 9bed5549c8..6cf63fc0e8 100644 --- a/apps/web/modules/ee/contacts/attributes/components/attributes-table.tsx +++ b/apps/web/modules/ee/contacts/attributes/components/attributes-table.tsx @@ -1,14 +1,14 @@ "use client"; import { - DndContext, - type DragEndEvent, - KeyboardSensor, - MouseSensor, - TouchSensor, - closestCenter, - useSensor, - useSensors, + DndContext, + type DragEndEvent, + KeyboardSensor, + MouseSensor, + TouchSensor, + closestCenter, + useSensor, + useSensors, } from "@dnd-kit/core"; import { restrictToHorizontalAxis } from "@dnd-kit/modifiers"; import { SortableContext, arrayMove, horizontalListSortingStrategy } from "@dnd-kit/sortable"; @@ -16,328 +16,328 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { VisibilityState, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; -import { cn } from "@/lib/cn"; import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { deleteContactAttributeKeyAction } from "../actions"; +import { TUserLocale } from "@formbricks/types/user"; +import { cn } from "@/lib/cn"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { - DataTableHeader, - DataTableSettingsModal, - DataTableToolbar, + DataTableHeader, + DataTableSettingsModal, + DataTableToolbar, } from "@/modules/ui/components/data-table"; import { getCommonPinningStyles } from "@/modules/ui/components/data-table/lib/utils"; import { SearchBar } from "@/modules/ui/components/search-bar"; import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/modules/ui/components/table"; +import { deleteContactAttributeKeyAction } from "../actions"; import { generateAttributeTableColumns } from "./attribute-table-column"; import { EditAttributeModal } from "./edit-attribute-modal"; -import { toast } from "react-hot-toast"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; interface AttributesTableProps { - contactAttributeKeys: TContactAttributeKey[]; - isReadOnly: boolean; - environmentId: string; + contactAttributeKeys: TContactAttributeKey[]; + isReadOnly: boolean; + environmentId: string; + locale: TUserLocale; } export const AttributesTable = ({ - contactAttributeKeys, - isReadOnly, - environmentId, + contactAttributeKeys, + isReadOnly, + environmentId, + locale, }: AttributesTableProps) => { - const [columnVisibility, setColumnVisibility] = useState({}); - const [columnOrder, setColumnOrder] = useState([]); - const [isTableSettingsModalOpen, setIsTableSettingsModalOpen] = useState(false); - const [isExpanded, setIsExpanded] = useState(null); - const [rowSelection, setRowSelection] = useState({}); - const [searchValue, setSearchValue] = useState(""); - const [editingAttribute, setEditingAttribute] = useState(null); - const router = useRouter(); - const { t } = useTranslation(); + const [columnVisibility, setColumnVisibility] = useState({}); + const [columnOrder, setColumnOrder] = useState([]); + const [isTableSettingsModalOpen, setIsTableSettingsModalOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(null); + const [rowSelection, setRowSelection] = useState({}); + const [searchValue, setSearchValue] = useState(""); + const [editingAttribute, setEditingAttribute] = useState(null); + const router = useRouter(); + const { t } = useTranslation(); - const [parent] = useAutoAnimate(); + const [parent] = useAutoAnimate(); - // Filter attributes based on search - const filteredAttributes = useMemo(() => { - if (!searchValue) return contactAttributeKeys; - const searchLower = searchValue.toLowerCase(); - return contactAttributeKeys.filter( - (attr) => - attr.key.toLowerCase().includes(searchLower) || - attr.name?.toLowerCase().includes(searchLower) || - attr.description?.toLowerCase().includes(searchLower) - ); - }, [contactAttributeKeys, searchValue]); - - // Generate columns - const columns = useMemo(() => { - return generateAttributeTableColumns(searchValue, isReadOnly, isExpanded ?? false, t); - }, [searchValue, isReadOnly, isExpanded]); - - // Load saved settings from localStorage - useEffect(() => { - const savedColumnOrder = localStorage.getItem(`${environmentId}-attributes-columnOrder`); - const savedColumnVisibility = localStorage.getItem(`${environmentId}-attributes-columnVisibility`); - const savedExpandedSettings = localStorage.getItem(`${environmentId}-attributes-rowExpand`); - - let savedColumnOrderParsed: string[] = []; - if (savedColumnOrder) { - try { - savedColumnOrderParsed = JSON.parse(savedColumnOrder); - } catch (err) { - console.error(err); - } - } - - if ( - savedColumnOrderParsed.length > 0 && - table.getAllLeafColumns().length === savedColumnOrderParsed.length - ) { - setColumnOrder(savedColumnOrderParsed); - } else { - setColumnOrder(table.getAllLeafColumns().map((d) => d.id)); - } - - let savedColumnVisibilityParsed: VisibilityState = {}; - if (savedColumnVisibility) { - try { - savedColumnVisibilityParsed = JSON.parse(savedColumnVisibility); - } catch (err) { - console.error(err); - } - } - - if ( - savedColumnVisibilityParsed && - Object.keys(savedColumnVisibilityParsed).length === table.getAllLeafColumns().length - ) { - setColumnVisibility(savedColumnVisibilityParsed); - } else { - const initialVisibility = table - .getAllLeafColumns() - .map((column) => column.id) - .reduce((acc, curr) => { - acc[curr] = true; - return acc; - }, {}) as Record; - - setColumnVisibility(initialVisibility); - } - - if (savedExpandedSettings !== null) { - setIsExpanded(JSON.parse(savedExpandedSettings)); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [environmentId]); - - // Save settings to localStorage when they change - useEffect(() => { - if (columnOrder.length > 0) { - localStorage.setItem(`${environmentId}-attributes-columnOrder`, JSON.stringify(columnOrder)); - } - - if (Object.keys(columnVisibility).length > 0) { - localStorage.setItem( - `${environmentId}-attributes-columnVisibility`, - JSON.stringify(columnVisibility) - ); - } - - if (isExpanded !== null) { - localStorage.setItem(`${environmentId}-attributes-rowExpand`, JSON.stringify(isExpanded)); - } - }, [columnOrder, columnVisibility, isExpanded, environmentId]); - - // Initialize DnD sensors - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) + // Filter attributes based on search + const filteredAttributes = useMemo(() => { + if (!searchValue) return contactAttributeKeys; + const searchLower = searchValue.toLowerCase(); + return contactAttributeKeys.filter( + (attr) => + attr.key.toLowerCase().includes(searchLower) || + attr.name?.toLowerCase().includes(searchLower) || + attr.description?.toLowerCase().includes(searchLower) ); + }, [contactAttributeKeys, searchValue]); - // React Table instance - const table = useReactTable({ - data: filteredAttributes, - columns, - getRowId: (originalRow) => originalRow.id, - getCoreRowModel: getCoreRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - onColumnOrderChange: setColumnOrder, - columnResizeMode: "onChange", - columnResizeDirection: "ltr", - defaultColumn: { maxSize: 1000, size: 300 }, - enableRowSelection: (row) => { - // Only allow selection of custom attributes - return row.original.type === "custom"; - }, - state: { - columnOrder, - columnVisibility, - rowSelection, - columnPinning: { - left: ["select", "createdAt"], - }, - }, - }); + // Generate columns + const columns = useMemo(() => { + return generateAttributeTableColumns(searchValue, isReadOnly, isExpanded ?? false, t, locale); + }, [searchValue, isReadOnly, isExpanded]); - // Handle column drag end - const handleDragEnd = (event: DragEndEvent) => { - const { active, over } = event; - if (active && over && active.id !== over.id) { - setColumnOrder((prevOrder) => { - const oldIndex = prevOrder.indexOf(active.id as string); - const newIndex = prevOrder.indexOf(over.id as string); - return arrayMove(prevOrder, oldIndex, newIndex); - }); - } - }; + // Load saved settings from localStorage + useEffect(() => { + const savedColumnOrder = localStorage.getItem(`${environmentId}-attributes-columnOrder`); + const savedColumnVisibility = localStorage.getItem(`${environmentId}-attributes-columnVisibility`); + const savedExpandedSettings = localStorage.getItem(`${environmentId}-attributes-rowExpand`); - const deleteAttribute = async (attributeId: string) => { - const deleteContactAttributeKeyResponse = await deleteContactAttributeKeyAction({ id: attributeId }); - if (!deleteContactAttributeKeyResponse?.data) { - const errorMessage = getFormattedErrorMessage(deleteContactAttributeKeyResponse); - toast.error(errorMessage); - } - }; + let savedColumnOrderParsed: string[] = []; + if (savedColumnOrder) { + try { + savedColumnOrderParsed = JSON.parse(savedColumnOrder); + } catch (err) { + console.error(err); + } + } - const updateAttributeList = () => { - router.refresh(); - }; + if ( + savedColumnOrderParsed.length > 0 && + table.getAllLeafColumns().length === savedColumnOrderParsed.length + ) { + setColumnOrder(savedColumnOrderParsed); + } else { + setColumnOrder(table.getAllLeafColumns().map((d) => d.id)); + } - return ( -
- - - -
- } - /> -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - - {headerGroup.headers.map((header) => ( - - ))} - - - ))} - + let savedColumnVisibilityParsed: VisibilityState = {}; + if (savedColumnVisibility) { + try { + savedColumnVisibilityParsed = JSON.parse(savedColumnVisibility); + } catch (err) { + console.error(err); + } + } - - {table.getRowModel().rows.map((row) => { - const attribute = row.original; - const isSystemAttribute = attribute.type === "default"; - const isSelectable = !isSystemAttribute && !isReadOnly; + if ( + savedColumnVisibilityParsed && + Object.keys(savedColumnVisibilityParsed).length === table.getAllLeafColumns().length + ) { + setColumnVisibility(savedColumnVisibilityParsed); + } else { + const initialVisibility = table + .getAllLeafColumns() + .map((column) => column.id) + .reduce((acc, curr) => { + acc[curr] = true; + return acc; + }, {}) as Record; - return ( - - {row.getVisibleCells().map((cell) => { - // Disable selection for system attributes - if (cell.column.id === "select" && isSystemAttribute) { - return ( - -
- {/* Empty checkbox space for system attributes */} -
-
- ); - } + setColumnVisibility(initialVisibility); + } - return ( - { - if (cell.column.id === "select") return; - if (isSelectable) { - setEditingAttribute(attribute); - } - }} - style={cell.column.id === "select" ? getCommonPinningStyles(cell.column) : {}} - className={cn( - "border-slate-200 bg-white px-4 py-2 shadow-none", - { - "group-hover:bg-slate-100": isSelectable, - "bg-slate-100": row.getIsSelected() && isSelectable, - } - )}> -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- ); - })} -
- ); - })} - {table.getRowModel().rows.length === 0 && ( - - - {t("common.no_results")} - - - )} -
-
-
+ if (savedExpandedSettings !== null) { + try { + setIsExpanded(JSON.parse(savedExpandedSettings)); + } catch (err) { + console.error(err); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [environmentId]); - - + // Save settings to localStorage when they change + useEffect(() => { + if (columnOrder.length > 0) { + localStorage.setItem(`${environmentId}-attributes-columnOrder`, JSON.stringify(columnOrder)); + } - {editingAttribute && ( - { - if (!open) setEditingAttribute(null); - }} - /> - )} + if (Object.keys(columnVisibility).length > 0) { + localStorage.setItem(`${environmentId}-attributes-columnVisibility`, JSON.stringify(columnVisibility)); + } + + if (isExpanded !== null) { + localStorage.setItem(`${environmentId}-attributes-rowExpand`, JSON.stringify(isExpanded)); + } + }, [columnOrder, columnVisibility, isExpanded, environmentId]); + + // Initialize DnD sensors + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}) + ); + + // React Table instance + const table = useReactTable({ + data: filteredAttributes, + columns, + getRowId: (originalRow) => originalRow.id, + getCoreRowModel: getCoreRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onColumnOrderChange: setColumnOrder, + columnResizeMode: "onChange", + columnResizeDirection: "ltr", + defaultColumn: { maxSize: 1000, size: 300 }, + enableRowSelection: (row) => { + // Only allow selection of custom attributes + return row.original.type === "custom"; + }, + state: { + columnOrder, + columnVisibility, + rowSelection, + columnPinning: { + left: ["select", "createdAt"], + }, + }, + }); + + // Handle column drag end + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (active && over && active.id !== over.id) { + setColumnOrder((prevOrder) => { + const oldIndex = prevOrder.indexOf(active.id as string); + const newIndex = prevOrder.indexOf(over.id as string); + return arrayMove(prevOrder, oldIndex, newIndex); + }); + } + }; + + const deleteAttribute = async (attributeId: string) => { + const deleteContactAttributeKeyResponse = await deleteContactAttributeKeyAction({ id: attributeId }); + if (!deleteContactAttributeKeyResponse?.data) { + const errorMessage = getFormattedErrorMessage(deleteContactAttributeKeyResponse); + toast.error(errorMessage); + } + }; + + const updateAttributeList = () => { + router.refresh(); + }; + + return ( +
+ + + +
+ } + /> +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + + {headerGroup.headers.map((header) => ( + + ))} + + + ))} + + + + {table.getRowModel().rows.map((row) => { + const attribute = row.original; + const isSystemAttribute = attribute.type === "default"; + const isSelectable = !isSystemAttribute && !isReadOnly; + + return ( + + {row.getVisibleCells().map((cell) => { + // Disable selection for system attributes + if (cell.column.id === "select" && isSystemAttribute) { + return ( + +
+ {/* Empty checkbox space for system attributes */} +
+
+ ); + } + + return ( + { + if (cell.column.id === "select") return; + if (isSelectable) { + setEditingAttribute(attribute); + } + }} + style={cell.column.id === "select" ? getCommonPinningStyles(cell.column) : {}} + className={cn("border-slate-200 bg-white px-4 py-2 shadow-none", { + "group-hover:bg-slate-100": isSelectable, + "bg-slate-100": row.getIsSelected() && isSelectable, + })}> +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ); + })} +
+ ); + })} + {table.getRowModel().rows.length === 0 && ( + + + {t("common.no_results")} + + + )} +
+
- ); -}; + + + + {editingAttribute && ( + { + if (!open) setEditingAttribute(null); + }} + /> + )} + + ); +}; diff --git a/apps/web/modules/ee/contacts/attributes/components/create-attribute-modal.tsx b/apps/web/modules/ee/contacts/attributes/components/create-attribute-modal.tsx index 993d8aba08..41056c1735 100644 --- a/apps/web/modules/ee/contacts/attributes/components/create-attribute-modal.tsx +++ b/apps/web/modules/ee/contacts/attributes/components/create-attribute-modal.tsx @@ -5,193 +5,195 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; -import { createContactAttributeKeyAction } from "../actions"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { isSafeIdentifier, toSafeIdentifier } from "@/lib/utils/safe-identifier"; import { Button } from "@/modules/ui/components/button"; import { - Dialog, - DialogBody, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/modules/ui/components/dialog"; import { Input } from "@/modules/ui/components/input"; -import { isSafeIdentifier, toSafeIdentifier } from "@/lib/utils/safe-identifier"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { createContactAttributeKeyAction } from "../actions"; interface CreateAttributeModalProps { - environmentId: string; + environmentId: string; } export function CreateAttributeModal({ environmentId }: Readonly) { - const { t } = useTranslation(); - const router = useRouter(); - const [open, setOpen] = useState(false); - const [isCreating, setIsCreating] = useState(false); - const [formData, setFormData] = useState({ - key: "", - name: "", - description: "", + const { t } = useTranslation(); + const router = useRouter(); + const [open, setOpen] = useState(false); + const [isCreating, setIsCreating] = useState(false); + const [formData, setFormData] = useState({ + key: "", + name: "", + description: "", + }); + const [keyError, setKeyError] = useState(""); + + const handleResetState = () => { + setFormData({ + key: "", + name: "", + description: "", }); - const [keyError, setKeyError] = useState(""); + setKeyError(""); + setOpen(false); + }; - const handleResetState = () => { - setFormData({ - key: "", - name: "", - description: "", - }); - setKeyError(""); - setOpen(false); - }; + const handleNameChange = (value: string) => { + setFormData((prev) => { + const newName = value; + // Auto-suggest key from name if key is empty or matches previous name suggestion + let newKey = prev.key; + if (!prev.key || prev.key === toSafeIdentifier(prev.name)) { + newKey = toSafeIdentifier(newName); + } + return { ...prev, name: newName, key: newKey }; + }); + if (keyError) { + validateKey(formData.key || toSafeIdentifier(value)); + } + }; - const handleNameChange = (value: string) => { - setFormData((prev) => { - const newName = value; - // Auto-suggest key from name if key is empty or matches previous name suggestion - let newKey = prev.key; - if (!prev.key || prev.key === toSafeIdentifier(prev.name)) { - newKey = toSafeIdentifier(newName); - } - return { ...prev, name: newName, key: newKey }; - }); - if (keyError) { - validateKey(formData.key || toSafeIdentifier(value)); - } - }; + const handleKeyChange = (value: string) => { + setFormData((prev) => ({ ...prev, key: value })); + validateKey(value); + }; - const handleKeyChange = (value: string) => { - setFormData((prev) => ({ ...prev, key: value })); - validateKey(value); - }; + const validateKey = (key: string) => { + if (!key) { + setKeyError(t("environments.contacts.attribute_key_required")); + return false; + } + if (!isSafeIdentifier(key)) { + setKeyError( + t("environments.contacts.attribute_key_safe_identifier_required") || + "Key must be a safe identifier: only lowercase letters, numbers, and underscores, and must start with a letter" + ); + return false; + } + setKeyError(""); + return true; + }; - const validateKey = (key: string) => { - if (!key) { - setKeyError(t("environments.contacts.attribute_key_required")); - return false; - } - if (!isSafeIdentifier(key)) { - setKeyError( - t("environments.contacts.attribute_key_safe_identifier_required") || - "Key must be a safe identifier: only lowercase letters, numbers, and underscores, and must start with a letter" - ); - return false; - } - setKeyError(""); - return true; - }; + const handleCreate = async () => { + if (!formData.key) { + setKeyError(t("environments.contacts.attribute_key_required")); + return; + } - const handleCreate = async () => { - if (!formData.key) { - setKeyError(t("environments.contacts.attribute_key_required")); - return; - } + if (!validateKey(formData.key)) { + return; + } - if (!validateKey(formData.key)) { - return; - } + setIsCreating(true); + const createContactAttributeKeyResponse = await createContactAttributeKeyAction({ + environmentId, + key: formData.key, + name: formData.name || formData.key, + description: formData.description || undefined, + }); - setIsCreating(true); - const createContactAttributeKeyResponse = await createContactAttributeKeyAction({ - environmentId, - key: formData.key, - name: formData.name || formData.key, - description: formData.description || undefined, - }); + if (!createContactAttributeKeyResponse?.data) { + const errorMessage = getFormattedErrorMessage(createContactAttributeKeyResponse); + toast.error(errorMessage); + setIsCreating(false); + return; + } - if (!createContactAttributeKeyResponse?.data) { - const errorMessage = getFormattedErrorMessage(createContactAttributeKeyResponse); - toast.error(errorMessage); - return; - } + toast.success(t("environments.contacts.attribute_created_successfully")); + handleResetState(); + router.refresh(); + setIsCreating(false); + }; - toast.success(t("environments.contacts.attribute_created_successfully")); - handleResetState(); - router.refresh(); - setIsCreating(false); - }; + return ( + <> + - return ( - <> - - - { - if (!open) { - handleResetState(); - } - }}> - - - {t("environments.contacts.create_new_attribute")} - - {t("environments.contacts.create_new_attribute_description")} - - - - -
-
- - handleKeyChange(e.target.value)} - placeholder={t("environments.contacts.attribute_key_placeholder")} - className={keyError ? "border-red-500" : ""} - /> - {keyError &&

{keyError}

} -

- {t("environments.contacts.attribute_key_hint")} -

-
- -
- - handleNameChange(e.target.value)} - placeholder={t("environments.contacts.attribute_label_placeholder")} - /> -
- -
- - setFormData((prev) => ({ ...prev, description: e.target.value }))} - placeholder={t("environments.contacts.attribute_description_placeholder")} - /> -
-
-
- - - - - -
-
- - ); + + + + + + ); } - diff --git a/apps/web/modules/ee/contacts/attributes/components/edit-attribute-modal.tsx b/apps/web/modules/ee/contacts/attributes/components/edit-attribute-modal.tsx index 234eb6e909..75ba3a181b 100644 --- a/apps/web/modules/ee/contacts/attributes/components/edit-attribute-modal.tsx +++ b/apps/web/modules/ee/contacts/attributes/components/edit-attribute-modal.tsx @@ -5,115 +5,109 @@ import { useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key"; -import { updateContactAttributeKeyAction } from "../actions"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Button } from "@/modules/ui/components/button"; import { - Dialog, - DialogBody, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/modules/ui/components/dialog"; import { Input } from "@/modules/ui/components/input"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { updateContactAttributeKeyAction } from "../actions"; interface EditAttributeModalProps { - attribute: TContactAttributeKey; - open: boolean; - setOpen: (open: boolean) => void; + attribute: TContactAttributeKey; + open: boolean; + setOpen: (open: boolean) => void; } -export function EditAttributeModal({ - attribute, - open, - setOpen, -}: Readonly) { - const { t } = useTranslation(); - const router = useRouter(); - const [isUpdating, setIsUpdating] = useState(false); - const [formData, setFormData] = useState({ - name: attribute.name ?? "", - description: attribute.description ?? "", +export function EditAttributeModal({ attribute, open, setOpen }: Readonly) { + const { t } = useTranslation(); + const router = useRouter(); + const [isUpdating, setIsUpdating] = useState(false); + const [formData, setFormData] = useState({ + name: attribute.name ?? "", + description: attribute.description ?? "", + }); + + const handleUpdate = async () => { + setIsUpdating(true); + const updateContactAttributeKeyResponse = await updateContactAttributeKeyAction({ + id: attribute.id, + name: formData.name || undefined, + description: formData.description || undefined, }); - const handleUpdate = async () => { - setIsUpdating(true); - const updateContactAttributeKeyResponse = await updateContactAttributeKeyAction({ - id: attribute.id, - name: formData.name || undefined, - description: formData.description || undefined, - }); + if (!updateContactAttributeKeyResponse?.data) { + const errorMessage = getFormattedErrorMessage(updateContactAttributeKeyResponse); + toast.error(errorMessage); + setIsUpdating(false); + return; + } - if (!updateContactAttributeKeyResponse?.data) { - const errorMessage = getFormattedErrorMessage(updateContactAttributeKeyResponse); - toast.error(errorMessage); - return; - } + toast.success(t("environments.contacts.attribute_updated_successfully")); + setOpen(false); + router.refresh(); + setIsUpdating(false); + }; - toast.success(t("environments.contacts.attribute_updated_successfully")); - setOpen(false); - router.refresh(); + return ( + + + + {t("environments.contacts.edit_attribute")} + {t("environments.contacts.edit_attribute_description")} + - }; + +
+
+ + +

+ {t("environments.contacts.attribute_key_cannot_be_changed")} +

+
- return ( - - - - {t("environments.contacts.edit_attribute")} - - {t("environments.contacts.edit_attribute_description")} - - +
+ + setFormData((prev) => ({ ...prev, name: e.target.value }))} + placeholder={t("environments.contacts.attribute_label_placeholder")} + /> +
- -
-
- - -

- {t("environments.contacts.attribute_key_cannot_be_changed")} -

-
+
+ + setFormData((prev) => ({ ...prev, description: e.target.value }))} + placeholder={t("environments.contacts.attribute_description_placeholder")} + /> +
+
+
-
- - setFormData((prev) => ({ ...prev, name: e.target.value }))} - placeholder={t("environments.contacts.attribute_label_placeholder")} - /> -
- -
- - setFormData((prev) => ({ ...prev, description: e.target.value }))} - placeholder={t("environments.contacts.attribute_description_placeholder")} - /> -
-
-
- - - - - -
-
- ); + + + + + + + ); } - diff --git a/apps/web/modules/ee/contacts/attributes/page.tsx b/apps/web/modules/ee/contacts/attributes/page.tsx index 565542da5a..760bffdece 100644 --- a/apps/web/modules/ee/contacts/attributes/page.tsx +++ b/apps/web/modules/ee/contacts/attributes/page.tsx @@ -1,4 +1,5 @@ import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getLocale } from "@/lingodotdev/language"; import { getTranslate } from "@/lingodotdev/server"; import { ContactsSecondaryNavigation } from "@/modules/ee/contacts/components/contacts-secondary-navigation"; import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys"; @@ -11,61 +12,62 @@ import { AttributesTable } from "./components/attributes-table"; import { CreateAttributeModal } from "./components/create-attribute-modal"; export const AttributesPage = async ({ - params: paramsProps, + params: paramsProps, }: { - params: Promise<{ environmentId: string }>; + params: Promise<{ environmentId: string }>; }) => { - const params = await paramsProps; - const t = await getTranslate(); + const params = await paramsProps; + const t = await getTranslate(); + const locale = await getLocale(); - const [{ isReadOnly }, contactAttributeKeys] = await Promise.all([ - getEnvironmentAuth(params.environmentId), - getContactAttributeKeys(params.environmentId), - ]); + const [{ isReadOnly }, contactAttributeKeys] = await Promise.all([ + getEnvironmentAuth(params.environmentId), + getContactAttributeKeys(params.environmentId), + ]); - const isContactsEnabled = await getIsContactsEnabled(); + const isContactsEnabled = await getIsContactsEnabled(); - return ( - - - ) : undefined - }> - - + return ( + + + ) : undefined + }> + + - {isContactsEnabled ? ( - - ) : ( -
- -
- )} -
- ); + {isContactsEnabled ? ( + + ) : ( +
+ +
+ )} +
+ ); }; -