Person
{
handleAddFilter({
type: "person",
@@ -180,7 +181,16 @@ const AttributeTabContent = ({ attributeClasses, onAddFilter, setOpen }: Attribu
setOpen,
});
}}
- className="flex cursor-pointer items-center gap-4 rounded-lg px-2 py-1 text-sm hover:bg-slate-50">
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ handleAddFilter({
+ type: "person",
+ onAddFilter,
+ setOpen,
+ });
+ }
+ }}>
userId
@@ -192,7 +202,7 @@ const AttributeTabContent = ({ attributeClasses, onAddFilter, setOpen }: Attribu
Attributes
- {attributeClasses?.length === 0 && (
+ {attributeClasses.length === 0 && (
There are no attributes yet!
@@ -200,6 +210,8 @@ const AttributeTabContent = ({ attributeClasses, onAddFilter, setOpen }: Attribu
{attributeClasses.map((attributeClass) => {
return (
{
handleAddFilter({
type: "attribute",
@@ -207,8 +219,7 @@ const AttributeTabContent = ({ attributeClasses, onAddFilter, setOpen }: Attribu
setOpen,
attributeClassName: attributeClass.name,
});
- }}
- className="flex cursor-pointer items-center gap-4 rounded-lg px-2 py-1 text-sm hover:bg-slate-50">
+ }}>
{attributeClass.name}
@@ -216,16 +227,16 @@ const AttributeTabContent = ({ attributeClasses, onAddFilter, setOpen }: Attribu
})}
);
-};
+}
-export const AddFilterModal = ({
+export function AddFilterModal({
onAddFilter,
open,
setOpen,
actionClasses,
attributeClasses,
segments,
-}: TAddFilterModalProps) => {
+}: TAddFilterModalProps) {
const [activeTabId, setActiveTabId] = useState("all");
const [searchValue, setSearchValue] = useState("");
@@ -241,15 +252,12 @@ export const AddFilterModal = ({
{ id: "devices", label: "Devices", icon:
},
];
- // eslint-disable-next-line react-hooks/exhaustive-deps
const devices = [
{ id: "phone", name: "Phone" },
{ id: "desktop", name: "Desktop" },
];
const actionClassesFiltered = useMemo(() => {
- if (!actionClasses) return [];
-
if (!searchValue) return actionClasses;
return actionClasses.filter((actionClass) =>
@@ -313,7 +321,7 @@ export const AddFilterModal = ({
const getAllTabContent = () => {
return (
<>
- {allFiltersFiltered?.every((filterArr) => {
+ {allFiltersFiltered.every((filterArr) => {
return (
filterArr.actions.length === 0 &&
filterArr.attributes.length === 0 &&
@@ -321,11 +329,11 @@ export const AddFilterModal = ({
filterArr.devices.length === 0 &&
filterArr.personAttributes.length === 0
);
- }) && (
+ }) ? (
There are no filters yet!
- )}
+ ) : null}
{allFiltersFiltered.map((filters) => {
return (
@@ -333,6 +341,8 @@ export const AddFilterModal = ({
{filters.actions.map((actionClass) => {
return (
{
handleAddFilter({
type: "action",
@@ -340,8 +350,7 @@ export const AddFilterModal = ({
setOpen,
actionClassId: actionClass.id,
});
- }}
- className="flex cursor-pointer items-center gap-4 rounded-lg px-2 py-1 text-sm hover:bg-slate-50">
+ }}>
{actionClass.name}
@@ -351,6 +360,7 @@ export const AddFilterModal = ({
{filters.attributes.map((attributeClass) => {
return (
{
handleAddFilter({
type: "attribute",
@@ -358,8 +368,7 @@ export const AddFilterModal = ({
setOpen,
attributeClassName: attributeClass.name,
});
- }}
- className="flex cursor-pointer items-center gap-4 rounded-lg px-2 py-1 text-sm hover:bg-slate-50">
+ }}>
{attributeClass.name}
@@ -369,14 +378,14 @@ export const AddFilterModal = ({
{filters.personAttributes.map((personAttribute) => {
return (
{
handleAddFilter({
type: "person",
onAddFilter,
setOpen,
});
- }}
- className="flex cursor-pointer items-center gap-4 rounded-lg px-2 py-1 text-sm hover:bg-slate-50">
+ }}>
{personAttribute.name}
@@ -386,6 +395,7 @@ export const AddFilterModal = ({
{filters.segments.map((segment) => {
return (
{
handleAddFilter({
type: "segment",
@@ -393,8 +403,7 @@ export const AddFilterModal = ({
setOpen,
segmentId: segment.id,
});
- }}
- className="flex cursor-pointer items-center gap-4 rounded-lg px-2 py-1 text-sm hover:bg-slate-50">
+ }}>
{segment.title}
@@ -403,8 +412,8 @@ export const AddFilterModal = ({
{filters.devices.map((deviceType) => (
{
handleAddFilter({
type: "device",
@@ -427,7 +436,7 @@ export const AddFilterModal = ({
const getActionsTabContent = () => {
return (
<>
- {actionClassesFiltered?.length === 0 && (
+ {actionClassesFiltered.length === 0 && (
There are no actions yet!
@@ -435,6 +444,7 @@ export const AddFilterModal = ({
{actionClassesFiltered.map((actionClass) => {
return (
{
handleAddFilter({
type: "action",
@@ -442,8 +452,7 @@ export const AddFilterModal = ({
setOpen,
actionClassId: actionClass.id,
});
- }}
- className="flex cursor-pointer items-center gap-4 rounded-lg px-2 py-1 text-sm hover:bg-slate-50">
+ }}>
{actionClass.name}
@@ -466,16 +475,17 @@ export const AddFilterModal = ({
const getSegmentsTabContent = () => {
return (
<>
- {segmentsFiltered?.length === 0 && (
+ {segmentsFiltered.length === 0 && (
You currently have no saved segments.
)}
{segmentsFiltered
- ?.filter((segment) => !segment.isPrivate)
- ?.map((segment) => {
+ .filter((segment) => !segment.isPrivate)
+ .map((segment) => {
return (
{
handleAddFilter({
type: "segment",
@@ -483,8 +493,7 @@ export const AddFilterModal = ({
setOpen,
segmentId: segment.id,
});
- }}
- className="flex cursor-pointer items-center gap-4 rounded-lg px-2 py-1 text-sm hover:bg-slate-50">
+ }}>
{segment.title}
@@ -499,8 +508,8 @@ export const AddFilterModal = ({
{deviceTypesFiltered.map((deviceType) => (
{
handleAddFilter({
type: "device",
@@ -542,14 +551,20 @@ export const AddFilterModal = ({
return (
+ setOpen={setOpen}>
- setSearchValue(e.target.value)} />
-
+ {
+ setSearchValue(e.target.value);
+ }}
+ placeholder="Browse filters..."
+ />
+
@@ -557,4 +572,4 @@ export const AddFilterModal = ({
);
-};
+}
diff --git a/packages/ee/advancedTargeting/components/AdvancedTargetingCard.tsx b/packages/ee/advanced-targeting/components/advanced-targeting-card.tsx
similarity index 75%
rename from packages/ee/advancedTargeting/components/AdvancedTargetingCard.tsx
rename to packages/ee/advanced-targeting/components/advanced-targeting-card.tsx
index a9ef735f01..94aeff3638 100644
--- a/packages/ee/advancedTargeting/components/AdvancedTargetingCard.tsx
+++ b/packages/ee/advanced-targeting/components/advanced-targeting-card.tsx
@@ -9,10 +9,15 @@ import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
-import { TActionClass } from "@formbricks/types/actionClasses";
-import { TAttributeClass } from "@formbricks/types/attributeClasses";
-import { TBaseFilter, TSegment, TSegmentCreateInput, TSegmentUpdateInput } from "@formbricks/types/segment";
-import { TSurvey } from "@formbricks/types/surveys";
+import type { TActionClass } from "@formbricks/types/actionClasses";
+import type { TAttributeClass } from "@formbricks/types/attributeClasses";
+import type {
+ TBaseFilter,
+ TSegment,
+ TSegmentCreateInput,
+ TSegmentUpdateInput,
+} from "@formbricks/types/segment";
+import type { TSurvey } from "@formbricks/types/surveys";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
import { LoadSegmentModal } from "@formbricks/ui/LoadSegmentModal";
@@ -28,8 +33,8 @@ import {
updateSegmentAction,
} from "../lib/actions";
import { ACTIONS_TO_EXCLUDE } from "../lib/constants";
-import { AddFilterModal } from "./AddFilterModal";
-import { SegmentEditor } from "./SegmentEditor";
+import { AddFilterModal } from "./add-filter-modal";
+import { SegmentEditor } from "./segment-editor";
interface UserTargetingAdvancedCardProps {
localSurvey: TSurvey;
@@ -41,7 +46,7 @@ interface UserTargetingAdvancedCardProps {
initialSegment?: TSegment;
}
-export const AdvancedTargetingCard = ({
+export function AdvancedTargetingCard({
localSurvey,
setLocalSurvey,
environmentId,
@@ -49,7 +54,7 @@ export const AdvancedTargetingCard = ({
attributeClasses,
segments,
initialSegment,
-}: UserTargetingAdvancedCardProps) => {
+}: UserTargetingAdvancedCardProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [segment, setSegment] = useState
(localSurvey.segment);
@@ -58,7 +63,7 @@ export const AdvancedTargetingCard = ({
const [saveAsNewSegmentModalOpen, setSaveAsNewSegmentModalOpen] = useState(false);
const [resetAllFiltersModalOpen, setResetAllFiltersModalOpen] = useState(false);
const [loadSegmentModalOpen, setLoadSegmentModalOpen] = useState(false);
- const [isSegmentEditorOpen, setIsSegmentEditorOpen] = useState(!!localSurvey.segment?.isPrivate);
+ const [isSegmentEditorOpen, setIsSegmentEditorOpen] = useState(Boolean(localSurvey.segment?.isPrivate));
const [segmentEditorViewOnly, setSegmentEditorViewOnly] = useState(true);
const actionClasses = actionClassesProps.filter((actionClass) => {
@@ -76,12 +81,12 @@ export const AdvancedTargetingCard = ({
useEffect(() => {
setLocalSurvey((localSurveyOld) => ({
...localSurveyOld,
- segment: segment,
+ segment,
}));
}, [setLocalSurvey, segment]);
const isSegmentUsedInOtherSurveys = useMemo(
- () => (localSurvey?.segment ? localSurvey.segment?.surveys?.length > 1 : false),
+ () => (localSurvey.segment ? localSurvey.segment.surveys.length > 1 : false),
[localSurvey.segment]
);
@@ -97,10 +102,10 @@ export const AdvancedTargetingCard = ({
};
useEffect(() => {
- if (!!segment && segment?.filters?.length > 0) {
+ if (segment && segment.filters.length > 0) {
setOpen(true);
}
- }, [segment, segment?.filters?.length]);
+ }, [segment, segment?.filters.length]);
useEffect(() => {
if (localSurvey.type === "link") {
@@ -110,7 +115,7 @@ export const AdvancedTargetingCard = ({
const handleAddFilterInGroup = (filter: TBaseFilter) => {
const updatedSegment = structuredClone(segment);
- if (updatedSegment?.filters?.length === 0) {
+ if (updatedSegment?.filters.length === 0) {
updatedSegment.filters.push({
...filter,
connector: null,
@@ -144,7 +149,7 @@ export const AdvancedTargetingCard = ({
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
try {
if (!segment) throw new Error("Invalid segment");
- await updateSegmentAction(environmentId, segment?.id, data);
+ await updateSegmentAction(environmentId, segment.id, data);
toast.success("Segment saved successfully");
setIsSegmentEditorOpen(false);
@@ -166,19 +171,23 @@ export const AdvancedTargetingCard = ({
return null; // Hide card completely
}
+ if (!segment) {
+ throw new Error("Survey segment is missing");
+ }
+
return (
+ open={open}>
@@ -194,37 +203,37 @@ export const AdvancedTargetingCard = ({
- {!!segment && (
+ {Boolean(segment) && (
)}
{isSegmentEditorOpen ? (
- {!!segment?.filters?.length && (
+ {Boolean(segment?.filters.length) && (
)}
@@ -232,27 +241,30 @@ export const AdvancedTargetingCard = ({
-
- <>
-
{
- handleAddFilterInGroup(filter);
- }}
- open={addFilterModalOpen}
- setOpen={setAddFilterModalOpen}
- actionClasses={actionClasses}
- attributeClasses={attributeClasses}
- segments={segments}
+ {
+ handleAddFilterInGroup(filter);
+ }}
+ open={addFilterModalOpen}
+ segments={segments}
+ setOpen={setAddFilterModalOpen}
+ />
+ {Boolean(segment) && (
+
- {!!segment && (
-
- )}
- >
+ )}
) : (
- {segmentEditorViewOnly && segment && (
+ {segmentEditorViewOnly && segment ? (
- )}
+ ) : null}
{
setSegmentEditorViewOnly(!segmentEditorViewOnly);
- }}>
+ }}
+ size="sm"
+ variant="secondary">
{segmentEditorViewOnly ? "Hide" : "View"} Filters{" "}
{segmentEditorViewOnly ? (
@@ -331,71 +343,78 @@ export const AdvancedTargetingCard = ({
)}
- {isSegmentUsedInOtherSurveys && (
-
handleCloneSegment()}>
+ {isSegmentUsedInOtherSurveys ? (
+ handleCloneSegment()} size="sm" variant="secondary">
Clone & Edit Segment
- )}
+ ) : null}
{!isSegmentUsedInOtherSurveys && (
{
setIsSegmentEditorOpen(true);
setSegmentEditorViewOnly(false);
- }}>
+ }}
+ size="sm"
+ variant={isSegmentUsedInOtherSurveys ? "minimal" : "secondary"}>
Edit Segment
)}
- {isSegmentUsedInOtherSurveys && (
+ {isSegmentUsedInOtherSurveys ? (
This segment is used in other surveys. Make changes{" "}
+ target="_blank">
here.
- )}
+ ) : null}
)}
-
setLoadSegmentModalOpen(true)}>
+ {
+ setLoadSegmentModalOpen(true);
+ }}
+ size="sm"
+ variant="secondary">
Load Segment
- {!segment?.isPrivate && !!segment?.filters?.length && (
- setResetAllFiltersModalOpen(true)}>
+ {!segment?.isPrivate && Boolean(segment?.filters.length) && (
+ {
+ setResetAllFiltersModalOpen(true);
+ }}
+ size="sm"
+ variant="secondary">
Reset all filters
)}
- {isSegmentEditorOpen && !!segment?.filters?.length && (
+ {isSegmentEditorOpen && Boolean(segment?.filters.length) ? (
setSaveAsNewSegmentModalOpen(true)}>
+ onClick={() => {
+ setSaveAsNewSegmentModalOpen(true);
+ }}
+ size="sm"
+ variant="secondary">
Save as new Segment
- )}
+ ) : null}
{
- setResetAllFiltersModalOpen(false);
- }}
confirmBtnLabel="Remove all filters"
+ declineBtnLabel="Cancel"
+ headerText="Are you sure?"
+ mainText="This action resets all filters in this survey."
onConfirm={async () => {
const segment = await handleResetAllFilters();
if (segment) {
@@ -407,10 +426,15 @@ export const AdvancedTargetingCard = ({
router.refresh();
}
}}
+ onDecline={() => {
+ setResetAllFiltersModalOpen(false);
+ }}
+ open={resetAllFiltersModalOpen}
+ setOpen={setResetAllFiltersModalOpen}
/>
);
-};
+}
diff --git a/packages/ee/advancedTargeting/components/CreateSegmentModal.tsx b/packages/ee/advanced-targeting/components/create-segment-modal.tsx
similarity index 87%
rename from packages/ee/advancedTargeting/components/CreateSegmentModal.tsx
rename to packages/ee/advanced-targeting/components/create-segment-modal.tsx
index b91707731e..af214c731f 100644
--- a/packages/ee/advancedTargeting/components/CreateSegmentModal.tsx
+++ b/packages/ee/advanced-targeting/components/create-segment-modal.tsx
@@ -6,30 +6,31 @@ import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
-import { TActionClass } from "@formbricks/types/actionClasses";
-import { TAttributeClass } from "@formbricks/types/attributeClasses";
-import { TBaseFilter, TSegment, ZSegmentFilters } from "@formbricks/types/segment";
+import type { TActionClass } from "@formbricks/types/actionClasses";
+import type { TAttributeClass } from "@formbricks/types/attributeClasses";
+import type { TBaseFilter, TSegment } from "@formbricks/types/segment";
+import { ZSegmentFilters } from "@formbricks/types/segment";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Modal } from "@formbricks/ui/Modal";
import { createSegmentAction } from "../lib/actions";
-import { AddFilterModal } from "./AddFilterModal";
-import { SegmentEditor } from "./SegmentEditor";
+import { AddFilterModal } from "./add-filter-modal";
+import { SegmentEditor } from "./segment-editor";
-type TCreateSegmentModalProps = {
+interface TCreateSegmentModalProps {
environmentId: string;
segments: TSegment[];
attributeClasses: TAttributeClass[];
actionClasses: TActionClass[];
-};
+}
-export const CreateSegmentModal = ({
+export function CreateSegmentModal({
environmentId,
actionClasses,
attributeClasses,
segments,
-}: TCreateSegmentModalProps) => {
+}: TCreateSegmentModalProps) {
const router = useRouter();
const initialSegmentState = {
title: "",
@@ -55,13 +56,13 @@ export const CreateSegmentModal = ({
const handleAddFilterInGroup = (filter: TBaseFilter) => {
const updatedSegment = structuredClone(segment);
- if (updatedSegment?.filters?.length === 0) {
+ if (updatedSegment.filters.length === 0) {
updatedSegment.filters.push({
...filter,
connector: null,
});
} else {
- updatedSegment?.filters.push(filter);
+ updatedSegment.filters.push(filter);
}
setSegment(updatedSegment);
@@ -121,18 +122,24 @@ export const CreateSegmentModal = ({
return (
<>
-
setOpen(true)} EndIcon={PlusIcon}>
+ {
+ setOpen(true);
+ }}
+ size="sm"
+ variant="darkCTA">
Create segment
{
handleResetState();
}}
- noPadding
- closeOnOutsideClick={false}
- className="md:w-full"
size="lg">
- {segment?.filters?.length === 0 && (
+ {segment.filters.length === 0 && (
Add your first filter to get started
@@ -193,53 +200,55 @@ export const CreateSegmentModal = ({
)}
{
+ setAddFilterModalOpen(true);
+ }}
size="sm"
- onClick={() => setAddFilterModalOpen(true)}>
+ variant="secondary">
Add Filter
{
handleAddFilterInGroup(filter);
}}
open={addFilterModalOpen}
- setOpen={setAddFilterModalOpen}
- actionClasses={actionClasses}
- attributeClasses={attributeClasses}
segments={segments}
+ setOpen={setAddFilterModalOpen}
/>
{
handleResetState();
- }}>
+ }}
+ type="button"
+ variant="minimal">
Cancel
{
handleCreateSegment();
- }}>
+ }}
+ type="submit"
+ variant="darkCTA">
Create segment
@@ -249,4 +258,4 @@ export const CreateSegmentModal = ({
>
);
-};
+}
diff --git a/packages/ee/advanced-targeting/components/segment-editor.tsx b/packages/ee/advanced-targeting/components/segment-editor.tsx
new file mode 100644
index 0000000000..42045a188a
--- /dev/null
+++ b/packages/ee/advanced-targeting/components/segment-editor.tsx
@@ -0,0 +1,266 @@
+import { MoreVertical, Trash2 } from "lucide-react";
+import { useState } from "react";
+
+import { cn } from "@formbricks/lib/cn";
+import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
+import {
+ addFilterBelow,
+ addFilterInGroup,
+ createGroupFromResource,
+ deleteResource,
+ isResourceFilter,
+ moveResource,
+ toggleGroupConnector,
+} from "@formbricks/lib/segment/utils";
+import type { TActionClass } from "@formbricks/types/actionClasses";
+import type { TAttributeClass } from "@formbricks/types/attributeClasses";
+import type { TBaseFilter, TBaseFilters, TSegment, TSegmentConnector } from "@formbricks/types/segment";
+import { Button } from "@formbricks/ui/Button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@formbricks/ui/DropdownMenu";
+
+import { AddFilterModal } from "./add-filter-modal";
+import { SegmentFilter } from "./segment-filter";
+
+interface TSegmentEditorProps {
+ group: TBaseFilters;
+ environmentId: string;
+ segment: TSegment;
+ segments: TSegment[];
+ actionClasses: TActionClass[];
+ attributeClasses: TAttributeClass[];
+ setSegment: React.Dispatch
>;
+ viewOnly?: boolean;
+}
+
+export function SegmentEditor({
+ group,
+ environmentId,
+ setSegment,
+ segment,
+ actionClasses,
+ attributeClasses,
+ segments,
+ viewOnly = false,
+}: TSegmentEditorProps) {
+ const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
+ const [addFilterModalOpenedFromBelow, setAddFilterModalOpenedFromBelow] = useState(false);
+
+ const handleAddFilterBelow = (resourceId: string, filter: TBaseFilter) => {
+ const localSegmentCopy = structuredClone(segment);
+
+ if (localSegmentCopy.filters) {
+ addFilterBelow(localSegmentCopy.filters, resourceId, filter);
+ }
+
+ setSegment(localSegmentCopy);
+ };
+
+ const handleCreateGroup = (resourceId: string) => {
+ const localSegmentCopy = structuredClone(segment);
+ if (localSegmentCopy.filters) {
+ createGroupFromResource(localSegmentCopy.filters, resourceId);
+ }
+
+ setSegment(localSegmentCopy);
+ };
+
+ const handleMoveResource = (resourceId: string, direction: "up" | "down") => {
+ const localSegmentCopy = structuredClone(segment);
+ if (localSegmentCopy.filters) {
+ moveResource(localSegmentCopy.filters, resourceId, direction);
+ }
+
+ setSegment(localSegmentCopy);
+ };
+
+ const handleDeleteResource = (resourceId: string) => {
+ const localSegmentCopy = structuredClone(segment);
+
+ if (localSegmentCopy.filters) {
+ deleteResource(localSegmentCopy.filters, resourceId);
+ }
+
+ setSegment(localSegmentCopy);
+ };
+
+ const handleToggleGroupConnector = (groupId: string, newConnectorValue: TSegmentConnector) => {
+ const localSegmentCopy = structuredClone(segment);
+ if (localSegmentCopy.filters) {
+ toggleGroupConnector(localSegmentCopy.filters, groupId, newConnectorValue);
+ }
+
+ setSegment(localSegmentCopy);
+ };
+
+ const onConnectorChange = (groupId: string, connector: TSegmentConnector) => {
+ if (!connector) return;
+
+ if (connector === "and") {
+ handleToggleGroupConnector(groupId, "or");
+ } else {
+ handleToggleGroupConnector(groupId, "and");
+ }
+ };
+
+ const handleAddFilterInGroup = (groupId: string, filter: TBaseFilter) => {
+ const localSegmentCopy = structuredClone(segment);
+
+ if (localSegmentCopy.filters) {
+ addFilterInGroup(localSegmentCopy.filters, groupId, filter);
+ }
+ setSegment(localSegmentCopy);
+ };
+
+ return (
+
+ {group.map((groupItem) => {
+ const { connector, resource, id: groupId } = groupItem;
+
+ if (isResourceFilter(resource)) {
+ return (
+
{
+ handleCreateGroup(filterId);
+ }}
+ onDeleteFilter={(filterId: string) => {
+ handleDeleteResource(filterId);
+ }}
+ onMoveFilter={(filterId: string, direction: "up" | "down") => {
+ handleMoveResource(filterId, direction);
+ }}
+ resource={resource}
+ segment={segment}
+ segments={segments}
+ setSegment={setSegment}
+ viewOnly={viewOnly}
+ />
+ );
+ }
+ return (
+
+
+
+ {
+ if (viewOnly) return;
+ onConnectorChange(groupId, connector);
+ }}>
+ {connector ? connector : "Where"}
+
+
+
+
+
+
+
+ {
+ if (viewOnly) return;
+ setAddFilterModalOpen(true);
+ }}
+ size="sm"
+ variant="secondary">
+ Add filter
+
+
+
+
{
+ if (addFilterModalOpenedFromBelow) {
+ handleAddFilterBelow(groupId, filter);
+ setAddFilterModalOpenedFromBelow(false);
+ } else {
+ handleAddFilterInGroup(groupId, filter);
+ }
+ }}
+ open={addFilterModalOpen}
+ segments={segments}
+ setOpen={setAddFilterModalOpen}
+ />
+
+
+
+
+
+
+
+
+
+ {
+ setAddFilterModalOpenedFromBelow(true);
+ setAddFilterModalOpen(true);
+ }}>
+ Add filter below
+
+
+ {
+ handleCreateGroup(groupId);
+ }}>
+ Create group
+
+
+ {
+ handleMoveResource(groupId, "up");
+ }}>
+ Move up
+
+
+ {
+ if (viewOnly) return;
+ handleMoveResource(groupId, "down");
+ }}>
+ Move down
+
+
+
+
+ {
+ if (viewOnly) return;
+ handleDeleteResource(groupId);
+ }}
+ variant="minimal">
+
+
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/packages/ee/advancedTargeting/components/SegmentFilter.tsx b/packages/ee/advanced-targeting/components/segment-filter.tsx
similarity index 89%
rename from packages/ee/advancedTargeting/components/SegmentFilter.tsx
rename to packages/ee/advanced-targeting/components/segment-filter.tsx
index 24583251bf..5813b5dc87 100644
--- a/packages/ee/advancedTargeting/components/SegmentFilter.tsx
+++ b/packages/ee/advanced-targeting/components/segment-filter.tsx
@@ -27,15 +27,9 @@ import {
updateSegmentIdInFilter,
} from "@formbricks/lib/segment/utils";
import { isCapitalized } from "@formbricks/lib/utils/strings";
-import { TActionClass } from "@formbricks/types/actionClasses";
-import { TAttributeClass } from "@formbricks/types/attributeClasses";
-import {
- ACTION_METRICS,
- ARITHMETIC_OPERATORS,
- ATTRIBUTE_OPERATORS,
- BASE_OPERATORS,
- DEVICE_OPERATORS,
- PERSON_OPERATORS,
+import type { TActionClass } from "@formbricks/types/actionClasses";
+import type { TAttributeClass } from "@formbricks/types/attributeClasses";
+import type {
TActionMetric,
TArithmeticOperator,
TAttributeOperator,
@@ -53,6 +47,14 @@ import {
TSegmentPersonFilter,
TSegmentSegmentFilter,
} from "@formbricks/types/segment";
+import {
+ ACTION_METRICS,
+ ARITHMETIC_OPERATORS,
+ ATTRIBUTE_OPERATORS,
+ BASE_OPERATORS,
+ DEVICE_OPERATORS,
+ PERSON_OPERATORS,
+} from "@formbricks/types/segment";
import { Button } from "@formbricks/ui/Button";
import {
DropdownMenu,
@@ -63,9 +65,9 @@ import {
import { Input } from "@formbricks/ui/Input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
-import { AddFilterModal } from "./AddFilterModal";
+import { AddFilterModal } from "./add-filter-modal";
-type TSegmentFilterProps = {
+interface TSegmentFilterProps {
connector: TSegmentConnector;
resource: TSegmentFilter;
environmentId: string;
@@ -79,9 +81,9 @@ type TSegmentFilterProps = {
onDeleteFilter: (filterId: string) => void;
onMoveFilter: (filterId: string, direction: "up" | "down") => void;
viewOnly?: boolean;
-};
+}
-const SegmentFilterItemConnector = ({
+function SegmentFilterItemConnector({
connector,
segment,
setSegment,
@@ -93,7 +95,7 @@ const SegmentFilterItemConnector = ({
setSegment: (segment: TSegment) => void;
filterId: string;
viewOnly?: boolean;
-}) => {
+}) {
const updateLocalSurvey = (newConnector: TSegmentConnector) => {
const updatedSegment = structuredClone(segment);
if (updatedSegment.filters) {
@@ -116,18 +118,18 @@ const SegmentFilterItemConnector = ({
return (
{
if (viewOnly) return;
onConnectorChange();
}}>
- {!!connector ? connector : "Where"}
+ {connector ? connector : "Where"}
);
-};
+}
-const SegmentFilterItemContextMenu = ({
+function SegmentFilterItemContextMenu({
filterId,
onAddFilterBelow,
onCreateGroup,
@@ -141,7 +143,7 @@ const SegmentFilterItemContextMenu = ({
onDeleteFilter: (filterId: string) => void;
onMoveFilter: (filterId: string, direction: "up" | "down") => void;
viewOnly?: boolean;
-}) => {
+}) {
return (
@@ -150,27 +152,47 @@ const SegmentFilterItemContextMenu = ({
- onAddFilterBelow()}>Add filter below
+ {
+ onAddFilterBelow();
+ }}>
+ Add filter below
+
- onCreateGroup(filterId)}>Create group
- onMoveFilter(filterId, "up")}>Move up
- onMoveFilter(filterId, "down")}>Move down
+ {
+ onCreateGroup(filterId);
+ }}>
+ Create group
+
+ {
+ onMoveFilter(filterId, "up");
+ }}>
+ Move up
+
+ {
+ onMoveFilter(filterId, "down");
+ }}>
+ Move down
+
{
if (viewOnly) return;
onDeleteFilter(filterId);
- }}>
-
+ }}
+ variant="minimal">
+
);
-};
+}
type TAttributeSegmentFilterProps = TSegmentFilterProps & {
onAddFilterBelow: () => void;
@@ -178,7 +200,7 @@ type TAttributeSegmentFilterProps = TSegmentFilterProps & {
updateValueInLocalSurvey: (filterId: string, newValue: TSegmentFilterValue) => void;
};
-const AttributeSegmentFilter = ({
+function AttributeSegmentFilter({
connector,
resource,
onAddFilterBelow,
@@ -190,7 +212,7 @@ const AttributeSegmentFilter = ({
setSegment,
attributeClasses,
viewOnly,
-}: TAttributeSegmentFilterProps) => {
+}: TAttributeSegmentFilterProps) {
const { attributeClassName } = resource.root;
const operatorText = convertOperatorToText(resource.qualifier.operator);
@@ -218,7 +240,7 @@ const AttributeSegmentFilter = ({
};
});
- const attributeClass = attributeClasses?.find((attrClass) => attrClass?.name === attributeClassName)?.name;
+ const attributeClass = attributeClasses.find((attrClass) => attrClass.name === attributeClassName)?.name;
const updateOperatorInLocalSurvey = (filterId: string, newOperator: TAttributeOperator) => {
const updatedSegment = structuredClone(segment);
@@ -270,20 +292,20 @@ const AttributeSegmentFilter = ({
return (
);
-};
+}
type TPersonSegmentFilterProps = TSegmentFilterProps & {
onAddFilterBelow: () => void;
@@ -366,7 +388,7 @@ type TPersonSegmentFilterProps = TSegmentFilterProps & {
updateValueInLocalSurvey: (filterId: string, newValue: TSegmentFilterValue) => void;
};
-const PersonSegmentFilter = ({
+function PersonSegmentFilter({
connector,
resource,
onAddFilterBelow,
@@ -377,7 +399,7 @@ const PersonSegmentFilter = ({
segment,
setSegment,
viewOnly,
-}: TPersonSegmentFilterProps) => {
+}: TPersonSegmentFilterProps) {
const { personIdentifier } = resource.root;
const operatorText = convertOperatorToText(resource.qualifier.operator);
@@ -455,20 +477,20 @@ const PersonSegmentFilter = ({
return (
);
-};
+}
type TActionSegmentFilterProps = TSegmentFilterProps & {
onAddFilterBelow: () => void;
resource: TSegmentActionFilter;
updateValueInLocalSurvey: (filterId: string, newValue: TSegmentFilterValue) => void;
};
-const ActionSegmentFilter = ({
+function ActionSegmentFilter({
connector,
resource,
segment,
@@ -557,7 +579,7 @@ const ActionSegmentFilter = ({
updateValueInLocalSurvey,
actionClasses,
viewOnly,
-}: TActionSegmentFilterProps) => {
+}: TActionSegmentFilterProps) {
const { actionClassId } = resource.root;
const operatorText = convertOperatorToText(resource.qualifier.operator);
const qualifierMetric = resource.qualifier.metric;
@@ -626,20 +648,20 @@ const ActionSegmentFilter = ({
return (
);
-};
+}
type TSegmentSegmentFilterProps = TSegmentFilterProps & {
onAddFilterBelow: () => void;
resource: TSegmentSegmentFilter;
};
-const SegmentSegmentFilter = ({
+function SegmentSegmentFilter({
connector,
onAddFilterBelow,
onCreateGroup,
@@ -742,11 +764,11 @@ const SegmentSegmentFilter = ({
segments,
setSegment,
viewOnly,
-}: TSegmentSegmentFilterProps) => {
+}: TSegmentSegmentFilterProps) {
const { segmentId } = resource.root;
const operatorText = convertOperatorToText(resource.qualifier.operator);
- const currentSegment = segments?.find((segment) => segment.id === segmentId);
+ const currentSegment = segments.find((segment) => segment.id === segmentId);
const updateOperatorInSegment = (filterId: string, newOperator: TSegmentOperator) => {
const updatedSegment = structuredClone(segment);
@@ -780,9 +802,9 @@ const SegmentSegmentFilter = ({
return (
@@ -831,13 +855,13 @@ const SegmentSegmentFilter = ({
/>
);
-};
+}
type TDeviceFilterProps = TSegmentFilterProps & {
onAddFilterBelow: () => void;
resource: TSegmentDeviceFilter;
};
-const DeviceFilter = ({
+function DeviceFilter({
connector,
onAddFilterBelow,
onCreateGroup,
@@ -847,7 +871,7 @@ const DeviceFilter = ({
segment,
setSegment,
viewOnly,
-}: TDeviceFilterProps) => {
+}: TDeviceFilterProps) {
const { value } = resource;
const operatorText = convertOperatorToText(resource.qualifier.operator);
@@ -877,9 +901,9 @@ const DeviceFilter = ({
return (
);
-};
+}
-export const SegmentFilter = ({
+export function SegmentFilter({
resource,
connector,
environmentId,
@@ -957,7 +981,7 @@ export const SegmentFilter = ({
onDeleteFilter,
onMoveFilter,
viewOnly = false,
-}: TSegmentFilterProps) => {
+}: TSegmentFilterProps) {
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
const updateFilterValueInSegment = (filterId: string, newValue: string | number) => {
const updatedSegment = structuredClone(segment);
@@ -972,35 +996,39 @@ export const SegmentFilter = ({
setAddFilterModalOpen(true);
};
- const RenderFilterModal = () => (
- handleAddFilterBelow(resource.id, filter)}
- actionClasses={actionClasses}
- attributeClasses={attributeClasses}
- segments={segments}
- />
- );
+ function RenderFilterModal() {
+ return (
+ {
+ handleAddFilterBelow(resource.id, filter);
+ }}
+ open={addFilterModalOpen}
+ segments={segments}
+ setOpen={setAddFilterModalOpen}
+ />
+ );
+ }
switch (resource.root.type) {
case "action":
return (
<>
@@ -1013,19 +1041,19 @@ export const SegmentFilter = ({
return (
<>
@@ -1038,19 +1066,19 @@ export const SegmentFilter = ({
return (
<>
@@ -1063,19 +1091,19 @@ export const SegmentFilter = ({
return (
<>
@@ -1087,19 +1115,19 @@ export const SegmentFilter = ({
return (
<>
@@ -1110,4 +1138,4 @@ export const SegmentFilter = ({
default:
return Unknown filter type
;
}
-};
+}
diff --git a/packages/ee/advanced-targeting/components/segment-settings.tsx b/packages/ee/advanced-targeting/components/segment-settings.tsx
new file mode 100644
index 0000000000..233e650961
--- /dev/null
+++ b/packages/ee/advanced-targeting/components/segment-settings.tsx
@@ -0,0 +1,252 @@
+"use client";
+
+import { FilterIcon, Trash2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useMemo, useState } from "react";
+import toast from "react-hot-toast";
+
+import { cn } from "@formbricks/lib/cn";
+import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
+import type { TActionClass } from "@formbricks/types/actionClasses";
+import type { TAttributeClass } from "@formbricks/types/attributeClasses";
+import type { TBaseFilter, TSegment, TSegmentWithSurveyNames } from "@formbricks/types/segment";
+import { ZSegmentFilters } from "@formbricks/types/segment";
+import { Button } from "@formbricks/ui/Button";
+import { ConfirmDeleteSegmentModal } from "@formbricks/ui/ConfirmDeleteSegmentModal";
+import { Input } from "@formbricks/ui/Input";
+
+import { deleteSegmentAction, updateSegmentAction } from "../lib/actions";
+import { AddFilterModal } from "./add-filter-modal";
+import { SegmentEditor } from "./segment-editor";
+
+interface TSegmentSettingsTabProps {
+ environmentId: string;
+ setOpen: (open: boolean) => void;
+ initialSegment: TSegmentWithSurveyNames;
+ segments: TSegment[];
+ attributeClasses: TAttributeClass[];
+ actionClasses: TActionClass[];
+}
+
+export function SegmentSettings({
+ environmentId,
+ initialSegment,
+ setOpen,
+ actionClasses,
+ attributeClasses,
+ segments,
+}: TSegmentSettingsTabProps) {
+ const router = useRouter();
+
+ const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
+ const [segment, setSegment] = useState(initialSegment);
+
+ const [isUpdatingSegment, setIsUpdatingSegment] = useState(false);
+ const [isDeletingSegment, setIsDeletingSegment] = useState(false);
+
+ const [isDeleteSegmentModalOpen, setIsDeleteSegmentModalOpen] = useState(false);
+
+ const handleResetState = () => {
+ setSegment(initialSegment);
+ setOpen(false);
+
+ router.refresh();
+ };
+
+ const handleAddFilterInGroup = (filter: TBaseFilter) => {
+ const updatedSegment = structuredClone(segment);
+ if (updatedSegment.filters.length === 0) {
+ updatedSegment.filters.push({
+ ...filter,
+ connector: null,
+ });
+ } else {
+ updatedSegment.filters.push(filter);
+ }
+
+ setSegment(updatedSegment);
+ };
+
+ const handleUpdateSegment = async () => {
+ if (!segment.title) {
+ toast.error("Title is required");
+ return;
+ }
+
+ try {
+ setIsUpdatingSegment(true);
+ await updateSegmentAction(segment.environmentId, segment.id, {
+ title: segment.title,
+ description: segment.description ?? "",
+ isPrivate: segment.isPrivate,
+ filters: segment.filters,
+ });
+
+ setIsUpdatingSegment(false);
+ toast.success("Segment updated successfully!");
+ } catch (err: any) {
+ const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
+ if (!parsedFilters.success) {
+ toast.error("Invalid filters. Please check the filters and try again.");
+ } else {
+ toast.error("Something went wrong. Please try again.");
+ }
+ setIsUpdatingSegment(false);
+ return;
+ }
+
+ setIsUpdatingSegment(false);
+ handleResetState();
+ router.refresh();
+ };
+
+ const handleDeleteSegment = async () => {
+ try {
+ setIsDeletingSegment(true);
+ await deleteSegmentAction(segment.environmentId, segment.id);
+
+ setIsDeletingSegment(false);
+ toast.success("Segment deleted successfully!");
+ handleResetState();
+ } catch (err: any) {
+ toast.error("Something went wrong. Please try again.");
+ }
+
+ setIsDeletingSegment(false);
+ };
+
+ const isSaveDisabled = useMemo(() => {
+ // check if title is empty
+
+ if (!segment.title) {
+ return true;
+ }
+
+ // parse the filters to check if they are valid
+ const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
+ if (!parsedFilters.success) {
+ return true;
+ }
+
+ return false;
+ }, [segment]);
+
+ return (
+
+
+
+
+
+
+
+ {
+ setSegment((prev) => ({
+ ...prev,
+ title: e.target.value,
+ }));
+ }}
+ placeholder="Ex. Power Users"
+ value={segment.title}
+ />
+
+
+
+
+
+
+ {
+ setSegment((prev) => ({
+ ...prev,
+ description: e.target.value,
+ }));
+ }}
+ placeholder="Ex. Power Users"
+ value={segment.description ?? ""}
+ />
+
+
+
+
+
+
+ {segment.filters.length === 0 && (
+
+
+
Add your first filter to get started
+
+ )}
+
+
+
+
+ {
+ setAddFilterModalOpen(true);
+ }}
+ size="sm"
+ variant="secondary">
+ Add Filter
+
+
+
+
{
+ handleAddFilterInGroup(filter);
+ }}
+ open={addFilterModalOpen}
+ segments={segments}
+ setOpen={setAddFilterModalOpen}
+ />
+
+
+
+ {
+ setIsDeleteSegmentModalOpen(true);
+ }}
+ type="button"
+ variant="warn">
+ Delete
+
+ {
+ handleUpdateSegment();
+ }}
+ type="submit"
+ variant="darkCTA">
+ Save Changes
+
+
+ {isDeleteSegmentModalOpen ? (
+
+ ) : null}
+
+
+
+
+ );
+}
diff --git a/packages/ee/advancedTargeting/lib/actions.ts b/packages/ee/advanced-targeting/lib/actions.ts
similarity index 96%
rename from packages/ee/advancedTargeting/lib/actions.ts
rename to packages/ee/advanced-targeting/lib/actions.ts
index bc14bf3a4c..3b1b1f6e3e 100644
--- a/packages/ee/advancedTargeting/lib/actions.ts
+++ b/packages/ee/advanced-targeting/lib/actions.ts
@@ -15,7 +15,8 @@ import {
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
import { loadNewSegmentInSurvey } from "@formbricks/lib/survey/service";
import { AuthorizationError } from "@formbricks/types/errors";
-import { TSegmentCreateInput, TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
+import type { TSegmentCreateInput, TSegmentUpdateInput } from "@formbricks/types/segment";
+import { ZSegmentFilters } from "@formbricks/types/segment";
export const createSegmentAction = async ({
description,
diff --git a/packages/ee/advancedTargeting/lib/constants.ts b/packages/ee/advanced-targeting/lib/constants.ts
similarity index 100%
rename from packages/ee/advancedTargeting/lib/constants.ts
rename to packages/ee/advanced-targeting/lib/constants.ts
diff --git a/packages/ee/advancedTargeting/components/SegmentEditor.tsx b/packages/ee/advancedTargeting/components/SegmentEditor.tsx
deleted file mode 100644
index 584c28385f..0000000000
--- a/packages/ee/advancedTargeting/components/SegmentEditor.tsx
+++ /dev/null
@@ -1,263 +0,0 @@
-import { MoreVertical, Trash2 } from "lucide-react";
-import { useState } from "react";
-
-import { cn } from "@formbricks/lib/cn";
-import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
-import {
- addFilterBelow,
- addFilterInGroup,
- createGroupFromResource,
- deleteResource,
- isResourceFilter,
- moveResource,
- toggleGroupConnector,
-} from "@formbricks/lib/segment/utils";
-import { TActionClass } from "@formbricks/types/actionClasses";
-import { TAttributeClass } from "@formbricks/types/attributeClasses";
-import { TBaseFilter, TBaseFilters, TSegment, TSegmentConnector } from "@formbricks/types/segment";
-import { Button } from "@formbricks/ui/Button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@formbricks/ui/DropdownMenu";
-
-import { AddFilterModal } from "./AddFilterModal";
-import { SegmentFilter } from "./SegmentFilter";
-
-type TSegmentEditorProps = {
- group: TBaseFilters;
- environmentId: string;
- segment: TSegment;
- segments: TSegment[];
- actionClasses: TActionClass[];
- attributeClasses: TAttributeClass[];
- setSegment: React.Dispatch>;
- viewOnly?: boolean;
-};
-
-export const SegmentEditor = ({
- group,
- environmentId,
- setSegment,
- segment,
- actionClasses,
- attributeClasses,
- segments,
- viewOnly = false,
-}: TSegmentEditorProps) => {
- const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
- const [addFilterModalOpenedFromBelow, setAddFilterModalOpenedFromBelow] = useState(false);
-
- const handleAddFilterBelow = (resourceId: string, filter: TBaseFilter) => {
- const localSegmentCopy = structuredClone(segment);
-
- if (localSegmentCopy.filters) {
- addFilterBelow(localSegmentCopy.filters, resourceId, filter);
- }
-
- setSegment(localSegmentCopy);
- };
-
- const handleCreateGroup = (resourceId: string) => {
- const localSegmentCopy = structuredClone(segment);
- if (localSegmentCopy.filters) {
- createGroupFromResource(localSegmentCopy.filters, resourceId);
- }
-
- setSegment(localSegmentCopy);
- };
-
- const handleMoveResource = (resourceId: string, direction: "up" | "down") => {
- const localSegmentCopy = structuredClone(segment);
- if (localSegmentCopy.filters) {
- moveResource(localSegmentCopy.filters, resourceId, direction);
- }
-
- setSegment(localSegmentCopy);
- };
-
- const handleDeleteResource = (resourceId: string) => {
- const localSegmentCopy = structuredClone(segment);
-
- if (localSegmentCopy.filters) {
- deleteResource(localSegmentCopy.filters, resourceId);
- }
-
- setSegment(localSegmentCopy);
- };
-
- const handleToggleGroupConnector = (groupId: string, newConnectorValue: TSegmentConnector) => {
- const localSegmentCopy = structuredClone(segment);
- if (localSegmentCopy.filters) {
- toggleGroupConnector(localSegmentCopy.filters, groupId, newConnectorValue);
- }
-
- setSegment(localSegmentCopy);
- };
-
- const onConnectorChange = (groupId: string, connector: TSegmentConnector) => {
- if (!connector) return;
-
- if (connector === "and") {
- handleToggleGroupConnector(groupId, "or");
- } else {
- handleToggleGroupConnector(groupId, "and");
- }
- };
-
- const handleAddFilterInGroup = (groupId: string, filter: TBaseFilter) => {
- const localSegmentCopy = structuredClone(segment);
-
- if (localSegmentCopy.filters) {
- addFilterInGroup(localSegmentCopy.filters, groupId, filter);
- }
- setSegment(localSegmentCopy);
- };
-
- return (
-
- {group?.map((groupItem) => {
- const { connector, resource, id: groupId } = groupItem;
-
- if (isResourceFilter(resource)) {
- return (
-
handleCreateGroup(filterId)}
- onDeleteFilter={(filterId: string) => handleDeleteResource(filterId)}
- onMoveFilter={(filterId: string, direction: "up" | "down") =>
- handleMoveResource(filterId, direction)
- }
- viewOnly={viewOnly}
- />
- );
- } else {
- return (
-
-
-
- {
- if (viewOnly) return;
- onConnectorChange(groupId, connector);
- }}>
- {!!connector ? connector : "Where"}
-
-
-
-
-
-
-
- {
- if (viewOnly) return;
- setAddFilterModalOpen(true);
- }}
- disabled={viewOnly}>
- Add filter
-
-
-
-
{
- if (addFilterModalOpenedFromBelow) {
- handleAddFilterBelow(groupId, filter);
- setAddFilterModalOpenedFromBelow(false);
- } else {
- handleAddFilterInGroup(groupId, filter);
- }
- }}
- actionClasses={actionClasses}
- attributeClasses={attributeClasses}
- segments={segments}
- />
-
-
-
-
-
-
-
-
-
- {
- setAddFilterModalOpenedFromBelow(true);
- setAddFilterModalOpen(true);
- }}>
- Add filter below
-
-
- {
- handleCreateGroup(groupId);
- }}>
- Create group
-
-
- {
- handleMoveResource(groupId, "up");
- }}>
- Move up
-
-
- {
- if (viewOnly) return;
- handleMoveResource(groupId, "down");
- }}>
- Move down
-
-
-
-
- {
- if (viewOnly) return;
- handleDeleteResource(groupId);
- }}>
-
-
-
-
-
- );
- }
- })}
-
- );
-};
diff --git a/packages/ee/advancedTargeting/components/SegmentSettings.tsx b/packages/ee/advancedTargeting/components/SegmentSettings.tsx
deleted file mode 100644
index d69e9aa761..0000000000
--- a/packages/ee/advancedTargeting/components/SegmentSettings.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-"use client";
-
-import { FilterIcon, Trash2 } from "lucide-react";
-import { useRouter } from "next/navigation";
-import { useMemo, useState } from "react";
-import toast from "react-hot-toast";
-
-import { cn } from "@formbricks/lib/cn";
-import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
-import { TActionClass } from "@formbricks/types/actionClasses";
-import { TAttributeClass } from "@formbricks/types/attributeClasses";
-import { TBaseFilter, TSegment, TSegmentWithSurveyNames, ZSegmentFilters } from "@formbricks/types/segment";
-import { Button } from "@formbricks/ui/Button";
-import { ConfirmDeleteSegmentModal } from "@formbricks/ui/ConfirmDeleteSegmentModal";
-import { Input } from "@formbricks/ui/Input";
-
-import { deleteSegmentAction, updateSegmentAction } from "../lib/actions";
-import { AddFilterModal } from "./AddFilterModal";
-import { SegmentEditor } from "./SegmentEditor";
-
-type TSegmentSettingsTabProps = {
- environmentId: string;
- setOpen: (open: boolean) => void;
- initialSegment: TSegmentWithSurveyNames;
- segments: TSegment[];
- attributeClasses: TAttributeClass[];
- actionClasses: TActionClass[];
-};
-
-export const SegmentSettings = ({
- environmentId,
- initialSegment,
- setOpen,
- actionClasses,
- attributeClasses,
- segments,
-}: TSegmentSettingsTabProps) => {
- const router = useRouter();
-
- const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
- const [segment, setSegment] = useState(initialSegment);
-
- const [isUpdatingSegment, setIsUpdatingSegment] = useState(false);
- const [isDeletingSegment, setIsDeletingSegment] = useState(false);
-
- const [isDeleteSegmentModalOpen, setIsDeleteSegmentModalOpen] = useState(false);
-
- const handleResetState = () => {
- setSegment(initialSegment);
- setOpen(false);
-
- router.refresh();
- };
-
- const handleAddFilterInGroup = (filter: TBaseFilter) => {
- const updatedSegment = structuredClone(segment);
- if (updatedSegment?.filters?.length === 0) {
- updatedSegment.filters.push({
- ...filter,
- connector: null,
- });
- } else {
- updatedSegment?.filters.push(filter);
- }
-
- setSegment(updatedSegment);
- };
-
- const handleUpdateSegment = async () => {
- if (!segment.title) {
- toast.error("Title is required");
- return;
- }
-
- try {
- setIsUpdatingSegment(true);
- await updateSegmentAction(segment.environmentId, segment.id, {
- title: segment.title,
- description: segment.description ?? "",
- isPrivate: segment.isPrivate,
- filters: segment.filters,
- });
-
- setIsUpdatingSegment(false);
- toast.success("Segment updated successfully!");
- } catch (err: any) {
- const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
- if (!parsedFilters.success) {
- toast.error("Invalid filters. Please check the filters and try again.");
- } else {
- toast.error("Something went wrong. Please try again.");
- }
- setIsUpdatingSegment(false);
- return;
- }
-
- setIsUpdatingSegment(false);
- handleResetState();
- router.refresh();
- };
-
- const handleDeleteSegment = async () => {
- try {
- setIsDeletingSegment(true);
- await deleteSegmentAction(segment.environmentId, segment.id);
-
- setIsDeletingSegment(false);
- toast.success("Segment deleted successfully!");
- handleResetState();
- } catch (err: any) {
- toast.error("Something went wrong. Please try again.");
- }
-
- setIsDeletingSegment(false);
- };
-
- const isSaveDisabled = useMemo(() => {
- // check if title is empty
-
- if (!segment.title) {
- return true;
- }
-
- // parse the filters to check if they are valid
- const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
- if (!parsedFilters.success) {
- return true;
- }
-
- return false;
- }, [segment]);
-
- return (
- <>
-
-
-
-
-
-
-
- {
- setSegment((prev) => ({
- ...prev,
- title: e.target.value,
- }));
- }}
- className="w-auto"
- />
-
-
-
-
-
-
- {
- setSegment((prev) => ({
- ...prev,
- description: e.target.value,
- }));
- }}
- className={cn("w-auto")}
- />
-
-
-
-
-
-
- {segment?.filters?.length === 0 && (
-
-
-
Add your first filter to get started
-
- )}
-
-
-
-
- setAddFilterModalOpen(true)}>
- Add Filter
-
-
-
-
{
- handleAddFilterInGroup(filter);
- }}
- open={addFilterModalOpen}
- setOpen={setAddFilterModalOpen}
- actionClasses={actionClasses}
- attributeClasses={attributeClasses}
- segments={segments}
- />
-
-
-
- {
- setIsDeleteSegmentModalOpen(true);
- }}
- EndIcon={Trash2}
- endIconClassName="p-0.5">
- Delete
-
- {
- handleUpdateSegment();
- }}
- disabled={isSaveDisabled}>
- Save Changes
-
-
- {isDeleteSegmentModalOpen && (
-
- )}
-
-
-
-
- >
- );
-};
diff --git a/packages/ee/billing/api/stripe-webhook.ts b/packages/ee/billing/api/stripe-webhook.ts
index 6402e9caa9..0a42c3ad99 100644
--- a/packages/ee/billing/api/stripe-webhook.ts
+++ b/packages/ee/billing/api/stripe-webhook.ts
@@ -3,21 +3,24 @@ import Stripe from "stripe";
import { STRIPE_API_VERSION } from "@formbricks/lib/constants";
import { env } from "@formbricks/lib/env";
-import { handleCheckoutSessionCompleted } from "../handlers/checkoutSessionCompleted";
-import { handleSubscriptionUpdatedOrCreated } from "../handlers/subscriptionCreatedOrUpdated";
-import { handleSubscriptionDeleted } from "../handlers/subscriptionDeleted";
-
-const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
- apiVersion: STRIPE_API_VERSION,
-});
-
-const webhookSecret: string = env.STRIPE_WEBHOOK_SECRET!;
+import { handleCheckoutSessionCompleted } from "../handlers/checkout-session-completed";
+import { handleSubscriptionUpdatedOrCreated } from "../handlers/subscription-created-or-updated";
+import { handleSubscriptionDeleted } from "../handlers/subscription-deleted";
export const webhookHandler = async (requestBody: string, stripeSignature: string) => {
let event: Stripe.Event;
+ if (!env.STRIPE_SECRET_KEY || !env.STRIPE_WEBHOOK_SECRET) {
+ console.error("Stripe is not enabled, skipping webhook");
+ return { status: 400, message: "Stripe is not enabled, skipping webhook" };
+ }
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
try {
- event = stripe.webhooks.constructEvent(requestBody, stripeSignature, webhookSecret);
+ event = stripe.webhooks.constructEvent(requestBody, stripeSignature, env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
if (err! instanceof Error) console.error(err);
diff --git a/packages/ee/billing/handlers/checkoutSessionCompleted.ts b/packages/ee/billing/handlers/checkout-session-completed.ts
similarity index 91%
rename from packages/ee/billing/handlers/checkoutSessionCompleted.ts
rename to packages/ee/billing/handlers/checkout-session-completed.ts
index 070c620b50..53b8229f32 100644
--- a/packages/ee/billing/handlers/checkoutSessionCompleted.ts
+++ b/packages/ee/billing/handlers/checkout-session-completed.ts
@@ -10,15 +10,17 @@ import {
} from "@formbricks/lib/organization/service";
import { ProductFeatureKeys, StripePriceLookupKeys, StripeProductNames } from "../lib/constants";
-import { reportUsage } from "../lib/reportUsage";
-
-const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
- // https://github.com/stripe/stripe-node#configuration
- apiVersion: STRIPE_API_VERSION,
-});
+import { reportUsage } from "../lib/report-usage";
export const handleCheckoutSessionCompleted = async (event: Stripe.Event) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
const checkoutSession = event.data.object as Stripe.Checkout.Session;
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
const stripeSubscriptionObject = await stripe.subscriptions.retrieve(
checkoutSession.subscription as string
);
@@ -29,7 +31,7 @@ export const handleCheckoutSessionCompleted = async (event: Stripe.Event) => {
const organization = await getOrganization(stripeSubscriptionObject.metadata.organizationId);
if (!organization) throw new Error("Organization not found.");
- let updatedFeatures = organization.billing.features;
+ const updatedFeatures = organization.billing.features;
for (const item of stripeSubscriptionObject.items.data) {
const product = await stripe.products.retrieve(item.price.product as string);
diff --git a/packages/ee/billing/handlers/subscriptionCreatedOrUpdated.ts b/packages/ee/billing/handlers/subscription-created-or-updated.ts
similarity index 92%
rename from packages/ee/billing/handlers/subscriptionCreatedOrUpdated.ts
rename to packages/ee/billing/handlers/subscription-created-or-updated.ts
index 440ec221b2..b4d862d5bf 100644
--- a/packages/ee/billing/handlers/subscriptionCreatedOrUpdated.ts
+++ b/packages/ee/billing/handlers/subscription-created-or-updated.ts
@@ -10,17 +10,18 @@ import {
} from "@formbricks/lib/organization/service";
import { ProductFeatureKeys, StripePriceLookupKeys, StripeProductNames } from "../lib/constants";
-import { reportUsage } from "../lib/reportUsage";
-
-const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
- // https://github.com/stripe/stripe-node#configuration
- apiVersion: STRIPE_API_VERSION,
-});
+import { reportUsage } from "../lib/report-usage";
const isProductScheduled = async (
scheduledSubscriptions: Stripe.SubscriptionSchedule[],
productName: StripeProductNames
) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
for (const scheduledSub of scheduledSubscriptions) {
if (scheduledSub.phases && scheduledSub.phases.length > 0) {
const firstPhase = scheduledSub.phases[0];
@@ -38,6 +39,12 @@ const isProductScheduled = async (
};
export const handleSubscriptionUpdatedOrCreated = async (event: Stripe.Event) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
const stripeSubscriptionObject = event.data.object as Stripe.Subscription;
const organizationId = stripeSubscriptionObject.metadata.organizationId;
@@ -52,12 +59,12 @@ export const handleSubscriptionUpdatedOrCreated = async (event: Stripe.Event) =>
const organization = await getOrganization(organizationId);
if (!organization) throw new Error("Organization not found.");
- let updatedFeatures = organization.billing.features;
+ const updatedFeatures = organization.billing.features;
let scheduledSubscriptions: Stripe.SubscriptionSchedule[] = [];
if (stripeSubscriptionObject.cancel_at_period_end) {
const allScheduledSubscriptions = await stripe.subscriptionSchedules.list({
- customer: organization.billing.stripeCustomerId as string,
+ customer: organization.billing.stripeCustomerId!,
});
scheduledSubscriptions = allScheduledSubscriptions.data.filter(
(scheduledSub) => scheduledSub.status === "not_started"
diff --git a/packages/ee/billing/handlers/subscriptionDeleted.ts b/packages/ee/billing/handlers/subscription-deleted.ts
similarity index 88%
rename from packages/ee/billing/handlers/subscriptionDeleted.ts
rename to packages/ee/billing/handlers/subscription-deleted.ts
index 1ffafc24b6..8f7acae72c 100644
--- a/packages/ee/billing/handlers/subscriptionDeleted.ts
+++ b/packages/ee/billing/handlers/subscription-deleted.ts
@@ -5,14 +5,15 @@ import { env } from "@formbricks/lib/env";
import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service";
import { ProductFeatureKeys, StripeProductNames } from "../lib/constants";
-import { unsubscribeCoreAndAppSurveyFeatures, unsubscribeLinkSurveyProFeatures } from "../lib/downgradePlan";
-
-const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
- // https://github.com/stripe/stripe-node#configuration
- apiVersion: STRIPE_API_VERSION,
-});
+import { unsubscribeCoreAndAppSurveyFeatures, unsubscribeLinkSurveyProFeatures } from "../lib/downgrade-plan";
export const handleSubscriptionDeleted = async (event: Stripe.Event) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
const stripeSubscriptionObject = event.data.object as Stripe.Subscription;
const organizationId = stripeSubscriptionObject.metadata.organizationId;
if (!organizationId) {
@@ -23,7 +24,7 @@ export const handleSubscriptionDeleted = async (event: Stripe.Event) => {
const organization = await getOrganization(organizationId);
if (!organization) throw new Error("Organization not found.");
- let updatedFeatures = organization.billing.features;
+ const updatedFeatures = organization.billing.features;
for (const item of stripeSubscriptionObject.items.data) {
const product = await stripe.products.retrieve(item.price.product as string);
diff --git a/packages/ee/billing/lib/createCustomerPortalSession.ts b/packages/ee/billing/lib/create-customer-portal-session.ts
similarity index 66%
rename from packages/ee/billing/lib/createCustomerPortalSession.ts
rename to packages/ee/billing/lib/create-customer-portal-session.ts
index 6a3459ff5b..2e68e3016d 100644
--- a/packages/ee/billing/lib/createCustomerPortalSession.ts
+++ b/packages/ee/billing/lib/create-customer-portal-session.ts
@@ -3,12 +3,13 @@ import Stripe from "stripe";
import { STRIPE_API_VERSION } from "@formbricks/lib/constants";
import { env } from "@formbricks/lib/env";
-const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
- // https://github.com/stripe/stripe-node#configuration
- apiVersion: STRIPE_API_VERSION,
-});
-
export const createCustomerPortalSession = async (stripeCustomerId: string, returnUrl: string) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
const session = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: returnUrl,
diff --git a/packages/ee/billing/lib/createSubscription.ts b/packages/ee/billing/lib/create-subscription.ts
similarity index 84%
rename from packages/ee/billing/lib/createSubscription.ts
rename to packages/ee/billing/lib/create-subscription.ts
index f2b2bf054a..8a17da9f21 100644
--- a/packages/ee/billing/lib/createSubscription.ts
+++ b/packages/ee/billing/lib/create-subscription.ts
@@ -4,13 +4,7 @@ import { STRIPE_API_VERSION, WEBAPP_URL } from "@formbricks/lib/constants";
import { env } from "@formbricks/lib/env";
import { getOrganization } from "@formbricks/lib/organization/service";
-import { StripePriceLookupKeys } from "./constants";
-
-const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
- apiVersion: STRIPE_API_VERSION,
-});
-
-const baseUrl = process.env.NODE_ENV === "production" ? WEBAPP_URL : "http://localhost:3000";
+import type { StripePriceLookupKeys } from "./constants";
export const getFirstOfNextMonthTimestamp = (): number => {
const nextMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1);
@@ -22,14 +16,20 @@ export const createSubscription = async (
environmentId: string,
priceLookupKeys: StripePriceLookupKeys[]
) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
try {
const organization = await getOrganization(organizationId);
if (!organization) throw new Error("Organization not found.");
- let isNewOrganization =
+ const isNewOrganization =
!organization.billing.stripeCustomerId ||
!(await stripe.customers.retrieve(organization.billing.stripeCustomerId));
- let lineItems: { price: string; quantity?: number }[] = [];
+ const lineItems: { price: string; quantity?: number }[] = [];
const prices = (
await stripe.prices.list({
@@ -50,8 +50,8 @@ export const createSubscription = async (
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: lineItems,
- success_url: `${baseUrl}/billing-confirmation?environmentId=${environmentId}`,
- cancel_url: `${baseUrl}/environments/${environmentId}/settings/billing`,
+ success_url: `${WEBAPP_URL}/billing-confirmation?environmentId=${environmentId}`,
+ cancel_url: `${WEBAPP_URL}/environments/${environmentId}/settings/billing`,
allow_promotion_codes: true,
subscription_data: {
billing_cycle_anchor: getFirstOfNextMonthTimestamp(),
@@ -64,7 +64,7 @@ export const createSubscription = async (
}
const existingSubscription = (
- (await stripe.customers.retrieve(organization.billing.stripeCustomerId as string, {
+ (await stripe.customers.retrieve(organization.billing.stripeCustomerId!, {
expand: ["subscriptions"],
})) as any
).subscriptions.data[0] as Stripe.Subscription;
@@ -75,7 +75,7 @@ export const createSubscription = async (
// this is a case where the organization cancelled an already purchased product
if (existingSubscription.cancel_at_period_end) {
const allScheduledSubscriptions = await stripe.subscriptionSchedules.list({
- customer: organization.billing.stripeCustomerId as string,
+ customer: organization.billing.stripeCustomerId!,
});
const scheduledSubscriptions = allScheduledSubscriptions.data.filter(
(scheduledSub) => scheduledSub.status === "not_started"
@@ -95,13 +95,12 @@ export const createSubscription = async (
const combinedLineItems = [...lineItems, ...existingItemsInScheduledSubscription];
- const uniqueItemsMap = combinedLineItems.reduce(
- (acc, item) => {
- acc[item.price] = item; // This will overwrite duplicate items based on price
- return acc;
- },
- {} as { [key: string]: { price: string; quantity?: number } }
- );
+ const uniqueItemsMap = combinedLineItems.reduce<
+ Record
+ >((acc, item) => {
+ acc[item.price] = item; // This will overwrite duplicate items based on price
+ return acc;
+ }, {});
const lineItemsForScheduledSubscription = Object.values(uniqueItemsMap);
@@ -122,7 +121,7 @@ export const createSubscription = async (
// we create one since the current one with other products is expiring
// so the new schedule only has the new product the organization has subscribed to
await stripe.subscriptionSchedules.create({
- customer: organization.billing.stripeCustomerId as string,
+ customer: organization.billing.stripeCustomerId!,
start_date: getFirstOfNextMonthTimestamp(),
end_behavior: "release",
phases: [
@@ -165,7 +164,7 @@ export const createSubscription = async (
// case where organization does not have a subscription but has a stripe customer id
// so we just attach that to a new subscription
await stripe.subscriptions.create({
- customer: organization.billing.stripeCustomerId as string,
+ customer: organization.billing.stripeCustomerId!,
items: lineItems,
billing_cycle_anchor: getFirstOfNextMonthTimestamp(),
metadata: { organizationId },
@@ -184,7 +183,7 @@ export const createSubscription = async (
status: 500,
data: "Something went wrong!",
newPlan: true,
- url: `${baseUrl}/environments/${environmentId}/settings/billing`,
+ url: `${WEBAPP_URL}/environments/${environmentId}/settings/billing`,
};
}
};
diff --git a/packages/ee/billing/lib/downgradePlan.ts b/packages/ee/billing/lib/downgrade-plan.ts
similarity index 100%
rename from packages/ee/billing/lib/downgradePlan.ts
rename to packages/ee/billing/lib/downgrade-plan.ts
diff --git a/packages/ee/billing/lib/removeSubscription.ts b/packages/ee/billing/lib/remove-subscription.ts
similarity index 84%
rename from packages/ee/billing/lib/removeSubscription.ts
rename to packages/ee/billing/lib/remove-subscription.ts
index 51a6723bab..4beeaf6e7f 100644
--- a/packages/ee/billing/lib/removeSubscription.ts
+++ b/packages/ee/billing/lib/remove-subscription.ts
@@ -4,22 +4,32 @@ import { STRIPE_API_VERSION, WEBAPP_URL } from "@formbricks/lib/constants";
import { env } from "@formbricks/lib/env";
import { getOrganization, updateOrganization } from "@formbricks/lib/organization/service";
-import { StripePriceLookupKeys } from "./constants";
-import { getFirstOfNextMonthTimestamp } from "./createSubscription";
-
-const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
- apiVersion: STRIPE_API_VERSION,
-});
+import type { StripePriceLookupKeys } from "./constants";
+import { getFirstOfNextMonthTimestamp } from "./create-subscription";
const baseUrl = process.env.NODE_ENV === "production" ? WEBAPP_URL : "http://localhost:3000";
-const retrievePriceLookup = async (priceId: string) => (await stripe.prices.retrieve(priceId)).lookup_key;
+const retrievePriceLookup = async (priceId: string) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
+ return (await stripe.prices.retrieve(priceId)).lookup_key;
+};
export const removeSubscription = async (
organizationId: string,
environmentId: string,
priceLookupKeys: StripePriceLookupKeys[]
) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
try {
const organization = await getOrganization(organizationId);
if (!organization) throw new Error("Organization not found.");
@@ -30,7 +40,7 @@ export const removeSubscription = async (
const existingCustomer = (await stripe.customers.retrieve(organization.billing.stripeCustomerId, {
expand: ["subscriptions"],
})) as Stripe.Customer;
- const existingSubscription = existingCustomer.subscriptions?.data[0] as Stripe.Subscription;
+ const existingSubscription = existingCustomer.subscriptions?.data[0]!;
const allScheduledSubscriptions = await stripe.subscriptionSchedules.list({
customer: organization.billing.stripeCustomerId,
@@ -93,7 +103,7 @@ export const removeSubscription = async (
await stripe.subscriptions.update(existingSubscription.id, { cancel_at_period_end: true });
- let updatedFeatures = organization.billing.features;
+ const updatedFeatures = organization.billing.features;
for (const priceLookupKey of priceLookupKeys) {
updatedFeatures[priceLookupKey as keyof typeof updatedFeatures].status = "cancelled";
}
diff --git a/packages/ee/billing/lib/reportUsage.ts b/packages/ee/billing/lib/report-usage.ts
similarity index 74%
rename from packages/ee/billing/lib/reportUsage.ts
rename to packages/ee/billing/lib/report-usage.ts
index 41050dd289..2fdaff7faa 100644
--- a/packages/ee/billing/lib/reportUsage.ts
+++ b/packages/ee/billing/lib/report-usage.ts
@@ -5,16 +5,17 @@ import { env } from "@formbricks/lib/env";
import { ProductFeatureKeys } from "./constants";
-const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
- // https://github.com/stripe/stripe-node#configuration
- apiVersion: STRIPE_API_VERSION,
-});
-
export const reportUsage = async (
items: Stripe.SubscriptionItem[],
lookupKey: ProductFeatureKeys,
quantity: number
) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
const subscriptionItem = items.find(
(subItem) => subItem.price.lookup_key === ProductFeatureKeys[lookupKey]
);
@@ -25,7 +26,7 @@ export const reportUsage = async (
await stripe.subscriptionItems.createUsageRecord(subscriptionItem.id, {
action: "set",
- quantity: quantity,
+ quantity,
timestamp: Math.floor(Date.now() / 1000),
});
};
@@ -36,6 +37,12 @@ export const reportUsageToStripe = async (
lookupKey: ProductFeatureKeys,
timestamp: number
) => {
+ if (!env.STRIPE_SECRET_KEY) throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
+
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
+ apiVersion: STRIPE_API_VERSION,
+ });
+
try {
const subscription = await stripe.subscriptions.list({
customer: stripeCustomerId,
@@ -53,11 +60,11 @@ export const reportUsageToStripe = async (
const usageRecord = await stripe.subscriptionItems.createUsageRecord(subId, {
action: "set",
quantity: usage,
- timestamp: timestamp,
+ timestamp,
});
return { status: 200, data: usageRecord.quantity };
} catch (error) {
- return { status: 500, data: "Something went wrong: " + error };
+ return { status: 500, data: `Something went wrong: ${error}` };
}
};
diff --git a/packages/ee/lib/service.ts b/packages/ee/lib/service.ts
index dc105a43ac..26454f0c2b 100644
--- a/packages/ee/lib/service.ts
+++ b/packages/ee/lib/service.ts
@@ -2,12 +2,11 @@ import "server-only";
import axios from "axios";
+import { prisma } from "@formbricks/database";
import { cache, revalidateTag } from "@formbricks/lib/cache";
import { E2E_TESTING, ENTERPRISE_LICENSE_KEY, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { hashString } from "@formbricks/lib/hashString";
-import { TOrganization } from "@formbricks/types/organizations";
-
-import { prisma } from "../../database/src";
+import type { TOrganization } from "@formbricks/types/organizations";
const hashedKey = ENTERPRISE_LICENSE_KEY ? hashString(ENTERPRISE_LICENSE_KEY) : undefined;
const PREVIOUS_RESULTS_CACHE_TAG_KEY = `getPreviousResult-${hashedKey}` as const;
@@ -86,7 +85,7 @@ export const getIsEnterpriseEdition = async (): Promise => {
const response = await axios.post("https://ee.formbricks.com/api/licenses/check", {
licenseKey: process.env.ENTERPRISE_LICENSE_KEY,
- usage: { responseCount: responseCount },
+ usage: { responseCount },
});
if (response.status === 200) {
@@ -116,45 +115,44 @@ export const getIsEnterpriseEdition = async (): Promise => {
if (isValid !== null) {
await setPreviousResult({ active: isValid, lastChecked: new Date() });
return isValid;
- } else {
- // if result is undefined -> error
- // if the last check was less than 72 hours, return the previous value:
- if (new Date().getTime() - previousResult.lastChecked.getTime() <= 3 * 24 * 60 * 60 * 1000) {
- return previousResult.active !== null ? previousResult.active : false;
- }
-
- // if the last check was more than 72 hours, return false and log the error
- console.error("Error while checking license: The license check failed");
- return false;
}
+ // if result is undefined -> error
+ // if the last check was less than 72 hours, return the previous value:
+ if (new Date().getTime() - previousResult.lastChecked.getTime() <= 3 * 24 * 60 * 60 * 1000) {
+ return previousResult.active !== null ? previousResult.active : false;
+ }
+
+ // if the last check was more than 72 hours, return false and log the error
+ console.error("Error while checking license: The license check failed");
+ return false;
};
export const getRemoveInAppBrandingPermission = (organization: TOrganization): boolean => {
if (IS_FORMBRICKS_CLOUD) return organization.billing.features.inAppSurvey.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return true;
- else return false;
+ return false;
};
export const getRemoveLinkBrandingPermission = (organization: TOrganization): boolean => {
if (IS_FORMBRICKS_CLOUD) return organization.billing.features.linkSurvey.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return true;
- else return false;
+ return false;
};
export const getRoleManagementPermission = async (organization: TOrganization): Promise => {
if (IS_FORMBRICKS_CLOUD) return organization.billing.features.inAppSurvey.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
- else return false;
+ return false;
};
export const getAdvancedTargetingPermission = async (organization: TOrganization): Promise => {
if (IS_FORMBRICKS_CLOUD) return organization.billing.features.userTargeting.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
- else return false;
+ return false;
};
export const getMultiLanguagePermission = async (organization: TOrganization): Promise => {
if (IS_FORMBRICKS_CLOUD) return organization.billing.features.inAppSurvey.status !== "inactive";
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
- else return false;
+ return false;
};
diff --git a/packages/ee/multiLanguage/components/DefaultLanguageSelect.tsx b/packages/ee/multi-language/components/default-language-select.tsx
similarity index 80%
rename from packages/ee/multiLanguage/components/DefaultLanguageSelect.tsx
rename to packages/ee/multi-language/components/default-language-select.tsx
index 7d176cde90..0602d7adc3 100644
--- a/packages/ee/multiLanguage/components/DefaultLanguageSelect.tsx
+++ b/packages/ee/multi-language/components/default-language-select.tsx
@@ -1,9 +1,9 @@
-import { TLanguage, TProduct } from "@formbricks/types/product";
+import type { TLanguage, TProduct } from "@formbricks/types/product";
import { DefaultTag } from "@formbricks/ui/DefaultTag";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
-import { getLanguageLabel } from "../lib/isoLanguages";
-import { ConfirmationModalProps } from "./MultiLanguageCard";
+import { getLanguageLabel } from "../lib/iso-languages";
+import type { ConfirmationModalProps } from "./multi-language-card";
interface DefaultLanguageSelectProps {
defaultLanguage?: TLanguage;
@@ -12,39 +12,41 @@ interface DefaultLanguageSelectProps {
setConfirmationModalInfo: (confirmationModal: ConfirmationModalProps) => void;
}
-export const DefaultLanguageSelect = ({
+export function DefaultLanguageSelect({
defaultLanguage,
handleDefaultLanguageChange,
product,
setConfirmationModalInfo,
-}: DefaultLanguageSelectProps) => {
+}: DefaultLanguageSelectProps) {
return (
1. Choose the default language for this survey:
);
-};
+}
diff --git a/packages/ee/multiLanguage/components/EditLanguage.tsx b/packages/ee/multi-language/components/edit-language.tsx
similarity index 86%
rename from packages/ee/multiLanguage/components/EditLanguage.tsx
rename to packages/ee/multi-language/components/edit-language.tsx
index 536283e2ea..e60da3817d 100644
--- a/packages/ee/multiLanguage/components/EditLanguage.tsx
+++ b/packages/ee/multi-language/components/edit-language.tsx
@@ -4,7 +4,7 @@ import { InfoIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
-import { TLanguage, TProduct } from "@formbricks/types/product";
+import type { TLanguage, TProduct } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { ConfirmationModal } from "@formbricks/ui/ConfirmationModal";
import { Label } from "@formbricks/ui/Label";
@@ -16,8 +16,8 @@ import {
getSurveysUsingGivenLanguageAction,
updateLanguageAction,
} from "../lib/actions";
-import { iso639Languages } from "../lib/isoLanguages";
-import { LanguageRow } from "./LanguageRow";
+import { iso639Languages } from "../lib/iso-languages";
+import { LanguageRow } from "./language-row";
interface EditLanguageProps {
product: TProduct;
@@ -55,7 +55,7 @@ const validateLanguages = (languages: TLanguage[]) => {
}
// Check if the chosen alias matches an ISO identifier of a language that hasnโt been added
- for (let alias of languageAliases) {
+ for (const alias of languageAliases) {
if (iso639Languages.some((language) => language.alpha2 === alias && !languageCodes.includes(alias))) {
toast.error(
"There is a conflict between the selected alias and another language that has this identifier. Please add the language with this identifier to your product instead to avoid inconsistencies.",
@@ -68,7 +68,7 @@ const validateLanguages = (languages: TLanguage[]) => {
return true;
};
-export const EditLanguage = ({ product, environmentId }: EditLanguageProps) => {
+export function EditLanguage({ product, environmentId }: EditLanguageProps) {
const [languages, setLanguages] = useState
(product.languages);
const [isEditing, setIsEditing] = useState(false);
const [confirmationModal, setConfirmationModal] = useState({
@@ -146,7 +146,7 @@ export const EditLanguage = ({ product, environmentId }: EditLanguageProps) => {
const AddLanguageButton: React.FC<{ onClick: () => void }> = ({ onClick }) =>
isEditing && languages.length === product.languages.length ? (
-
+
Add Language
) : null;
@@ -159,16 +159,16 @@ export const EditLanguage = ({ product, environmentId }: EditLanguageProps) => {
{languages.map((language, index) => (
handleDeleteLanguage(language.id)}
onLanguageChange={(newLanguage: TLanguage) => {
const updatedLanguages = [...languages];
updatedLanguages[index] = newLanguage;
setLanguages(updatedLanguages);
}}
- onDelete={() => handleDeleteLanguage(language.id)}
/>
))}
>
@@ -179,24 +179,28 @@ export const EditLanguage = ({ product, environmentId }: EditLanguageProps) => {
setIsEditing(true)}
+ onEdit={() => {
+ setIsEditing(true);
+ }}
+ onSave={handleSaveChanges}
/>
setConfirmationModal((prev) => ({ ...prev, isOpen: !prev.isOpen }))}
- text={confirmationModal.text}
- onConfirm={() => performLanguageDeletion(confirmationModal.languageId)}
+ buttonText="Remove Language"
isButtonDisabled={confirmationModal.isButtonDisabled}
+ onConfirm={() => performLanguageDeletion(confirmationModal.languageId)}
+ open={confirmationModal.isOpen}
+ setOpen={() => {
+ setConfirmationModal((prev) => ({ ...prev, isOpen: !prev.isOpen }));
+ }}
+ text={confirmationModal.text}
+ title="Remove Language"
/>
);
-};
+}
-const AliasTooltip = () => {
+function AliasTooltip() {
return (
@@ -211,17 +215,19 @@ const AliasTooltip = () => {
);
-};
+}
-const LanguageLabels = () => (
-
-
-
-
-
-);
+function LanguageLabels() {
+ return (
+
+
+
+
+
+ );
+}
const EditSaveButtons: React.FC<{
isEditing: boolean;
@@ -231,15 +237,15 @@ const EditSaveButtons: React.FC<{
}> = ({ isEditing, onEdit, onSave, onCancel }) =>
isEditing ? (
-
+
Save Changes
-
+
Cancel
) : (
-
+
Edit Languages
);
diff --git a/packages/ee/multiLanguage/components/LanguageIndicator.tsx b/packages/ee/multi-language/components/language-indicator.tsx
similarity index 75%
rename from packages/ee/multiLanguage/components/LanguageIndicator.tsx
rename to packages/ee/multi-language/components/language-indicator.tsx
index 7cdaf5326d..1f9ebb1753 100644
--- a/packages/ee/multiLanguage/components/LanguageIndicator.tsx
+++ b/packages/ee/multi-language/components/language-indicator.tsx
@@ -2,9 +2,9 @@ import { ChevronDown } from "lucide-react";
import { useRef, useState } from "react";
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
-import { TSurveyLanguage } from "@formbricks/types/surveys";
+import type { TSurveyLanguage } from "@formbricks/types/surveys";
-import { getLanguageLabel } from "../lib/isoLanguages";
+import { getLanguageLabel } from "../lib/iso-languages";
interface LanguageIndicatorProps {
selectedLanguageCode: string;
@@ -12,14 +12,16 @@ interface LanguageIndicatorProps {
setSelectedLanguageCode: (languageCode: string) => void;
setFirstRender?: (firstRender: boolean) => void;
}
-export const LanguageIndicator = ({
+export function LanguageIndicator({
surveyLanguages,
selectedLanguageCode,
setSelectedLanguageCode,
setFirstRender,
-}: LanguageIndicatorProps) => {
+}: LanguageIndicatorProps) {
const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
- const toggleDropdown = () => setShowLanguageDropdown((prev) => !prev);
+ const toggleDropdown = () => {
+ setShowLanguageDropdown((prev) => !prev);
+ };
const languageDropdownRef = useRef(null);
const changeLanguage = (language: TSurveyLanguage) => {
@@ -33,25 +35,27 @@ export const LanguageIndicator = ({
const languageToBeDisplayed = surveyLanguages.find((language) => {
return selectedLanguageCode === "default"
- ? language.default === true
+ ? language.default
: language.language.code === selectedLanguageCode;
});
- useClickOutside(languageDropdownRef, () => setShowLanguageDropdown(false));
+ useClickOutside(languageDropdownRef, () => {
+ setShowLanguageDropdown(false);
+ });
return (
- {languageToBeDisplayed ? getLanguageLabel(languageToBeDisplayed?.language.code) : ""}
+ type="button">
+ {languageToBeDisplayed ? getLanguageLabel(languageToBeDisplayed.language.code) : ""}
- {showLanguageDropdown && (
+ {showLanguageDropdown ? (
@@ -59,16 +63,18 @@ export const LanguageIndicator = ({
(language) =>
language.language.code !== languageToBeDisplayed?.language.code && (
changeLanguage(language)}>
+ key={language.language.id}
+ onClick={() => {
+ changeLanguage(language);
+ }}
+ type="button">
{getLanguageLabel(language.language.code)}
)
)}
- )}
+ ) : null}
);
-};
+}
diff --git a/packages/ee/multiLanguage/components/LanguageRow.tsx b/packages/ee/multi-language/components/language-row.tsx
similarity index 60%
rename from packages/ee/multiLanguage/components/LanguageRow.tsx
rename to packages/ee/multi-language/components/language-row.tsx
index 30e2cb552a..ed840625c1 100644
--- a/packages/ee/multiLanguage/components/LanguageRow.tsx
+++ b/packages/ee/multi-language/components/language-row.tsx
@@ -1,8 +1,8 @@
-import { TLanguage } from "@formbricks/types/product";
+import type { TLanguage } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
-import { LanguageSelect } from "./LanguageSelect";
+import { LanguageSelect } from "./language-select";
interface LanguageRowProps {
language: TLanguage;
@@ -12,26 +12,28 @@ interface LanguageRowProps {
onDelete: () => void;
}
-export const LanguageRow = ({ language, isEditing, onLanguageChange, onDelete }: LanguageRowProps) => {
+export function LanguageRow({ language, isEditing, onLanguageChange, onDelete }: LanguageRowProps) {
return (
{
+ onLanguageChange({ ...language, alias: e.target.value });
+ }}
placeholder="e.g. en_us"
- onChange={(e) => onLanguageChange({ ...language, alias: e.target.value })}
+ value={language.alias || ""}
/>
- {language.id !== "new" && isEditing && (
-
+ {language.id !== "new" && isEditing ? (
+
Remove
- )}
+ ) : null}
);
-};
+}
diff --git a/packages/ee/multiLanguage/components/LanguageSelect.tsx b/packages/ee/multi-language/components/language-select.tsx
similarity index 75%
rename from packages/ee/multiLanguage/components/LanguageSelect.tsx
rename to packages/ee/multi-language/components/language-select.tsx
index 8bb3a9b6ba..3cb4930eff 100644
--- a/packages/ee/multiLanguage/components/LanguageSelect.tsx
+++ b/packages/ee/multi-language/components/language-select.tsx
@@ -2,11 +2,12 @@ import { ChevronDown } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
-import { TLanguage } from "@formbricks/types/product";
+import type { TLanguage } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
-import { TIso639Language, iso639Languages } from "../lib/isoLanguages";
+import type { TIso639Language } from "../lib/iso-languages";
+import { iso639Languages } from "../lib/iso-languages";
interface LanguageSelectProps {
language: TLanguage;
@@ -14,7 +15,7 @@ interface LanguageSelectProps {
disabled: boolean;
}
-export const LanguageSelect = ({ language, onLanguageChange, disabled }: LanguageSelectProps) => {
+export function LanguageSelect({ language, onLanguageChange, disabled }: LanguageSelectProps) {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [selectedOption, setSelectedOption] = useState(
@@ -29,11 +30,13 @@ export const LanguageSelect = ({ language, onLanguageChange, disabled }: Languag
setIsOpen(!isOpen);
};
- useClickOutside(languageSelectRef, () => setIsOpen(false));
+ useClickOutside(languageSelectRef, () => {
+ setIsOpen(false);
+ });
const handleOptionSelect = (option: TIso639Language) => {
setSelectedOption(option);
- onLanguageChange({ ...language, code: option?.alpha2 || "" });
+ onLanguageChange({ ...language, code: option.alpha2 || "" });
setIsOpen(false);
};
@@ -49,29 +52,33 @@ export const LanguageSelect = ({ language, onLanguageChange, disabled }: Languag
return (
+ variant="minimal">
{selectedOption?.english ?? "Select"}
setSearchTerm(e.target.value)}
autoComplete="off"
+ onChange={(e) => {
+ setSearchTerm(e.target.value);
+ }}
+ placeholder="Search items"
ref={inputRef}
+ type="text"
+ value={searchTerm}
/>
{filteredItems.map((item, index) => (
handleOptionSelect(item)}
- className="block cursor-pointer rounded-md px-4 py-2 text-gray-700 hover:bg-gray-100 active:bg-blue-100">
+ onClick={() => {
+ handleOptionSelect(item);
+ }}>
{item.english}
))}
@@ -79,4 +86,4 @@ export const LanguageSelect = ({ language, onLanguageChange, disabled }: Languag
);
-};
+}
diff --git a/packages/ee/multiLanguage/components/LanguageSwitch.tsx b/packages/ee/multi-language/components/language-switch.tsx
similarity index 90%
rename from packages/ee/multiLanguage/components/LanguageSwitch.tsx
rename to packages/ee/multi-language/components/language-switch.tsx
index e47d87f31e..5074a1cae1 100644
--- a/packages/ee/multiLanguage/components/LanguageSwitch.tsx
+++ b/packages/ee/multi-language/components/language-switch.tsx
@@ -2,7 +2,7 @@
import { ChevronDownIcon } from "lucide-react";
-import { TSurveyLanguage } from "@formbricks/types/surveys";
+import type { TSurveyLanguage } from "@formbricks/types/surveys";
import {
DropdownMenu,
DropdownMenuContent,
@@ -10,22 +10,22 @@ import {
DropdownMenuTrigger,
} from "@formbricks/ui/DropdownMenu";
-import { getLanguageLabel } from "../lib/isoLanguages";
+import { getLanguageLabel } from "../lib/iso-languages";
interface LanguageSwitchProps {
surveyLanguages: TSurveyLanguage[];
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
}
-export const LanguageSwitch = ({
+export function LanguageSwitch({
surveyLanguages,
selectedLanguageCode,
setSelectedLanguageCode,
-}: LanguageSwitchProps) => {
+}: LanguageSwitchProps) {
if (selectedLanguageCode === "default") {
selectedLanguageCode =
surveyLanguages.find((surveyLanguage) => {
- return surveyLanguage.default === true;
+ return surveyLanguage.default;
})?.language.code ?? "default";
}
return (
@@ -45,14 +45,15 @@ export const LanguageSwitch = ({
{surveyLanguages.length > 0 ? (
surveyLanguages.map((surveyLanguage) => (
{
setSelectedLanguageCode(surveyLanguage.language.code);
}}>
+ className={`h-4 w-4 rounded-full border ${surveyLanguage.language.code === selectedLanguageCode ? "bg-brand-dark outline-brand-dark border-black outline" : "border-white"}`}
+ />
{getLanguageLabel(surveyLanguage.language.code)}
@@ -64,4 +65,4 @@ export const LanguageSwitch = ({
);
-};
+}
diff --git a/packages/ee/multiLanguage/components/LanguageToggle.tsx b/packages/ee/multi-language/components/language-toggle.tsx
similarity index 69%
rename from packages/ee/multiLanguage/components/LanguageToggle.tsx
rename to packages/ee/multi-language/components/language-toggle.tsx
index c188da70d4..1023c0afdb 100644
--- a/packages/ee/multiLanguage/components/LanguageToggle.tsx
+++ b/packages/ee/multi-language/components/language-toggle.tsx
@@ -1,8 +1,8 @@
-import { TLanguage } from "@formbricks/types/product";
+import type { TLanguage } from "@formbricks/types/product";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
-import { getLanguageLabel } from "../lib/isoLanguages";
+import { getLanguageLabel } from "../lib/iso-languages";
interface LanguageToggleProps {
language: TLanguage;
@@ -11,27 +11,27 @@ interface LanguageToggleProps {
onEdit: () => void;
}
-export const LanguageToggle = ({ language, isChecked, onToggle, onEdit }: LanguageToggleProps) => {
+export function LanguageToggle({ language, isChecked, onToggle, onEdit }: LanguageToggleProps) {
return (
{
e.stopPropagation();
onToggle();
}}
/>
-
);
-};
+}
diff --git a/packages/ee/multiLanguage/components/LocalizedEditor.tsx b/packages/ee/multi-language/components/localized-editor.tsx
similarity index 80%
rename from packages/ee/multiLanguage/components/LocalizedEditor.tsx
rename to packages/ee/multi-language/components/localized-editor.tsx
index d080a6ecfd..3720f31cc4 100644
--- a/packages/ee/multiLanguage/components/LocalizedEditor.tsx
+++ b/packages/ee/multi-language/components/localized-editor.tsx
@@ -5,10 +5,10 @@ import { useMemo } from "react";
import { extractLanguageCodes, isLabelValidForAllLanguages } from "@formbricks/lib/i18n/utils";
import { md } from "@formbricks/lib/markdownIt";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
-import { TI18nString, TSurvey } from "@formbricks/types/surveys";
+import type { TI18nString, TSurvey } from "@formbricks/types/surveys";
import { Editor } from "@formbricks/ui/Editor";
-import { LanguageIndicator } from "./LanguageIndicator";
+import { LanguageIndicator } from "./language-indicator";
interface LocalizedEditorProps {
id: string;
@@ -31,11 +31,11 @@ const checkIfValueIsIncomplete = (
) => {
const labelIds = ["subheader"];
if (value === undefined) return false;
- const isDefaultIncomplete = labelIds.includes(id) ? value["default"]?.trim() !== "" : false;
+ const isDefaultIncomplete = labelIds.includes(id) ? value.default.trim() !== "" : false;
return isInvalid && !isLabelValidForAllLanguages(value, surveyLanguageCodes) && isDefaultIncomplete;
};
-export const LocalizedEditor = ({
+export function LocalizedEditor({
id,
value,
localSurvey,
@@ -46,24 +46,28 @@ export const LocalizedEditor = ({
questionIdx,
firstRender,
setFirstRender,
-}: LocalizedEditorProps) => {
+}: LocalizedEditorProps) {
const surveyLanguageCodes = useMemo(
() => extractLanguageCodes(localSurvey.languages),
[localSurvey.languages]
);
const isInComplete = useMemo(
() => checkIfValueIsIncomplete(id, isInvalid, surveyLanguageCodes, value),
- [id, isInvalid, surveyLanguageCodes, value, selectedLanguageCode]
+ [id, isInvalid, surveyLanguageCodes, value]
);
return (
md.render(value ? value[selectedLanguageCode] ?? "" : "")}
+ key={`${questionIdx}-${selectedLanguageCode}`}
+ setFirstRender={setFirstRender}
setText={(v: string) => {
if (!value) return;
- let translatedHtml = {
+ const translatedHtml = {
...value,
[selectedLanguageCode]: v,
};
@@ -74,36 +78,35 @@ export const LocalizedEditor = ({
}
updateQuestion(questionIdx, { html: translatedHtml });
}}
- excludedToolbarItems={["blockType"]}
- disableLists
- firstRender={firstRender}
- setFirstRender={setFirstRender}
/>
- {localSurvey.languages?.length > 1 && (
+ {localSurvey.languages.length > 1 && (
- {value && selectedLanguageCode !== "default" && value["default"] && (
+ {value && selectedLanguageCode !== "default" && value.default ? (
Translate:
+ }}
+ />
- )}
+ ) : null}
)}
- {isInComplete && Contains Incomplete translations
}
+ {isInComplete ? (
+ Contains Incomplete translations
+ ) : null}
);
-};
+}
diff --git a/packages/ee/multiLanguage/components/MultiLanguageCard.tsx b/packages/ee/multi-language/components/multi-language-card.tsx
similarity index 89%
rename from packages/ee/multiLanguage/components/MultiLanguageCard.tsx
rename to packages/ee/multi-language/components/multi-language-card.tsx
index c8bdde7792..4efd3b4944 100644
--- a/packages/ee/multiLanguage/components/MultiLanguageCard.tsx
+++ b/packages/ee/multi-language/components/multi-language-card.tsx
@@ -3,21 +3,23 @@
import * as Collapsible from "@radix-ui/react-collapsible";
import { ArrowUpRight, Languages } from "lucide-react";
import Link from "next/link";
-import { FC, useState } from "react";
+import type { FC } from "react";
+import { useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { extractLanguageCodes, translateSurvey } from "@formbricks/lib/i18n/utils";
-import { TLanguage, TProduct } from "@formbricks/types/product";
-import { TSurvey, TSurveyLanguage, ZSurvey } from "@formbricks/types/surveys";
+import type { TLanguage, TProduct } from "@formbricks/types/product";
+import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys";
+import { ZSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { ConfirmationModal } from "@formbricks/ui/ConfirmationModal";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
-import { DefaultLanguageSelect } from "./DefaultLanguageSelect";
-import { SecondaryLanguageSelect } from "./SecondaryLanguageSelect";
+import { DefaultLanguageSelect } from "./default-language-select";
+import { SecondaryLanguageSelect } from "./secondary-language-select";
interface MultiLanguageCardProps {
localSurvey: TSurvey;
@@ -50,8 +52,8 @@ export const MultiLanguageCard: FC = ({
setSelectedLanguageCode,
}) => {
const environmentId = localSurvey.environmentId;
- const open = activeQuestionId == "multiLanguage";
- const [isMultiLanguageActivated, setIsMultiLanguageActivated] = useState(localSurvey.languages?.length > 1);
+ const open = activeQuestionId === "multiLanguage";
+ const [isMultiLanguageActivated, setIsMultiLanguageActivated] = useState(localSurvey.languages.length > 1);
const [confirmationModalInfo, setConfirmationModalInfo] = useState({
title: "",
open: false,
@@ -61,8 +63,8 @@ export const MultiLanguageCard: FC = ({
});
const [defaultLanguage, setDefaultLanguage] = useState(
- localSurvey.languages?.find((language) => {
- return language.default === true;
+ localSurvey.languages.find((language) => {
+ return language.default;
})?.language
);
@@ -121,13 +123,12 @@ export const MultiLanguageCard: FC = ({
// Update all languages and check if the new default language already exists
const newLanguages =
- localSurvey.languages?.map((lang) => {
+ localSurvey.languages.map((lang) => {
if (lang.language.code === language.code) {
languageExists = true;
return { ...lang, default: true };
- } else {
- return { ...lang, default: false };
}
+ return { ...lang, default: false };
}) ?? [];
if (!languageExists) {
@@ -147,7 +148,7 @@ export const MultiLanguageCard: FC = ({
const handleActivationSwitchLogic = () => {
if (isMultiLanguageActivated) {
- if (localSurvey.languages?.length > 0) {
+ if (localSurvey.languages.length > 0) {
setConfirmationModalInfo({
open: true,
title: "Remove translations",
@@ -185,9 +186,9 @@ export const MultiLanguageCard: FC = ({
+ open={open}>
@@ -202,12 +203,12 @@ export const MultiLanguageCard: FC = ({
{isMultiLanguageActivated ? "On" : "Off"}
{
handleActivationSwitchLogic();
}}
- disabled={!isMultiLanguageAllowed || product.languages.length === 0}
/>
@@ -217,14 +218,14 @@ export const MultiLanguageCard: FC
= ({
{!isMultiLanguageAllowed && !isFormbricksCloud && !isMultiLanguageActivated ? (
) : !isMultiLanguageAllowed && isFormbricksCloud && !isMultiLanguageActivated ? (
) : (
<>
@@ -238,14 +239,14 @@ export const MultiLanguageCard: FC = ({
{product.languages.length > 1 && (
- {isMultiLanguageAllowed && !isMultiLanguageActivated && (
+ {isMultiLanguageAllowed && !isMultiLanguageActivated ? (
Switch multi-lanugage on to get started ๐
- )}
+ ) : null}
- {isMultiLanguageActivated && (
+ {isMultiLanguageActivated ? (
= ({
product={product}
setConfirmationModalInfo={setConfirmationModalInfo}
/>
- {defaultLanguage && (
+ {defaultLanguage ? (
- )}
+ ) : null}
- )}
+ ) : null}
)}
-
+
Manage Languages
@@ -277,13 +278,15 @@ export const MultiLanguageCard: FC = ({
)}
setConfirmationModalInfo((prev) => ({ ...prev, open: !prev.open }))}
- text={confirmationModalInfo.text}
- onConfirm={confirmationModalInfo.onConfirm}
buttonText={confirmationModalInfo.buttonText}
buttonVariant={confirmationModalInfo.buttonVariant}
+ onConfirm={confirmationModalInfo.onConfirm}
+ open={confirmationModalInfo.open}
+ setOpen={() => {
+ setConfirmationModalInfo((prev) => ({ ...prev, open: !prev.open }));
+ }}
+ text={confirmationModalInfo.text}
+ title={confirmationModalInfo.title}
/>
diff --git a/packages/ee/multiLanguage/components/SecondaryLanguageSelect.tsx b/packages/ee/multi-language/components/secondary-language-select.tsx
similarity index 74%
rename from packages/ee/multiLanguage/components/SecondaryLanguageSelect.tsx
rename to packages/ee/multi-language/components/secondary-language-select.tsx
index f5f700a981..6d85e3b6dc 100644
--- a/packages/ee/multiLanguage/components/SecondaryLanguageSelect.tsx
+++ b/packages/ee/multi-language/components/secondary-language-select.tsx
@@ -1,9 +1,9 @@
-import { TLanguage, TProduct } from "@formbricks/types/product";
-import { TSurvey } from "@formbricks/types/surveys";
+import type { TLanguage, TProduct } from "@formbricks/types/product";
+import type { TSurvey } from "@formbricks/types/surveys";
-import { LanguageToggle } from "./LanguageToggle";
+import { LanguageToggle } from "./language-toggle";
-interface secondaryLanguageSelectProps {
+interface SecondaryLanguageSelectProps {
product: TProduct;
defaultLanguage: TLanguage;
setSelectedLanguageCode: (languageCode: string) => void;
@@ -12,14 +12,14 @@ interface secondaryLanguageSelectProps {
updateSurveyLanguages: (language: TLanguage) => void;
}
-export const SecondaryLanguageSelect = ({
+export function SecondaryLanguageSelect({
product,
defaultLanguage,
setSelectedLanguageCode,
setActiveQuestionId,
localSurvey,
updateSurveyLanguages,
-}: secondaryLanguageSelectProps) => {
+}: SecondaryLanguageSelectProps) {
const isLanguageToggled = (language: TLanguage) => {
return localSurvey.languages.some(
(surveyLanguage) => surveyLanguage.language.code === language.code && surveyLanguage.enabled
@@ -33,16 +33,18 @@ export const SecondaryLanguageSelect = ({
.filter((lang) => lang.id !== defaultLanguage.id)
.map((language) => (
updateSurveyLanguages(language)}
onEdit={() => {
setSelectedLanguageCode(language.code);
setActiveQuestionId(localSurvey.questions[0]?.id);
}}
+ onToggle={() => {
+ updateSurveyLanguages(language);
+ }}
/>
))}
);
-};
+}
diff --git a/packages/ee/multiLanguage/lib/actions.ts b/packages/ee/multi-language/lib/actions.ts
similarity index 97%
rename from packages/ee/multiLanguage/lib/actions.ts
rename to packages/ee/multi-language/lib/actions.ts
index 27c84e9440..8bbeb7ece9 100644
--- a/packages/ee/multiLanguage/lib/actions.ts
+++ b/packages/ee/multi-language/lib/actions.ts
@@ -12,7 +12,7 @@ import {
import { canUserAccessProduct, verifyUserRoleAccess } from "@formbricks/lib/product/auth";
import { getProduct } from "@formbricks/lib/product/service";
import { AuthorizationError } from "@formbricks/types/errors";
-import { TLanguageInput } from "@formbricks/types/product";
+import type { TLanguageInput } from "@formbricks/types/product";
export const createLanguageAction = async (
productId: string,
diff --git a/packages/ee/multiLanguage/lib/isoLanguages.ts b/packages/ee/multi-language/lib/iso-languages.ts
similarity index 100%
rename from packages/ee/multiLanguage/lib/isoLanguages.ts
rename to packages/ee/multi-language/lib/iso-languages.ts
diff --git a/packages/ee/package.json b/packages/ee/package.json
index b11fe619aa..b71fcf8a68 100644
--- a/packages/ee/package.json
+++ b/packages/ee/package.json
@@ -7,18 +7,31 @@
"version": "1.0.0",
"main": "index.ts",
"scripts": {
- "clean": "rimraf node_modules .turbo"
+ "clean": "rimraf node_modules .turbo",
+ "lint": "eslint --ext .ts,.tsx --fix ."
},
"devDependencies": {
- "@formbricks/lib": "*",
"@formbricks/config-typescript": "*",
+ "@formbricks/eslint-config": "workspace:*",
+ "@formbricks/lib": "*",
"@formbricks/types": "*",
"@formbricks/ui": "*",
- "@formbricks/eslint-config": "workspace:*"
+ "@types/dompurify": "^3.0.5",
+ "@types/react": "18.3.3"
},
"dependencies": {
+ "@formbricks/database": "workspace:*",
"@formbricks/lib": "workspace:*",
+ "@paralleldrive/cuid2": "^2.2.2",
+ "@radix-ui/react-collapsible": "^1.0.3",
"axios": "^1.7.2",
- "stripe": "^15.8.0"
+ "lucide-react": "^0.390.0",
+ "next": "^14.2.3",
+ "next-auth": "^4.24.7",
+ "react-hook-form": "^7.51.5",
+ "react-hot-toast": "^2.4.1",
+ "server-only": "^0.0.1",
+ "stripe": "^15.8.0",
+ "zod": "^3.23.8"
}
}
diff --git a/packages/ee/RoleManagement/components/AddMemberRole.tsx b/packages/ee/role-management/components/add-member-role.tsx
similarity index 73%
rename from packages/ee/RoleManagement/components/AddMemberRole.tsx
rename to packages/ee/role-management/components/add-member-role.tsx
index 7b94d49e2d..abea27025c 100644
--- a/packages/ee/RoleManagement/components/AddMemberRole.tsx
+++ b/packages/ee/role-management/components/add-member-role.tsx
@@ -1,4 +1,5 @@
-import { Control, Controller } from "react-hook-form";
+import type { Control } from "react-hook-form";
+import { Controller } from "react-hook-form";
import { Label } from "@formbricks/ui/Label";
import {
@@ -17,24 +18,26 @@ enum MembershipRole {
Viewer = "viewer",
}
-type AddMemberRoleProps = {
- control: Control<{ name: string; email: string; role: MembershipRole }, any>;
+interface AddMemberRoleProps {
+ control: Control<{ name: string; email: string; role: MembershipRole }>;
canDoRoleManagement: boolean;
-};
+}
-export const AddMemberRole = ({ control, canDoRoleManagement }: AddMemberRoleProps) => {
+export function AddMemberRole({ control, canDoRoleManagement }: AddMemberRoleProps) {
return (
(
Role
Weekly Report for {productName}
- {getNotificationHeaderimePeriod(startDate, endDate, startYear, endYear)}
+ {getNotificationHeaderimePeriod()}
);
-};
+}
diff --git a/packages/email/components/weekly-summary/NotificationInsight.tsx b/packages/email/components/weekly-summary/notification-insight.tsx
similarity index 90%
rename from packages/email/components/weekly-summary/NotificationInsight.tsx
rename to packages/email/components/weekly-summary/notification-insight.tsx
index 64cf86f42d..da1fd22293 100644
--- a/packages/email/components/weekly-summary/NotificationInsight.tsx
+++ b/packages/email/components/weekly-summary/notification-insight.tsx
@@ -1,13 +1,12 @@
import { Column, Container, Row, Section, Text } from "@react-email/components";
import React from "react";
-
-import { TWeeklySummaryInsights } from "@formbricks/types/weeklySummary";
+import type { TWeeklySummaryInsights } from "@formbricks/types/weeklySummary";
interface NotificationInsightProps {
insights: TWeeklySummaryInsights;
}
-export const NotificationInsight = ({ insights }: NotificationInsightProps) => {
+export function NotificationInsight({ insights }: NotificationInsightProps) {
return (
@@ -40,4 +39,4 @@ export const NotificationInsight = ({ insights }: NotificationInsightProps) => {
);
-};
+}
diff --git a/packages/email/components/weekly-summary/WeeklySummaryNotificationEmail.tsx b/packages/email/components/weekly-summary/weekly-summary-notification-email.tsx
similarity index 64%
rename from packages/email/components/weekly-summary/WeeklySummaryNotificationEmail.tsx
rename to packages/email/components/weekly-summary/weekly-summary-notification-email.tsx
index 8a2d94f88d..8fae32a78d 100644
--- a/packages/email/components/weekly-summary/WeeklySummaryNotificationEmail.tsx
+++ b/packages/email/components/weekly-summary/weekly-summary-notification-email.tsx
@@ -1,11 +1,9 @@
import React from "react";
-
-import { TWeeklySummaryNotificationResponse } from "@formbricks/types/weeklySummary";
-
-import { LiveSurveyNotification } from "./LiveSurveyNotification";
-import { NotificationFooter } from "./NotificationFooter";
-import { NotificationHeader } from "./NotificationHeader";
-import { NotificationInsight } from "./NotificationInsight";
+import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weeklySummary";
+import { LiveSurveyNotification } from "./live-survey-notification";
+import { NotificationFooter } from "./notification-footer";
+import { NotificationHeader } from "./notification-header";
+import { NotificationInsight } from "./notification-insight";
interface WeeklySummaryNotificationEmailProps {
notificationData: TWeeklySummaryNotificationResponse;
@@ -15,28 +13,28 @@ interface WeeklySummaryNotificationEmailProps {
endYear: number;
}
-export const WeeklySummaryNotificationEmail = ({
+export function WeeklySummaryNotificationEmail({
notificationData,
startDate,
endDate,
startYear,
endYear,
-}: WeeklySummaryNotificationEmailProps) => {
+}: WeeklySummaryNotificationEmailProps) {
return (
);
-};
+}
diff --git a/packages/email/index.tsx b/packages/email/index.tsx
index 76ea7f1828..d7ff89c67e 100644
--- a/packages/email/index.tsx
+++ b/packages/email/index.tsx
@@ -1,6 +1,6 @@
import { render } from "@react-email/render";
import nodemailer from "nodemailer";
-
+import type SMTPTransport from "nodemailer/lib/smtp-transport";
import {
DEBUG,
MAIL_FROM,
@@ -13,26 +13,25 @@ import {
} from "@formbricks/lib/constants";
import { createInviteToken, createToken, createTokenForLinkSurvey } from "@formbricks/lib/jwt";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
-import { TResponse } from "@formbricks/types/responses";
-import { TSurvey } from "@formbricks/types/surveys";
-import { TWeeklySummaryNotificationResponse } from "@formbricks/types/weeklySummary";
+import type { TResponse } from "@formbricks/types/responses";
+import type { TSurvey } from "@formbricks/types/surveys";
+import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weeklySummary";
+import { ForgotPasswordEmail } from "./components/auth/forgot-password-email";
+import { PasswordResetNotifyEmail } from "./components/auth/password-reset-notify-email";
+import { VerificationEmail } from "./components/auth/verification-email";
+import { EmailTemplate } from "./components/general/email-template";
+import { InviteAcceptedEmail } from "./components/invite/invite-accepted-email";
+import { InviteEmail } from "./components/invite/invite-email";
+import { OnboardingInviteEmail } from "./components/invite/onboarding-invite-email";
+import { EmbedSurveyPreviewEmail } from "./components/survey/embed-survey-preview-email";
+import { LinkSurveyEmail } from "./components/survey/link-survey-email";
+import { ResponseFinishedEmail } from "./components/survey/response-finished-email";
+import { NoLiveSurveyNotificationEmail } from "./components/weekly-summary/no-live-survey-notification-email";
+import { WeeklySummaryNotificationEmail } from "./components/weekly-summary/weekly-summary-notification-email";
-import { ForgotPasswordEmail } from "./components/auth/ForgotPasswordEmail";
-import { PasswordResetNotifyEmail } from "./components/auth/PasswordResetNotifyEmail";
-import { VerificationEmail } from "./components/auth/VerificationEmail";
-import { EmailTemplate } from "./components/general/EmailTemplate";
-import { InviteAcceptedEmail } from "./components/invite/InviteAcceptedEmail";
-import { InviteEmail } from "./components/invite/InviteEmail";
-import { OnboardingInviteEmail } from "./components/invite/OnboardingInviteEmail";
-import { EmbedSurveyPreviewEmail } from "./components/survey/EmbedSurveyPreviewEmail";
-import { LinkSurveyEmail } from "./components/survey/LinkSurveyEmail";
-import { ResponseFinishedEmail } from "./components/survey/ResponseFinishedEmail";
-import { NoLiveSurveyNotificationEmail } from "./components/weekly-summary/NoLiveSurveyNotificationEmail";
-import { WeeklySummaryNotificationEmail } from "./components/weekly-summary/WeeklySummaryNotificationEmail";
+export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT);
-export const IS_SMTP_CONFIGURED: boolean = SMTP_HOST && SMTP_PORT ? true : false;
-
-interface sendEmailData {
+interface SendEmailDataProps {
to: string;
replyTo?: string;
subject: string;
@@ -64,29 +63,26 @@ const getEmailSubject = (productName: string): string => {
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
-export const sendEmail = async (emailData: sendEmailData) => {
- try {
- if (IS_SMTP_CONFIGURED) {
- let transporter = nodemailer.createTransport({
- host: SMTP_HOST,
- port: SMTP_PORT,
- secure: SMTP_SECURE_ENABLED, // true for 465, false for other ports
- auth: {
- user: SMTP_USER,
- pass: SMTP_PASSWORD,
- },
- logger: DEBUG,
- debug: DEBUG,
- });
- const emailDefaults = {
- from: `Formbricks <${MAIL_FROM || "noreply@formbricks.com"}>`,
- };
- await transporter.sendMail({ ...emailDefaults, ...emailData });
- } else {
- console.error(`Could not Email :: SMTP not configured :: ${emailData.subject}`);
- }
- } catch (error) {
- throw error;
+export const sendEmail = async (emailData: SendEmailDataProps) => {
+ if (IS_SMTP_CONFIGURED) {
+ const transporter = nodemailer.createTransport({
+ host: SMTP_HOST,
+ port: SMTP_PORT,
+ secure: SMTP_SECURE_ENABLED, // true for 465, false for other ports
+ auth: {
+ user: SMTP_USER,
+ pass: SMTP_PASSWORD,
+ },
+ logger: DEBUG,
+ debug: DEBUG,
+ } as SMTPTransport.Options);
+ const emailDefaults = {
+ from: `Formbricks <${MAIL_FROM || "noreply@formbricks.com"}>`,
+ };
+ await transporter.sendMail({ ...emailDefaults, ...emailData });
+ } else {
+ // eslint-disable-next-line no-console -- necessary for logging email configuration errors
+ console.error(`Could not Email :: SMTP not configured :: ${emailData.subject}`);
}
};
@@ -202,8 +198,8 @@ export const sendEmbedSurveyPreviewEmail = async (
environmentId: string
) => {
await sendEmail({
- to: to,
- subject: subject,
+ to,
+ subject,
html: render(EmailTemplate({ content: EmbedSurveyPreviewEmail({ html, environmentId }) })),
});
};
@@ -212,7 +208,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: LinkSurveyEmailData) =
const surveyId = data.surveyId;
const email = data.email;
const surveyData = data.surveyData;
- const singleUseId = data.suId ?? null;
+ const singleUseId = data.suId;
const token = createTokenForLinkSurvey(surveyId, email);
const getSurveyLink = () => {
if (singleUseId) {
diff --git a/packages/email/package.json b/packages/email/package.json
index 5dc313cc48..3d1d39427a 100644
--- a/packages/email/package.json
+++ b/packages/email/package.json
@@ -4,16 +4,22 @@
"description": "Email package",
"main": "./index.tsx",
"scripts": {
- "clean": "rimraf .turbo node_modules dist"
+ "clean": "rimraf .turbo node_modules dist",
+ "lint": "eslint --ext .ts,.tsx --fix ."
},
"dependencies": {
+ "@formbricks/config-typescript": "workspace:*",
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@react-email/components": "^0.0.19",
"@react-email/render": "^0.0.15",
- "lucide-react": "^0.379.0",
+ "lucide-react": "^0.390.0",
"nodemailer": "^6.9.13",
"react-email": "^2.1.4"
+ },
+ "devDependencies": {
+ "@types/nodemailer": "^6.4.15",
+ "@types/react": "18.3.3"
}
}
diff --git a/packages/email/tsconfig.json b/packages/email/tsconfig.json
new file mode 100644
index 0000000000..82881bf911
--- /dev/null
+++ b/packages/email/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "@formbricks/config-typescript/nextjs.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["/*"]
+ },
+ "resolveJsonModule": true,
+ "strictNullChecks": true
+ },
+ "include": [".", "../../packages/types/*.d.ts"],
+ "exclude": ["dist", "build", "node_modules"]
+}
diff --git a/packages/lib/i18n/i18n.mock.ts b/packages/lib/i18n/i18n.mock.ts
index 457b1725d3..7fbff9491d 100644
--- a/packages/lib/i18n/i18n.mock.ts
+++ b/packages/lib/i18n/i18n.mock.ts
@@ -1,7 +1,5 @@
import { mockSegment } from "segment/tests/__mocks__/segment.mock";
-
import { mockSurveyLanguages } from "survey/tests/__mock__/survey.mock";
-
import {
TSurvey,
TSurveyCTAQuestion,
@@ -13,7 +11,7 @@ import {
TSurveyNPSQuestion,
TSurveyOpenTextQuestion,
TSurveyPictureSelectionQuestion,
- TSurveyQuestionType,
+ TSurveyQuestionTypeEnum,
TSurveyRatingQuestion,
TSurveyThankYouCard,
TSurveyWelcomeCard,
@@ -34,7 +32,7 @@ export const mockWelcomeCard: TSurveyWelcomeCard = {
export const mockOpenTextQuestion: TSurveyOpenTextQuestion = {
id: "lqht9sj5s6andjkmr9k1n54q",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: {
default: "What would you like to know?",
},
@@ -51,7 +49,7 @@ export const mockOpenTextQuestion: TSurveyOpenTextQuestion = {
export const mockSingleSelectQuestion: TSurveyMultipleChoiceQuestion = {
id: "mvqx8t90np6isb6oel9eamzc",
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
choices: [
{
id: "r52sul8ag19upaicit0fyqzo",
@@ -104,7 +102,7 @@ export const mockMultiSelectQuestion: TSurveyMultipleChoiceQuestion = {
],
shuffleOption: "none",
id: "cpydxgsmjg8q9iwfa8wj4ida",
- type: TSurveyQuestionType.MultipleChoiceMulti,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
isDraft: true,
};
@@ -128,7 +126,7 @@ export const mockPictureSelectQuestion: TSurveyPictureSelectionQuestion = {
},
],
id: "a8monbe8hq0mivh3irfhd3i5",
- type: TSurveyQuestionType.PictureSelection,
+ type: TSurveyQuestionTypeEnum.PictureSelection,
isDraft: true,
};
@@ -149,7 +147,7 @@ export const mockRatingQuestion: TSurveyRatingQuestion = {
default: "Very good",
},
id: "waldsboahjtgqhg5p18d1awz",
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
isDraft: true,
};
@@ -165,7 +163,7 @@ export const mockNpsQuestion: TSurveyNPSQuestion = {
default: "Extremely likely",
},
id: "m9pemgdih2p4exvkmeeqq6jf",
- type: TSurveyQuestionType.NPS,
+ type: TSurveyQuestionTypeEnum.NPS,
isDraft: true,
};
@@ -182,7 +180,7 @@ export const mockCtaQuestion: TSurveyCTAQuestion = {
default: "Skip",
},
id: "gwn15urom4ffnhfimwbz3vgc",
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
isDraft: true,
};
@@ -195,7 +193,7 @@ export const mockConsentQuestion: TSurveyConsentQuestion = {
default: "I agree to the terms and conditions",
},
id: "av561aoif3i2hjlsl6krnsfm",
- type: TSurveyQuestionType.Consent,
+ type: TSurveyQuestionTypeEnum.Consent,
isDraft: true,
};
@@ -206,7 +204,7 @@ export const mockDateQuestion: TSurveyDateQuestion = {
},
format: "M-d-y",
id: "ts2f6v2oo9jfmfli9kk6lki9",
- type: TSurveyQuestionType.Date,
+ type: TSurveyQuestionTypeEnum.Date,
isDraft: true,
};
@@ -217,7 +215,7 @@ export const mockFileUploadQuestion: TSurveyFileUploadQuestion = {
},
allowMultipleFiles: false,
id: "ozzxo2jj1s6mj56c79q8pbef",
- type: TSurveyQuestionType.FileUpload,
+ type: TSurveyQuestionTypeEnum.FileUpload,
isDraft: true,
};
@@ -231,7 +229,7 @@ export const mockCalQuestion: TSurveyCalQuestion = {
},
calUserName: "rick/get-rick-rolled",
id: "o3bnux6p42u9ew9d02l14r26",
- type: TSurveyQuestionType.Cal,
+ type: TSurveyQuestionTypeEnum.Cal,
isDraft: true,
};
diff --git a/packages/lib/response/tests/__mocks__/data.mock.ts b/packages/lib/response/tests/__mocks__/data.mock.ts
index fce5f7ec87..fb5d48a693 100644
--- a/packages/lib/response/tests/__mocks__/data.mock.ts
+++ b/packages/lib/response/tests/__mocks__/data.mock.ts
@@ -1,11 +1,9 @@
import { Prisma } from "@prisma/client";
import { isAfter, isBefore, isSameDay } from "date-fns";
-
import { TDisplay } from "@formbricks/types/displays";
import { TResponse, TResponseFilterCriteria, TResponseUpdateInput } from "@formbricks/types/responses";
-import { TSurveyQuestionType } from "@formbricks/types/surveys";
+import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
-
import { responseNoteSelect } from "../../../responseNote/service";
import { responseSelection } from "../../service";
import { constantsForTests } from "../constants";
@@ -381,7 +379,7 @@ export const mockSurveySummaryOutput = {
id: "ars2tjk8hsi8oqk1uac00mo8",
inputType: "text",
required: false,
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
},
responseCount: 0,
samples: [],
diff --git a/packages/lib/response/utils.ts b/packages/lib/response/utils.ts
index 208d42e4fe..380eb2e7e4 100644
--- a/packages/lib/response/utils.ts
+++ b/packages/lib/response/utils.ts
@@ -1,7 +1,5 @@
import "server-only";
-
import { Prisma } from "@prisma/client";
-
import {
TResponse,
TResponseFilterCriteria,
@@ -22,10 +20,9 @@ import {
TSurveyQuestionSummaryOpenText,
TSurveyQuestionSummaryPictureSelection,
TSurveyQuestionSummaryRating,
- TSurveyQuestionType,
+ TSurveyQuestionTypeEnum,
TSurveySummary,
} from "@formbricks/types/surveys";
-
import { getLocalizedValue } from "../i18n/utils";
import { processResponseData } from "../responses";
import { getTodaysDateTimeFormatted } from "../time";
@@ -701,7 +698,7 @@ export const getQuestionWiseSummary = (
survey.questions.forEach((question, idx) => {
switch (question.type) {
- case TSurveyQuestionType.OpenText: {
+ case TSurveyQuestionTypeEnum.OpenText: {
let values: TSurveyQuestionSummaryOpenText["samples"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
@@ -726,8 +723,8 @@ export const getQuestionWiseSummary = (
values = [];
break;
}
- case TSurveyQuestionType.MultipleChoiceSingle:
- case TSurveyQuestionType.MultipleChoiceMulti: {
+ case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
+ case TSurveyQuestionTypeEnum.MultipleChoiceMulti: {
let values: TSurveyQuestionSummaryMultipleChoice["choices"] = [];
// check last choice is others or not
const lastChoice = question.choices[question.choices.length - 1];
@@ -808,7 +805,7 @@ export const getQuestionWiseSummary = (
values = [];
break;
}
- case TSurveyQuestionType.PictureSelection: {
+ case TSurveyQuestionTypeEnum.PictureSelection: {
let values: TSurveyQuestionSummaryPictureSelection["choices"] = [];
const choiceCountMap: Record
= {};
@@ -849,7 +846,7 @@ export const getQuestionWiseSummary = (
values = [];
break;
}
- case TSurveyQuestionType.Rating: {
+ case TSurveyQuestionTypeEnum.Rating: {
let values: TSurveyQuestionSummaryRating["choices"] = [];
const choiceCountMap: Record = {};
const range = question.range;
@@ -899,7 +896,7 @@ export const getQuestionWiseSummary = (
values = [];
break;
}
- case TSurveyQuestionType.NPS: {
+ case TSurveyQuestionTypeEnum.NPS: {
const data = {
promoters: 0,
passives: 0,
@@ -956,7 +953,7 @@ export const getQuestionWiseSummary = (
});
break;
}
- case TSurveyQuestionType.CTA: {
+ case TSurveyQuestionTypeEnum.CTA: {
const data = {
clicked: 0,
dismissed: 0,
@@ -988,7 +985,7 @@ export const getQuestionWiseSummary = (
});
break;
}
- case TSurveyQuestionType.Consent: {
+ case TSurveyQuestionTypeEnum.Consent: {
const data = {
accepted: 0,
dismissed: 0,
@@ -1023,7 +1020,7 @@ export const getQuestionWiseSummary = (
break;
}
- case TSurveyQuestionType.Date: {
+ case TSurveyQuestionTypeEnum.Date: {
let values: TSurveyQuestionSummaryDate["samples"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
@@ -1048,7 +1045,7 @@ export const getQuestionWiseSummary = (
values = [];
break;
}
- case TSurveyQuestionType.FileUpload: {
+ case TSurveyQuestionTypeEnum.FileUpload: {
let values: TSurveyQuestionSummaryFileUpload["files"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
@@ -1073,7 +1070,7 @@ export const getQuestionWiseSummary = (
values = [];
break;
}
- case TSurveyQuestionType.Cal: {
+ case TSurveyQuestionTypeEnum.Cal: {
const data = {
booked: 0,
skipped: 0,
@@ -1106,7 +1103,7 @@ export const getQuestionWiseSummary = (
break;
}
- case TSurveyQuestionType.Matrix: {
+ case TSurveyQuestionTypeEnum.Matrix: {
const rows = question.rows.map((row) => getLocalizedValue(row, "default"));
const columns = question.columns.map((column) => getLocalizedValue(column, "default"));
let totalResponseCount = 0;
@@ -1163,7 +1160,7 @@ export const getQuestionWiseSummary = (
});
break;
}
- case TSurveyQuestionType.Address: {
+ case TSurveyQuestionTypeEnum.Address: {
let values: TSurveyQuestionSummaryAddress["samples"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
diff --git a/packages/lib/segment/service.ts b/packages/lib/segment/service.ts
index 65cdb80477..e1025fc6a0 100644
--- a/packages/lib/segment/service.ts
+++ b/packages/lib/segment/service.ts
@@ -1,5 +1,4 @@
import { Prisma } from "@prisma/client";
-
import { prisma } from "@formbricks/database";
import { ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
@@ -23,7 +22,6 @@ import {
ZSegmentFilters,
ZSegmentUpdateInput,
} from "@formbricks/types/segment";
-
import {
getActionCountInLastMonth,
getActionCountInLastQuarter,
@@ -50,7 +48,7 @@ type PrismaSegment = Prisma.SegmentGetPayload<{
};
}>;
-export const selectSegment: Prisma.SegmentDefaultArgs["select"] = {
+export const selectSegment = {
id: true,
createdAt: true,
updatedAt: true,
@@ -62,6 +60,8 @@ export const selectSegment: Prisma.SegmentDefaultArgs["select"] = {
surveys: {
select: {
id: true,
+ name: true,
+ status: true,
},
},
};
diff --git a/packages/lib/survey/tests/__mock__/survey.mock.ts b/packages/lib/survey/tests/__mock__/survey.mock.ts
index 4b979b9945..51560a5642 100644
--- a/packages/lib/survey/tests/__mock__/survey.mock.ts
+++ b/packages/lib/survey/tests/__mock__/survey.mock.ts
@@ -1,5 +1,4 @@
import { Prisma } from "@prisma/client";
-
import { TActionClass } from "@formbricks/types/actionClasses";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TOrganization } from "@formbricks/types/organizations";
@@ -9,11 +8,10 @@ import {
TSurveyInput,
TSurveyLanguage,
TSurveyQuestion,
- TSurveyQuestionType,
+ TSurveyQuestionTypeEnum,
TSurveyWelcomeCard,
} from "@formbricks/types/surveys";
import { TUser } from "@formbricks/types/user";
-
import { selectPerson } from "../../../person/service";
import { selectSurvey } from "../../service";
@@ -147,7 +145,7 @@ export const mockAttributeClass: TAttributeClass = {
const mockQuestion: TSurveyQuestion = {
id: mockId,
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Question Text", de: "Fragetext" },
required: false,
inputType: "text",
diff --git a/packages/lib/templates.ts b/packages/lib/templates.ts
index 23fc907458..6759ccdda8 100644
--- a/packages/lib/templates.ts
+++ b/packages/lib/templates.ts
@@ -1,5 +1,4 @@
import { createId } from "@paralleldrive/cuid2";
-
import { TActionClass } from "@formbricks/types/actionClasses";
import {
TSurveyCTAQuestion,
@@ -7,7 +6,7 @@ import {
TSurveyHiddenFields,
TSurveyInput,
TSurveyOpenTextQuestion,
- TSurveyQuestionType,
+ TSurveyQuestionTypeEnum,
TSurveyStatus,
TSurveyThankYouCard,
TSurveyType,
@@ -54,7 +53,7 @@ export const testTemplate: TTemplate = {
questions: [
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter some text:" },
required: true,
@@ -63,7 +62,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter some text:" },
required: false,
@@ -73,7 +72,7 @@ export const testTemplate: TTemplate = {
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter an email" },
required: true,
@@ -82,7 +81,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter an email" },
required: false,
@@ -91,7 +90,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter a number" },
required: true,
@@ -100,7 +99,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter a number" },
required: false,
@@ -109,7 +108,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter a phone number" },
required: true,
@@ -118,7 +117,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter a phone number" },
required: false,
@@ -127,7 +126,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter a url" },
required: true,
@@ -136,7 +135,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "This is an open text question" },
subheader: { default: "Please enter a url" },
required: false,
@@ -145,7 +144,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "This ia a Multiple choice Single question" },
subheader: { default: "Please select one of the following" },
required: true,
@@ -163,7 +162,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "This ia a Multiple choice Single question" },
subheader: { default: "Please select one of the following" },
required: false,
@@ -181,7 +180,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceMulti,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
headline: { default: "This ia a Multiple choice Multiple question" },
subheader: { default: "Please select some from the following" },
required: true,
@@ -199,7 +198,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceMulti,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
headline: { default: "This ia a Multiple choice Multiple question" },
subheader: { default: "Please select some from the following" },
required: false,
@@ -217,7 +216,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "This is a rating question" },
required: true,
lowerLabel: { default: "Low" },
@@ -227,7 +226,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "This is a rating question" },
required: false,
lowerLabel: { default: "Low" },
@@ -237,7 +236,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "This is a rating question" },
required: true,
lowerLabel: { default: "Low" },
@@ -247,7 +246,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "This is a rating question" },
required: false,
lowerLabel: { default: "Low" },
@@ -257,7 +256,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "This is a rating question" },
required: true,
lowerLabel: { default: "Low" },
@@ -267,7 +266,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "This is a rating question" },
required: false,
lowerLabel: { default: "Low" },
@@ -278,7 +277,7 @@ export const testTemplate: TTemplate = {
{
id: createId(),
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: { default: "This is a CTA question" },
html: { default: "This is a test CTA" },
buttonLabel: { default: "Click" },
@@ -289,7 +288,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: { default: "This is a CTA question" },
html: { default: "This is a test CTA" },
buttonLabel: { default: "Click" },
@@ -300,7 +299,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.PictureSelection,
+ type: TSurveyQuestionTypeEnum.PictureSelection,
headline: { default: "This is a Picture select" },
allowMulti: true,
required: true,
@@ -317,7 +316,7 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.PictureSelection,
+ type: TSurveyQuestionTypeEnum.PictureSelection,
headline: { default: "This is a Picture select" },
allowMulti: true,
required: false,
@@ -334,14 +333,14 @@ export const testTemplate: TTemplate = {
},
{
id: createId(),
- type: TSurveyQuestionType.Consent,
+ type: TSurveyQuestionTypeEnum.Consent,
headline: { default: "This is a Consent question" },
required: true,
label: { default: "I agree to the terms and conditions" },
},
{
id: createId(),
- type: TSurveyQuestionType.Consent,
+ type: TSurveyQuestionTypeEnum.Consent,
headline: { default: "This is a Consent question" },
required: false,
label: { default: "I agree to the terms and conditions" },
@@ -366,7 +365,7 @@ export const templates: TTemplate[] = [
default:
'We would love to understand your user experience better. Sharing your insight helps a lot.
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
logic: [{ condition: "skipped", destination: "end" }],
headline: { default: "You are one of our power users! Do you have 5 minutes?" },
required: false,
@@ -376,7 +375,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "How disappointed would you be if you could no longer use {{productName}}?" },
subheader: { default: "Please select one of the following options:" },
required: true,
@@ -398,7 +397,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "What is your role?" },
subheader: { default: "Please select one of the following options:" },
required: true,
@@ -428,21 +427,21 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What type of people do you think would most benefit from {{productName}}?" },
required: true,
inputType: "text",
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What is the main benefit you receive from {{productName}}?" },
required: true,
inputType: "text",
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "How can we improve {{productName}} for you?" },
subheader: { default: "Please be as specific as possible." },
required: true,
@@ -462,7 +461,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "What is your role?" },
subheader: { default: "Please select one of the following options:" },
required: true,
@@ -492,7 +491,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "What's your company size?" },
subheader: { default: "Please select one of the following options:" },
required: true,
@@ -522,7 +521,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "How did you hear about us first?" },
subheader: { default: "Please select one of the following options:" },
required: true,
@@ -564,7 +563,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
logic: [
{ value: "Difficult to use", condition: "equals", destination: "sxwpskjgzzpmkgfxzi15inif" },
@@ -586,7 +585,7 @@ export const templates: TTemplate[] = [
},
{
id: "sxwpskjgzzpmkgfxzi15inif",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "What would have made {{productName}} easier to use?" },
required: true,
@@ -599,7 +598,7 @@ export const templates: TTemplate[] = [
default:
'We\'d love to keep you as a customer. Happy to offer a 30% discount for the next year.
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
logic: [{ condition: "clicked", destination: "end" }],
headline: { default: "Get 30% off for the next year!" },
required: true,
@@ -610,7 +609,7 @@ export const templates: TTemplate[] = [
},
{
id: "l054desub14syoie7n202vq4",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "What features are you missing?" },
required: true,
@@ -622,7 +621,7 @@ export const templates: TTemplate[] = [
default:
'We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
logic: [{ condition: "clicked", destination: "end" }],
headline: { default: "So sorry to hear ๐ Talk to our CEO directly!" },
required: true,
@@ -646,7 +645,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
logic: [{ value: "No", condition: "equals", destination: "duz2qp8eftix9wty1l221x1h" }],
shuffleOption: "none",
choices: [
@@ -658,7 +657,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "yhfew1j3ng6luy7t7qynwj79" }],
headline: { default: "Great to hear! Why did you recommend us?" },
required: true,
@@ -667,7 +666,7 @@ export const templates: TTemplate[] = [
},
{
id: "duz2qp8eftix9wty1l221x1h",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "So sad. Why not?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -675,7 +674,7 @@ export const templates: TTemplate[] = [
},
{
id: "yhfew1j3ng6luy7t7qynwj79",
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
logic: [{ value: "No", condition: "equals", destination: "end" }],
shuffleOption: "none",
choices: [
@@ -687,7 +686,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What made you discourage them?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -707,7 +706,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
logic: [
{
@@ -745,7 +744,7 @@ export const templates: TTemplate[] = [
},
{
id: "aew2ymg51mffnt9db7duz9t3",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "bqiyml1ym74ggx6htwdo7rlu" }],
headline: { default: "Sorry to hear. What was the biggest problem using {{productName}}?" },
required: true,
@@ -754,7 +753,7 @@ export const templates: TTemplate[] = [
},
{
id: "rnrfydttavtsf2t2nfx1df7m",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "bqiyml1ym74ggx6htwdo7rlu" }],
headline: { default: "What did you expect {{productName}} would do for you?" },
required: true,
@@ -767,7 +766,7 @@ export const templates: TTemplate[] = [
default:
'We\'re happy to offer you a 20% discount on a yearly plan.
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
logic: [{ condition: "clicked", destination: "end" }],
headline: { default: "Sorry to hear! Get 20% off the first year." },
required: true,
@@ -778,7 +777,7 @@ export const templates: TTemplate[] = [
},
{
id: "rbhww1pix03r6sl4xc511wqg",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "bqiyml1ym74ggx6htwdo7rlu" }],
headline: { default: "Which features are you missing?" },
required: true,
@@ -788,7 +787,7 @@ export const templates: TTemplate[] = [
},
{
id: "bqiyml1ym74ggx6htwdo7rlu",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [
{ condition: "submitted", destination: "end" },
{ condition: "skipped", destination: "end" },
@@ -813,7 +812,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: 3, condition: "lessEqual", destination: "tk9wpw2gxgb8fa6pbpp3qq5l" }],
range: 5,
scale: "star",
@@ -825,7 +824,7 @@ export const templates: TTemplate[] = [
{
id: createId(),
html: { default: 'This helps us a lot.
' },
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
logic: [{ condition: "clicked", destination: "end" }],
headline: { default: "Happy to hear ๐ Please write a review for us!" },
required: true,
@@ -835,7 +834,7 @@ export const templates: TTemplate[] = [
},
{
id: "tk9wpw2gxgb8fa6pbpp3qq5l",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Sorry to hear! What is ONE thing we can do better?" },
required: true,
subheader: { default: "Help us improve your experience." },
@@ -858,7 +857,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: { default: "Do you have 15 min to talk to us? ๐" },
html: { default: "You're one of our power users. We would love to interview you briefly!" },
buttonLabel: { default: "Book slot" },
@@ -880,7 +879,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
logic: [
{
@@ -918,7 +917,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "What made you think {{productName}} wouldn't be useful?" },
required: true,
@@ -927,7 +926,7 @@ export const templates: TTemplate[] = [
},
{
id: "r0zvi3vburf4hm7qewimzjux",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "What was difficult about setting up or using {{productName}}?" },
required: true,
@@ -936,7 +935,7 @@ export const templates: TTemplate[] = [
},
{
id: "rbwz3y6y9avzqcfj30nu0qj4",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "What features or functionality were missing?" },
required: true,
@@ -945,7 +944,7 @@ export const templates: TTemplate[] = [
},
{
id: "gn6298zogd2ipdz7js17qy5i",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "How could we make it easier for you to get started?" },
required: true,
@@ -954,7 +953,7 @@ export const templates: TTemplate[] = [
},
{
id: "c0exdyri3erugrv0ezkyseh6",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [],
headline: { default: "What was it? Please explain:" },
required: false,
@@ -976,7 +975,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
choices: [
{ id: createId(), label: { default: "Ease of use" } },
@@ -990,7 +989,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
choices: [
{ id: createId(), label: { default: "Documentation" } },
@@ -1004,7 +1003,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Would you like to add something?" },
required: false,
subheader: { default: "Feel free to speak your mind, we do too." },
@@ -1023,7 +1022,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "How disappointed would you be if you could no longer use {{productName}}?" },
subheader: { default: "Please select one of the following options:" },
required: true,
@@ -1045,7 +1044,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "How can we improve {{productName}} for you?" },
subheader: { default: "Please be as specific as possible." },
required: true,
@@ -1066,7 +1065,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "How did you hear about us first?" },
subheader: { default: "Please select one of the following options:" },
required: true,
@@ -1109,7 +1108,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "How easy was it to change your plan?" },
required: true,
shuffleOption: "none",
@@ -1138,7 +1137,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "Is the pricing information easy to understand?" },
required: true,
shuffleOption: "none",
@@ -1174,7 +1173,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "What's your primary goal for using {{productName}}?" },
required: true,
shuffleOption: "none",
@@ -1212,7 +1211,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "number",
headline: { default: "How important is [ADD FEATURE] for you?" },
@@ -1222,7 +1221,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
choices: [
{ id: createId(), label: { default: "Aspect 1" } },
@@ -1248,7 +1247,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "How important is this feature for you?" },
required: true,
lowerLabel: { default: "Not important" },
@@ -1258,7 +1257,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceMulti,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
headline: { default: "What should be definitely include building this?" },
required: false,
shuffleOption: "none",
@@ -1296,7 +1295,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
logic: [
{ value: "Bug report ๐", condition: "equals", destination: "dnbiuq4l33l7jypcf2cg6vhh" },
@@ -1312,7 +1311,7 @@ export const templates: TTemplate[] = [
},
{
id: "dnbiuq4l33l7jypcf2cg6vhh",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "a6c76m5oocw6xp9agf3d2tam" }],
headline: { default: "What's broken?" },
required: true,
@@ -1325,7 +1324,7 @@ export const templates: TTemplate[] = [
default:
'We will fix this as soon as possible. Do you want to be notified when we did?
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
logic: [
{ condition: "clicked", destination: "end" },
{ condition: "skipped", destination: "end" },
@@ -1338,7 +1337,7 @@ export const templates: TTemplate[] = [
},
{
id: "en9nuuevbf7g9oa9rzcs1l50",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Lovely, tell us more!" },
required: true,
subheader: { default: "What problem do you want us to solve?" },
@@ -1361,7 +1360,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: "s6ss6znzxdwjod1hv16fow4w",
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: 4, condition: "greaterEqual", destination: "ef0qo3l8iisd517ikp078u1p" }],
range: 5,
scale: "number",
@@ -1372,7 +1371,7 @@ export const templates: TTemplate[] = [
},
{
id: "mko13ptjj6tpi5u2pl7a5drz",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Why was it hard?" },
required: false,
placeholder: { default: "Type your answer here..." },
@@ -1380,7 +1379,7 @@ export const templates: TTemplate[] = [
},
{
id: "ef0qo3l8iisd517ikp078u1p",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What other tools would you like to use with {{productName}}?" },
required: false,
subheader: { default: "We keep building integrations, yours can be next:" },
@@ -1401,7 +1400,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "Which other tools are you using?" },
required: true,
shuffleOption: "none",
@@ -1443,7 +1442,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "Was this page helpful?" },
required: true,
shuffleOption: "none",
@@ -1460,14 +1459,14 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Please elaborate:" },
required: false,
inputType: "text",
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Page URL" },
required: false,
inputType: "text",
@@ -1487,7 +1486,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.NPS,
+ type: TSurveyQuestionTypeEnum.NPS,
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
required: false,
lowerLabel: { default: "Not likely" },
@@ -1495,7 +1494,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What made you give that rating?" },
required: false,
inputType: "text",
@@ -1514,7 +1513,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: 3, condition: "lessEqual", destination: "vyo4mkw4ln95ts4ya7qp2tth" }],
range: 5,
scale: "smiley",
@@ -1525,7 +1524,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "Lovely! Is there anything we can do to improve your experience?" },
required: false,
@@ -1534,7 +1533,7 @@ export const templates: TTemplate[] = [
},
{
id: "vyo4mkw4ln95ts4ya7qp2tth",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Ugh, sorry! Is there anything we can do to improve your experience?" },
required: false,
placeholder: { default: "Type your answer here..." },
@@ -1554,7 +1553,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: "3", condition: "lessEqual", destination: "dlpa0371pe7rphmggy2sgbap" }],
range: 5,
scale: "star",
@@ -1566,7 +1565,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "gwo0fq5kug13e83fcour4n1w" }],
headline: { default: "Lovely! What did you like about it?" },
required: true,
@@ -1576,7 +1575,7 @@ export const templates: TTemplate[] = [
},
{
id: "dlpa0371pe7rphmggy2sgbap",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Thanks for sharing! What did you not like?" },
required: true,
longAnswer: true,
@@ -1585,7 +1584,7 @@ export const templates: TTemplate[] = [
},
{
id: "gwo0fq5kug13e83fcour4n1w",
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "smiley",
headline: { default: "How do you rate our communication?" },
@@ -1595,7 +1594,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Anything else you'd like to share with our team?" },
required: false,
longAnswer: true,
@@ -1604,7 +1603,7 @@ export const templates: TTemplate[] = [
},
{
id: "sjbaghd1bi59pkjun2c97kw9",
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
logic: [],
choices: [
{ id: createId(), label: { default: "Google" } },
@@ -1619,7 +1618,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Lastly, we'd love to respond to your feedback. Please share your email:" },
required: false,
inputType: "email",
@@ -1641,7 +1640,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "How many hours does your team save per week by using {{productName}}?" },
required: true,
shuffleOption: "none",
@@ -1680,7 +1679,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
logic: [],
shuffleOption: "none",
choices: [
@@ -1694,7 +1693,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
logic: [],
shuffleOption: "none",
choices: [
@@ -1707,7 +1706,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "How else could we improve you experience with {{productName}}?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -1728,7 +1727,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
headline: { default: "How easy was it to achieve ... ?" },
required: true,
lowerLabel: { default: "Not easy" },
@@ -1738,7 +1737,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What is one thing we could do better?" },
required: false,
inputType: "text",
@@ -1760,7 +1759,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
headline: { default: "Do you have all the info you need to give {{productName}} a try?" },
required: true,
shuffleOption: "none",
@@ -1781,14 +1780,14 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Whatโs missing or unclear to you about {{productName}}?" },
required: false,
inputType: "text",
},
{
id: createId(),
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: { default: "Thanks for your answer! Get 25% off your first 6 months:" },
required: false,
buttonLabel: { default: "Get discount" },
@@ -1810,7 +1809,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "number",
headline: { default: "{{productName}} makes it easy for me to [ADD GOAL]" },
@@ -1820,7 +1819,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Thanks! How could we make it easier for you to [ADD GOAL]?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -1842,7 +1841,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: 4, condition: "greaterEqual", destination: "lpof3d9t9hmnqvyjlpksmxd7" }],
range: 5,
scale: "number",
@@ -1853,7 +1852,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "Sorry about that! What would have made it easier for you?" },
required: true,
@@ -1862,7 +1861,7 @@ export const templates: TTemplate[] = [
},
{
id: "lpof3d9t9hmnqvyjlpksmxd7",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Lovely! Is there anything we can do to improve your experience?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -1883,7 +1882,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: 4, condition: "greaterEqual", destination: "adcs3d9t9hmnqvyjlpksmxd7" }],
range: 5,
scale: "number",
@@ -1894,7 +1893,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "Ugh! What makes the results irrelevant for you?" },
required: true,
@@ -1903,7 +1902,7 @@ export const templates: TTemplate[] = [
},
{
id: "adcs3d9t9hmnqvyjlpksmxd7",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Lovely! Is there anything we can do to improve your experience?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -1924,7 +1923,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: 4, condition: "greaterEqual", destination: "adcs3d9t9hmnqvyjlpkswi38" }],
range: 5,
scale: "number",
@@ -1935,7 +1934,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "Hmpft! What were you hoping for?" },
required: true,
@@ -1944,7 +1943,7 @@ export const templates: TTemplate[] = [
},
{
id: "adcs3d9t9hmnqvyjlpkswi38",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Lovely! Is there anything else you would like us to cover?" },
required: true,
placeholder: { default: "Topics, trends, tutorials..." },
@@ -1965,7 +1964,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
logic: [
{ value: "Working on it, boss", condition: "equals", destination: "nq88udm0jjtylr16ax87xlyc" },
@@ -1982,7 +1981,7 @@ export const templates: TTemplate[] = [
},
{
id: "rjeac33gd13h3nnbrbid1fb2",
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: 4, condition: "greaterEqual", destination: "nq88udm0jjtylr16ax87xlyc" }],
range: 5,
scale: "number",
@@ -1993,7 +1992,7 @@ export const templates: TTemplate[] = [
},
{
id: "s0999bhpaz8vgf7ps264piek",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [
{ condition: "submitted", destination: "end" },
{ condition: "skipped", destination: "end" },
@@ -2005,7 +2004,7 @@ export const templates: TTemplate[] = [
},
{
id: "nq88udm0jjtylr16ax87xlyc",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [
{ condition: "skipped", destination: "end" },
{ condition: "submitted", destination: "end" },
@@ -2017,7 +2016,7 @@ export const templates: TTemplate[] = [
},
{
id: "u83zhr66knyfozccoqojx7bc",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What stopped you?" },
required: true,
buttonLabel: { default: "Send" },
@@ -2043,7 +2042,7 @@ export const templates: TTemplate[] = [
default:
'You seem to be considering signing up. Answer four questions and get 10% on any plan.
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
logic: [{ condition: "skipped", destination: "end" }],
headline: { default: "Answer this short survey, get 10% off!" },
required: false,
@@ -2053,7 +2052,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [{ value: "5", condition: "equals", destination: "end" }],
range: 5,
scale: "number",
@@ -2064,7 +2063,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
logic: [
{
@@ -2093,7 +2092,7 @@ export const templates: TTemplate[] = [
},
{
id: "atiw0j1oykb77zr0b7q4tixu",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "k3q0vt1ko0bzbsq076p7lnys" }],
headline: { default: "What do you need but {{productName}} does not offer?" },
required: true,
@@ -2102,7 +2101,7 @@ export const templates: TTemplate[] = [
},
{
id: "j7jkpolm5xl7u0zt3g0e4z7d",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "k3q0vt1ko0bzbsq076p7lnys" }],
headline: { default: "What options are you looking at?" },
required: true,
@@ -2111,7 +2110,7 @@ export const templates: TTemplate[] = [
},
{
id: "t5gvag2d7kq311szz5iyiy79",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "k3q0vt1ko0bzbsq076p7lnys" }],
headline: { default: "What seems complicated to you?" },
required: true,
@@ -2120,7 +2119,7 @@ export const templates: TTemplate[] = [
},
{
id: "or0yhhrof753sq9ug4mdavgz",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "k3q0vt1ko0bzbsq076p7lnys" }],
headline: { default: "What are you concerned about regarding pricing?" },
required: true,
@@ -2129,7 +2128,7 @@ export const templates: TTemplate[] = [
},
{
id: "v0pq1qcnm6ohiry5ywcd91qq",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Please explain:" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -2141,7 +2140,7 @@ export const templates: TTemplate[] = [
default:
'Thanks a lot for taking the time to share feedback ๐
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: { default: "Thanks! Here is your code: SIGNUPNOW10" },
required: false,
buttonUrl: "https://app.formbricks.com/auth/signup",
@@ -2164,7 +2163,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "number",
headline: {
@@ -2176,7 +2175,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: {
default: "What's ONE change we could make to improve your {{productName}} experience most?",
},
@@ -2199,7 +2198,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [
{ value: "2", condition: "lessEqual", destination: "y19mwcmstlc7pi7s4izxk1ll" },
{ value: "3", condition: "equals", destination: "zm1hs8qkeuidh3qm0hx8pnw7" },
@@ -2215,7 +2214,7 @@ export const templates: TTemplate[] = [
},
{
id: "y19mwcmstlc7pi7s4izxk1ll",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [
{ condition: "submitted", destination: "end" },
{ condition: "skipped", destination: "end" },
@@ -2227,7 +2226,7 @@ export const templates: TTemplate[] = [
},
{
id: "zm1hs8qkeuidh3qm0hx8pnw7",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What, if anything, is holding you back from making a purchase today?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -2247,7 +2246,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [
{ value: "5", condition: "equals", destination: "l2q1chqssong8n0xwaagyl8g" },
{ value: "5", condition: "lessThan", destination: "k3s6gm5ivkc5crpycdbpzkpa" },
@@ -2261,7 +2260,7 @@ export const templates: TTemplate[] = [
},
{
id: "k3s6gm5ivkc5crpycdbpzkpa",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [
{ condition: "submitted", destination: "end" },
{ condition: "skipped", destination: "end" },
@@ -2277,7 +2276,7 @@ export const templates: TTemplate[] = [
default:
'Who thinks like you? You\'d do us a huge favor if you\'d share this weeks episode with your brain friend!
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: { default: "Thanks! โค๏ธ Spread the love with ONE friend." },
required: false,
buttonUrl: "https://formbricks.com",
@@ -2304,7 +2303,7 @@ export const templates: TTemplate[] = [
default:
'We respect your time and kept it short ๐คธ
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: {
default:
"We love how you use {{productName}}! We'd love to pick your brain on a feature idea. Got a minute?",
@@ -2316,7 +2315,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [
{ value: "3", condition: "lessEqual", destination: "ndacjg9lqf5jcpq9w8ote666" },
{ value: "4", condition: "greaterEqual", destination: "jmzgbo73cfjswlvhoynn7o0q" },
@@ -2330,7 +2329,7 @@ export const templates: TTemplate[] = [
},
{
id: "ndacjg9lqf5jcpq9w8ote666",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What's most difficult for you when it comes to [PROBLEM AREA]?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -2342,7 +2341,7 @@ export const templates: TTemplate[] = [
default:
'
Read the text below, then answer 2 questions:
Insert concept brief here. Add neccessary details but keep it concise and easy to understand.
',
},
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: { default: "We're working on an idea to help with [PROBLEM AREA]." },
required: true,
buttonLabel: { default: "Next" },
@@ -2351,7 +2350,7 @@ export const templates: TTemplate[] = [
},
{
id: createId(),
- type: TSurveyQuestionType.Rating,
+ type: TSurveyQuestionTypeEnum.Rating,
logic: [
{ value: "3", condition: "lessEqual", destination: "mmiuun3z4e7gk4ufuwh8lq8q" },
{ value: "4", condition: "greaterEqual", destination: "gvzevzw4hkqd6dmlkcly6kd1" },
@@ -2365,7 +2364,7 @@ export const templates: TTemplate[] = [
},
{
id: "mmiuun3z4e7gk4ufuwh8lq8q",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "bqmnpyku9etsgbtb322luzb2" }],
headline: { default: "Got it. Why wouldn't this feature be valuable to you?" },
required: true,
@@ -2374,7 +2373,7 @@ export const templates: TTemplate[] = [
},
{
id: "gvzevzw4hkqd6dmlkcly6kd1",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Got it. What would be most valuable to you in this feature?" },
required: true,
placeholder: { default: "Type your answer here..." },
@@ -2382,7 +2381,7 @@ export const templates: TTemplate[] = [
},
{
id: "bqmnpyku9etsgbtb322luzb2",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Anything else we should keep in mind?" },
required: false,
placeholder: { default: "Type your answer here..." },
@@ -2403,7 +2402,7 @@ export const templates: TTemplate[] = [
questions: [
{
id: "aq9dafe9nxe0kpm67b1os2z9",
- type: TSurveyQuestionType.MultipleChoiceSingle,
+ type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
shuffleOption: "none",
logic: [
{ value: "Difficult to use", condition: "equals", destination: "r0zvi3vburf4hm7qewimzjux" },
@@ -2436,7 +2435,7 @@ export const templates: TTemplate[] = [
},
{
id: "r0zvi3vburf4hm7qewimzjux",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "What's difficult about using {{productName}}?" },
required: true,
@@ -2445,7 +2444,7 @@ export const templates: TTemplate[] = [
},
{
id: "g92s5wetp51ps6afmc6y7609",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "Got it. Which alternative are you using instead?" },
required: true,
@@ -2454,7 +2453,7 @@ export const templates: TTemplate[] = [
},
{
id: "gn6298zogd2ipdz7js17qy5i",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "Got it. How could we make it easier for you to get started?" },
required: true,
@@ -2463,7 +2462,7 @@ export const templates: TTemplate[] = [
},
{
id: "rbwz3y6y9avzqcfj30nu0qj4",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [{ condition: "submitted", destination: "end" }],
headline: { default: "Got it. What features or functionality were missing?" },
required: true,
@@ -2472,7 +2471,7 @@ export const templates: TTemplate[] = [
},
{
id: "c0exdyri3erugrv0ezkyseh6",
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
logic: [],
headline: { default: "Please add more details:" },
required: false,
@@ -2493,7 +2492,7 @@ export const customSurvey = {
questions: [
{
id: createId(),
- type: TSurveyQuestionType.OpenText,
+ type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "What would you like to know?" },
subheader: { default: "This is an example survey." },
placeholder: { default: "Type your answer here..." },
@@ -2510,7 +2509,7 @@ export const getExampleSurveyTemplate = (webAppUrl: string, trigger: TActionClas
(question) =>
({
...question,
- type: TSurveyQuestionType.CTA,
+ type: TSurveyQuestionTypeEnum.CTA,
headline: { default: "You did it ๐" },
html: {
default: "You're all set up. Create your own survey to gather exactly the feedback you need :)",
diff --git a/packages/surveys/package.json b/packages/surveys/package.json
index 5145731f5e..51d402f54b 100644
--- a/packages/surveys/package.json
+++ b/packages/surveys/package.json
@@ -31,7 +31,7 @@
"build": "tsc && vite build",
"build:dev": "tsc && vite build --mode dev",
"go": "vite build --watch --mode dev",
- "lint": "eslint . --ext .ts,.js,.tsx,.jsx",
+ "lint": "eslint src --fix --ext .ts,.js,.tsx,.jsx",
"preview": "vite preview",
"clean": "rimraf .turbo node_modules dist"
},
diff --git a/packages/surveys/src/components/general/QuestionConditional.tsx b/packages/surveys/src/components/general/QuestionConditional.tsx
index 2e76a1ba64..330a3c2915 100644
--- a/packages/surveys/src/components/general/QuestionConditional.tsx
+++ b/packages/surveys/src/components/general/QuestionConditional.tsx
@@ -11,10 +11,9 @@ import { NPSQuestion } from "@/components/questions/NPSQuestion";
import { OpenTextQuestion } from "@/components/questions/OpenTextQuestion";
import { PictureSelectionQuestion } from "@/components/questions/PictureSelectionQuestion";
import { RatingQuestion } from "@/components/questions/RatingQuestion";
-
import { TResponseData, TResponseDataValue, TResponseTtc } from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
-import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
+import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys";
interface QuestionConditionalProps {
question: TSurveyQuestion;
@@ -61,7 +60,7 @@ export const QuestionConditional = ({
}
}
- return question.type === TSurveyQuestionType.OpenText ? (
+ return question.type === TSurveyQuestionTypeEnum.OpenText ? (
- ) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
+ ) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ? (
- ) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
+ ) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ? (
- ) : question.type === TSurveyQuestionType.NPS ? (
+ ) : question.type === TSurveyQuestionTypeEnum.NPS ? (
- ) : question.type === TSurveyQuestionType.CTA ? (
+ ) : question.type === TSurveyQuestionTypeEnum.CTA ? (
- ) : question.type === TSurveyQuestionType.Rating ? (
+ ) : question.type === TSurveyQuestionTypeEnum.Rating ? (
- ) : question.type === TSurveyQuestionType.Consent ? (
+ ) : question.type === TSurveyQuestionTypeEnum.Consent ? (
- ) : question.type === TSurveyQuestionType.Date ? (
+ ) : question.type === TSurveyQuestionTypeEnum.Date ? (
- ) : question.type === TSurveyQuestionType.PictureSelection ? (
+ ) : question.type === TSurveyQuestionTypeEnum.PictureSelection ? (
- ) : question.type === TSurveyQuestionType.FileUpload ? (
+ ) : question.type === TSurveyQuestionTypeEnum.FileUpload ? (
- ) : question.type === TSurveyQuestionType.Cal ? (
+ ) : question.type === TSurveyQuestionTypeEnum.Cal ? (
- ) : question.type === TSurveyQuestionType.Matrix ? (
+ ) : question.type === TSurveyQuestionTypeEnum.Matrix ? (
- ) : question.type === TSurveyQuestionType.Address ? (
+ ) : question.type === TSurveyQuestionTypeEnum.Address ? (
;
export const ZLegacySurveyConsentQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Consent),
+ type: z.literal(TSurveyQuestionTypeEnum.Consent),
html: z.string().optional(),
label: z.string(),
placeholder: z.string().optional(),
@@ -53,7 +52,7 @@ export const ZLegacySurveyChoice = z.object({
export type TLegacySurveyChoice = z.infer;
export const ZLegacySurveyMultipleChoiceSingleQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.MultipleChoiceSingle),
+ type: z.literal(TSurveyQuestionTypeEnum.MultipleChoiceSingle),
choices: z.array(ZLegacySurveyChoice),
logic: z.array(ZSurveyMultipleChoiceLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
@@ -65,7 +64,7 @@ export type TLegacySurveyMultipleChoiceSingleQuestion = z.infer<
>;
export const ZLegacySurveyMultipleChoiceMultiQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.MultipleChoiceMulti),
+ type: z.literal(TSurveyQuestionTypeEnum.MultipleChoiceMulti),
choices: z.array(ZLegacySurveyChoice),
logic: z.array(ZSurveyMultipleChoiceLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
@@ -77,7 +76,7 @@ export type TLegacySurveyMultipleChoiceMultiQuestion = z.infer<
>;
export const ZLegacySurveyNPSQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.NPS),
+ type: z.literal(TSurveyQuestionTypeEnum.NPS),
lowerLabel: z.string(),
upperLabel: z.string(),
logic: z.array(ZSurveyNPSLogic).optional(),
@@ -86,7 +85,7 @@ export const ZLegacySurveyNPSQuestion = ZLegacySurveyQuestionBase.extend({
export type TLegacySurveyNPSQuestion = z.infer;
export const ZLegacySurveyCTAQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.CTA),
+ type: z.literal(TSurveyQuestionTypeEnum.CTA),
html: z.string().optional(),
buttonUrl: z.string().optional(),
buttonExternal: z.boolean(),
@@ -97,7 +96,7 @@ export const ZLegacySurveyCTAQuestion = ZLegacySurveyQuestionBase.extend({
export type TLegacySurveyCTAQuestion = z.infer;
export const ZLegacySurveyRatingQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Rating),
+ type: z.literal(TSurveyQuestionTypeEnum.Rating),
scale: z.enum(["number", "smiley", "star"]),
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]),
lowerLabel: z.string(),
@@ -106,7 +105,7 @@ export const ZLegacySurveyRatingQuestion = ZLegacySurveyQuestionBase.extend({
});
export const ZLegacySurveyDateQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Date),
+ type: z.literal(TSurveyQuestionTypeEnum.Date),
html: z.string().optional(),
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
});
@@ -116,7 +115,7 @@ export type TLegacySurveyDateQuestion = z.infer;
export const ZLegacySurveyPictureSelectionQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.PictureSelection),
+ type: z.literal(TSurveyQuestionTypeEnum.PictureSelection),
allowMulti: z.boolean().optional().default(false),
choices: z.array(ZSurveyPictureChoice),
logic: z.array(ZSurveyPictureSelectionLogic).optional(),
@@ -125,7 +124,7 @@ export const ZLegacySurveyPictureSelectionQuestion = ZLegacySurveyQuestionBase.e
export type TLegacySurveyPictureSelectionQuestion = z.infer;
export const ZLegacySurveyFileUploadQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.FileUpload),
+ type: z.literal(TSurveyQuestionTypeEnum.FileUpload),
allowMultipleFiles: z.boolean(),
maxSizeInMB: z.number().optional(),
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
@@ -135,7 +134,7 @@ export const ZLegacySurveyFileUploadQuestion = ZLegacySurveyQuestionBase.extend(
export type TLegacySurveyFileUploadQuestion = z.infer;
export const ZLegacySurveyCalQuestion = ZLegacySurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Cal),
+ type: z.literal(TSurveyQuestionTypeEnum.Cal),
calUserName: z.string(),
logic: z.array(ZSurveyCalLogic).optional(),
});
diff --git a/packages/types/surveys.ts b/packages/types/surveys.ts
index 4fd5610403..6f62df18ae 100644
--- a/packages/types/surveys.ts
+++ b/packages/types/surveys.ts
@@ -1,5 +1,4 @@
import { z } from "zod";
-
import { ZActionClass, ZNoCodeConfig } from "./actionClasses";
import { ZAttributes } from "./attributes";
import { ZAllowedFileExtension, ZColor, ZPlacement } from "./common";
@@ -24,7 +23,7 @@ export const ZSurveyThankYouCard = z.object({
videoUrl: z.string().optional(),
});
-export enum TSurveyQuestionType {
+export enum TSurveyQuestionTypeEnum {
FileUpload = "fileUpload",
OpenText = "openText",
MultipleChoiceSingle = "multipleChoiceSingle",
@@ -271,7 +270,7 @@ export const ZSurveyOpenTextQuestionInputType = z.enum(["text", "email", "url",
export type TSurveyOpenTextQuestionInputType = z.infer;
export const ZSurveyOpenTextQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.OpenText),
+ type: z.literal(TSurveyQuestionTypeEnum.OpenText),
placeholder: ZI18nString.optional(),
longAnswer: z.boolean().optional(),
logic: z.array(ZSurveyOpenTextLogic).optional(),
@@ -281,7 +280,7 @@ export const ZSurveyOpenTextQuestion = ZSurveyQuestionBase.extend({
export type TSurveyOpenTextQuestion = z.infer;
export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Consent),
+ type: z.literal(TSurveyQuestionTypeEnum.Consent),
html: ZI18nString.optional(),
label: ZI18nString,
placeholder: z.string().optional(),
@@ -296,8 +295,8 @@ export type TShuffleOption = z.infer;
export const ZSurveyMultipleChoiceQuestion = ZSurveyQuestionBase.extend({
type: z.union([
- z.literal(TSurveyQuestionType.MultipleChoiceSingle),
- z.literal(TSurveyQuestionType.MultipleChoiceMulti),
+ z.literal(TSurveyQuestionTypeEnum.MultipleChoiceSingle),
+ z.literal(TSurveyQuestionTypeEnum.MultipleChoiceMulti),
]),
choices: z.array(ZSurveyChoice),
logic: z.array(ZSurveyMultipleChoiceLogic).optional(),
@@ -307,7 +306,7 @@ export const ZSurveyMultipleChoiceQuestion = ZSurveyQuestionBase.extend({
(question) => {
const { logic, type } = question;
- if (type === TSurveyQuestionType.MultipleChoiceSingle) {
+ if (type === TSurveyQuestionTypeEnum.MultipleChoiceSingle) {
// The single choice question should not have 'includesAll' logic
return !logic?.some((l) => l.condition === "includesAll");
} else {
@@ -324,7 +323,7 @@ export const ZSurveyMultipleChoiceQuestion = ZSurveyQuestionBase.extend({
export type TSurveyMultipleChoiceQuestion = z.infer;
export const ZSurveyNPSQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.NPS),
+ type: z.literal(TSurveyQuestionTypeEnum.NPS),
lowerLabel: ZI18nString.optional(),
upperLabel: ZI18nString.optional(),
logic: z.array(ZSurveyNPSLogic).optional(),
@@ -333,7 +332,7 @@ export const ZSurveyNPSQuestion = ZSurveyQuestionBase.extend({
export type TSurveyNPSQuestion = z.infer;
export const ZSurveyCTAQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.CTA),
+ type: z.literal(TSurveyQuestionTypeEnum.CTA),
html: ZI18nString.optional(),
buttonUrl: z.string().optional(),
buttonExternal: z.boolean(),
@@ -344,7 +343,7 @@ export const ZSurveyCTAQuestion = ZSurveyQuestionBase.extend({
export type TSurveyCTAQuestion = z.infer;
export const ZSurveyRatingQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Rating),
+ type: z.literal(TSurveyQuestionTypeEnum.Rating),
scale: z.enum(["number", "smiley", "star"]),
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]),
lowerLabel: ZI18nString.optional(),
@@ -353,7 +352,7 @@ export const ZSurveyRatingQuestion = ZSurveyQuestionBase.extend({
});
export const ZSurveyDateQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Date),
+ type: z.literal(TSurveyQuestionTypeEnum.Date),
html: ZI18nString.optional(),
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
});
@@ -363,7 +362,7 @@ export type TSurveyDateQuestion = z.infer;
export type TSurveyRatingQuestion = z.infer;
export const ZSurveyPictureSelectionQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.PictureSelection),
+ type: z.literal(TSurveyQuestionTypeEnum.PictureSelection),
allowMulti: z.boolean().optional().default(false),
choices: z.array(ZSurveyPictureChoice),
logic: z.array(ZSurveyPictureSelectionLogic).optional(),
@@ -372,7 +371,7 @@ export const ZSurveyPictureSelectionQuestion = ZSurveyQuestionBase.extend({
export type TSurveyPictureSelectionQuestion = z.infer;
export const ZSurveyFileUploadQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.FileUpload),
+ type: z.literal(TSurveyQuestionTypeEnum.FileUpload),
allowMultipleFiles: z.boolean(),
maxSizeInMB: z.number().optional(),
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
@@ -382,7 +381,7 @@ export const ZSurveyFileUploadQuestion = ZSurveyQuestionBase.extend({
export type TSurveyFileUploadQuestion = z.infer;
export const ZSurveyCalQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Cal),
+ type: z.literal(TSurveyQuestionTypeEnum.Cal),
calUserName: z.string(),
logic: z.array(ZSurveyCalLogic).optional(),
});
@@ -390,7 +389,7 @@ export const ZSurveyCalQuestion = ZSurveyQuestionBase.extend({
export type TSurveyCalQuestion = z.infer;
export const ZSurveyMatrixQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Matrix),
+ type: z.literal(TSurveyQuestionTypeEnum.Matrix),
rows: z.array(ZI18nString),
columns: z.array(ZI18nString),
logic: z.array(ZSurveyMatrixLogic).optional(),
@@ -399,7 +398,7 @@ export const ZSurveyMatrixQuestion = ZSurveyQuestionBase.extend({
export type TSurveyMatrixQuestion = z.infer;
export const ZSurveyAddressQuestion = ZSurveyQuestionBase.extend({
- type: z.literal(TSurveyQuestionType.Address),
+ type: z.literal(TSurveyQuestionTypeEnum.Address),
isAddressLine1Required: z.boolean().default(false),
isAddressLine2Required: z.boolean().default(false),
isCityRequired: z.boolean().default(false),
@@ -424,6 +423,30 @@ export const ZSurveyQuestion = z.union([
ZSurveyAddressQuestion,
]);
+export type TSurveyQuestion = z.infer;
+
+export const ZSurveyQuestions = z.array(ZSurveyQuestion);
+
+export type TSurveyQuestions = z.infer;
+
+export const ZSurveyQuestionType = z.enum([
+ TSurveyQuestionTypeEnum.Address,
+ TSurveyQuestionTypeEnum.CTA,
+ TSurveyQuestionTypeEnum.Consent,
+ TSurveyQuestionTypeEnum.Date,
+ TSurveyQuestionTypeEnum.FileUpload,
+ TSurveyQuestionTypeEnum.Matrix,
+ TSurveyQuestionTypeEnum.MultipleChoiceMulti,
+ TSurveyQuestionTypeEnum.MultipleChoiceSingle,
+ TSurveyQuestionTypeEnum.NPS,
+ TSurveyQuestionTypeEnum.OpenText,
+ TSurveyQuestionTypeEnum.PictureSelection,
+ TSurveyQuestionTypeEnum.Rating,
+ TSurveyQuestionTypeEnum.Cal,
+]);
+
+export type TSurveyQuestionType = z.infer;
+
export const ZSurveyLanguage = z.object({
language: ZLanguage,
default: z.boolean(),
@@ -432,12 +455,6 @@ export const ZSurveyLanguage = z.object({
export type TSurveyLanguage = z.infer;
-export type TSurveyQuestion = z.infer;
-
-export const ZSurveyQuestions = z.array(ZSurveyQuestion);
-
-export type TSurveyQuestions = z.infer;
-
export const ZSurveyQuestionsObject = z.object({
questions: ZSurveyQuestions,
hiddenFields: ZSurveyHiddenFields,
diff --git a/packages/types/weeklySummary.ts b/packages/types/weeklySummary.ts
index 9452acad07..3e6e3a9a43 100644
--- a/packages/types/weeklySummary.ts
+++ b/packages/types/weeklySummary.ts
@@ -1,8 +1,7 @@
import { z } from "zod";
-
import { ZAttributeClass } from "./attributeClasses";
import { ZResponseData } from "./responses";
-import { ZSurveyHiddenFields, ZSurveyQuestion, ZSurveyStatus } from "./surveys";
+import { ZSurveyHiddenFields, ZSurveyQuestion, ZSurveyQuestionType, ZSurveyStatus } from "./surveys";
import { ZUserNotificationSettings } from "./user";
const ZWeeklySummaryInsights = z.object({
@@ -18,7 +17,7 @@ export type TWeeklySummaryInsights = z.infer;
export const ZWeeklySummarySurveyResponseData = z.object({
headline: z.string(),
responseValue: z.union([z.string(), z.array(z.string())]),
- questionType: z.string(),
+ questionType: ZSurveyQuestionType,
});
export type TWeeklySummarySurveyResponseData = z.infer;
@@ -28,7 +27,7 @@ export const ZWeeklySummaryNotificationDataSurvey = z.object({
name: z.string(),
responses: z.array(ZWeeklySummarySurveyResponseData),
responseCount: z.number(),
- status: z.string(),
+ status: ZSurveyStatus,
});
export type TWeeklySummaryNotificationDataSurvey = z.infer;
diff --git a/packages/ui/QuestionFormInput/index.tsx b/packages/ui/QuestionFormInput/index.tsx
index 240a0ff9ae..c5d4a84679 100644
--- a/packages/ui/QuestionFormInput/index.tsx
+++ b/packages/ui/QuestionFormInput/index.tsx
@@ -27,7 +27,7 @@ import {
TSurveyRecallItem,
} from "@formbricks/types/surveys";
-import { LanguageIndicator } from "../../ee/multiLanguage/components/LanguageIndicator";
+import { LanguageIndicator } from "../../ee/multi-language/components/language-indicator";
import { createI18nString } from "../../lib/i18n/utils";
import { FileInput } from "../FileInput";
import { Input } from "../Input";
diff --git a/packages/ui/ShareSurveyLink/components/LanguageDropdown.tsx b/packages/ui/ShareSurveyLink/components/LanguageDropdown.tsx
index c47d516b37..26622e6ecd 100644
--- a/packages/ui/ShareSurveyLink/components/LanguageDropdown.tsx
+++ b/packages/ui/ShareSurveyLink/components/LanguageDropdown.tsx
@@ -5,7 +5,7 @@ import { getEnabledLanguages } from "@formbricks/lib/i18n/utils";
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
import { TSurvey } from "@formbricks/types/surveys";
-import { getLanguageLabel } from "../../../ee/multiLanguage/lib/isoLanguages";
+import { getLanguageLabel } from "../../../ee/multi-language/lib/iso-languages";
import { Button } from "../../Button";
interface LanguageDropdownProps {
diff --git a/packages/ui/SingleResponseCard/components/SingleResponseCardBody.tsx b/packages/ui/SingleResponseCard/components/SingleResponseCardBody.tsx
index 274ccc9386..e845e76a99 100644
--- a/packages/ui/SingleResponseCard/components/SingleResponseCardBody.tsx
+++ b/packages/ui/SingleResponseCard/components/SingleResponseCardBody.tsx
@@ -1,5 +1,4 @@
import { CheckCircle2Icon } from "lucide-react";
-
import { getLanguageCode, getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
@@ -9,9 +8,8 @@ import {
TSurveyMatrixQuestion,
TSurveyPictureSelectionQuestion,
TSurveyQuestion,
- TSurveyQuestionType,
+ TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys";
-
import { AddressResponse } from "../../AddressResponse";
import { FileUploadResponse } from "../../FileUploadResponse";
import { PictureSelectionResponse } from "../../PictureSelectionResponse";
@@ -64,23 +62,23 @@ export const SingleResponseCardBody = ({
};
const renderResponse = (
- questionType: TSurveyQuestionType,
+ questionType: TSurveyQuestionTypeEnum,
responseData: string | number | string[] | Record,
question: TSurveyQuestion
) => {
switch (questionType) {
- case TSurveyQuestionType.Rating:
+ case TSurveyQuestionTypeEnum.Rating:
if (typeof responseData === "number")
return ;
- case TSurveyQuestionType.Date:
+ case TSurveyQuestionTypeEnum.Date:
if (typeof responseData === "string") {
const formattedDateString = formatDateWithOrdinal(new Date(responseData));
return {formattedDateString}
;
}
- case TSurveyQuestionType.Cal:
+ case TSurveyQuestionTypeEnum.Cal:
if (typeof responseData === "string")
return {responseData}
;
- case TSurveyQuestionType.PictureSelection:
+ case TSurveyQuestionTypeEnum.PictureSelection:
if (Array.isArray(responseData))
return (
);
- case TSurveyQuestionType.FileUpload:
+ case TSurveyQuestionTypeEnum.FileUpload:
if (Array.isArray(responseData)) return ;
- case TSurveyQuestionType.Matrix:
+ case TSurveyQuestionTypeEnum.Matrix:
if (typeof responseData === "object" && !Array.isArray(responseData)) {
return (question as TSurveyMatrixQuestion).rows.map((row) => {
const languagCode = getLanguageCode(survey.languages, response.language);
@@ -103,7 +101,7 @@ export const SingleResponseCardBody = ({
);
});
}
- case TSurveyQuestionType.Address:
+ case TSurveyQuestionTypeEnum.Address:
if (Array.isArray(responseData)) {
return ;
}
diff --git a/packages/ui/SingleResponseCard/components/SingleResponseCardHeader.tsx b/packages/ui/SingleResponseCard/components/SingleResponseCardHeader.tsx
index 27bba72ecb..4a13ed464b 100644
--- a/packages/ui/SingleResponseCard/components/SingleResponseCardHeader.tsx
+++ b/packages/ui/SingleResponseCard/components/SingleResponseCardHeader.tsx
@@ -9,7 +9,7 @@ import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import { TUser } from "@formbricks/types/user";
-import { getLanguageLabel } from "../../../ee/multiLanguage/lib/isoLanguages";
+import { getLanguageLabel } from "../../../ee/multi-language/lib/iso-languages";
import { PersonAvatar } from "../../Avatars";
import { SurveyStatusIndicator } from "../../SurveyStatusIndicator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../Tooltip";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c2a5677871..55f8fb1799 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -429,9 +429,6 @@ importers:
next:
specifier: 15.0.0-rc.0
version: 15.0.0-rc.0(@babel/core@7.24.6)(@opentelemetry/api@1.8.0)(@playwright/test@1.44.1)(react-dom@19.0.0-rc-935180c7e0-20240524)(react@19.0.0-rc-935180c7e0-20240524)
- nodemailer:
- specifier: ^6.9.13
- version: 6.9.13
optional:
specifier: ^0.1.4
version: 0.1.4
@@ -647,15 +644,45 @@ importers:
packages/ee:
dependencies:
+ '@formbricks/database':
+ specifier: workspace:*
+ version: link:../database
'@formbricks/lib':
specifier: workspace:*
version: link:../lib
+ '@paralleldrive/cuid2':
+ specifier: ^2.2.2
+ version: 2.2.2
+ '@radix-ui/react-collapsible':
+ specifier: ^1.0.3
+ version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
axios:
specifier: ^1.7.2
version: 1.7.2
+ lucide-react:
+ specifier: ^0.390.0
+ version: 0.390.0(react@18.3.1)
+ next:
+ specifier: ^14.2.3
+ version: 14.2.3(@playwright/test@1.44.1)(react-dom@18.3.1)(react@18.3.1)
+ next-auth:
+ specifier: ^4.24.7
+ version: 4.24.7(next@14.2.3)(react-dom@18.3.1)(react@18.3.1)
+ react-hook-form:
+ specifier: ^7.51.5
+ version: 7.51.5(react@18.3.1)
+ react-hot-toast:
+ specifier: ^2.4.1
+ version: 2.4.1(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1)
+ server-only:
+ specifier: ^0.0.1
+ version: 0.0.1
stripe:
specifier: ^15.8.0
version: 15.8.0
+ zod:
+ specifier: ^3.23.8
+ version: 3.23.8
devDependencies:
'@formbricks/config-typescript':
specifier: '*'
@@ -669,9 +696,18 @@ importers:
'@formbricks/ui':
specifier: '*'
version: link:../ui
+ '@types/dompurify':
+ specifier: ^3.0.5
+ version: 3.0.5
+ '@types/react':
+ specifier: 18.3.3
+ version: 18.3.3
packages/email:
dependencies:
+ '@formbricks/config-typescript':
+ specifier: workspace:*
+ version: link:../config-typescript
'@formbricks/lib':
specifier: workspace:*
version: link:../lib
@@ -683,19 +719,26 @@ importers:
version: link:../ui
'@react-email/components':
specifier: ^0.0.19
- version: 0.0.19(react@18.3.1)
+ version: 0.0.19(@types/react@18.3.3)(react@18.3.1)
'@react-email/render':
specifier: ^0.0.15
version: 0.0.15
lucide-react:
- specifier: ^0.379.0
- version: 0.379.0(react@18.3.1)
+ specifier: ^0.390.0
+ version: 0.390.0(react@18.3.1)
nodemailer:
specifier: ^6.9.13
version: 6.9.13
react-email:
specifier: ^2.1.4
version: 2.1.4(eslint@8.57.0)
+ devDependencies:
+ '@types/nodemailer':
+ specifier: ^6.4.15
+ version: 6.4.15
+ '@types/react':
+ specifier: 18.3.3
+ version: 18.3.3
packages/js:
devDependencies:
@@ -8180,7 +8223,7 @@ packages:
react: 19.0.0-rc-935180c7e0-20240524
dev: false
- /@react-email/components@0.0.19(react@18.3.1):
+ /@react-email/components@0.0.19(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-yf49eIq0NDDXzO2RTZaT8fKa16eKUFMdWWMx4V5Bq+b2JdGuAMobO5s9Ea6azSVL6RDcJ8epdY1TCR2kL2PPHw==}
engines: {node: '>=18.0.0'}
peerDependencies:
@@ -8194,7 +8237,7 @@ packages:
'@react-email/container': 0.0.12(react@18.3.1)
'@react-email/font': 0.0.6(react@18.3.1)
'@react-email/head': 0.0.9(react@18.3.1)
- '@react-email/heading': 0.0.12(react@18.3.1)
+ '@react-email/heading': 0.0.12(@types/react@18.3.3)(react@18.3.1)
'@react-email/hr': 0.0.8(react@18.3.1)
'@react-email/html': 0.0.8(react@18.3.1)
'@react-email/img': 0.0.8(react@18.3.1)
@@ -8294,7 +8337,7 @@ packages:
react: 19.0.0-rc-935180c7e0-20240524
dev: false
- /@react-email/heading@0.0.12(react@18.3.1):
+ /@react-email/heading@0.0.12(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-eB7mpnAvDmwvQLoPuwEiPRH4fPXWe6ltz6Ptbry2BlI88F0a2k11Ghb4+sZHBqg7vVw/MKbqEgtLqr3QJ/KfCQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
@@ -11049,6 +11092,12 @@ packages:
dependencies:
undici-types: 5.26.5
+ /@types/nodemailer@6.4.15:
+ resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==}
+ dependencies:
+ '@types/node': 20.12.12
+ dev: true
+
/@types/normalize-package-data@2.4.4:
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -17181,6 +17230,14 @@ packages:
react: 19.0.0-rc-935180c7e0-20240524
dev: false
+ /lucide-react@0.390.0(react@18.3.1):
+ resolution: {integrity: sha512-APqbfEcVuHnZbiy3E97gYWLeBdkE4e6NbY6AuVETZDZVn/bQCHYUoHyxcUHyvRopfPOHhFUEvDyyQzHwM+S9/w==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.3.1
+ dev: false
+
/lucide@0.378.0:
resolution: {integrity: sha512-bwWXuZf2jZbCI0Y+MWyv5bedWIxYKtgAEzC2Yl87Nrt/KcG9qTwAQxVFcZ6IwBioML06QUQsG+qRjyQvYHdbBQ==}
dev: false
@@ -19836,6 +19893,15 @@ packages:
react: 18.3.1
dev: false
+ /react-hook-form@7.51.5(react@18.3.1):
+ resolution: {integrity: sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==}
+ engines: {node: '>=12.22.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18
+ dependencies:
+ react: 18.3.1
+ dev: false
+
/react-hook-form@7.51.5(react@19.0.0-rc-935180c7e0-20240524):
resolution: {integrity: sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==}
engines: {node: '>=12.22.0'}