Compare commits

...

1 Commits

Author SHA1 Message Date
Johannes
66c066725c fix: ensure at least one filter is required for segments 2026-03-17 12:53:14 +01:00
5 changed files with 89 additions and 8 deletions

View File

@@ -30,16 +30,16 @@ export const ContactsSecondaryNavigation = async ({
label: t("common.contacts"),
href: `/environments/${environmentId}/contacts`,
},
{
id: "segments",
label: t("common.segments"),
href: `/environments/${environmentId}/segments`,
},
{
id: "attributes",
label: t("common.attributes"),
href: `/environments/${environmentId}/attributes`,
},
{
id: "segments",
label: t("common.segments"),
href: `/environments/${environmentId}/segments`,
},
];
return <SecondaryNavigation navigation={navigation} activeId={activeId} loading={loading} />;

View File

@@ -133,6 +133,10 @@ export function SegmentSettings({
return true;
}
if (segment.filters.length === 0) {
return true;
}
// parse the filters to check if they are valid
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
if (!parsedFilters.success) {

View File

@@ -0,0 +1,73 @@
import { createId } from "@paralleldrive/cuid2";
import { describe, expect, test } from "vitest";
import { ZSegmentCreateInput, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
const validFilters = [
{
id: createId(),
connector: null,
resource: {
id: createId(),
root: {
type: "attribute" as const,
contactAttributeKey: "email",
},
value: "user@example.com",
qualifier: {
operator: "equals" as const,
},
},
},
];
describe("segment schema validation", () => {
test("keeps base segment filters compatible with empty arrays", () => {
const result = ZSegmentFilters.safeParse([]);
expect(result.success).toBe(true);
});
test("requires at least one filter when creating a segment", () => {
const result = ZSegmentCreateInput.safeParse({
environmentId: "environmentId",
title: "Power users",
description: "Users with a matching email",
isPrivate: false,
filters: [],
surveyId: "surveyId",
});
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.message).toBe("At least one filter is required");
});
test("accepts segment creation with a valid filter", () => {
const result = ZSegmentCreateInput.safeParse({
environmentId: "environmentId",
title: "Power users",
description: "Users with a matching email",
isPrivate: false,
filters: validFilters,
surveyId: "surveyId",
});
expect(result.success).toBe(true);
});
test("requires at least one filter when updating a segment", () => {
const result = ZSegmentUpdateInput.safeParse({
filters: [],
});
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.message).toBe("At least one filter is required");
});
test("accepts segment updates with a valid filter", () => {
const result = ZSegmentUpdateInput.safeParse({
filters: validFilters,
});
expect(result.success).toBe(true);
});
});

View File

@@ -89,7 +89,7 @@ export const InviteMemberModal = ({
<DialogDescription>{t("environments.settings.teams.invite_member_description")}</DialogDescription>
</DialogHeader>
<DialogBody className="flex flex-col gap-6" unconstrained>
<DialogBody className="flex min-h-0 flex-col gap-6 overflow-y-auto">
{!showTeamAdminRestrictions && (
<TabToggle
id="type"

View File

@@ -333,6 +333,10 @@ export const ZSegmentFilters: z.ZodType<TBaseFilters> = z
error: "Invalid filters applied",
});
const ZRequiredSegmentFilters = ZSegmentFilters.refine((filters) => filters.length > 0, {
error: "At least one filter is required",
});
export const ZSegment = z.object({
id: z.string(),
title: z.string(),
@@ -350,7 +354,7 @@ export const ZSegmentCreateInput = z.object({
title: z.string(),
description: z.string().optional(),
isPrivate: z.boolean().prefault(true),
filters: ZSegmentFilters,
filters: ZRequiredSegmentFilters,
surveyId: z.string(),
});
@@ -367,7 +371,7 @@ export const ZSegmentUpdateInput = z
title: z.string(),
description: z.string().nullable(),
isPrivate: z.boolean().prefault(true),
filters: ZSegmentFilters,
filters: ZRequiredSegmentFilters,
surveys: z.array(z.string()),
})
.partial();