mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-23 09:10:55 -05:00
Compare commits
6 Commits
4.8.1-rc.1
...
4.8.4-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce576b33ac | ||
|
|
25fd161578 | ||
|
|
fac7369b3c | ||
|
|
913ab98d62 | ||
|
|
717a172ce0 | ||
|
|
8c935f20c2 |
@@ -1,7 +1,19 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { type Instrumentation } from "next";
|
||||
import { isExpectedError } from "@formbricks/types/errors";
|
||||
import { IS_PRODUCTION, PROMETHEUS_ENABLED, SENTRY_DSN } from "@/lib/constants";
|
||||
|
||||
export const onRequestError = Sentry.captureRequestError;
|
||||
export const onRequestError: Instrumentation.onRequestError = (...args) => {
|
||||
const [error] = args;
|
||||
|
||||
// Skip expected business-logic errors (AuthorizationError, ResourceNotFoundError, etc.)
|
||||
// These are handled gracefully in the UI and don't need server-side Sentry reporting
|
||||
if (error instanceof Error && isExpectedError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.captureRequestError(...args);
|
||||
};
|
||||
|
||||
export const register = async () => {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const TagsLoading = () => {
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.workspace_configuration")}>
|
||||
<ProjectConfigNavigation activeId="tags" />
|
||||
<ProjectConfigNavigation activeId="tags" loading />
|
||||
</PageHeader>
|
||||
<SettingsCard
|
||||
title={t("environments.workspace.tags.manage_tags")}
|
||||
|
||||
@@ -129,8 +129,10 @@ export const EditWelcomeCard = ({
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(url: string[] | undefined, _fileType: "image" | "video") => {
|
||||
if (url?.[0]) {
|
||||
if (url?.length) {
|
||||
updateSurvey({ fileUrl: url[0] });
|
||||
} else {
|
||||
updateSurvey({ fileUrl: undefined });
|
||||
}
|
||||
}}
|
||||
fileUrl={localSurvey?.welcomeCard?.fileUrl}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Logger } from "@/lib/common/logger";
|
||||
import { getIsSetup, setIsSetup } from "@/lib/common/status";
|
||||
import { filterSurveys, getIsDebug, isNowExpired, wrapThrows } from "@/lib/common/utils";
|
||||
import { fetchEnvironmentState } from "@/lib/environment/state";
|
||||
import { closeSurvey } from "@/lib/survey/widget";
|
||||
import { closeSurvey, preloadSurveysScript } from "@/lib/survey/widget";
|
||||
import { DEFAULT_USER_STATE_NO_USER_ID } from "@/lib/user/state";
|
||||
import { sendUpdatesToBackend } from "@/lib/user/update";
|
||||
import {
|
||||
@@ -316,6 +316,9 @@ export const setup = async (
|
||||
addEventListeners();
|
||||
addCleanupEventListeners();
|
||||
|
||||
// Preload surveys script so it's ready when a survey triggers
|
||||
preloadSurveysScript(configInput.appUrl);
|
||||
|
||||
setIsSetup(true);
|
||||
logger.debug("Set up complete");
|
||||
|
||||
|
||||
@@ -106,7 +106,15 @@ export const renderWidget = async (
|
||||
const overlay = projectOverwrites.overlay ?? project.overlay;
|
||||
const placement = projectOverwrites.placement ?? project.placement;
|
||||
const isBrandingEnabled = project.inAppSurveyBranding;
|
||||
const formbricksSurveys = await loadFormbricksSurveysExternally();
|
||||
|
||||
let formbricksSurveys: TFormbricksSurveys;
|
||||
try {
|
||||
formbricksSurveys = await loadFormbricksSurveysExternally();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to load surveys library: ${String(error)}`);
|
||||
setIsSurveyRunning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const recaptchaSiteKey = config.get().environment.data.recaptchaSiteKey;
|
||||
const isSpamProtectionEnabled = Boolean(recaptchaSiteKey && survey.recaptcha?.enabled);
|
||||
@@ -219,30 +227,87 @@ export const removeWidgetContainer = (): void => {
|
||||
document.getElementById(CONTAINER_ID)?.remove();
|
||||
};
|
||||
|
||||
const loadFormbricksSurveysExternally = (): Promise<typeof globalThis.window.formbricksSurveys> => {
|
||||
const config = Config.getInstance();
|
||||
const SURVEYS_LOAD_TIMEOUT_MS = 10000;
|
||||
const SURVEYS_POLL_INTERVAL_MS = 200;
|
||||
|
||||
type TFormbricksSurveys = typeof globalThis.window.formbricksSurveys;
|
||||
|
||||
let surveysLoadPromise: Promise<TFormbricksSurveys> | null = null;
|
||||
|
||||
const waitForSurveysGlobal = (): Promise<TFormbricksSurveys> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- We need to check if the formbricksSurveys object exists
|
||||
if (globalThis.window.formbricksSurveys) {
|
||||
resolve(globalThis.window.formbricksSurveys);
|
||||
} else {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${config.get().appUrl}/js/surveys.umd.cjs`;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
// Apply stored nonce if it was set before surveys package loaded
|
||||
const startTime = Date.now();
|
||||
|
||||
const check = (): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Runtime check for surveys package availability
|
||||
if (globalThis.window.formbricksSurveys) {
|
||||
const storedNonce = globalThis.window.__formbricksNonce;
|
||||
if (storedNonce) {
|
||||
globalThis.window.formbricksSurveys.setNonce(storedNonce);
|
||||
}
|
||||
resolve(globalThis.window.formbricksSurveys);
|
||||
};
|
||||
script.onerror = (error) => {
|
||||
console.error("Failed to load Formbricks Surveys library:", error);
|
||||
reject(new Error(`Failed to load Formbricks Surveys library: ${error as string}`));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Date.now() - startTime >= SURVEYS_LOAD_TIMEOUT_MS) {
|
||||
reject(new Error("Formbricks Surveys library did not become available within timeout"));
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(check, SURVEYS_POLL_INTERVAL_MS);
|
||||
};
|
||||
|
||||
check();
|
||||
});
|
||||
};
|
||||
|
||||
const loadFormbricksSurveysExternally = (): Promise<TFormbricksSurveys> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Runtime check for surveys package availability
|
||||
if (globalThis.window.formbricksSurveys) {
|
||||
return Promise.resolve(globalThis.window.formbricksSurveys);
|
||||
}
|
||||
|
||||
if (surveysLoadPromise) {
|
||||
return surveysLoadPromise;
|
||||
}
|
||||
|
||||
surveysLoadPromise = new Promise<TFormbricksSurveys>((resolve, reject: (error: unknown) => void) => {
|
||||
const config = Config.getInstance();
|
||||
const script = document.createElement("script");
|
||||
script.src = `${config.get().appUrl}/js/surveys.umd.cjs`;
|
||||
script.async = true;
|
||||
script.onload = () => {
|
||||
waitForSurveysGlobal()
|
||||
.then(resolve)
|
||||
.catch((error: unknown) => {
|
||||
surveysLoadPromise = null;
|
||||
console.error("Failed to load Formbricks Surveys library:", error);
|
||||
reject(new Error(`Failed to load Formbricks Surveys library`));
|
||||
});
|
||||
};
|
||||
script.onerror = (error) => {
|
||||
surveysLoadPromise = null;
|
||||
console.error("Failed to load Formbricks Surveys library:", error);
|
||||
reject(new Error(`Failed to load Formbricks Surveys library`));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
return surveysLoadPromise;
|
||||
};
|
||||
|
||||
let isPreloaded = false;
|
||||
|
||||
export const preloadSurveysScript = (appUrl: string): void => {
|
||||
// Don't preload if already loaded or already preloading
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Runtime check for surveys package availability
|
||||
if (globalThis.window.formbricksSurveys) return;
|
||||
if (isPreloaded) return;
|
||||
|
||||
isPreloaded = true;
|
||||
const link = document.createElement("link");
|
||||
link.rel = "preload";
|
||||
link.as = "script";
|
||||
link.href = `${appUrl}/js/surveys.umd.cjs`;
|
||||
document.head.appendChild(link);
|
||||
};
|
||||
|
||||
@@ -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