mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-23 09:10:55 -05:00
Compare commits
2 Commits
4.8.3-rc.1
...
4.8.4-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce576b33ac | ||
|
|
25fd161578 |
@@ -4,7 +4,7 @@ import { UsersIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSegment, TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import { TSegment, TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
import { SegmentSettings } from "@/modules/ee/contacts/segments/components/segment-settings";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -15,23 +15,63 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
import { SegmentActivityTab } from "./segment-activity-tab";
|
||||
import { TSegmentActivitySummary } from "./segment-activity-utils";
|
||||
|
||||
interface EditSegmentModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
currentSegment: TSegmentWithSurveyNames;
|
||||
currentSegment: TSegmentWithSurveyRefs;
|
||||
activitySummary: TSegmentActivitySummary;
|
||||
segments: TSegment[];
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isContactsEnabled: boolean;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
const SegmentSettingsTab = ({
|
||||
activitySummary,
|
||||
contactAttributeKeys,
|
||||
currentSegment,
|
||||
environmentId,
|
||||
isContactsEnabled,
|
||||
isReadOnly,
|
||||
segments,
|
||||
setOpen,
|
||||
}: Pick<
|
||||
EditSegmentModalProps,
|
||||
| "activitySummary"
|
||||
| "contactAttributeKeys"
|
||||
| "currentSegment"
|
||||
| "environmentId"
|
||||
| "isContactsEnabled"
|
||||
| "isReadOnly"
|
||||
| "segments"
|
||||
| "setOpen"
|
||||
>) => {
|
||||
if (!isContactsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SegmentSettings
|
||||
activitySummary={activitySummary}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
environmentId={environmentId}
|
||||
initialSegment={currentSegment}
|
||||
segments={segments}
|
||||
setOpen={setOpen}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditSegmentModal = ({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
currentSegment,
|
||||
activitySummary,
|
||||
contactAttributeKeys,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
@@ -40,31 +80,25 @@ export const EditSegmentModal = ({
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const SettingsTab = () => {
|
||||
if (isContactsEnabled) {
|
||||
return (
|
||||
<SegmentSettings
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
environmentId={environmentId}
|
||||
initialSegment={currentSegment}
|
||||
segments={segments}
|
||||
setOpen={setOpen}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: t("common.activity"),
|
||||
children: <SegmentActivityTab currentSegment={currentSegment} />,
|
||||
children: <SegmentActivityTab currentSegment={currentSegment} activitySummary={activitySummary} />,
|
||||
},
|
||||
{
|
||||
title: t("common.settings"),
|
||||
children: <SettingsTab />,
|
||||
children: (
|
||||
<SegmentSettingsTab
|
||||
activitySummary={activitySummary}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
currentSegment={currentSegment}
|
||||
environmentId={environmentId}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
isReadOnly={isReadOnly}
|
||||
segments={segments}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import { TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
import { convertDateTimeStringShort } from "@/lib/time";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { TSegmentActivitySummary } from "./segment-activity-utils";
|
||||
|
||||
interface SegmentActivityTabProps {
|
||||
currentSegment: TSegmentWithSurveyNames;
|
||||
currentSegment: TSegmentWithSurveyRefs;
|
||||
activitySummary: TSegmentActivitySummary;
|
||||
}
|
||||
|
||||
export const SegmentActivityTab = ({ currentSegment }: SegmentActivityTabProps) => {
|
||||
export const SegmentActivityTab = ({ currentSegment, activitySummary }: SegmentActivityTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { activeSurveys, inactiveSurveys } = currentSegment;
|
||||
const { activeSurveys, inactiveSurveys } = activitySummary;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 pb-2">
|
||||
@@ -22,20 +23,20 @@ export const SegmentActivityTab = ({ currentSegment }: SegmentActivityTabProps)
|
||||
<Label className="text-slate-500">{t("common.active_surveys")}</Label>
|
||||
{!activeSurveys?.length && <p className="text-sm text-slate-900">-</p>}
|
||||
|
||||
{activeSurveys?.map((survey, index) => (
|
||||
<p className="text-sm text-slate-900" key={index + survey}>
|
||||
{survey}
|
||||
</p>
|
||||
{activeSurveys?.map((surveyName) => (
|
||||
<div className="py-0.5" key={surveyName}>
|
||||
<p className="text-sm text-slate-900">{surveyName}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500">{t("common.inactive_surveys")}</Label>
|
||||
{!inactiveSurveys?.length && <p className="text-sm text-slate-900">-</p>}
|
||||
|
||||
{inactiveSurveys?.map((survey, index) => (
|
||||
<p className="text-sm text-slate-900" key={index + survey}>
|
||||
{survey}
|
||||
</p>
|
||||
{inactiveSurveys?.map((surveyName) => (
|
||||
<div className="py-0.5" key={surveyName}>
|
||||
<p className="text-sm text-slate-900">{surveyName}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { TBaseFilters, TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
type TSurveySummary = Pick<TSurvey, "id" | "name" | "status">;
|
||||
type TReferencingSegmentSurveyGroup = {
|
||||
segmentId: string;
|
||||
segmentTitle: string;
|
||||
surveys: TSurveySummary[];
|
||||
};
|
||||
|
||||
export type TSegmentActivitySummary = {
|
||||
activeSurveys: string[];
|
||||
inactiveSurveys: string[];
|
||||
};
|
||||
|
||||
export const doesSegmentReferenceSegment = (filters: TBaseFilters, targetSegmentId: string): boolean => {
|
||||
for (const filter of filters) {
|
||||
const { resource } = filter;
|
||||
|
||||
if (Array.isArray(resource)) {
|
||||
if (doesSegmentReferenceSegment(resource, targetSegmentId)) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resource.root.type === "segment" && resource.root.segmentId === targetSegmentId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getReferencingSegments = (
|
||||
segments: TSegmentWithSurveyRefs[],
|
||||
targetSegmentId: string
|
||||
): TSegmentWithSurveyRefs[] =>
|
||||
segments.filter(
|
||||
(segment) =>
|
||||
segment.id !== targetSegmentId && doesSegmentReferenceSegment(segment.filters, targetSegmentId)
|
||||
);
|
||||
|
||||
export const buildSegmentActivitySummary = (
|
||||
directSurveys: TSurveySummary[],
|
||||
indirectSurveyGroups: TReferencingSegmentSurveyGroup[]
|
||||
): TSegmentActivitySummary => {
|
||||
const surveyMap = new Map<string, TSurveySummary>();
|
||||
|
||||
for (const survey of directSurveys) {
|
||||
surveyMap.set(survey.id, survey);
|
||||
}
|
||||
|
||||
for (const segment of indirectSurveyGroups) {
|
||||
for (const survey of segment.surveys) {
|
||||
if (!surveyMap.has(survey.id)) {
|
||||
surveyMap.set(survey.id, survey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const surveys = Array.from(surveyMap.values());
|
||||
|
||||
return {
|
||||
activeSurveys: surveys.filter((survey) => survey.status === "inProgress").map((survey) => survey.name),
|
||||
inactiveSurveys: surveys
|
||||
.filter((survey) => survey.status === "draft" || survey.status === "paused")
|
||||
.map((survey) => survey.name),
|
||||
};
|
||||
};
|
||||
|
||||
export const buildSegmentActivitySummaryFromSegments = (
|
||||
currentSegment: TSegmentWithSurveyRefs,
|
||||
segments: TSegmentWithSurveyRefs[]
|
||||
): TSegmentActivitySummary => {
|
||||
const activeSurveyMap = new Map(currentSegment.activeSurveys.map((s) => [s.id, s.name]));
|
||||
const inactiveSurveyMap = new Map(currentSegment.inactiveSurveys.map((s) => [s.id, s.name]));
|
||||
const allDirectIds = new Set([...activeSurveyMap.keys(), ...inactiveSurveyMap.keys()]);
|
||||
|
||||
const referencingSegments = getReferencingSegments(segments, currentSegment.id);
|
||||
for (const segment of referencingSegments) {
|
||||
for (const survey of segment.activeSurveys) {
|
||||
if (!allDirectIds.has(survey.id) && !activeSurveyMap.has(survey.id)) {
|
||||
activeSurveyMap.set(survey.id, survey.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const survey of segment.inactiveSurveys) {
|
||||
if (!allDirectIds.has(survey.id) && !inactiveSurveyMap.has(survey.id)) {
|
||||
inactiveSurveyMap.set(survey.id, survey.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeSurveys: Array.from(activeSurveyMap.values()),
|
||||
inactiveSurveys: Array.from(inactiveSurveyMap.values()),
|
||||
};
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import type { TBaseFilter, TSegment, TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import type { TBaseFilter, TSegment, TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
import { ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
@@ -16,18 +16,21 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { ConfirmDeleteSegmentModal } from "@/modules/ui/components/confirm-delete-segment-modal";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { AddFilterModal } from "./add-filter-modal";
|
||||
import { TSegmentActivitySummary } from "./segment-activity-utils";
|
||||
import { SegmentEditor } from "./segment-editor";
|
||||
|
||||
interface TSegmentSettingsTabProps {
|
||||
activitySummary: TSegmentActivitySummary;
|
||||
environmentId: string;
|
||||
setOpen: (open: boolean) => void;
|
||||
initialSegment: TSegmentWithSurveyNames;
|
||||
initialSegment: TSegmentWithSurveyRefs;
|
||||
segments: TSegment[];
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export function SegmentSettings({
|
||||
activitySummary,
|
||||
environmentId,
|
||||
initialSegment,
|
||||
setOpen,
|
||||
@@ -38,7 +41,7 @@ export function SegmentSettings({
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
||||
const [segment, setSegment] = useState<TSegmentWithSurveyNames>(initialSegment);
|
||||
const [segment, setSegment] = useState<TSegmentWithSurveyRefs>(initialSegment);
|
||||
|
||||
const [isUpdatingSegment, setIsUpdatingSegment] = useState(false);
|
||||
const [isDeletingSegment, setIsDeletingSegment] = useState(false);
|
||||
@@ -257,9 +260,9 @@ export function SegmentSettings({
|
||||
|
||||
{isDeleteSegmentModalOpen ? (
|
||||
<ConfirmDeleteSegmentModal
|
||||
activitySummary={activitySummary}
|
||||
onDelete={handleDeleteSegment}
|
||||
open={isDeleteSegmentModalOpen}
|
||||
segment={initialSegment}
|
||||
setOpen={setIsDeleteSegmentModalOpen}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { ColumnDef } from "@tanstack/react-table";
|
||||
import { format, formatDistanceToNow } from "date-fns";
|
||||
import { TFunction } from "i18next";
|
||||
import { UsersIcon } from "lucide-react";
|
||||
import { TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import { TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
|
||||
export const generateSegmentTableColumns = (t: TFunction): ColumnDef<TSegmentWithSurveyNames>[] => {
|
||||
const titleColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||
export const generateSegmentTableColumns = (t: TFunction): ColumnDef<TSegmentWithSurveyRefs>[] => {
|
||||
const titleColumn: ColumnDef<TSegmentWithSurveyRefs> = {
|
||||
id: "title",
|
||||
accessorKey: "title",
|
||||
header: t("common.title"),
|
||||
@@ -28,7 +28,7 @@ export const generateSegmentTableColumns = (t: TFunction): ColumnDef<TSegmentWit
|
||||
},
|
||||
};
|
||||
|
||||
const updatedAtColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||
const updatedAtColumn: ColumnDef<TSegmentWithSurveyRefs> = {
|
||||
id: "updatedAt",
|
||||
accessorKey: "updatedAt",
|
||||
header: t("common.updated_at"),
|
||||
@@ -41,7 +41,7 @@ export const generateSegmentTableColumns = (t: TFunction): ColumnDef<TSegmentWit
|
||||
},
|
||||
};
|
||||
|
||||
const createdAtColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||
const createdAtColumn: ColumnDef<TSegmentWithSurveyRefs> = {
|
||||
id: "createdAt",
|
||||
accessorKey: "createdAt",
|
||||
header: t("common.created_at"),
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { getSurveysBySegmentId } from "@/lib/survey/service";
|
||||
import { SegmentTableDataRow } from "./segment-table-data-row";
|
||||
|
||||
type TSegmentTableDataRowProps = {
|
||||
currentSegment: TSegment;
|
||||
segments: TSegment[];
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isContactsEnabled: boolean;
|
||||
isReadOnly: boolean;
|
||||
};
|
||||
|
||||
export const SegmentTableDataRowContainer = async ({
|
||||
currentSegment,
|
||||
segments,
|
||||
contactAttributeKeys,
|
||||
isContactsEnabled,
|
||||
isReadOnly,
|
||||
}: TSegmentTableDataRowProps) => {
|
||||
const surveys = await getSurveysBySegmentId(currentSegment.id);
|
||||
|
||||
const activeSurveys = surveys?.length
|
||||
? surveys.filter((survey) => survey.status === "inProgress").map((survey) => survey.name)
|
||||
: [];
|
||||
|
||||
const inactiveSurveys = surveys?.length
|
||||
? surveys.filter((survey) => ["draft", "paused"].includes(survey.status)).map((survey) => survey.name)
|
||||
: [];
|
||||
|
||||
const filteredSegments = segments.filter((segment) => segment.id !== currentSegment.id);
|
||||
|
||||
return (
|
||||
<SegmentTableDataRow
|
||||
currentSegment={{
|
||||
...currentSegment,
|
||||
activeSurveys,
|
||||
inactiveSurveys,
|
||||
}}
|
||||
segments={filteredSegments}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -4,11 +4,13 @@ import { format, formatDistanceToNow } from "date-fns";
|
||||
import { UsersIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSegment, TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import { TSegment, TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
import { EditSegmentModal } from "./edit-segment-modal";
|
||||
import { TSegmentActivitySummary } from "./segment-activity-utils";
|
||||
|
||||
type TSegmentTableDataRowProps = {
|
||||
currentSegment: TSegmentWithSurveyNames;
|
||||
currentSegment: TSegmentWithSurveyRefs;
|
||||
activitySummary: TSegmentActivitySummary;
|
||||
segments: TSegment[];
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isContactsEnabled: boolean;
|
||||
@@ -17,6 +19,7 @@ type TSegmentTableDataRowProps = {
|
||||
|
||||
export const SegmentTableDataRow = ({
|
||||
currentSegment,
|
||||
activitySummary,
|
||||
contactAttributeKeys,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
@@ -62,6 +65,7 @@ export const SegmentTableDataRow = ({
|
||||
open={isEditSegmentModalOpen}
|
||||
setOpen={setIsEditSegmentModalOpen}
|
||||
currentSegment={currentSegment}
|
||||
activitySummary={activitySummary}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
segments={segments}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
|
||||
@@ -4,13 +4,15 @@ import { Header, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import { TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||
import { EditSegmentModal } from "./edit-segment-modal";
|
||||
import { buildSegmentActivitySummaryFromSegments } from "./segment-activity-utils";
|
||||
import { generateSegmentTableColumns } from "./segment-table-columns";
|
||||
|
||||
interface SegmentTableUpdatedProps {
|
||||
segments: TSegmentWithSurveyNames[];
|
||||
segments: TSegmentWithSurveyRefs[];
|
||||
allSegments: TSegmentWithSurveyRefs[];
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isContactsEnabled: boolean;
|
||||
isReadOnly: boolean;
|
||||
@@ -18,16 +20,17 @@ interface SegmentTableUpdatedProps {
|
||||
|
||||
export function SegmentTable({
|
||||
segments,
|
||||
allSegments,
|
||||
contactAttributeKeys,
|
||||
isContactsEnabled,
|
||||
isReadOnly,
|
||||
}: SegmentTableUpdatedProps) {
|
||||
}: Readonly<SegmentTableUpdatedProps>) {
|
||||
const { t } = useTranslation();
|
||||
const [editingSegment, setEditingSegment] = useState<TSegmentWithSurveyNames | null>(null);
|
||||
const [editingSegment, setEditingSegment] = useState<TSegmentWithSurveyRefs | null>(null);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return generateSegmentTableColumns(t);
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: segments,
|
||||
@@ -35,7 +38,7 @@ export function SegmentTable({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const getHeader = (header: Header<TSegmentWithSurveyNames, unknown>) => {
|
||||
const getHeader = (header: Header<TSegmentWithSurveyRefs, unknown>) => {
|
||||
if (header.isPlaceholder) {
|
||||
return null;
|
||||
}
|
||||
@@ -136,6 +139,7 @@ export function SegmentTable({
|
||||
open={!!editingSegment}
|
||||
setOpen={(open) => !open && setEditingSegment(null)}
|
||||
currentSegment={editingSegment}
|
||||
activitySummary={buildSegmentActivitySummaryFromSegments(editingSegment, allSegments)}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
segments={segments}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TBaseFilters, TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import { TBaseFilters, TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
import { getSegment } from "../segments";
|
||||
import { segmentFilterToPrismaQuery } from "./prisma-query";
|
||||
|
||||
@@ -270,7 +270,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
];
|
||||
|
||||
// Mock the getSegment function to return a segment with filters
|
||||
const mockSegment: TSegmentWithSurveyNames = {
|
||||
const mockSegment: TSegmentWithSurveyRefs = {
|
||||
id: nestedSegmentId,
|
||||
filters: nestedFilters,
|
||||
environmentId: mockEnvironmentId,
|
||||
@@ -336,7 +336,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
|
||||
// Mock getSegment to return null for the non-existent segment
|
||||
vi.mocked(getSegment).mockResolvedValueOnce(mockSegment);
|
||||
vi.mocked(getSegment).mockResolvedValueOnce(null as unknown as TSegmentWithSurveyNames);
|
||||
vi.mocked(getSegment).mockResolvedValueOnce(null as unknown as TSegmentWithSurveyRefs);
|
||||
|
||||
const result = await segmentFilterToPrismaQuery(mockSegmentId, filters, mockEnvironmentId);
|
||||
|
||||
@@ -426,7 +426,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
];
|
||||
|
||||
// Mock the getSegment function to return a segment with filters
|
||||
const mockSegment: TSegmentWithSurveyNames = {
|
||||
const mockSegment: TSegmentWithSurveyRefs = {
|
||||
id: nestedSegmentId,
|
||||
filters: nestedFilters,
|
||||
environmentId: mockEnvironmentId,
|
||||
@@ -490,7 +490,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
|
||||
test("handle circular references in segment filters", async () => {
|
||||
// Mock getSegment to simulate a circular reference
|
||||
const circularSegment: TSegmentWithSurveyNames = {
|
||||
const circularSegment: TSegmentWithSurveyRefs = {
|
||||
id: mockSegmentId, // Same ID creates the circular reference
|
||||
filters: [
|
||||
{
|
||||
@@ -550,7 +550,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
test("handle missing segments in segment filters", async () => {
|
||||
const nestedSegmentId = "segment-missing-123";
|
||||
|
||||
vi.mocked(getSegment).mockResolvedValue(null as unknown as TSegmentWithSurveyNames);
|
||||
vi.mocked(getSegment).mockResolvedValue(null as unknown as TSegmentWithSurveyRefs);
|
||||
|
||||
const filters: TBaseFilters = [
|
||||
{
|
||||
@@ -599,7 +599,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
];
|
||||
|
||||
// Mock the nested segment
|
||||
const mockNestedSegment: TSegmentWithSurveyNames = {
|
||||
const mockNestedSegment: TSegmentWithSurveyRefs = {
|
||||
id: nestedSegmentId,
|
||||
filters: nestedFilters,
|
||||
environmentId: mockEnvironmentId,
|
||||
@@ -890,7 +890,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
];
|
||||
|
||||
// Set up the mocks
|
||||
const mockCircularSegment: TSegmentWithSurveyNames = {
|
||||
const mockCircularSegment: TSegmentWithSurveyRefs = {
|
||||
id: circularSegmentId,
|
||||
filters: circularFilters,
|
||||
environmentId: mockEnvironmentId,
|
||||
@@ -904,7 +904,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
inactiveSurveys: [],
|
||||
};
|
||||
|
||||
const mockSecondSegment: TSegmentWithSurveyNames = {
|
||||
const mockSecondSegment: TSegmentWithSurveyRefs = {
|
||||
id: secondSegmentId,
|
||||
filters: secondFilters,
|
||||
environmentId: mockEnvironmentId,
|
||||
@@ -922,7 +922,7 @@ describe("segmentFilterToPrismaQuery", () => {
|
||||
vi.mocked(getSegment)
|
||||
.mockResolvedValueOnce(mockCircularSegment) // First call for circularSegmentId
|
||||
.mockResolvedValueOnce(mockSecondSegment) // Third call for secondSegmentId
|
||||
.mockResolvedValueOnce(null as unknown as TSegmentWithSurveyNames); // Fourth call for non-existent-segment
|
||||
.mockResolvedValueOnce(null as unknown as TSegmentWithSurveyRefs); // Fourth call for non-existent-segment
|
||||
|
||||
// Complex filters with mixed error conditions
|
||||
const filters: TBaseFilters = [
|
||||
|
||||
@@ -361,7 +361,7 @@ const buildSegmentFilterWhereClause = async (
|
||||
environmentId: string,
|
||||
deviceType?: "phone" | "desktop"
|
||||
): Promise<Prisma.ContactWhereInput> => {
|
||||
const { root } = filter;
|
||||
const { root, qualifier } = filter;
|
||||
const { segmentId } = root;
|
||||
|
||||
if (segmentPath.has(segmentId)) {
|
||||
@@ -382,7 +382,22 @@ const buildSegmentFilterWhereClause = async (
|
||||
const newPath = new Set(segmentPath);
|
||||
newPath.add(segmentId);
|
||||
|
||||
return processFilters(segment.filters, newPath, environmentId, deviceType);
|
||||
const nestedWhereClause = await processFilters(segment.filters, newPath, environmentId, deviceType);
|
||||
const hasNestedConditions = Object.keys(nestedWhereClause).length > 0;
|
||||
|
||||
if (qualifier.operator === "userIsIn") {
|
||||
return nestedWhereClause;
|
||||
}
|
||||
|
||||
if (qualifier.operator === "userIsNotIn") {
|
||||
if (!hasNestedConditions) {
|
||||
return { id: "__SEGMENT_FILTER_NO_MATCH__" };
|
||||
}
|
||||
|
||||
return { NOT: nestedWhereClause };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TBaseFilters, TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import { TBaseFilters, TSegmentWithSurveyRefs } from "@formbricks/types/segment";
|
||||
import { checkForRecursiveSegmentFilter } from "@/modules/ee/contacts/segments/lib/helper";
|
||||
import { getSegment } from "@/modules/ee/contacts/segments/lib/segments";
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("checkForRecursiveSegmentFilter", () => {
|
||||
],
|
||||
};
|
||||
|
||||
vi.mocked(getSegment).mockResolvedValue(referencedSegment as unknown as TSegmentWithSurveyNames);
|
||||
vi.mocked(getSegment).mockResolvedValue(referencedSegment as unknown as TSegmentWithSurveyRefs);
|
||||
|
||||
// Act & Assert
|
||||
// The function should complete without throwing an error
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
TEvaluateSegmentUserData,
|
||||
TSegmentCreateInput,
|
||||
TSegmentUpdateInput,
|
||||
TSegmentWithSurveyNames,
|
||||
TSegmentWithSurveyRefs,
|
||||
} from "@formbricks/types/segment";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
@@ -79,10 +79,10 @@ const mockSegmentPrisma = {
|
||||
surveys: [{ id: surveyId, name: "Test Survey", status: "inProgress" }],
|
||||
};
|
||||
|
||||
const mockSegment: TSegmentWithSurveyNames = {
|
||||
const mockSegment: TSegmentWithSurveyRefs = {
|
||||
...mockSegmentPrisma,
|
||||
surveys: [surveyId],
|
||||
activeSurveys: ["Test Survey"],
|
||||
activeSurveys: [{ id: surveyId, name: "Test Survey" }],
|
||||
inactiveSurveys: [],
|
||||
};
|
||||
|
||||
@@ -287,7 +287,7 @@ describe("Segment Service Tests", () => {
|
||||
...mockSegment,
|
||||
id: clonedSegmentId,
|
||||
title: "Copy of Test Segment (1)",
|
||||
activeSurveys: ["Test Survey"],
|
||||
activeSurveys: [{ id: surveyId, name: "Test Survey" }],
|
||||
inactiveSurveys: [],
|
||||
};
|
||||
|
||||
@@ -327,7 +327,7 @@ describe("Segment Service Tests", () => {
|
||||
const clonedSegment2 = {
|
||||
...clonedSegment,
|
||||
title: "Copy of Test Segment (2)",
|
||||
activeSurveys: ["Test Survey"],
|
||||
activeSurveys: [{ id: surveyId, name: "Test Survey" }],
|
||||
inactiveSurveys: [],
|
||||
};
|
||||
|
||||
@@ -415,7 +415,7 @@ describe("Segment Service Tests", () => {
|
||||
title: surveyId,
|
||||
isPrivate: true,
|
||||
filters: [],
|
||||
activeSurveys: ["Test Survey"],
|
||||
activeSurveys: [{ id: surveyId, name: "Test Survey" }],
|
||||
inactiveSurveys: [],
|
||||
};
|
||||
|
||||
@@ -487,7 +487,7 @@ describe("Segment Service Tests", () => {
|
||||
const updatedSegment = {
|
||||
...mockSegment,
|
||||
title: "Updated Segment",
|
||||
activeSurveys: ["Test Survey"],
|
||||
activeSurveys: [{ id: surveyId, name: "Test Survey" }],
|
||||
inactiveSurveys: [],
|
||||
};
|
||||
const updateData: TSegmentUpdateInput = { title: "Updated Segment" };
|
||||
@@ -531,7 +531,7 @@ describe("Segment Service Tests", () => {
|
||||
...updatedSegment,
|
||||
surveys: [newSurveyId],
|
||||
activeSurveys: [],
|
||||
inactiveSurveys: ["New Survey"],
|
||||
inactiveSurveys: [{ id: newSurveyId, name: "New Survey" }],
|
||||
};
|
||||
|
||||
vi.mocked(prisma.segment.update).mockResolvedValue(updatedSegmentPrismaWithSurvey);
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
TSegmentPersonFilter,
|
||||
TSegmentSegmentFilter,
|
||||
TSegmentUpdateInput,
|
||||
TSegmentWithSurveyNames,
|
||||
TSegmentWithSurveyRefs,
|
||||
ZRelativeDateValue,
|
||||
ZSegmentCreateInput,
|
||||
ZSegmentFilters,
|
||||
@@ -66,14 +66,14 @@ export const selectSegment = {
|
||||
},
|
||||
} satisfies Prisma.SegmentSelect;
|
||||
|
||||
export const transformPrismaSegment = (segment: PrismaSegment): TSegmentWithSurveyNames => {
|
||||
export const transformPrismaSegment = (segment: PrismaSegment): TSegmentWithSurveyRefs => {
|
||||
const activeSurveys = segment.surveys
|
||||
.filter((survey) => survey.status === "inProgress")
|
||||
.map((survey) => survey.name);
|
||||
.map((survey) => ({ id: survey.id, name: survey.name }));
|
||||
|
||||
const inactiveSurveys = segment.surveys
|
||||
.filter((survey) => survey.status !== "inProgress")
|
||||
.map((survey) => survey.name);
|
||||
.map((survey) => ({ id: survey.id, name: survey.name }));
|
||||
|
||||
return {
|
||||
...segment,
|
||||
@@ -83,7 +83,7 @@ export const transformPrismaSegment = (segment: PrismaSegment): TSegmentWithSurv
|
||||
};
|
||||
};
|
||||
|
||||
export const getSegment = reactCache(async (segmentId: string): Promise<TSegmentWithSurveyNames> => {
|
||||
export const getSegment = reactCache(async (segmentId: string): Promise<TSegmentWithSurveyRefs> => {
|
||||
validateInputs([segmentId, ZId]);
|
||||
try {
|
||||
const segment = await prisma.segment.findUnique({
|
||||
@@ -107,7 +107,7 @@ export const getSegment = reactCache(async (segmentId: string): Promise<TSegment
|
||||
}
|
||||
});
|
||||
|
||||
export const getSegments = reactCache(async (environmentId: string): Promise<TSegmentWithSurveyNames[]> => {
|
||||
export const getSegments = reactCache(async (environmentId: string): Promise<TSegmentWithSurveyRefs[]> => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
try {
|
||||
const segments = await prisma.segment.findMany({
|
||||
|
||||
@@ -47,6 +47,7 @@ export const SegmentsPage = async ({
|
||||
upgradePromptTitle={t("environments.segments.unlock_segments_title")}
|
||||
upgradePromptDescription={t("environments.segments.unlock_segments_description")}>
|
||||
<SegmentTable
|
||||
allSegments={segments}
|
||||
segments={filteredSegments}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
isContactsEnabled={isContactsEnabled}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||
import { TSegmentActivitySummary } from "@/modules/ee/contacts/segments/components/segment-activity-utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -15,16 +15,16 @@ import {
|
||||
} from "@/modules/ui/components/dialog";
|
||||
|
||||
interface ConfirmDeleteSegmentModalProps {
|
||||
activitySummary: TSegmentActivitySummary;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
segment: TSegmentWithSurveyNames;
|
||||
onDelete: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const ConfirmDeleteSegmentModal = ({
|
||||
activitySummary,
|
||||
onDelete,
|
||||
open,
|
||||
segment,
|
||||
setOpen,
|
||||
}: ConfirmDeleteSegmentModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -32,9 +32,9 @@ export const ConfirmDeleteSegmentModal = ({
|
||||
await onDelete();
|
||||
};
|
||||
|
||||
const segmentHasSurveys = useMemo(() => {
|
||||
return segment.activeSurveys.length > 0 || segment.inactiveSurveys.length > 0;
|
||||
}, [segment.activeSurveys.length, segment.inactiveSurveys.length]);
|
||||
const allSurveys = useMemo(() => {
|
||||
return [...activitySummary.activeSurveys, ...activitySummary.inactiveSurveys];
|
||||
}, [activitySummary.activeSurveys, activitySummary.inactiveSurveys]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@@ -46,16 +46,13 @@ export const ConfirmDeleteSegmentModal = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{segmentHasSurveys && (
|
||||
{allSurveys.length > 0 && (
|
||||
<DialogBody>
|
||||
<div className="space-y-2">
|
||||
<p>{t("environments.segments.cannot_delete_segment_used_in_surveys")}</p>
|
||||
<ol className="my-2 ml-4 list-decimal">
|
||||
{segment.activeSurveys.map((survey) => (
|
||||
<li key={survey}>{survey}</li>
|
||||
))}
|
||||
{segment.inactiveSurveys.map((survey) => (
|
||||
<li key={survey}>{survey}</li>
|
||||
{allSurveys.map((surveyName) => (
|
||||
<li key={surveyName}>{surveyName}</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
@@ -69,7 +66,7 @@ export const ConfirmDeleteSegmentModal = ({
|
||||
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDelete} disabled={segmentHasSurveys}>
|
||||
<Button variant="destructive" onClick={handleDelete} disabled={allSurveys.length > 0}>
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -357,9 +357,13 @@ export const ZSegmentCreateInput = z.object({
|
||||
export type TSegmentCreateInput = z.infer<typeof ZSegmentCreateInput>;
|
||||
|
||||
export type TSegment = z.infer<typeof ZSegment>;
|
||||
export type TSegmentWithSurveyNames = TSegment & {
|
||||
activeSurveys: string[];
|
||||
inactiveSurveys: string[];
|
||||
export interface TSegmentSurveyReference {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
export type TSegmentWithSurveyRefs = TSegment & {
|
||||
activeSurveys: TSegmentSurveyReference[];
|
||||
inactiveSurveys: TSegmentSurveyReference[];
|
||||
};
|
||||
|
||||
export const ZSegmentUpdateInput = z
|
||||
|
||||
Reference in New Issue
Block a user