mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-25 10:20:03 -06:00
fix: targeting ui dir structure (#2708)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)]);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
},
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [".", "../ui/Targeting/TargetingIndicator.tsx"],
|
||||
"include": [".", "../ui/TargetingIndicator.tsx"],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
3
packages/lib/utils/promises.ts
Normal file
3
packages/lib/utils/promises.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const delay = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
63
packages/ui/BasicAddFilterModal/lib/utils.ts
Normal file
63
packages/ui/BasicAddFilterModal/lib/utils.ts
Normal 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);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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) {
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user