fix: improve Contacts and Segments UX and functionality (#6855)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
Johannes
2025-11-25 23:49:23 -08:00
committed by GitHub
parent aab6798b29
commit f57497d8b3
24 changed files with 997 additions and 702 deletions

View File

@@ -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

View File

@@ -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": {

View File

@@ -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"

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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>
);

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
))}

View File

@@ -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 (

View File

@@ -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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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";

View File

@@ -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>
);
};