diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings.tsx index e79c48885e..e0a3c6b5f4 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings.tsx @@ -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"; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/TargetingCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/TargetingCard.tsx index 772046aeea..ebf91ab134 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/TargetingCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/TargetingCard.tsx @@ -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 { diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/components/AttributeActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/components/AttributeActivityTab.tsx index e0e4a09a20..68cbe1a2bd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/components/AttributeActivityTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/components/AttributeActivityTab.tsx @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/AttributesSection.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/AttributesSection.tsx index b268ae7535..30e9f87879 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/AttributesSection.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/AttributesSection.tsx @@ -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)]); diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal.tsx index 7dab0348b8..827cd77777 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal.tsx @@ -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 = { diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/components/BasicSegmentSettings.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/components/BasicSegmentSettings.tsx index 97f6b5bbf8..12597525bd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/components/BasicSegmentSettings.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/components/BasicSegmentSettings.tsx @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/actions/components/ActionActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/actions/components/ActionActivityTab.tsx index 0c09a7ee9e..b3f90dcada 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions/components/ActionActivityTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/actions/components/ActionActivityTab.tsx @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx index 2706f9395a..05c634b359 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab.tsx index 3a8098a91f..eb2069baa7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookOverviewTab.tsx @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData.tsx index 1d60737dd2..8d8fd8c888 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData.tsx @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/api-keys/components/EditApiKeys.tsx b/apps/web/app/(app)/environments/[environmentId]/product/api-keys/components/EditApiKeys.tsx index f85bbf5f17..ae5d284227 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/api-keys/components/EditApiKeys.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/api-keys/components/EditApiKeys.tsx @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/general/components/DeleteProductRender.tsx b/apps/web/app/(app)/environments/[environmentId]/product/general/components/DeleteProductRender.tsx index 8dec2c6af7..0cc7afa2c6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/general/components/DeleteProductRender.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/general/components/DeleteProductRender.tsx @@ -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"; diff --git a/packages/ee/RoleManagement/components/EditMembershipRole.tsx b/packages/ee/RoleManagement/components/EditMembershipRole.tsx index ab0bfb5d84..7bc1bc5f48 100644 --- a/packages/ee/RoleManagement/components/EditMembershipRole.tsx +++ b/packages/ee/RoleManagement/components/EditMembershipRole.tsx @@ -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"; diff --git a/packages/ee/advancedTargeting/components/AdvancedTargetingCard.tsx b/packages/ee/advancedTargeting/components/AdvancedTargetingCard.tsx index 802b491051..a9ef735f01 100644 --- a/packages/ee/advancedTargeting/components/AdvancedTargetingCard.tsx +++ b/packages/ee/advancedTargeting/components/AdvancedTargetingCard.tsx @@ -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, diff --git a/packages/ee/advancedTargeting/components/SegmentFilter.tsx b/packages/ee/advancedTargeting/components/SegmentFilter.tsx index 0f871369f5..24583251bf 100644 --- a/packages/ee/advancedTargeting/components/SegmentFilter.tsx +++ b/packages/ee/advancedTargeting/components/SegmentFilter.tsx @@ -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, diff --git a/packages/ee/advancedTargeting/components/SegmentSettings.tsx b/packages/ee/advancedTargeting/components/SegmentSettings.tsx index dabcbcdd92..d69e9aa761 100644 --- a/packages/ee/advancedTargeting/components/SegmentSettings.tsx +++ b/packages/ee/advancedTargeting/components/SegmentSettings.tsx @@ -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"; diff --git a/packages/ee/tsconfig.json b/packages/ee/tsconfig.json index 4fbb6cc31e..58909315d9 100644 --- a/packages/ee/tsconfig.json +++ b/packages/ee/tsconfig.json @@ -7,6 +7,6 @@ }, "resolveJsonModule": true }, - "include": [".", "../ui/Targeting/TargetingIndicator.tsx"], + "include": [".", "../ui/TargetingIndicator.tsx"], "exclude": ["dist", "build", "node_modules"] } diff --git a/packages/email/components/survey/PreviewEmailTemplate.tsx b/packages/email/components/survey/PreviewEmailTemplate.tsx index f5cabc7b2c..53898236c2 100644 --- a/packages/email/components/survey/PreviewEmailTemplate.tsx +++ b/packages/email/components/survey/PreviewEmailTemplate.tsx @@ -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"; diff --git a/packages/lib/response/utils.ts b/packages/lib/response/utils.ts index 50424d3b9b..208d42e4fe 100644 --- a/packages/lib/response/utils.ts +++ b/packages/lib/response/utils.ts @@ -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 }; diff --git a/packages/lib/responseQueue.ts b/packages/lib/responseQueue.ts index d6abf4690f..b8e601af5d 100644 --- a/packages/lib/responseQueue.ts +++ b/packages/lib/responseQueue.ts @@ -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; diff --git a/packages/lib/utils.ts b/packages/lib/utils/colors.ts similarity index 96% rename from packages/lib/utils.ts rename to packages/lib/utils/colors.ts index 8687c7375a..9f11e68947 100644 --- a/packages/lib/utils.ts +++ b/packages/lib/utils/colors.ts @@ -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 diff --git a/packages/lib/utils/promises.ts b/packages/lib/utils/promises.ts new file mode 100644 index 0000000000..f9070711bb --- /dev/null +++ b/packages/lib/utils/promises.ts @@ -0,0 +1,3 @@ +export const delay = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; diff --git a/packages/lib/strings.ts b/packages/lib/utils/strings.ts similarity index 88% rename from packages/lib/strings.ts rename to packages/lib/utils/strings.ts index 3c00843395..eabf3451a3 100644 --- a/packages/lib/strings.ts +++ b/packages/lib/utils/strings.ts @@ -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(); diff --git a/packages/surveys/src/lib/styles.ts b/packages/surveys/src/lib/styles.ts index bf4342c4bd..4bc4eea9a2 100644 --- a/packages/surveys/src/lib/styles.ts +++ b/packages/surveys/src/lib/styles.ts @@ -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"; diff --git a/packages/ui/Targeting/BasicAddFilterModal.tsx b/packages/ui/BasicAddFilterModal/index.tsx similarity index 66% rename from packages/ui/Targeting/BasicAddFilterModal.tsx rename to packages/ui/BasicAddFilterModal/index.tsx index 9cd6d74f33..94813c39a9 100644 --- a/packages/ui/Targeting/BasicAddFilterModal.tsx +++ b/packages/ui/BasicAddFilterModal/index.tsx @@ -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, diff --git a/packages/ui/BasicAddFilterModal/lib/utils.ts b/packages/ui/BasicAddFilterModal/lib/utils.ts new file mode 100644 index 0000000000..cb93d0299d --- /dev/null +++ b/packages/ui/BasicAddFilterModal/lib/utils.ts @@ -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); +}; diff --git a/packages/ui/BasicSegmentEditor/components/AttributeSegmentFilter.tsx b/packages/ui/BasicSegmentEditor/components/AttributeSegmentFilter.tsx new file mode 100644 index 0000000000..b1fb4bed42 --- /dev/null +++ b/packages/ui/BasicSegmentEditor/components/AttributeSegmentFilter.tsx @@ -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) => { + 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 ( +
+ + + + + + + {!["isSet", "isNotSet"].includes(resource.qualifier.operator) && ( +
+ { + checkValueAndUpdate(e); + }} + className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")} + /> + + {valueError && ( +

+ {valueError} +

+ )} +
+ )} + + +
+ ); +}; diff --git a/packages/ui/BasicSegmentEditor/components/BasicSegmentFilter.tsx b/packages/ui/BasicSegmentEditor/components/BasicSegmentFilter.tsx new file mode 100644 index 0000000000..0bf7d2acf2 --- /dev/null +++ b/packages/ui/BasicSegmentEditor/components/BasicSegmentFilter.tsx @@ -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 ( + <> + + + ); + + case "person": + return ( + <> + + + ); + + default: + return null; + } +}; diff --git a/packages/ui/BasicSegmentEditor/components/PersonSegmentFilter.tsx b/packages/ui/BasicSegmentEditor/components/PersonSegmentFilter.tsx new file mode 100644 index 0000000000..b6f25bba0f --- /dev/null +++ b/packages/ui/BasicSegmentEditor/components/PersonSegmentFilter.tsx @@ -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) => { + 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 ( +
+ + + + + + + {!["isSet", "isNotSet"].includes(resource.qualifier.operator) && ( +
+ { + checkValueAndUpdate(e); + }} + className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")} + disabled={viewOnly} + /> + + {valueError && ( +

+ {valueError} +

+ )} +
+ )} + + +
+ ); +}; diff --git a/packages/ui/BasicSegmentEditor/components/SegmentFilterItemConnector.tsx b/packages/ui/BasicSegmentEditor/components/SegmentFilterItemConnector.tsx new file mode 100644 index 0000000000..0aced8d66e --- /dev/null +++ b/packages/ui/BasicSegmentEditor/components/SegmentFilterItemConnector.tsx @@ -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 ( +
+ { + if (viewOnly) return; + onConnectorChange(); + }}> + {!!connector ? connector : "Where"} + +
+ ); +}; diff --git a/packages/ui/BasicSegmentEditor/components/SegmentFilterItemContextMenu.tsx b/packages/ui/BasicSegmentEditor/components/SegmentFilterItemContextMenu.tsx new file mode 100644 index 0000000000..4a2ac88ea8 --- /dev/null +++ b/packages/ui/BasicSegmentEditor/components/SegmentFilterItemContextMenu.tsx @@ -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 ( +
+ + + + + + + onMoveFilter(filterId, "up")}>Move up + onMoveFilter(filterId, "down")}>Move down + + + + +
+ ); +}; diff --git a/packages/ui/Targeting/BasicSegmentEditor.tsx b/packages/ui/BasicSegmentEditor/index.tsx similarity index 93% rename from packages/ui/Targeting/BasicSegmentEditor.tsx rename to packages/ui/BasicSegmentEditor/index.tsx index 551de73f5d..df4a7c9787 100644 --- a/packages/ui/Targeting/BasicSegmentEditor.tsx +++ b/packages/ui/BasicSegmentEditor/index.tsx @@ -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>; 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) { diff --git a/packages/ui/Targeting/ConfirmDeleteSegmentModal.tsx b/packages/ui/ConfirmDeleteSegmentModal/index.tsx similarity index 97% rename from packages/ui/Targeting/ConfirmDeleteSegmentModal.tsx rename to packages/ui/ConfirmDeleteSegmentModal/index.tsx index 59a9e75b50..751d9191c9 100644 --- a/packages/ui/Targeting/ConfirmDeleteSegmentModal.tsx +++ b/packages/ui/ConfirmDeleteSegmentModal/index.tsx @@ -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>; segment: TSegmentWithSurveyNames; onDelete: () => Promise; -}; +} export const ConfirmDeleteSegmentModal = ({ onDelete, diff --git a/packages/ui/Targeting/LoadSegmentModal.tsx b/packages/ui/LoadSegmentModal/index.tsx similarity index 99% rename from packages/ui/Targeting/LoadSegmentModal.tsx rename to packages/ui/LoadSegmentModal/index.tsx index b865b01ec7..adc5573895 100644 --- a/packages/ui/Targeting/LoadSegmentModal.tsx +++ b/packages/ui/LoadSegmentModal/index.tsx @@ -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; surveyId: string; currentSegment: TSegment; -}; +} const SegmentDetail = ({ segment, diff --git a/packages/ui/Targeting/SaveAsNewSegmentModal.tsx b/packages/ui/SaveAsNewSegmentModal/index.tsx similarity index 99% rename from packages/ui/Targeting/SaveAsNewSegmentModal.tsx rename to packages/ui/SaveAsNewSegmentModal/index.tsx index 6e01b8caec..7ec51072ed 100644 --- a/packages/ui/Targeting/SaveAsNewSegmentModal.tsx +++ b/packages/ui/SaveAsNewSegmentModal/index.tsx @@ -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; onUpdateSegment: (environmentId: string, segmentId: string, data: TSegmentUpdateInput) => Promise; -}; +} type SaveAsNewSegmentModalForm = { title: string; diff --git a/packages/ui/Targeting/SegmentTitle.tsx b/packages/ui/SegmentTitle/index.tsx similarity index 100% rename from packages/ui/Targeting/SegmentTitle.tsx rename to packages/ui/SegmentTitle/index.tsx diff --git a/packages/ui/Targeting/BasicSegmentFilter.tsx b/packages/ui/Targeting/BasicSegmentFilter.tsx deleted file mode 100644 index 2333c6f11d..0000000000 --- a/packages/ui/Targeting/BasicSegmentFilter.tsx +++ /dev/null @@ -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 ( -
- { - if (viewOnly) return; - onConnectorChange(); - }}> - {!!connector ? connector : "Where"} - -
- ); -}; - -const SegmentFilterItemContextMenu = ({ - filterId, - onDeleteFilter, - onMoveFilter, - viewOnly, -}: { - filterId: string; - onDeleteFilter: (filterId: string) => void; - onMoveFilter: (filterId: string, direction: "up" | "down") => void; - viewOnly?: boolean; -}) => { - return ( -
- - - - - - - onMoveFilter(filterId, "up")}>Move up - onMoveFilter(filterId, "down")}>Move down - - - - -
- ); -}; - -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) => { - 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 ( -
- - - - - - - {!["isSet", "isNotSet"].includes(resource.qualifier.operator) && ( -
- { - checkValueAndUpdate(e); - }} - className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")} - /> - - {valueError && ( -

- {valueError} -

- )} -
- )} - - -
- ); -}; - -type TPersonSegmentFilterProps = Omit & { - 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) => { - 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 ( -
- - - - - - - {!["isSet", "isNotSet"].includes(resource.qualifier.operator) && ( -
- { - checkValueAndUpdate(e); - }} - className={cn("w-auto bg-white", valueError && "border border-red-500 focus:border-red-500")} - disabled={viewOnly} - /> - - {valueError && ( -

- {valueError} -

- )} -
- )} - - -
- ); -}; - -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 ( - <> - - - ); - - case "person": - return ( - <> - - - ); - - default: - return null; - } -}; diff --git a/packages/ui/Targeting/SegmentAlreadyUsedModal.tsx b/packages/ui/Targeting/SegmentAlreadyUsedModal.tsx deleted file mode 100644 index 1eaaa8d53d..0000000000 --- a/packages/ui/Targeting/SegmentAlreadyUsedModal.tsx +++ /dev/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 ( - -

This segment is used in other surveys. To assure consistent data you cannot edit it here.

-
- - -
-
- ); -}; diff --git a/packages/ui/Targeting/TargetingIndicator.tsx b/packages/ui/TargetingIndicator/index.tsx similarity index 100% rename from packages/ui/Targeting/TargetingIndicator.tsx rename to packages/ui/TargetingIndicator/index.tsx