fix: [Backport]backports indirect segment activity (#7569)

Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
Anshuman Pandey
2026-03-23 18:35:39 +05:30
committed by GitHub
parent 25fd161578
commit ce576b33ac
15 changed files with 240 additions and 139 deletions

View File

@@ -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}
/>
),
},
];

View File

@@ -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>

View File

@@ -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()),
};
};

View File

@@ -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}

View File

@@ -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"),

View File

@@ -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}
/>
);
};

View File

@@ -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}

View File

@@ -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}

View File

@@ -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 = [

View File

@@ -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

View File

@@ -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);

View File

@@ -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({

View File

@@ -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}

View File

@@ -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>

View File

@@ -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