mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-03 11:30:50 -05:00
fix: improve Contacts and Segments UX and functionality (#6855)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user