mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-12 18:59:38 -06:00
fix: improve Contacts and Segments UX and functionality (#6855)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -596,6 +596,7 @@ checksums:
|
||||
environments/contacts/upload_contacts_modal_pick_different_file: e748a6e81a425ef9aa33f96ca4edc157
|
||||
environments/contacts/upload_contacts_modal_preview: c4406f8d9a54f131abfff4e9928228bb
|
||||
environments/contacts/upload_contacts_modal_upload_btn: 47b7f3bcf478a7d8dc258d2efc80af37
|
||||
environments/contacts/upload_contacts_success: cd5d6b6d587586dd4f944868c92835bc
|
||||
environments/formbricks_logo: b7ee57de32c8b13463cc8ca8643eddd4
|
||||
environments/integrations/activepieces_integration_description: 62a8fbf86762bab01c7d2db2ba60fff4
|
||||
environments/integrations/additional_settings: 20936205a75745fba2c4047375a04db3
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Aktualisieren",
|
||||
"upload_contacts_modal_pick_different_file": "Wähle eine andere Datei",
|
||||
"upload_contacts_modal_preview": "Hier ist eine Vorschau deiner Daten.",
|
||||
"upload_contacts_modal_upload_btn": "Kontakte hochladen"
|
||||
"upload_contacts_modal_upload_btn": "Kontakte hochladen",
|
||||
"upload_contacts_success": "Kontakte erfolgreich hochgeladen"
|
||||
},
|
||||
"formbricks_logo": "Formbricks-Logo",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Update",
|
||||
"upload_contacts_modal_pick_different_file": "Pick a different file",
|
||||
"upload_contacts_modal_preview": "Here's a preview of your data.",
|
||||
"upload_contacts_modal_upload_btn": "Upload contacts"
|
||||
"upload_contacts_modal_upload_btn": "Upload contacts",
|
||||
"upload_contacts_success": "Contacts uploaded successfully"
|
||||
},
|
||||
"formbricks_logo": "Formbricks Logo",
|
||||
"integrations": {
|
||||
@@ -794,8 +795,6 @@
|
||||
"cache_update_delay_title": "Changes will be reflected after ~1 minute due to caching",
|
||||
"environment_id": "Your Environment ID",
|
||||
"environment_id_description": "This id uniquely identifies this Formbricks environment.",
|
||||
"sdk_connection_details": "SDK Connection Details",
|
||||
"sdk_connection_details_description": "Your unique environment ID and SDK connection URL for integrating Formbricks with your application.",
|
||||
"formbricks_sdk_connected": "Formbricks SDK is connected",
|
||||
"formbricks_sdk_not_connected": "Formbricks SDK is not yet connected.",
|
||||
"formbricks_sdk_not_connected_description": "Add the Formbricks SDK to your website or app to connect it with Formbricks",
|
||||
@@ -803,6 +802,8 @@
|
||||
"how_to_setup_description": "Follow these steps to setup the Formbricks widget within your app.",
|
||||
"receiving_data": "Receiving data \uD83D\uDC83\uD83D\uDD7A",
|
||||
"recheck": "Re-check",
|
||||
"sdk_connection_details": "SDK Connection Details",
|
||||
"sdk_connection_details_description": "Your unique environment ID and SDK connection URL for integrating Formbricks with your application.",
|
||||
"setup_alert_description": "Follow this step-by-step tutorial to connect your app or website in under 5 minutes.",
|
||||
"setup_alert_title": "How to connect",
|
||||
"webapp_url": "SDK Connection URL"
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Actualizar",
|
||||
"upload_contacts_modal_pick_different_file": "Selecciona un archivo diferente",
|
||||
"upload_contacts_modal_preview": "Aquí tienes una vista previa de tus datos.",
|
||||
"upload_contacts_modal_upload_btn": "Subir contactos"
|
||||
"upload_contacts_modal_upload_btn": "Subir contactos",
|
||||
"upload_contacts_success": "Contactos subidos correctamente"
|
||||
},
|
||||
"formbricks_logo": "Logo de Formbricks",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Mettre à jour",
|
||||
"upload_contacts_modal_pick_different_file": "Choisissez un fichier différent",
|
||||
"upload_contacts_modal_preview": "Voici un aperçu de vos données.",
|
||||
"upload_contacts_modal_upload_btn": "Importer des contacts"
|
||||
"upload_contacts_modal_upload_btn": "Importer des contacts",
|
||||
"upload_contacts_success": "Contacts téléchargés avec succès"
|
||||
},
|
||||
"formbricks_logo": "Logo Formbricks",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "更新",
|
||||
"upload_contacts_modal_pick_different_file": "別のファイルを選択",
|
||||
"upload_contacts_modal_preview": "データのプレビューです。",
|
||||
"upload_contacts_modal_upload_btn": "連絡先をアップロード"
|
||||
"upload_contacts_modal_upload_btn": "連絡先をアップロード",
|
||||
"upload_contacts_success": "連絡先のアップロードに成功しました"
|
||||
},
|
||||
"formbricks_logo": "Formbricksのロゴ",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Update",
|
||||
"upload_contacts_modal_pick_different_file": "Kies een ander bestand",
|
||||
"upload_contacts_modal_preview": "Hier ziet u een voorbeeld van uw gegevens.",
|
||||
"upload_contacts_modal_upload_btn": "Contacten uploaden"
|
||||
"upload_contacts_modal_upload_btn": "Contacten uploaden",
|
||||
"upload_contacts_success": "Contacten succesvol geüpload"
|
||||
},
|
||||
"formbricks_logo": "Formbricks-logo",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Atualizar",
|
||||
"upload_contacts_modal_pick_different_file": "Escolha um arquivo diferente",
|
||||
"upload_contacts_modal_preview": "Aqui está uma prévia dos seus dados.",
|
||||
"upload_contacts_modal_upload_btn": "Fazer upload de contatos"
|
||||
"upload_contacts_modal_upload_btn": "Fazer upload de contatos",
|
||||
"upload_contacts_success": "Contatos carregados com sucesso"
|
||||
},
|
||||
"formbricks_logo": "Logo da Formbricks",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Atualizar",
|
||||
"upload_contacts_modal_pick_different_file": "Escolher um ficheiro diferente",
|
||||
"upload_contacts_modal_preview": "Aqui está uma pré-visualização dos seus dados.",
|
||||
"upload_contacts_modal_upload_btn": "Carregar contactos"
|
||||
"upload_contacts_modal_upload_btn": "Carregar contactos",
|
||||
"upload_contacts_success": "Contactos carregados com sucesso"
|
||||
},
|
||||
"formbricks_logo": "Logotipo do Formbricks",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "Actualizare",
|
||||
"upload_contacts_modal_pick_different_file": "Selectați un alt fișier",
|
||||
"upload_contacts_modal_preview": "Iată o previzualizare a datelor tale.",
|
||||
"upload_contacts_modal_upload_btn": "Încărcați contacte"
|
||||
"upload_contacts_modal_upload_btn": "Încărcați contacte",
|
||||
"upload_contacts_success": "Contactele au fost încărcate cu succes"
|
||||
},
|
||||
"formbricks_logo": "Logo Formbricks",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "更新",
|
||||
"upload_contacts_modal_pick_different_file": "选择不同的文件",
|
||||
"upload_contacts_modal_preview": "这是 你 的 数据 预览。",
|
||||
"upload_contacts_modal_upload_btn": "上传 联系人"
|
||||
"upload_contacts_modal_upload_btn": "上传 联系人",
|
||||
"upload_contacts_success": "联系人上传成功"
|
||||
},
|
||||
"formbricks_logo": "Formbricks Logo",
|
||||
"integrations": {
|
||||
|
||||
@@ -631,7 +631,8 @@
|
||||
"upload_contacts_modal_duplicates_update_title": "更新",
|
||||
"upload_contacts_modal_pick_different_file": "選取不同的檔案",
|
||||
"upload_contacts_modal_preview": "這是您的資料預覽。",
|
||||
"upload_contacts_modal_upload_btn": "上傳聯絡人"
|
||||
"upload_contacts_modal_upload_btn": "上傳聯絡人",
|
||||
"upload_contacts_success": "聯絡人已成功上傳"
|
||||
},
|
||||
"formbricks_logo": "Formbricks 標誌",
|
||||
"integrations": {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-bold text-slate-700">{t("common.attributes")}</h2>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">{t("common.email")}</dt>
|
||||
<dt className="text-sm font-medium text-slate-500">email</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
{attributes.email ? (
|
||||
<span>{attributes.email}</span>
|
||||
@@ -29,7 +29,7 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">{t("common.language")}</dt>
|
||||
<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>
|
||||
@@ -39,7 +39,7 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">{t("common.user_id")}</dt>
|
||||
<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} />
|
||||
@@ -49,7 +49,7 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">ID</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>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
|
||||
.map(([key, attributeData]) => {
|
||||
return (
|
||||
<div key={key}>
|
||||
<dt className="text-sm font-medium text-slate-500">{key.toString()}</dt>
|
||||
<dt className="text-sm font-medium text-slate-500">{key}</dt>
|
||||
<dd className="mt-1 text-sm text-slate-900">{attributeData}</dd>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getPublishedLinkSurveys } from "@/modules/ee/contacts/lib/surveys";
|
||||
import { getContactIdentifier } from "@/modules/ee/contacts/lib/utils";
|
||||
import { getIsQuotasEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { ResponseSection } from "./components/response-section";
|
||||
@@ -47,6 +48,7 @@ export const SingleContactPage = async (props: {
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton url={`/environments/${params.environmentId}/contacts`} />
|
||||
<PageHeader pageTitle={getContactIdentifier(contactAttributes)} cta={getContactControlBar()} />
|
||||
<section className="pb-24 pt-6">
|
||||
<div className="grid grid-cols-4 gap-x-8">
|
||||
|
||||
@@ -274,7 +274,7 @@ export const ContactsTable = ({
|
||||
}}
|
||||
style={cell.column.id === "select" ? getCommonPinningStyles(cell.column) : {}}
|
||||
className={cn(
|
||||
"border-slate-200 bg-white shadow-none group-hover:bg-slate-100",
|
||||
"px-4 py-2 border-slate-200 bg-white shadow-none group-hover:bg-slate-100",
|
||||
row.getIsSelected() && "bg-slate-100",
|
||||
{
|
||||
"border-r": !cell.column.getIsLastColumn(),
|
||||
@@ -282,7 +282,7 @@ export const ContactsTable = ({
|
||||
}
|
||||
)}>
|
||||
<div
|
||||
className={cn("flex flex-1 items-center truncate", isExpanded ? "h-full" : "h-10")}>
|
||||
className={cn("flex flex-1 items-center truncate", isExpanded ? "h-10" : "h-full")}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -12,14 +12,14 @@ export const CsvTable = ({ data }: CsvTableProps) => {
|
||||
const columns = Object.keys(data[0]);
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-md hover:overflow-auto">
|
||||
<div className="w-full overflow-x-auto rounded-md">
|
||||
<div
|
||||
className="grid gap-4 border-b-2 border-slate-100 bg-slate-100 p-4 text-left"
|
||||
style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))` }}>
|
||||
className="sticky top-0 z-10 grid gap-2 border-b-2 border-slate-100 bg-slate-100 px-3 py-2 text-left"
|
||||
style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(100px, 1fr))` }}>
|
||||
{columns.map((header, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-semibold capitalize">
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-semibold capitalize leading-tight">
|
||||
{header.replace(/_/g, " ")}
|
||||
</span>
|
||||
))}
|
||||
@@ -28,10 +28,10 @@ export const CsvTable = ({ data }: CsvTableProps) => {
|
||||
{data.map((row, rowIndex) => (
|
||||
<div
|
||||
key={rowIndex}
|
||||
className="grid gap-4 border-b border-gray-200 bg-white p-4 text-left last:border-b-0"
|
||||
style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))` }}>
|
||||
className="grid gap-2 border-b border-gray-200 bg-white px-3 py-2 text-left leading-tight last:border-b-0"
|
||||
style={{ gridTemplateColumns: `repeat(${columns.length}, minmax(100px, 1fr))` }}>
|
||||
{columns.map((header, colIndex) => (
|
||||
<span key={colIndex} className="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||
<span key={colIndex} className="overflow-hidden text-ellipsis whitespace-nowrap text-xs">
|
||||
{row[header]}
|
||||
</span>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -50,16 +51,26 @@ export const UploadContactsAttributeCombobox = ({
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
{currentKey ? (
|
||||
<Button variant="ghost" size="sm" className="border border-slate-300" aria-expanded={open}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-between border border-slate-300"
|
||||
aria-expanded={open}>
|
||||
{currentKey.label}
|
||||
<ChevronDownIcon className="h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="ghost" size="sm" className="border border-slate-300" aria-expanded={open}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-between border border-slate-300"
|
||||
aria-expanded={open}>
|
||||
{t("environments.contacts.select_attribute")}
|
||||
<ChevronDownIcon className="h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="h-full w-[200px] overflow-y-auto p-0">
|
||||
<PopoverContent className="h-full w-[200px] p-0">
|
||||
<Command
|
||||
filter={(value, search) => {
|
||||
if (value === "_create") {
|
||||
@@ -92,7 +103,7 @@ export const UploadContactsAttributeCombobox = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<CommandList className="border-0">
|
||||
<CommandList className="max-h-[300px] overflow-y-auto border-0">
|
||||
<CommandGroup>
|
||||
{keys.map((tag) => {
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { parse } from "csv-parse/sync";
|
||||
import { ArrowUpFromLineIcon, FileUpIcon, PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { cn } from "@/lib/cn";
|
||||
@@ -44,7 +45,7 @@ export const UploadContactsCSVButton = ({
|
||||
);
|
||||
const [csvResponse, setCSVResponse] = useState<TContactCSVUploadResponse>([]);
|
||||
const [attributeMap, setAttributeMap] = useState<Record<string, string>>({});
|
||||
const [error, setErrror] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const processCSVFile = async (file: File) => {
|
||||
@@ -52,25 +53,25 @@ export const UploadContactsCSVButton = ({
|
||||
|
||||
// Check file type
|
||||
if (!file.type && !file.name.endsWith(".csv")) {
|
||||
setErrror("Please upload a CSV file");
|
||||
setError("Please upload a CSV file");
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.type && file.type !== "text/csv" && !file.type.includes("csv")) {
|
||||
setErrror("Please upload a CSV file");
|
||||
setError("Please upload a CSV file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Max file size check (800KB)
|
||||
const maxSizeInBytes = 800 * 1024;
|
||||
if (file.size > maxSizeInBytes) {
|
||||
setErrror("File size exceeds the maximum limit of 800KB");
|
||||
setError("File size exceeds the maximum limit of 800KB");
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
setErrror("");
|
||||
setError("");
|
||||
const csv = e.target?.result as string;
|
||||
|
||||
try {
|
||||
@@ -82,12 +83,12 @@ export const UploadContactsCSVButton = ({
|
||||
const parsedRecords = ZContactCSVUploadResponse.safeParse(records);
|
||||
if (!parsedRecords.success) {
|
||||
console.error("Error parsing CSV:", parsedRecords.error);
|
||||
setErrror(parsedRecords.error.errors[0].message);
|
||||
setError(parsedRecords.error.errors[0].message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parsedRecords.data.length) {
|
||||
setErrror(
|
||||
setError(
|
||||
"The uploaded CSV file does not contain any valid contacts, please see the sample CSV file for the correct format."
|
||||
);
|
||||
return;
|
||||
@@ -123,7 +124,7 @@ export const UploadContactsCSVButton = ({
|
||||
const resetState = (closeModal?: boolean) => {
|
||||
setCSVResponse([]);
|
||||
setDuplicateContactsAction("skip");
|
||||
setErrror("");
|
||||
setError("");
|
||||
setAttributeMap({});
|
||||
setLoading(false);
|
||||
|
||||
@@ -138,7 +139,7 @@ export const UploadContactsCSVButton = ({
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setErrror("");
|
||||
setError("");
|
||||
|
||||
const values = Object.values(attributeMap);
|
||||
|
||||
@@ -156,9 +157,7 @@ export const UploadContactsCSVButton = ({
|
||||
.filter(([_, value]) => duplicateValues.includes(value))
|
||||
.map(([key, _]) => key);
|
||||
|
||||
setErrror(
|
||||
`Duplicate mappings found for the following attributes: ${duplicateAttributeKeys.join(", ")}`
|
||||
);
|
||||
setError(`Duplicate mappings found for the following attributes: ${duplicateAttributeKeys.join(", ")}`);
|
||||
errorContainerRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -193,7 +192,8 @@ export const UploadContactsCSVButton = ({
|
||||
});
|
||||
|
||||
if (result?.data) {
|
||||
setErrror("");
|
||||
setError("");
|
||||
toast.success(t("environments.contacts.upload_contacts_success"));
|
||||
resetState(true);
|
||||
|
||||
router.refresh();
|
||||
@@ -201,7 +201,7 @@ export const UploadContactsCSVButton = ({
|
||||
}
|
||||
|
||||
if (result?.serverError) {
|
||||
setErrror(result.serverError);
|
||||
setError(result.serverError);
|
||||
}
|
||||
|
||||
if (result?.validationErrors) {
|
||||
@@ -210,9 +210,9 @@ export const UploadContactsCSVButton = ({
|
||||
: result.validationErrors.csvData?._errors?.[0];
|
||||
|
||||
if (csvDataErrors) {
|
||||
setErrror(csvDataErrors);
|
||||
setError(csvDataErrors);
|
||||
} else {
|
||||
setErrror("An error occurred while uploading the contacts. Please try again later.");
|
||||
setError("An error occurred while uploading the contacts. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,8 +295,18 @@ export const UploadContactsCSVButton = ({
|
||||
{t("common.upload")} CSV
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent disableCloseOnOutsideClick={true} className="overflow-auto">
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(newOpen) => {
|
||||
setOpen(newOpen);
|
||||
if (!newOpen) {
|
||||
setError("");
|
||||
}
|
||||
}}>
|
||||
<DialogContent
|
||||
disableCloseOnOutsideClick={true}
|
||||
unconstrained={true}
|
||||
style={{ scrollbarGutter: "stable" }}>
|
||||
<DialogHeader>
|
||||
<FileUpIcon />
|
||||
<DialogTitle>{t("common.upload")} CSV</DialogTitle>
|
||||
@@ -305,8 +315,8 @@ export const UploadContactsCSVButton = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
<div className="flex flex-col gap-6">
|
||||
<DialogBody unconstrained={false}>
|
||||
<div className="flex flex-col gap-4">
|
||||
{error ? (
|
||||
<Alert variant="error" size="small">
|
||||
{error}
|
||||
@@ -340,11 +350,11 @@ export const UploadContactsCSVButton = ({
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-8">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<h3 className="font-medium text-slate-500">
|
||||
{t("environments.contacts.upload_contacts_modal_preview")}
|
||||
</h3>
|
||||
<div className="h-[300px] w-full overflow-auto rounded-md border border-slate-300">
|
||||
<div className="max-h-[300px] w-full overflow-auto rounded-md border border-slate-300">
|
||||
<CsvTable data={[...csvResponse.slice(0, 11)]} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,11 +394,11 @@ export const UploadContactsCSVButton = ({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="font-medium text-slate-900">
|
||||
{t("environments.contacts.upload_contacts_modal_duplicates_title")}
|
||||
</h3>
|
||||
<p className="mb-2 text-slate-500">
|
||||
<p className="text-slate-500">
|
||||
{t("environments.contacts.upload_contacts_modal_duplicates_description")}
|
||||
</p>
|
||||
<StylingTabs
|
||||
@@ -413,8 +423,8 @@ export const UploadContactsCSVButton = ({
|
||||
tabsContainerClassName="p-1 rounded-lg"
|
||||
/>
|
||||
|
||||
<div className="mt-1">
|
||||
<p className="text-sm font-medium text-slate-500">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-500">
|
||||
{duplicateContactsAction === "skip" &&
|
||||
t("environments.contacts.upload_contacts_modal_duplicates_skip_description")}
|
||||
{duplicateContactsAction === "update" &&
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -291,10 +291,10 @@ export const createContactsFromCSV = async (
|
||||
attributeKeyMap.set(attrKey.key, attrKey.id);
|
||||
});
|
||||
|
||||
// Identify missing attribute keys
|
||||
// Identify missing attribute keys (normalize keys to lowercase)
|
||||
const csvKeys = new Set<string>();
|
||||
csvData.forEach((record) => {
|
||||
Object.keys(record).forEach((key) => csvKeys.add(key));
|
||||
Object.keys(record).forEach((key) => csvKeys.add(key.toLowerCase()));
|
||||
});
|
||||
|
||||
const missingKeys = Array.from(csvKeys).filter((key) => !attributeKeyMap.has(key));
|
||||
@@ -328,12 +328,18 @@ export const createContactsFromCSV = async (
|
||||
|
||||
// Process contacts in parallel
|
||||
const contactPromises = csvData.map(async (record) => {
|
||||
// Normalize record keys to lowercase
|
||||
const normalizedRecord: Record<string, string> = {};
|
||||
Object.entries(record).forEach(([key, value]) => {
|
||||
normalizedRecord[key.toLowerCase()] = value;
|
||||
});
|
||||
|
||||
// Skip records without email
|
||||
if (!record.email) {
|
||||
if (!normalizedRecord.email) {
|
||||
throw new ValidationError("Email is required for all contacts");
|
||||
}
|
||||
|
||||
const existingContact = emailToContactMap.get(record.email);
|
||||
const existingContact = emailToContactMap.get(normalizedRecord.email);
|
||||
|
||||
if (existingContact) {
|
||||
// Handle duplicates based on duplicateContactsAction
|
||||
@@ -344,11 +350,11 @@ export const createContactsFromCSV = async (
|
||||
case "update": {
|
||||
// if the record has a userId, check if it already exists
|
||||
const existingUserId = existingUserIds.find(
|
||||
(attr) => attr.value === record.userId && attr.contactId !== existingContact.id
|
||||
(attr) => attr.value === normalizedRecord.userid && attr.contactId !== existingContact.id
|
||||
);
|
||||
let recordToProcess = { ...record };
|
||||
let recordToProcess = { ...normalizedRecord };
|
||||
if (existingUserId) {
|
||||
const { userId, ...rest } = recordToProcess;
|
||||
const { userid, ...rest } = recordToProcess;
|
||||
|
||||
const existingContactUserId = existingContact.attributes.find(
|
||||
(attr) => attr.attributeKey.key === "userId"
|
||||
@@ -401,11 +407,11 @@ export const createContactsFromCSV = async (
|
||||
case "overwrite": {
|
||||
// if the record has a userId, check if it already exists
|
||||
const existingUserId = existingUserIds.find(
|
||||
(attr) => attr.value === record.userId && attr.contactId !== existingContact.id
|
||||
(attr) => attr.value === normalizedRecord.userid && attr.contactId !== existingContact.id
|
||||
);
|
||||
let recordToProcess = { ...record };
|
||||
let recordToProcess = { ...normalizedRecord };
|
||||
if (existingUserId) {
|
||||
const { userId, ...rest } = recordToProcess;
|
||||
const { userid, ...rest } = recordToProcess;
|
||||
const existingContactUserId = existingContact.attributes.find(
|
||||
(attr) => attr.attributeKey.key === "userId"
|
||||
)?.value;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import type { TBaseFilter, TSegment } from "@formbricks/types/segment";
|
||||
import { ZSegmentFilters } from "@formbricks/types/segment";
|
||||
@@ -100,13 +101,14 @@ export function CreateSegmentModal({
|
||||
toast.error(errorMessage);
|
||||
setIsCreatingSegment(false);
|
||||
}
|
||||
} catch (err: any) {
|
||||
} catch (error_) {
|
||||
logger.error("Error creating segment:", error_);
|
||||
// parse the segment filters to check if they are valid
|
||||
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
||||
if (!parsedFilters.success) {
|
||||
toast.error(t("environments.segments.invalid_segment_filters"));
|
||||
} else {
|
||||
if (parsedFilters.success) {
|
||||
toast.error(t("common.something_went_wrong_please_try_again"));
|
||||
} else {
|
||||
toast.error(t("environments.segments.invalid_segment_filters"));
|
||||
}
|
||||
setIsCreatingSegment(false);
|
||||
return;
|
||||
@@ -115,11 +117,15 @@ export function CreateSegmentModal({
|
||||
|
||||
const isSaveDisabled = useMemo(() => {
|
||||
// check if title is empty
|
||||
|
||||
if (!segment.title || segment.title.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if filters are empty
|
||||
if (segment.filters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// parse the filters to check if they are valid
|
||||
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
|
||||
if (!parsedFilters.success) {
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
} from "@formbricks/types/segment";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
import { isCapitalized } from "@/lib/utils/strings";
|
||||
import {
|
||||
convertOperatorToText,
|
||||
convertOperatorToTitle,
|
||||
@@ -149,8 +148,10 @@ function SegmentFilterItemContextMenu({
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger disabled={viewOnly}>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
<DropdownMenuTrigger asChild disabled={viewOnly}>
|
||||
<Button variant="outline" size="icon">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
@@ -185,13 +186,13 @@ function SegmentFilterItemContextMenu({
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
className="mr-4 p-0"
|
||||
size="icon"
|
||||
disabled={viewOnly}
|
||||
onClick={() => {
|
||||
if (viewOnly) return;
|
||||
onDeleteFilter(filterId);
|
||||
}}
|
||||
variant="ghost">
|
||||
variant="outline">
|
||||
<Trash2 className={cn("h-4 w-4 cursor-pointer", viewOnly && "cursor-not-allowed")} />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -317,7 +318,7 @@ function AttributeSegmentFilter({
|
||||
className="flex w-auto items-center justify-center whitespace-nowrap bg-white"
|
||||
hideArrow>
|
||||
<SelectValue>
|
||||
<div className={cn("flex items-center gap-2", !isCapitalized(attrKeyValue ?? "") && "lowercase")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<TagIcon className="h-4 w-4 text-sm" />
|
||||
<p>{attrKeyValue}</p>
|
||||
</div>
|
||||
@@ -357,7 +358,7 @@ function AttributeSegmentFilter({
|
||||
{!["isSet", "isNotSet"].includes(resource.qualifier.operator) && (
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<Input
|
||||
className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
|
||||
className={cn("h-9 w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
|
||||
disabled={viewOnly}
|
||||
onChange={(e) => {
|
||||
if (viewOnly) return;
|
||||
@@ -537,7 +538,7 @@ function PersonSegmentFilter({
|
||||
{!["isSet", "isNotSet"].includes(resource.qualifier.operator) && (
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<Input
|
||||
className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
|
||||
className={cn("h-8 w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
|
||||
disabled={viewOnly}
|
||||
onChange={(e) => {
|
||||
if (viewOnly) return;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import Link from "next/link";
|
||||
import { WidgetStatusIndicator } from "@/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator";
|
||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import { WEBAPP_URL } from "@/lib/constants";
|
||||
import { getActionClasses } from "@/lib/actionClass/service";
|
||||
import { WEBAPP_URL } from "@/lib/constants";
|
||||
import { getEnvironments } from "@/lib/environment/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
|
||||
@@ -258,7 +258,9 @@ export function ConditionsEditor({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{quotaError && isSubmitted && <p className="text-error mt-2 w-full text-right text-sm">{quotaError}</p>}
|
||||
{quotaError && isSubmitted && (
|
||||
<p className="text-error mt-2 w-full text-right text-sm">{quotaError}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user