fix: targeting ui dir structure (#2708)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
Anshuman Pandey
2024-06-04 10:26:38 +05:30
committed by GitHub
parent 4e39f45446
commit 681c559c79
39 changed files with 721 additions and 695 deletions

View File

@@ -7,7 +7,7 @@ import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { mixColor } from "@formbricks/lib/utils";
import { mixColor } from "@formbricks/lib/utils/colors";
import { TProductStyling } from "@formbricks/types/product";
import { TSurveyStyling } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";

View File

@@ -14,13 +14,13 @@ import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TBaseFilter, TSegment, TSegmentCreateInput, TSegmentUpdateInput } from "@formbricks/types/segment";
import { TSurvey } from "@formbricks/types/surveys";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { BasicAddFilterModal } from "@formbricks/ui/BasicAddFilterModal";
import { BasicSegmentEditor } from "@formbricks/ui/BasicSegmentEditor";
import { Button } from "@formbricks/ui/Button";
import { BasicAddFilterModal } from "@formbricks/ui/Targeting/BasicAddFilterModal";
import { BasicSegmentEditor } from "@formbricks/ui/Targeting/BasicSegmentEditor";
import { LoadSegmentModal } from "@formbricks/ui/Targeting/LoadSegmentModal";
import { SaveAsNewSegmentModal } from "@formbricks/ui/Targeting/SaveAsNewSegmentModal";
import { SegmentTitle } from "@formbricks/ui/Targeting/SegmentTitle";
import { TargetingIndicator } from "@formbricks/ui/Targeting/TargetingIndicator";
import { LoadSegmentModal } from "@formbricks/ui/LoadSegmentModal";
import { SaveAsNewSegmentModal } from "@formbricks/ui/SaveAsNewSegmentModal";
import { SegmentTitle } from "@formbricks/ui/SegmentTitle";
import { TargetingIndicator } from "@formbricks/ui/TargetingIndicator";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
import {

View File

@@ -3,8 +3,8 @@
import { TagIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { Label } from "@formbricks/ui/Label";

View File

@@ -1,7 +1,7 @@
import { getAttributes } from "@formbricks/lib/attribute/service";
import { getPerson } from "@formbricks/lib/person/service";
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
export const AttributesSection = async ({ personId }: { personId: string }) => {
const [person, attributes] = await Promise.all([getPerson(personId), getAttributes(personId)]);

View File

@@ -9,11 +9,11 @@ import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/action
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TBaseFilter, TSegment, ZSegmentFilters } from "@formbricks/types/segment";
import { BasicAddFilterModal } from "@formbricks/ui/BasicAddFilterModal";
import { BasicSegmentEditor } from "@formbricks/ui/BasicSegmentEditor";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Modal } from "@formbricks/ui/Modal";
import { BasicAddFilterModal } from "@formbricks/ui/Targeting/BasicAddFilterModal";
import { BasicSegmentEditor } from "@formbricks/ui/Targeting/BasicSegmentEditor";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
type TCreateSegmentModalProps = {

View File

@@ -9,11 +9,11 @@ import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TBaseFilter, TSegment, TSegmentWithSurveyNames, ZSegmentFilters } from "@formbricks/types/segment";
import { BasicAddFilterModal } from "@formbricks/ui/BasicAddFilterModal";
import { BasicSegmentEditor } from "@formbricks/ui/BasicSegmentEditor";
import { Button } from "@formbricks/ui/Button";
import { ConfirmDeleteSegmentModal } from "@formbricks/ui/ConfirmDeleteSegmentModal";
import { Input } from "@formbricks/ui/Input";
import { BasicAddFilterModal } from "@formbricks/ui/Targeting/BasicAddFilterModal";
import { BasicSegmentEditor } from "@formbricks/ui/Targeting/BasicSegmentEditor";
import { ConfirmDeleteSegmentModal } from "@formbricks/ui/Targeting/ConfirmDeleteSegmentModal";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
import { deleteBasicSegmentAction, updateBasicSegmentAction } from "../actions";

View File

@@ -3,8 +3,8 @@
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TActionClass } from "@formbricks/types/actionClasses";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { Label } from "@formbricks/ui/Label";

View File

@@ -29,7 +29,7 @@ import { useEffect, useMemo, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { capitalizeFirstLetter, truncate } from "@formbricks/lib/strings";
import { capitalizeFirstLetter, truncate } from "@formbricks/lib/utils/strings";
import { TEnvironment } from "@formbricks/types/environment";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";

View File

@@ -1,5 +1,5 @@
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TSurvey } from "@formbricks/types/surveys";
import { TWebhook } from "@formbricks/types/webhooks";
import { Label } from "@formbricks/ui/Label";

View File

@@ -1,5 +1,5 @@
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
import { timeSinceConditionally } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TSurvey } from "@formbricks/types/surveys";
import { TWebhook } from "@formbricks/types/webhooks";
import { Badge } from "@formbricks/ui/Badge";

View File

@@ -4,8 +4,8 @@ import { FilesIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
import { timeSince } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TApiKey } from "@formbricks/types/apiKeys";
import { Button } from "@formbricks/ui/Button";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";

View File

@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
import React, { useState } from "react";
import toast from "react-hot-toast";
import { truncate } from "@formbricks/lib/strings";
import { truncate } from "@formbricks/lib/utils/strings";
import { TProduct } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";

View File

@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TMembershipRole } from "@formbricks/types/memberships";
import { Badge } from "@formbricks/ui/Badge";
import { Button } from "@formbricks/ui/Button";

View File

@@ -15,10 +15,10 @@ import { TBaseFilter, TSegment, TSegmentCreateInput, TSegmentUpdateInput } from
import { TSurvey } from "@formbricks/types/surveys";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
import { LoadSegmentModal } from "@formbricks/ui/Targeting/LoadSegmentModal";
import { SaveAsNewSegmentModal } from "@formbricks/ui/Targeting/SaveAsNewSegmentModal";
import { SegmentTitle } from "@formbricks/ui/Targeting/SegmentTitle";
import { TargetingIndicator } from "@formbricks/ui/Targeting/TargetingIndicator";
import { LoadSegmentModal } from "@formbricks/ui/LoadSegmentModal";
import { SaveAsNewSegmentModal } from "@formbricks/ui/SaveAsNewSegmentModal";
import { SegmentTitle } from "@formbricks/ui/SegmentTitle";
import { TargetingIndicator } from "@formbricks/ui/TargetingIndicator";
import {
cloneSegmentAction,

View File

@@ -26,6 +26,7 @@ import {
updatePersonIdentifierInFilter,
updateSegmentIdInFilter,
} from "@formbricks/lib/segment/utils";
import { isCapitalized } from "@formbricks/lib/utils/strings";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import {
@@ -80,8 +81,6 @@ type TSegmentFilterProps = {
viewOnly?: boolean;
};
const isCapitalized = (str: string) => str.charAt(0) === str.charAt(0).toUpperCase();
const SegmentFilterItemConnector = ({
connector,
segment,

View File

@@ -11,8 +11,8 @@ import { TActionClass } from "@formbricks/types/actionClasses";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TBaseFilter, TSegment, TSegmentWithSurveyNames, ZSegmentFilters } from "@formbricks/types/segment";
import { Button } from "@formbricks/ui/Button";
import { ConfirmDeleteSegmentModal } from "@formbricks/ui/ConfirmDeleteSegmentModal";
import { Input } from "@formbricks/ui/Input";
import { ConfirmDeleteSegmentModal } from "@formbricks/ui/Targeting/ConfirmDeleteSegmentModal";
import { deleteSegmentAction, updateSegmentAction } from "../lib/actions";
import { AddFilterModal } from "./AddFilterModal";

View File

@@ -7,6 +7,6 @@
},
"resolveJsonModule": true
},
"include": [".", "../ui/Targeting/TargetingIndicator.tsx"],
"include": [".", "../ui/TargetingIndicator.tsx"],
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -16,7 +16,7 @@ import React from "react";
import { cn } from "@formbricks/lib/cn";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { isLight, mixColor } from "@formbricks/lib/utils";
import { isLight, mixColor } from "@formbricks/lib/utils/colors";
import { TSurvey, TSurveyQuestionType, TSurveyStyling } from "@formbricks/types/surveys";
import { RatingSmiley } from "@formbricks/ui/RatingSmiley";

View File

@@ -28,9 +28,9 @@ import {
import { getLocalizedValue } from "../i18n/utils";
import { processResponseData } from "../responses";
import { sanitizeString } from "../strings";
import { getTodaysDateTimeFormatted } from "../time";
import { evaluateCondition } from "../utils/evaluateLogic";
import { sanitizeString } from "../utils/strings";
export const calculateTtcTotal = (ttc: TResponseTtc) => {
const result = { ...ttc };

View File

@@ -2,7 +2,7 @@ import { FormbricksAPI } from "@formbricks/api";
import { TResponseUpdate } from "@formbricks/types/responses";
import { SurveyState } from "./surveyState";
import { delay } from "./utils";
import { delay } from "./utils/promises";
interface QueueConfig {
apiHost: string;

View File

@@ -1,7 +1,3 @@
export const delay = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export const hexToRGBA = (hex: string | undefined, opacity: number): string | undefined => {
// return undefined if hex is undefined, this is important for adding the default values to the CSS variables
// TODO: find a better way to handle this

View File

@@ -0,0 +1,3 @@
export const delay = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};

View File

@@ -18,3 +18,5 @@ export const truncate = (str: string, length: number) => {
export const sanitizeString = (str: string, delimiter: string = "_", length: number = 255) => {
return str.replace(/[^0-9a-zA-Z\-._]+/g, delimiter).substring(0, length);
};
export const isCapitalized = (str: string) => str.charAt(0) === str.charAt(0).toUpperCase();

View File

@@ -3,7 +3,7 @@ import preflight from "@/styles/preflight.css?inline";
import calendarCss from "react-calendar/dist/Calendar.css?inline";
import datePickerCss from "react-date-picker/dist/DatePicker.css?inline";
import { isLight, mixColor } from "@formbricks/lib/utils";
import { isLight, mixColor } from "@formbricks/lib/utils/colors";
import { TProductStyling } from "@formbricks/types/product";
import { TSurveyStyling } from "@formbricks/types/surveys";

View File

@@ -1,81 +1,21 @@
"use client";
import { createId } from "@paralleldrive/cuid2";
import { FingerprintIcon, TagIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TBaseFilter, TSegmentAttributeFilter, TSegmentPersonFilter } from "@formbricks/types/segment";
import { TBaseFilter } from "@formbricks/types/segment";
import { Input } from "../Input";
import { Modal } from "../Modal";
import { handleAddFilter } from "./lib/utils";
const handleAddFilter = ({
type,
attributeClassName,
isUserId = false,
onAddFilter,
setOpen,
}: {
type: "person" | "attribute";
attributeClassName?: string;
isUserId?: boolean;
onAddFilter: (filter: TBaseFilter) => void;
setOpen: (open: boolean) => void;
}) => {
if (type === "person") {
const newResource: TSegmentPersonFilter = {
id: createId(),
root: { type: "person", personIdentifier: "userId" },
qualifier: {
operator: "equals",
},
value: "",
};
const newFilter: TBaseFilter = {
id: createId(),
connector: "and",
resource: newResource,
};
onAddFilter(newFilter);
setOpen(false);
return;
}
if (!attributeClassName) return;
const newFilterResource: TSegmentAttributeFilter = {
id: createId(),
root: {
type: "attribute",
attributeClassName,
},
qualifier: {
operator: "equals",
},
value: "",
...(isUserId && { meta: { isUserId } }),
};
const newFilter: TBaseFilter = {
id: createId(),
connector: "and",
resource: newFilterResource,
};
onAddFilter(newFilter);
setOpen(false);
};
type TBasicAddFilterModalProps = {
interface TBasicAddFilterModalProps {
open: boolean;
setOpen: (open: boolean) => void;
onAddFilter: (filter: TBaseFilter) => void;
attributeClasses: TAttributeClass[];
};
}
export const BasicAddFilterModal = ({
onAddFilter,

View File

@@ -0,0 +1,63 @@
import { createId } from "@paralleldrive/cuid2";
import { TBaseFilter, TSegmentAttributeFilter, TSegmentPersonFilter } from "@formbricks/types/segment";
export const handleAddFilter = ({
type,
attributeClassName,
isUserId = false,
onAddFilter,
setOpen,
}: {
type: "person" | "attribute";
attributeClassName?: string;
isUserId?: boolean;
onAddFilter: (filter: TBaseFilter) => void;
setOpen: (open: boolean) => void;
}) => {
if (type === "person") {
const newResource: TSegmentPersonFilter = {
id: createId(),
root: { type: "person", personIdentifier: "userId" },
qualifier: {
operator: "equals",
},
value: "",
};
const newFilter: TBaseFilter = {
id: createId(),
connector: "and",
resource: newResource,
};
onAddFilter(newFilter);
setOpen(false);
return;
}
if (!attributeClassName) return;
const newFilterResource: TSegmentAttributeFilter = {
id: createId(),
root: {
type: "attribute",
attributeClassName,
},
qualifier: {
operator: "equals",
},
value: "",
...(isUserId && { meta: { isUserId } }),
};
const newFilter: TBaseFilter = {
id: createId(),
connector: "and",
resource: newFilterResource,
};
onAddFilter(newFilter);
setOpen(false);
};

View File

@@ -0,0 +1,218 @@
import { TagIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { z } from "zod";
import { cn } from "@formbricks/lib/cn";
import {
convertOperatorToText,
convertOperatorToTitle,
updateAttributeClassNameInFilter,
updateOperatorInFilter,
} from "@formbricks/lib/segment/utils";
import { isCapitalized } from "@formbricks/lib/utils/strings";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import {
ARITHMETIC_OPERATORS,
ATTRIBUTE_OPERATORS,
TArithmeticOperator,
TAttributeOperator,
TSegment,
TSegmentAttributeFilter,
TSegmentConnector,
TSegmentFilterValue,
} from "@formbricks/types/segment";
import { Input } from "../../Input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../Select";
import { SegmentFilterItemConnector } from "./SegmentFilterItemConnector";
import { SegmentFilterItemContextMenu } from "./SegmentFilterItemContextMenu";
interface AttributeSegmentFilterProps {
connector: TSegmentConnector;
environmentId: string;
segment: TSegment;
attributeClasses: TAttributeClass[];
setSegment: (segment: TSegment) => void;
onDeleteFilter: (filterId: string) => void;
onMoveFilter: (filterId: string, direction: "up" | "down") => void;
viewOnly?: boolean;
resource: TSegmentAttributeFilter;
updateValueInLocalSurvey: (filterId: string, newValue: TSegmentFilterValue) => void;
}
export const AttributeSegmentFilter = ({
connector,
resource,
onDeleteFilter,
onMoveFilter,
updateValueInLocalSurvey,
segment,
setSegment,
attributeClasses,
viewOnly,
}: AttributeSegmentFilterProps) => {
const { attributeClassName } = resource.root;
const operatorText = convertOperatorToText(resource.qualifier.operator);
const [valueError, setValueError] = useState("");
// when the operator changes, we need to check if the value is valid
useEffect(() => {
const { operator } = resource.qualifier;
if (ARITHMETIC_OPERATORS.includes(operator as TArithmeticOperator)) {
const isNumber = z.coerce.number().safeParse(resource.value);
if (isNumber.success) {
setValueError("");
} else {
setValueError("Value must be a number");
}
}
}, [resource.qualifier, resource.value]);
const operatorArr = ATTRIBUTE_OPERATORS.map((operator) => {
return {
id: operator,
name: convertOperatorToText(operator),
};
});
const attributeClass = attributeClasses?.find((attrClass) => attrClass?.name === attributeClassName)?.name;
const updateOperatorInLocalSurvey = (filterId: string, newOperator: TAttributeOperator) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updateOperatorInFilter(updatedSegment.filters, filterId, newOperator);
}
setSegment(updatedSegment);
};
const updateAttributeClassNameInLocalSurvey = (filterId: string, newAttributeClassName: string) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updateAttributeClassNameInFilter(updatedSegment.filters, filterId, newAttributeClassName);
}
setSegment(updatedSegment);
};
const checkValueAndUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
updateValueInLocalSurvey(resource.id, value);
if (!value) {
setValueError("Value cannot be empty");
return;
}
const { operator } = resource.qualifier;
if (ARITHMETIC_OPERATORS.includes(operator as TArithmeticOperator)) {
const isNumber = z.coerce.number().safeParse(value);
if (isNumber.success) {
setValueError("");
updateValueInLocalSurvey(resource.id, parseInt(value, 10));
} else {
setValueError("Value must be a number");
updateValueInLocalSurvey(resource.id, value);
}
return;
}
setValueError("");
updateValueInLocalSurvey(resource.id, value);
};
return (
<div className="flex items-center gap-2 text-sm">
<SegmentFilterItemConnector
key={connector}
connector={connector}
filterId={resource.id}
setSegment={setSegment}
segment={segment}
viewOnly={viewOnly}
/>
<Select
value={attributeClass}
onValueChange={(value) => {
updateAttributeClassNameInLocalSurvey(resource.id, value);
}}
disabled={viewOnly}>
<SelectTrigger
className="flex w-auto items-center justify-center whitespace-nowrap bg-white capitalize"
hideArrow>
<SelectValue>
<div
className={cn("flex items-center gap-1", !isCapitalized(attributeClass ?? "") && "lowercase")}>
<TagIcon className="h-4 w-4 text-sm" />
<p>{attributeClass}</p>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
{attributeClasses
?.filter((attributeClass) => !attributeClass.archived)
?.map((attrClass) => (
<SelectItem value={attrClass.name} key={attrClass.id}>
{attrClass.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={operatorText}
onValueChange={(operator: TAttributeOperator) => {
updateOperatorInLocalSurvey(resource.id, operator);
}}
disabled={viewOnly}>
<SelectTrigger className="flex w-auto items-center justify-center bg-white text-center" hideArrow>
<SelectValue>
<p>{operatorText}</p>
</SelectValue>
</SelectTrigger>
<SelectContent>
{operatorArr.map((operator) => (
<SelectItem value={operator.id} title={convertOperatorToTitle(operator.id)}>
{operator.name}
</SelectItem>
))}
</SelectContent>
</Select>
{!["isSet", "isNotSet"].includes(resource.qualifier.operator) && (
<div className="relative flex flex-col gap-1">
<Input
disabled={viewOnly}
value={resource.value}
onChange={(e) => {
checkValueAndUpdate(e);
}}
className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
/>
{valueError && (
<p className="absolute right-2 -mt-1 rounded-md bg-white px-2 text-xs text-red-500">
{valueError}
</p>
)}
</div>
)}
<SegmentFilterItemContextMenu
filterId={resource.id}
onDeleteFilter={onDeleteFilter}
onMoveFilter={onMoveFilter}
viewOnly={viewOnly}
/>
</div>
);
};

View File

@@ -0,0 +1,86 @@
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { updateFilterValue } from "@formbricks/lib/segment/utils";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import {
TSegment,
TSegmentAttributeFilter,
TSegmentConnector,
TSegmentFilter,
TSegmentPersonFilter,
} from "@formbricks/types/segment";
import { AttributeSegmentFilter } from "./AttributeSegmentFilter";
import { PersonSegmentFilter } from "./PersonSegmentFilter";
interface BasicSegmentFilterProps {
connector: TSegmentConnector;
resource: TSegmentFilter;
environmentId: string;
segment: TSegment;
attributeClasses: TAttributeClass[];
setSegment: (segment: TSegment) => void;
onDeleteFilter: (filterId: string) => void;
onMoveFilter: (filterId: string, direction: "up" | "down") => void;
viewOnly?: boolean;
}
export const BasicSegmentFilter = ({
resource,
connector,
environmentId,
segment,
attributeClasses,
setSegment,
onDeleteFilter,
onMoveFilter,
viewOnly,
}: BasicSegmentFilterProps) => {
const updateFilterValueInSegment = (filterId: string, newValue: string | number) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updateFilterValue(updatedSegment.filters, filterId, newValue);
}
setSegment(updatedSegment);
};
switch (resource.root.type) {
case "attribute":
return (
<>
<AttributeSegmentFilter
connector={connector}
resource={resource as TSegmentAttributeFilter}
environmentId={environmentId}
segment={segment}
attributeClasses={attributeClasses}
setSegment={setSegment}
onDeleteFilter={onDeleteFilter}
onMoveFilter={onMoveFilter}
updateValueInLocalSurvey={updateFilterValueInSegment}
viewOnly={viewOnly}
/>
</>
);
case "person":
return (
<>
<PersonSegmentFilter
connector={connector}
resource={resource as TSegmentPersonFilter}
environmentId={environmentId}
segment={segment}
setSegment={setSegment}
onDeleteFilter={onDeleteFilter}
onMoveFilter={onMoveFilter}
updateValueInLocalSurvey={updateFilterValueInSegment}
viewOnly={viewOnly}
/>
</>
);
default:
return null;
}
};

View File

@@ -0,0 +1,207 @@
import { FingerprintIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { z } from "zod";
import { cn } from "@formbricks/lib/cn";
import {
convertOperatorToText,
convertOperatorToTitle,
updateOperatorInFilter,
updatePersonIdentifierInFilter,
} from "@formbricks/lib/segment/utils";
import {
ARITHMETIC_OPERATORS,
PERSON_OPERATORS,
TArithmeticOperator,
TAttributeOperator,
TSegment,
TSegmentConnector,
TSegmentFilterValue,
TSegmentPersonFilter,
} from "@formbricks/types/segment";
import { Input } from "../../Input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../Select";
import { SegmentFilterItemConnector } from "./SegmentFilterItemConnector";
import { SegmentFilterItemContextMenu } from "./SegmentFilterItemContextMenu";
interface PersonSegmentFilterProps {
connector: TSegmentConnector;
environmentId: string;
segment: TSegment;
setSegment: (segment: TSegment) => void;
onDeleteFilter: (filterId: string) => void;
onMoveFilter: (filterId: string, direction: "up" | "down") => void;
viewOnly?: boolean;
resource: TSegmentPersonFilter;
updateValueInLocalSurvey: (filterId: string, newValue: TSegmentFilterValue) => void;
}
export const PersonSegmentFilter = ({
connector,
resource,
onDeleteFilter,
onMoveFilter,
updateValueInLocalSurvey,
segment,
setSegment,
viewOnly,
}: PersonSegmentFilterProps) => {
const { personIdentifier } = resource.root;
const operatorText = convertOperatorToText(resource.qualifier.operator);
const [valueError, setValueError] = useState("");
// when the operator changes, we need to check if the value is valid
useEffect(() => {
const { operator } = resource.qualifier;
if (ARITHMETIC_OPERATORS.includes(operator as TArithmeticOperator)) {
const isNumber = z.coerce.number().safeParse(resource.value);
if (isNumber.success) {
setValueError("");
} else {
setValueError("Value must be a number");
}
}
}, [resource.qualifier, resource.value]);
const operatorArr = PERSON_OPERATORS.map((operator) => {
return {
id: operator,
name: convertOperatorToText(operator),
};
});
const updateOperatorInLocalSurvey = (filterId: string, newOperator: TAttributeOperator) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updateOperatorInFilter(updatedSegment.filters, filterId, newOperator);
}
setSegment(updatedSegment);
};
const updatePersonIdentifierInLocalSurvey = (filterId: string, newPersonIdentifier: string) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updatePersonIdentifierInFilter(updatedSegment.filters, filterId, newPersonIdentifier);
}
setSegment(updatedSegment);
};
const checkValueAndUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
updateValueInLocalSurvey(resource.id, value);
if (!value) {
setValueError("Value cannot be empty");
return;
}
const { operator } = resource.qualifier;
if (ARITHMETIC_OPERATORS.includes(operator as TArithmeticOperator)) {
const isNumber = z.coerce.number().safeParse(value);
if (isNumber.success) {
setValueError("");
updateValueInLocalSurvey(resource.id, parseInt(value, 10));
} else {
setValueError("Value must be a number");
updateValueInLocalSurvey(resource.id, value);
}
return;
}
setValueError("");
updateValueInLocalSurvey(resource.id, value);
};
return (
<div className="flex items-center gap-2 text-sm">
<SegmentFilterItemConnector
key={connector}
connector={connector}
filterId={resource.id}
setSegment={setSegment}
segment={segment}
viewOnly={viewOnly}
/>
<Select
value={personIdentifier}
onValueChange={(value) => {
updatePersonIdentifierInLocalSurvey(resource.id, value);
}}
disabled={viewOnly}>
<SelectTrigger
className="flex w-auto items-center justify-center whitespace-nowrap bg-white capitalize"
hideArrow>
<SelectValue>
<div className="flex items-center gap-1 lowercase">
<FingerprintIcon className="h-4 w-4 text-sm" />
<p>{personIdentifier}</p>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={personIdentifier} key={personIdentifier}>
{personIdentifier}
</SelectItem>
</SelectContent>
</Select>
<Select
value={operatorText}
onValueChange={(operator: TAttributeOperator) => {
updateOperatorInLocalSurvey(resource.id, operator);
}}
disabled={viewOnly}>
<SelectTrigger className="flex w-auto items-center justify-center bg-white text-center" hideArrow>
<SelectValue>
<p>{operatorText}</p>
</SelectValue>
</SelectTrigger>
<SelectContent>
{operatorArr.map((operator) => (
<SelectItem value={operator.id} title={convertOperatorToTitle(operator.id)}>
{operator.name}
</SelectItem>
))}
</SelectContent>
</Select>
{!["isSet", "isNotSet"].includes(resource.qualifier.operator) && (
<div className="relative flex flex-col gap-1">
<Input
value={resource.value}
onChange={(e) => {
checkValueAndUpdate(e);
}}
className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
disabled={viewOnly}
/>
{valueError && (
<p className="absolute right-2 -mt-1 rounded-md bg-white px-2 text-xs text-red-500">
{valueError}
</p>
)}
</div>
)}
<SegmentFilterItemContextMenu
filterId={resource.id}
onDeleteFilter={onDeleteFilter}
onMoveFilter={onMoveFilter}
viewOnly={viewOnly}
/>
</div>
);
};

View File

@@ -0,0 +1,51 @@
import { cn } from "@formbricks/lib/cn";
import { toggleFilterConnector } from "@formbricks/lib/segment/utils";
import { TSegment, TSegmentConnector } from "@formbricks/types/segment";
interface SegmentFilterItemConnectorProps {
connector: TSegmentConnector;
segment: TSegment;
setSegment: (segment: TSegment) => void;
filterId: string;
viewOnly?: boolean;
}
export const SegmentFilterItemConnector = ({
connector,
segment,
setSegment,
filterId,
viewOnly,
}: SegmentFilterItemConnectorProps) => {
const updateLocalSurvey = (newConnector: TSegmentConnector) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
toggleFilterConnector(updatedSegment.filters, filterId, newConnector);
}
setSegment(updatedSegment);
};
const onConnectorChange = () => {
if (!connector) return;
if (connector === "and") {
updateLocalSurvey("or");
} else {
updateLocalSurvey("and");
}
};
return (
<div className="w-[40px]">
<span
className={cn(!!connector && "cursor-pointer underline", viewOnly && "cursor-not-allowed")}
onClick={() => {
if (viewOnly) return;
onConnectorChange();
}}>
{!!connector ? connector : "Where"}
</span>
</div>
);
};

View File

@@ -0,0 +1,45 @@
import { MoreVerticalIcon, Trash2Icon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
import { Button } from "../../Button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../DropdownMenu";
interface SegmentFilterItemContextMenuProps {
filterId: string;
onDeleteFilter: (filterId: string) => void;
onMoveFilter: (filterId: string, direction: "up" | "down") => void;
viewOnly?: boolean;
}
export const SegmentFilterItemContextMenu = ({
filterId,
onDeleteFilter,
onMoveFilter,
viewOnly,
}: SegmentFilterItemContextMenuProps) => {
return (
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger disabled={viewOnly}>
<MoreVerticalIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => onMoveFilter(filterId, "up")}>Move up</DropdownMenuItem>
<DropdownMenuItem onClick={() => onMoveFilter(filterId, "down")}>Move down</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="minimal"
className="mr-4 p-0"
onClick={() => {
onDeleteFilter(filterId);
}}
disabled={viewOnly}>
<Trash2Icon className={cn("h-4 w-4 cursor-pointer", viewOnly && "cursor-not-allowed")} />
</Button>
</div>
);
};

View File

@@ -3,16 +3,16 @@ import { deleteResource, isResourceFilter, moveResource } from "@formbricks/lib/
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TBaseFilters, TSegment } from "@formbricks/types/segment";
import { BasicSegmentFilter } from "./BasicSegmentFilter";
import { BasicSegmentFilter } from "./components/BasicSegmentFilter";
type TBasicSegmentEditorProps = {
interface BasicSegmentEditorProps {
group: TBaseFilters;
environmentId: string;
segment: TSegment;
attributeClasses: TAttributeClass[];
setSegment: React.Dispatch<React.SetStateAction<TSegment>>;
viewOnly?: boolean;
};
}
export const BasicSegmentEditor = ({
group,
@@ -21,7 +21,7 @@ export const BasicSegmentEditor = ({
segment,
attributeClasses,
viewOnly,
}: TBasicSegmentEditorProps) => {
}: BasicSegmentEditorProps) => {
const handleMoveResource = (resourceId: string, direction: "up" | "down") => {
const localSegmentCopy = structuredClone(segment);
if (localSegmentCopy.filters) {

View File

@@ -5,12 +5,12 @@ import { TSegmentWithSurveyNames } from "@formbricks/types/segment";
import { Button } from "../Button";
import { Modal } from "../Modal";
type ConfirmDeleteSegmentModalProps = {
interface ConfirmDeleteSegmentModalProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
segment: TSegmentWithSurveyNames;
onDelete: () => Promise<void>;
};
}
export const ConfirmDeleteSegmentModal = ({
onDelete,

View File

@@ -11,7 +11,7 @@ import { TSurvey } from "@formbricks/types/surveys";
import { Modal } from "../Modal";
type SegmentDetailProps = {
interface SegmentDetailProps {
segment: TSegment;
setSegment: (segment: TSegment) => void;
setOpen: (open: boolean) => void;
@@ -19,7 +19,7 @@ type SegmentDetailProps = {
onSegmentLoad: (surveyId: string, segmentId: string) => Promise<TSurvey>;
surveyId: string;
currentSegment: TSegment;
};
}
const SegmentDetail = ({
segment,

View File

@@ -12,7 +12,7 @@ import { Button } from "../Button";
import { Input } from "../Input";
import { Modal } from "../Modal";
type SaveAsNewSegmentModalProps = {
interface SaveAsNewSegmentModalProps {
open: boolean;
setOpen: (open: boolean) => void;
localSurvey: TSurvey;
@@ -21,7 +21,7 @@ type SaveAsNewSegmentModalProps = {
setIsSegmentEditorOpen: (isOpen: boolean) => void;
onCreateSegment: (data: TSegmentCreateInput) => Promise<TSegment>;
onUpdateSegment: (environmentId: string, segmentId: string, data: TSegmentUpdateInput) => Promise<TSegment>;
};
}
type SaveAsNewSegmentModalForm = {
title: string;

View File

@@ -1,548 +0,0 @@
import { FingerprintIcon, MoreVertical, TagIcon, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import z from "zod";
import { cn } from "@formbricks/lib/cn";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import {
convertOperatorToText,
convertOperatorToTitle,
toggleFilterConnector,
updateAttributeClassNameInFilter,
updateFilterValue,
updateOperatorInFilter,
updatePersonIdentifierInFilter,
} from "@formbricks/lib/segment/utils";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import {
ARITHMETIC_OPERATORS,
ATTRIBUTE_OPERATORS,
PERSON_OPERATORS,
TArithmeticOperator,
TAttributeOperator,
TSegment,
TSegmentAttributeFilter,
TSegmentConnector,
TSegmentFilter,
TSegmentFilterValue,
TSegmentPersonFilter,
} from "@formbricks/types/segment";
import { Button } from "../Button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../DropdownMenu";
import { Input } from "../Input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../Select";
type TBasicSegmentFilterProps = {
connector: TSegmentConnector;
resource: TSegmentFilter;
environmentId: string;
segment: TSegment;
attributeClasses: TAttributeClass[];
setSegment: (segment: TSegment) => void;
onDeleteFilter: (filterId: string) => void;
onMoveFilter: (filterId: string, direction: "up" | "down") => void;
viewOnly?: boolean;
};
const isCapitalized = (str: string) => str.charAt(0) === str.charAt(0).toUpperCase();
const SegmentFilterItemConnector = ({
connector,
segment,
setSegment,
filterId,
viewOnly,
}: {
connector: TSegmentConnector;
segment: TSegment;
setSegment: (segment: TSegment) => void;
filterId: string;
viewOnly?: boolean;
}) => {
const updateLocalSurvey = (newConnector: TSegmentConnector) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
toggleFilterConnector(updatedSegment.filters, filterId, newConnector);
}
setSegment(updatedSegment);
};
const onConnectorChange = () => {
if (!connector) return;
if (connector === "and") {
updateLocalSurvey("or");
} else {
updateLocalSurvey("and");
}
};
return (
<div className="w-[40px]">
<span
className={cn(!!connector && "cursor-pointer underline", viewOnly && "cursor-not-allowed")}
onClick={() => {
if (viewOnly) return;
onConnectorChange();
}}>
{!!connector ? connector : "Where"}
</span>
</div>
);
};
const SegmentFilterItemContextMenu = ({
filterId,
onDeleteFilter,
onMoveFilter,
viewOnly,
}: {
filterId: string;
onDeleteFilter: (filterId: string) => void;
onMoveFilter: (filterId: string, direction: "up" | "down") => void;
viewOnly?: boolean;
}) => {
return (
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger disabled={viewOnly}>
<MoreVertical className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => onMoveFilter(filterId, "up")}>Move up</DropdownMenuItem>
<DropdownMenuItem onClick={() => onMoveFilter(filterId, "down")}>Move down</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant="minimal"
className="mr-4 p-0"
onClick={() => {
onDeleteFilter(filterId);
}}
disabled={viewOnly}>
<Trash2 className={cn("h-4 w-4 cursor-pointer", viewOnly && "cursor-not-allowed")}></Trash2>
</Button>
</div>
);
};
type TAttributeSegmentFilterProps = TBasicSegmentFilterProps & {
resource: TSegmentAttributeFilter;
updateValueInLocalSurvey: (filterId: string, newValue: TSegmentFilterValue) => void;
};
const AttributeSegmentFilter = ({
connector,
resource,
onDeleteFilter,
onMoveFilter,
updateValueInLocalSurvey,
segment,
setSegment,
attributeClasses,
viewOnly,
}: TAttributeSegmentFilterProps) => {
const { attributeClassName } = resource.root;
const operatorText = convertOperatorToText(resource.qualifier.operator);
const [valueError, setValueError] = useState("");
// when the operator changes, we need to check if the value is valid
useEffect(() => {
const { operator } = resource.qualifier;
if (ARITHMETIC_OPERATORS.includes(operator as TArithmeticOperator)) {
const isNumber = z.coerce.number().safeParse(resource.value);
if (isNumber.success) {
setValueError("");
} else {
setValueError("Value must be a number");
}
}
}, [resource.qualifier, resource.value]);
const operatorArr = ATTRIBUTE_OPERATORS.map((operator) => {
return {
id: operator,
name: convertOperatorToText(operator),
};
});
const attributeClass = attributeClasses?.find((attrClass) => attrClass?.name === attributeClassName)?.name;
const updateOperatorInLocalSurvey = (filterId: string, newOperator: TAttributeOperator) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updateOperatorInFilter(updatedSegment.filters, filterId, newOperator);
}
setSegment(updatedSegment);
};
const updateAttributeClassNameInLocalSurvey = (filterId: string, newAttributeClassName: string) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updateAttributeClassNameInFilter(updatedSegment.filters, filterId, newAttributeClassName);
}
setSegment(updatedSegment);
};
const checkValueAndUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
updateValueInLocalSurvey(resource.id, value);
if (!value) {
setValueError("Value cannot be empty");
return;
}
const { operator } = resource.qualifier;
if (ARITHMETIC_OPERATORS.includes(operator as TArithmeticOperator)) {
const isNumber = z.coerce.number().safeParse(value);
if (isNumber.success) {
setValueError("");
updateValueInLocalSurvey(resource.id, parseInt(value, 10));
} else {
setValueError("Value must be a number");
updateValueInLocalSurvey(resource.id, value);
}
return;
}
setValueError("");
updateValueInLocalSurvey(resource.id, value);
};
return (
<div className="flex items-center gap-2 text-sm">
<SegmentFilterItemConnector
key={connector}
connector={connector}
filterId={resource.id}
setSegment={setSegment}
segment={segment}
viewOnly={viewOnly}
/>
<Select
value={attributeClass}
onValueChange={(value) => {
updateAttributeClassNameInLocalSurvey(resource.id, value);
}}
disabled={viewOnly}>
<SelectTrigger
className="flex w-auto items-center justify-center whitespace-nowrap bg-white capitalize"
hideArrow>
<SelectValue>
<div
className={cn("flex items-center gap-1", !isCapitalized(attributeClass ?? "") && "lowercase")}>
<TagIcon className="h-4 w-4 text-sm" />
<p>{attributeClass}</p>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
{attributeClasses
?.filter((attributeClass) => !attributeClass.archived)
?.map((attrClass) => (
<SelectItem value={attrClass.name} key={attrClass.id}>
{attrClass.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={operatorText}
onValueChange={(operator: TAttributeOperator) => {
updateOperatorInLocalSurvey(resource.id, operator);
}}
disabled={viewOnly}>
<SelectTrigger className="flex w-auto items-center justify-center bg-white text-center" hideArrow>
<SelectValue>
<p>{operatorText}</p>
</SelectValue>
</SelectTrigger>
<SelectContent>
{operatorArr.map((operator) => (
<SelectItem value={operator.id} title={convertOperatorToTitle(operator.id)}>
{operator.name}
</SelectItem>
))}
</SelectContent>
</Select>
{!["isSet", "isNotSet"].includes(resource.qualifier.operator) && (
<div className="relative flex flex-col gap-1">
<Input
disabled={viewOnly}
value={resource.value}
onChange={(e) => {
checkValueAndUpdate(e);
}}
className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
/>
{valueError && (
<p className="absolute right-2 -mt-1 rounded-md bg-white px-2 text-xs text-red-500">
{valueError}
</p>
)}
</div>
)}
<SegmentFilterItemContextMenu
filterId={resource.id}
onDeleteFilter={onDeleteFilter}
onMoveFilter={onMoveFilter}
viewOnly={viewOnly}
/>
</div>
);
};
type TPersonSegmentFilterProps = Omit<TBasicSegmentFilterProps, "attributeClasses"> & {
resource: TSegmentPersonFilter;
updateValueInLocalSurvey: (filterId: string, newValue: TSegmentFilterValue) => void;
};
const PersonSegmentFilter = ({
connector,
resource,
onDeleteFilter,
onMoveFilter,
updateValueInLocalSurvey,
segment,
setSegment,
viewOnly,
}: TPersonSegmentFilterProps) => {
const { personIdentifier } = resource.root;
const operatorText = convertOperatorToText(resource.qualifier.operator);
const [valueError, setValueError] = useState("");
// when the operator changes, we need to check if the value is valid
useEffect(() => {
const { operator } = resource.qualifier;
if (ARITHMETIC_OPERATORS.includes(operator as TArithmeticOperator)) {
const isNumber = z.coerce.number().safeParse(resource.value);
if (isNumber.success) {
setValueError("");
} else {
setValueError("Value must be a number");
}
}
}, [resource.qualifier, resource.value]);
const operatorArr = PERSON_OPERATORS.map((operator) => {
return {
id: operator,
name: convertOperatorToText(operator),
};
});
const updateOperatorInLocalSurvey = (filterId: string, newOperator: TAttributeOperator) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updateOperatorInFilter(updatedSegment.filters, filterId, newOperator);
}
setSegment(updatedSegment);
};
const updatePersonIdentifierInLocalSurvey = (filterId: string, newPersonIdentifier: string) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updatePersonIdentifierInFilter(updatedSegment.filters, filterId, newPersonIdentifier);
}
setSegment(updatedSegment);
};
const checkValueAndUpdate = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
updateValueInLocalSurvey(resource.id, value);
if (!value) {
setValueError("Value cannot be empty");
return;
}
const { operator } = resource.qualifier;
if (ARITHMETIC_OPERATORS.includes(operator as TArithmeticOperator)) {
const isNumber = z.coerce.number().safeParse(value);
if (isNumber.success) {
setValueError("");
updateValueInLocalSurvey(resource.id, parseInt(value, 10));
} else {
setValueError("Value must be a number");
updateValueInLocalSurvey(resource.id, value);
}
return;
}
setValueError("");
updateValueInLocalSurvey(resource.id, value);
};
return (
<div className="flex items-center gap-2 text-sm">
<SegmentFilterItemConnector
key={connector}
connector={connector}
filterId={resource.id}
setSegment={setSegment}
segment={segment}
viewOnly={viewOnly}
/>
<Select
value={personIdentifier}
onValueChange={(value) => {
updatePersonIdentifierInLocalSurvey(resource.id, value);
}}
disabled={viewOnly}>
<SelectTrigger
className="flex w-auto items-center justify-center whitespace-nowrap bg-white capitalize"
hideArrow>
<SelectValue>
<div className="flex items-center gap-1 lowercase">
<FingerprintIcon className="h-4 w-4 text-sm" />
<p>{personIdentifier}</p>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value={personIdentifier} key={personIdentifier}>
{personIdentifier}
</SelectItem>
</SelectContent>
</Select>
<Select
value={operatorText}
onValueChange={(operator: TAttributeOperator) => {
updateOperatorInLocalSurvey(resource.id, operator);
}}
disabled={viewOnly}>
<SelectTrigger className="flex w-auto items-center justify-center bg-white text-center" hideArrow>
<SelectValue>
<p>{operatorText}</p>
</SelectValue>
</SelectTrigger>
<SelectContent>
{operatorArr.map((operator) => (
<SelectItem value={operator.id} title={convertOperatorToTitle(operator.id)}>
{operator.name}
</SelectItem>
))}
</SelectContent>
</Select>
{!["isSet", "isNotSet"].includes(resource.qualifier.operator) && (
<div className="relative flex flex-col gap-1">
<Input
value={resource.value}
onChange={(e) => {
checkValueAndUpdate(e);
}}
className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")}
disabled={viewOnly}
/>
{valueError && (
<p className="absolute right-2 -mt-1 rounded-md bg-white px-2 text-xs text-red-500">
{valueError}
</p>
)}
</div>
)}
<SegmentFilterItemContextMenu
filterId={resource.id}
onDeleteFilter={onDeleteFilter}
onMoveFilter={onMoveFilter}
viewOnly={viewOnly}
/>
</div>
);
};
export const BasicSegmentFilter = ({
resource,
connector,
environmentId,
segment,
attributeClasses,
setSegment,
onDeleteFilter,
onMoveFilter,
viewOnly,
}: TBasicSegmentFilterProps) => {
const updateFilterValueInSegment = (filterId: string, newValue: string | number) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
updateFilterValue(updatedSegment.filters, filterId, newValue);
}
setSegment(updatedSegment);
};
switch (resource.root.type) {
case "attribute":
return (
<>
<AttributeSegmentFilter
connector={connector}
resource={resource as TSegmentAttributeFilter}
environmentId={environmentId}
segment={segment}
attributeClasses={attributeClasses}
setSegment={setSegment}
onDeleteFilter={onDeleteFilter}
onMoveFilter={onMoveFilter}
updateValueInLocalSurvey={updateFilterValueInSegment}
viewOnly={viewOnly}
/>
</>
);
case "person":
return (
<>
<PersonSegmentFilter
connector={connector}
resource={resource as TSegmentPersonFilter}
environmentId={environmentId}
segment={segment}
setSegment={setSegment}
onDeleteFilter={onDeleteFilter}
onMoveFilter={onMoveFilter}
updateValueInLocalSurvey={updateFilterValueInSegment}
viewOnly={viewOnly}
/>
</>
);
default:
return null;
}
};

View File

@@ -1,36 +0,0 @@
import { useRouter } from "next/navigation";
import { Button } from "../Button";
import { Modal } from "../Modal";
type TSegmentAlreadyUsedModalProps = {
open: boolean;
setOpen: (open: boolean) => void;
environmentId: string;
};
export const SegmentAlreadyUsedModal = ({ open, setOpen, environmentId }: TSegmentAlreadyUsedModalProps) => {
const router = useRouter();
return (
<Modal open={open} setOpen={setOpen} title="Forward to Segments View">
<p>This segment is used in other surveys. To assure consistent data you cannot edit it here.</p>
<div className="space-x-2 text-right">
<Button
variant="warn"
onClick={() => {
setOpen(false);
}}>
Discard
</Button>
<Button
variant="darkCTA"
onClick={() => {
router.push(`/environments/${environmentId}/segments`);
}}>
Go to Segments
</Button>
</div>
</Modal>
);
};