feat: contacts revamp (#3399)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Anshuman Pandey
2024-12-03 18:08:35 +05:30
committed by GitHub
parent 61f5c66444
commit 56d8c3f50f
288 changed files with 10249 additions and 10746 deletions

View File

@@ -1,51 +0,0 @@
import { MdxImage } from "@/components/MdxImage";
import PeopleView from "./people-view.webp";
export const metadata = {
title: "Effectively identify users in Formbricks link surveys",
description:
"Discover how to seamlessly connect responses from Formbricks link surveys to existing users in your database. Learn the intricacies of the userId URL parameter to enhance user tracking, profiling, and segmentation, ensuring more personalized interactions and data-driven decisions.",
};
#### Link Surveys
# Identify Users in Link Surveys
Identifying users in link features lets you connect responses from link surveys with existing users in your Formbricks database.
## Purpose
Identifying users in link surveys comes in handy when you:
- Want to send out link surveys to existing users in your database
- Want to connect responses from link surveys with existing users in your database
- Want to gather data and later connect it to a user who has not signed up yet
## Quick Example
<Col>
<CodeGroup title="User Identification Example">
```txt
https://app.formbricks.com/s/clin3dxja02k8l80hpwmx4bjy?userId=ABC123
```
</CodeGroup>
</Col>
## How it works
To link a response to a user in your Formbricks database, you can pass your internal user Id as a URL parameter.
## Where do I find my userId?
The `userId` we are referring to is the `userId` of your own system. For example, a user signs up to your app and gets the Id `ABC123` assigned then this is the Id you pass along in the URL parameter.
This allows you to connect the response to the user profile of this specific in the Formbricks database. You can then use the response data to create segments for further surveying or invite them to an interview, etc.
<MdxImage
src={PeopleView}
alt="If users are identified by email, it will show. If not, the internal Id will be set."
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -32,7 +32,6 @@ export const navigation: Array<NavGroup> = [
title: "Features",
children: [
{ title: "Data Prefilling", href: "/link-surveys/data-prefilling" },
{ title: "Identify Users", href: "/link-surveys/user-identification" },
{ title: "Single Use Links", href: "/link-surveys/single-use-links" },
{ title: "Source Tracking", href: "/link-surveys/source-tracking" },
{ title: "Hidden Fields", href: "/link-surveys/hidden-fields" },

View File

@@ -5,13 +5,10 @@ import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware"
import {
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromProjectId,
getOrganizationIdFromSegmentId,
getOrganizationIdFromSurveyId,
getProjectIdFromEnvironmentId,
getProjectIdFromSegmentId,
getProjectIdFromSurveyId,
} from "@/lib/utils/helper";
import { getSegment, getSurvey } from "@/lib/utils/services";
import { getSurveyFollowUpsPermission } from "@/modules/ee/license-check/lib/utils";
import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions";
import { z } from "zod";
@@ -19,18 +16,10 @@ import { createActionClass } from "@formbricks/lib/actionClass/service";
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@formbricks/lib/constants";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getProject } from "@formbricks/lib/project/service";
import {
cloneSegment,
createSegment,
resetSegmentInSurvey,
updateSegment,
} from "@formbricks/lib/segment/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { loadNewSegmentInSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { updateSurvey } from "@formbricks/lib/survey/service";
import { ZActionClassInput } from "@formbricks/types/action-classes";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZBaseFilters, ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
import { ZSurvey } from "@formbricks/types/surveys/types";
/**
@@ -110,217 +99,6 @@ export const refetchProjectAction = authenticatedActionClient
return await getProject(parsedInput.projectId);
});
const ZCreateBasicSegmentAction = z.object({
description: z.string().optional(),
environmentId: ZId,
filters: ZBaseFilters,
isPrivate: z.boolean(),
surveyId: ZId,
title: z.string(),
});
export const createBasicSegmentAction = authenticatedActionClient
.schema(ZCreateBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
const surveyEnvironment = await getSurvey(parsedInput.surveyId);
if (!surveyEnvironment) {
throw new Error("Survey not found");
}
if (surveyEnvironment.environmentId !== parsedInput.environmentId) {
throw new Error("Survey and segment are not in the same environment");
}
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(surveyEnvironment.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
const parsedFilters = ZSegmentFilters.safeParse(parsedInput.filters);
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
throw new Error(errMsg);
}
const segment = await createSegment({
environmentId: parsedInput.environmentId,
surveyId: parsedInput.surveyId,
title: parsedInput.title,
description: parsedInput.description || "",
isPrivate: parsedInput.isPrivate,
filters: parsedInput.filters,
});
surveyCache.revalidate({ id: parsedInput.surveyId });
return segment;
});
const ZUpdateBasicSegmentAction = z.object({
segmentId: ZId,
data: ZSegmentUpdateInput,
});
export const updateBasicSegmentAction = authenticatedActionClient
.schema(ZUpdateBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
access: [
{
schema: ZSegmentUpdateInput,
data: parsedInput.data,
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
},
],
});
const { filters } = parsedInput.data;
if (filters) {
const parsedFilters = ZSegmentFilters.safeParse(filters);
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
throw new Error(errMsg);
}
}
return await updateSegment(parsedInput.segmentId, parsedInput.data);
});
const ZLoadNewBasicSegmentAction = z.object({
surveyId: ZId,
segmentId: ZId,
});
export const loadNewBasicSegmentAction = authenticatedActionClient
.schema(ZLoadNewBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
const surveyEnvironment = await getSurvey(parsedInput.surveyId);
const segmentEnvironment = await getSegment(parsedInput.segmentId);
if (!surveyEnvironment || !segmentEnvironment) {
if (!surveyEnvironment) {
throw new Error("Survey not found");
}
if (!segmentEnvironment) {
throw new Error("Segment not found");
}
}
if (surveyEnvironment.environmentId !== segmentEnvironment.environmentId) {
throw new Error("Segment and survey are not in the same environment");
}
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
return await loadNewSegmentInSurvey(parsedInput.surveyId, parsedInput.segmentId);
});
const ZCloneBasicSegmentAction = z.object({
segmentId: ZId,
surveyId: ZId,
});
export const cloneBasicSegmentAction = authenticatedActionClient
.schema(ZCloneBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
const surveyEnvironment = await getSurvey(parsedInput.surveyId);
const segmentEnvironment = await getSegment(parsedInput.segmentId);
if (!surveyEnvironment || !segmentEnvironment) {
if (!surveyEnvironment) {
throw new Error("Survey not found");
}
if (!segmentEnvironment) {
throw new Error("Segment not found");
}
}
if (surveyEnvironment.environmentId !== segmentEnvironment.environmentId) {
throw new Error("Segment and survey are not in the same environment");
}
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
return await cloneSegment(parsedInput.segmentId, parsedInput.surveyId);
});
const ZResetBasicSegmentFiltersAction = z.object({
surveyId: ZId,
});
export const resetBasicSegmentFiltersAction = authenticatedActionClient
.schema(ZResetBasicSegmentFiltersAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
return await resetSegmentInSurvey(parsedInput.surveyId);
});
const ZGetImagesFromUnsplashAction = z.object({
searchQuery: z.string(),
page: z.number().optional(),

View File

@@ -8,7 +8,7 @@ import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -21,7 +21,7 @@ interface AddressQuestionFormProps {
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -33,7 +33,7 @@ export const AddressQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: AddressQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
@@ -109,7 +109,7 @@ export const AddressQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -127,7 +127,7 @@ export const AddressQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -1,5 +1,5 @@
import { ConditionalLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { UpdateQuestionId } from "./UpdateQuestionId";
@@ -8,7 +8,7 @@ interface AdvancedSettingsProps {
questionIdx: number;
localSurvey: TSurvey;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
}
export const AdvancedSettings = ({
@@ -16,7 +16,7 @@ export const AdvancedSettings = ({
questionIdx,
localSurvey,
updateQuestion,
attributeClasses,
contactAttributeKeys,
}: AdvancedSettingsProps) => {
return (
<div className="flex flex-col gap-4">
@@ -25,7 +25,7 @@ export const AdvancedSettings = ({
updateQuestion={updateQuestion}
localSurvey={localSurvey}
questionIdx={questionIdx}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
/>
<UpdateQuestionId

View File

@@ -8,7 +8,7 @@ import { OptionsSwitch } from "@/modules/ui/components/options-switch";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslations } from "next-intl";
import { type JSX, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -29,7 +29,7 @@ interface CTAQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -42,7 +42,7 @@ export const CTAQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: CTAQuestionFormProps): JSX.Element => {
const [firstRender, setFirstRender] = useState(true);
@@ -60,7 +60,7 @@ export const CTAQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -104,7 +104,7 @@ export const CTAQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -121,7 +121,7 @@ export const CTAQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
)}
@@ -156,7 +156,7 @@ export const CTAQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -7,7 +7,7 @@ import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -20,7 +20,7 @@ interface CalQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -32,7 +32,7 @@ export const CalQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
isInvalid,
attributeClasses,
contactAttributeKeys,
locale,
}: CalQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
@@ -60,7 +60,7 @@ export const CalQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div>
@@ -77,7 +77,7 @@ export const CalQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -26,7 +26,7 @@ import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
interface ConditionalLogicProps {
@@ -34,11 +34,11 @@ interface ConditionalLogicProps {
questionIdx: number;
question: TSurveyQuestion;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
}
export function ConditionalLogic({
attributeClasses,
contactAttributeKeys,
localSurvey,
question,
questionIdx,
@@ -46,11 +46,11 @@ export function ConditionalLogic({
}: ConditionalLogicProps) {
const t = useTranslations();
const transformedSurvey = useMemo(() => {
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", attributeClasses);
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", attributeClasses);
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", contactAttributeKeys);
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", contactAttributeKeys);
return modifiedSurvey;
}, [localSurvey, attributeClasses]);
}, [localSurvey, contactAttributeKeys]);
const addLogic = () => {
const operator = getDefaultOperatorForQuestion(question);

View File

@@ -5,7 +5,7 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { Label } from "@/modules/ui/components/label";
import { useTranslations } from "next-intl";
import { type JSX, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -17,7 +17,7 @@ interface ConsentQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -29,7 +29,7 @@ export const ConsentQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: ConsentQuestionFormProps): JSX.Element => {
const [firstRender, setFirstRender] = useState(true);
@@ -46,7 +46,7 @@ export const ConsentQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -80,7 +80,7 @@ export const ConsentQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</form>

View File

@@ -8,7 +8,7 @@ import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -21,7 +21,7 @@ interface ContactInfoQuestionFormProps {
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -33,7 +33,7 @@ export const ContactInfoQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: ContactInfoQuestionFormProps): JSX.Element => {
const t = useTranslations();
@@ -99,7 +99,7 @@ export const ContactInfoQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -117,7 +117,7 @@ export const ContactInfoQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -7,7 +7,7 @@ import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -20,7 +20,7 @@ interface IDateQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -47,7 +47,7 @@ export const DateQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: IDateQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
@@ -65,7 +65,7 @@ export const DateQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div ref={parent}>
@@ -82,7 +82,7 @@ export const DateQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -20,7 +20,7 @@ import { useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import {
TSurvey,
@@ -39,7 +39,7 @@ interface EditEndingCardProps {
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
plan: TOrganizationBillingPlan;
addEndingCard: (index: number) => void;
isFormbricksCloud: boolean;
@@ -55,7 +55,7 @@ export const EditEndingCard = ({
isInvalid,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
plan,
addEndingCard,
isFormbricksCloud,
@@ -205,7 +205,7 @@ export const EditEndingCard = ({
localSurvey,
true,
selectedLanguageCode,
attributeClasses
contactAttributeKeys
)[selectedLanguageCode]
? formatTextWithSlashes(
recallToHeadline(
@@ -213,7 +213,7 @@ export const EditEndingCard = ({
localSurvey,
true,
selectedLanguageCode,
attributeClasses
contactAttributeKeys
)[selectedLanguageCode]
)
: t("environments.surveys.edit.ending_card"))}
@@ -274,7 +274,7 @@ export const EditEndingCard = ({
isInvalid={isInvalid}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
updateSurvey={updateSurvey}
endingCard={endingCard}
locale={locale}

View File

@@ -11,7 +11,7 @@ import { useTranslations } from "next-intl";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionId, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -23,7 +23,7 @@ interface EditWelcomeCardProps {
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -35,7 +35,7 @@ export const EditWelcomeCard = ({
isInvalid,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: EditWelcomeCardProps) => {
const t = useTranslations();
@@ -136,7 +136,7 @@ export const EditWelcomeCard = ({
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -173,7 +173,7 @@ export const EditWelcomeCard = ({
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
label={t("environments.surveys.edit.next_button_label")}
locale={locale}
/>

View File

@@ -7,7 +7,7 @@ import { Switch } from "@/modules/ui/components/switch";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyEndScreenCard } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -17,7 +17,7 @@ interface EndScreenFormProps {
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
updateSurvey: (input: Partial<TSurveyEndScreenCard>) => void;
endingCard: TSurveyEndScreenCard;
locale: TUserLocale;
@@ -29,7 +29,7 @@ export const EndScreenForm = ({
isInvalid,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
updateSurvey,
endingCard,
locale,
@@ -51,7 +51,7 @@ export const EndScreenForm = ({
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -65,7 +65,7 @@ export const EndScreenForm = ({
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div className="mt-4">
@@ -111,7 +111,7 @@ export const EndScreenForm = ({
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -11,9 +11,10 @@ import { useTranslations } from "next-intl";
import Link from "next/link";
import { type JSX, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { createI18nString } from "@formbricks/lib/i18n/utils";
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TProject } from "@formbricks/types/project";
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -28,7 +29,7 @@ interface FileUploadFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
isFormbricksCloud: boolean;
locale: TUserLocale;
}
@@ -42,7 +43,7 @@ export const FileUploadQuestionForm = ({
project,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
isFormbricksCloud,
locale,
}: FileUploadFormProps): JSX.Element => {
@@ -140,7 +141,7 @@ export const FileUploadQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div ref={parent}>
@@ -157,7 +158,7 @@ export const FileUploadQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -9,7 +9,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { isLabelValidForAllLanguages } from "../lib/validation";
@@ -23,7 +23,7 @@ interface MatrixQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -35,7 +35,7 @@ export const MatrixQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: MatrixQuestionFormProps): JSX.Element => {
const languageCodes = extractLanguageCodes(localSurvey.languages);
@@ -117,7 +117,7 @@ export const MatrixQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div ref={parent}>
@@ -134,7 +134,7 @@ export const MatrixQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -178,7 +178,7 @@ export const MatrixQuestionForm = ({
isInvalid={
isInvalid && !isLabelValidForAllLanguages(question.rows[index], localSurvey.languages)
}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
{question.rows.length > 2 && (
@@ -230,7 +230,7 @@ export const MatrixQuestionForm = ({
isInvalid={
isInvalid && !isLabelValidForAllLanguages(question.columns[index], localSurvey.languages)
}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
{question.columns.length > 2 && (

View File

@@ -14,7 +14,7 @@ import { useTranslations } from "next-intl";
import { type JSX, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TI18nString,
TShuffleOption,
@@ -34,7 +34,7 @@ interface MultipleChoiceQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -46,7 +46,7 @@ export const MultipleChoiceQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: MultipleChoiceQuestionFormProps): JSX.Element => {
const t = useTranslations();
@@ -180,7 +180,7 @@ export const MultipleChoiceQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -198,7 +198,7 @@ export const MultipleChoiceQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -267,7 +267,7 @@ export const MultipleChoiceQuestionForm = ({
question={question}
updateQuestion={updateQuestion}
surveyLanguageCodes={surveyLanguageCodes}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
))}

View File

@@ -8,7 +8,7 @@ import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -21,7 +21,7 @@ interface NPSQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -34,7 +34,7 @@ export const NPSQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: NPSQuestionFormProps): JSX.Element => {
const t = useTranslations();
@@ -54,7 +54,7 @@ export const NPSQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -72,7 +72,7 @@ export const NPSQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -108,7 +108,7 @@ export const NPSQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -123,7 +123,7 @@ export const NPSQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -143,7 +143,7 @@ export const NPSQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -9,7 +9,7 @@ import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIco
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TSurvey,
TSurveyOpenTextQuestion,
@@ -34,7 +34,7 @@ interface OpenQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -46,7 +46,7 @@ export const OpenQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: OpenQuestionFormProps): JSX.Element => {
const t = useTranslations();
@@ -74,7 +74,7 @@ export const OpenQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
label={t("environments.surveys.edit.question") + "*"}
locale={locale}
/>
@@ -92,7 +92,7 @@ export const OpenQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
label={t("common.description")}
locale={locale}
/>
@@ -129,7 +129,7 @@ export const OpenQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
label={t("common.placeholder")}
locale={locale}
/>

View File

@@ -10,7 +10,7 @@ import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { cn } from "@formbricks/lib/cn";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -23,7 +23,7 @@ interface PictureSelectionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -35,7 +35,7 @@ export const PictureSelectionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
isInvalid,
attributeClasses,
contactAttributeKeys,
locale,
}: PictureSelectionFormProps): JSX.Element => {
const environmentId = localSurvey.environmentId;
@@ -84,7 +84,7 @@ export const PictureSelectionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div ref={parent}>
@@ -101,7 +101,7 @@ export const PictureSelectionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -16,7 +16,7 @@ import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TProject } from "@formbricks/types/project";
import {
TI18nString,
@@ -56,7 +56,7 @@ interface QuestionCardProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
addQuestion: (question: any, index?: number) => void;
isFormbricksCloud: boolean;
isCxMode: boolean;
@@ -78,7 +78,7 @@ export const QuestionCard = ({
selectedLanguageCode,
setSelectedLanguageCode,
isInvalid,
attributeClasses,
contactAttributeKeys,
addQuestion,
isFormbricksCloud,
isCxMode,
@@ -196,7 +196,7 @@ export const QuestionCard = ({
localSurvey,
true,
selectedLanguageCode,
attributeClasses
contactAttributeKeys
)[selectedLanguageCode]
? formatTextWithSlashes(
recallToHeadline(
@@ -204,7 +204,7 @@ export const QuestionCard = ({
localSurvey,
true,
selectedLanguageCode,
attributeClasses
contactAttributeKeys
)[selectedLanguageCode] ?? ""
)
: getTSurveyQuestionTypeEnumName(question.type, locale)}
@@ -249,7 +249,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ? (
@@ -262,7 +262,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ? (
@@ -275,7 +275,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.NPS ? (
@@ -288,7 +288,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.CTA ? (
@@ -301,7 +301,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Rating ? (
@@ -314,7 +314,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Consent ? (
@@ -326,7 +326,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Date ? (
@@ -339,7 +339,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.PictureSelection ? (
@@ -352,7 +352,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.FileUpload ? (
@@ -366,7 +366,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
isFormbricksCloud={isFormbricksCloud}
locale={locale}
/>
@@ -380,7 +380,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Matrix ? (
@@ -393,7 +393,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Address ? (
@@ -406,7 +406,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Ranking ? (
@@ -419,7 +419,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.ContactInfo ? (
@@ -432,7 +432,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : null}
@@ -481,7 +481,7 @@ export const QuestionCard = ({
localSurvey.questions.length - 1
);
}}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -498,7 +498,7 @@ export const QuestionCard = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
onBlur={(e) => {
if (!question.backButtonLabel) return;
@@ -528,7 +528,7 @@ export const QuestionCard = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -539,7 +539,7 @@ export const QuestionCard = ({
questionIdx={questionIdx}
localSurvey={localSurvey}
updateQuestion={updateQuestion}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
/>
</Collapsible.CollapsibleContent>
</Collapsible.Root>

View File

@@ -6,7 +6,7 @@ import { GripVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import { createI18nString } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TI18nString,
TSurvey,
@@ -36,7 +36,7 @@ interface ChoiceProps {
updatedAttributes: Partial<TSurveyMultipleChoiceQuestion> | Partial<TSurveyRankingQuestion>
) => void;
surveyLanguageCodes: string[];
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -55,7 +55,7 @@ export const QuestionOptionChoice = ({
question,
surveyLanguageCodes,
updateQuestion,
attributeClasses,
contactAttributeKeys,
locale,
}: ChoiceProps) => {
const t = useTranslations();
@@ -97,7 +97,7 @@ export const QuestionOptionChoice = ({
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
}
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
{choice.id === "other" && (
@@ -119,7 +119,7 @@ export const QuestionOptionChoice = ({
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
}
className="border border-dashed"
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
)}

View File

@@ -1,6 +1,6 @@
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TProject } from "@formbricks/types/project";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -19,7 +19,7 @@ interface QuestionsDraggableProps {
setSelectedLanguageCode: (language: string) => void;
invalidQuestions: string[] | null;
internalQuestionIdMap: Record<string, string>;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
addQuestion: (question: any, index?: number) => void;
isFormbricksCloud: boolean;
isCxMode: boolean;
@@ -39,7 +39,7 @@ export const QuestionsDroppable = ({
setSelectedLanguageCode,
updateQuestion,
internalQuestionIdMap,
attributeClasses,
contactAttributeKeys,
addQuestion,
isFormbricksCloud,
isCxMode,
@@ -67,7 +67,7 @@ export const QuestionsDroppable = ({
setActiveQuestionId={setActiveQuestionId}
lastQuestion={questionIdx === localSurvey.questions.length - 1}
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
addQuestion={addQuestion}
isFormbricksCloud={isFormbricksCloud}
isCxMode={isCxMode}

View File

@@ -23,7 +23,7 @@ import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import { TProject } from "@formbricks/types/project";
import {
@@ -60,7 +60,7 @@ interface QuestionsViewProps {
setSelectedLanguageCode: (languageCode: string) => void;
isMultiLanguageAllowed?: boolean;
isFormbricksCloud: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
plan: TOrganizationBillingPlan;
isCxMode: boolean;
locale: TUserLocale;
@@ -78,7 +78,7 @@ export const QuestionsView = ({
selectedLanguageCode,
isMultiLanguageAllowed,
isFormbricksCloud,
attributeClasses,
contactAttributeKeys,
plan,
isCxMode,
locale,
@@ -434,7 +434,7 @@ export const QuestionsView = ({
isInvalid={invalidQuestions ? invalidQuestions.includes("start") : false}
setSelectedLanguageCode={setSelectedLanguageCode}
selectedLanguageCode={selectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -458,7 +458,7 @@ export const QuestionsView = ({
setActiveQuestionId={setActiveQuestionId}
invalidQuestions={invalidQuestions}
internalQuestionIdMap={internalQuestionIdMap}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
addQuestion={addQuestion}
isFormbricksCloud={isFormbricksCloud}
isCxMode={isCxMode}
@@ -487,7 +487,7 @@ export const QuestionsView = ({
isInvalid={invalidQuestions ? invalidQuestions.includes(ending.id) : false}
setSelectedLanguageCode={setSelectedLanguageCode}
selectedLanguageCode={selectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
plan={plan}
addEndingCard={addEndingCard}
isFormbricksCloud={isFormbricksCloud}

View File

@@ -12,7 +12,7 @@ import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect, useRef, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TI18nString, TSurvey, TSurveyRankingQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionOptionChoice } from "./QuestionOptionChoice";
@@ -26,7 +26,7 @@ interface RankingQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -38,7 +38,7 @@ export const RankingQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: RankingQuestionFormProps): JSX.Element => {
const t = useTranslations();
@@ -131,7 +131,7 @@ export const RankingQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -149,7 +149,7 @@ export const RankingQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -214,7 +214,7 @@ export const RankingQuestionForm = ({
question={question}
updateQuestion={updateQuestion}
surveyLanguageCodes={surveyLanguageCodes}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
))}

View File

@@ -6,7 +6,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Dropdown } from "./RatingTypeDropdown";
@@ -20,7 +20,7 @@ interface RatingQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -32,7 +32,7 @@ export const RatingQuestionForm = ({
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
contactAttributeKeys,
locale,
}: RatingQuestionFormProps) => {
const t = useTranslations();
@@ -50,7 +50,7 @@ export const RatingQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
@@ -68,7 +68,7 @@ export const RatingQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -144,7 +144,7 @@ export const RatingQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -160,7 +160,7 @@ export const RatingQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>
@@ -180,7 +180,7 @@ export const RatingQuestionForm = ({
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -1,7 +1,8 @@
import { AdvancedTargetingCard } from "@/modules/ee/advanced-targeting/components/advanced-targeting-card";
import { TargetingLockedCard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/TargetingLockedCard";
import { TargetingCard } from "@/modules/ee/contacts/segments/components/targeting-card";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/teams";
import { TActionClass } from "@formbricks/types/action-classes";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TSegment } from "@formbricks/types/segment";
@@ -10,7 +11,6 @@ import { HowToSendCard } from "./HowToSendCard";
import { RecontactOptionsCard } from "./RecontactOptionsCard";
import { ResponseOptionsCard } from "./ResponseOptionsCard";
import { SurveyPlacementCard } from "./SurveyPlacementCard";
import { TargetingCard } from "./TargetingCard";
import { WhenToSendCard } from "./WhenToSendCard";
interface SettingsViewProps {
@@ -18,12 +18,11 @@ interface SettingsViewProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;
actionClasses: TActionClass[];
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
segments: TSegment[];
responseCount: number;
membershipRole?: TOrganizationRole;
isUserTargetingAllowed?: boolean;
isFormbricksCloud: boolean;
locale: string;
projectPermission: TTeamPermission | null;
}
@@ -33,12 +32,11 @@ export const SettingsView = ({
localSurvey,
setLocalSurvey,
actionClasses,
attributeClasses,
contactAttributeKeys,
segments,
responseCount,
membershipRole,
isUserTargetingAllowed = false,
isFormbricksCloud,
locale,
projectPermission,
}: SettingsViewProps) => {
@@ -55,27 +53,22 @@ export const SettingsView = ({
{localSurvey.type === "app" ? (
<div>
{!isUserTargetingAllowed ? (
<TargetingCard
key={localSurvey.segment?.id}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environmentId={environment.id}
attributeClasses={attributeClasses}
segments={segments}
initialSegment={segments.find((segment) => segment.id === localSurvey.segment?.id)}
isFormbricksCloud={isFormbricksCloud}
/>
{isUserTargetingAllowed ? (
<div className="relative">
<div className="blur-none">
<TargetingCard
key={localSurvey.segment?.id}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environmentId={environment.id}
contactAttributeKeys={contactAttributeKeys}
segments={segments}
initialSegment={segments.find((segment) => segment.id === localSurvey.segment?.id)}
/>
</div>
</div>
) : (
<AdvancedTargetingCard
key={localSurvey.segment?.id}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environmentId={environment.id}
attributeClasses={attributeClasses}
segments={segments}
initialSegment={segments.find((segment) => segment.id === localSurvey.segment?.id)}
/>
<TargetingLockedCard />
)}
</div>
) : null}

View File

@@ -8,7 +8,7 @@ import { extractLanguageCodes, getEnabledLanguages } from "@formbricks/lib/i18n/
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { useDocumentVisibility } from "@formbricks/lib/useDocumentVisibility";
import { TActionClass } from "@formbricks/types/action-classes";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
@@ -29,7 +29,7 @@ interface SurveyEditorProps {
project: TProject;
environment: TEnvironment;
actionClasses: TActionClass[];
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
segments: TSegment[];
responseCount: number;
membershipRole?: TOrganizationRole;
@@ -52,7 +52,7 @@ export const SurveyEditor = ({
project,
environment,
actionClasses,
attributeClasses,
contactAttributeKeys,
segments,
responseCount,
membershipRole,
@@ -184,7 +184,7 @@ export const SurveyEditor = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={isFormbricksCloud}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
plan={plan}
isCxMode={isCxMode}
locale={locale}
@@ -213,12 +213,11 @@ export const SurveyEditor = ({
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
segments={segments}
responseCount={responseCount}
membershipRole={membershipRole}
isUserTargetingAllowed={isUserTargetingAllowed}
isFormbricksCloud={isFormbricksCloud}
locale={locale}
projectPermission={projectPermission}
/>

View File

@@ -2,7 +2,7 @@
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createSegmentAction } from "@/modules/ee/advanced-targeting/lib/actions";
import { createSegmentAction } from "@/modules/ee/contacts/segments/actions";
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";

View File

@@ -1,469 +0,0 @@
"use client";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
import { BasicAddFilterModal } from "@/modules/ui/components/basic-add-filter-modal";
import { BasicSegmentEditor } from "@/modules/ui/components/basic-segment-editor";
import { Button } from "@/modules/ui/components/button";
import { LoadSegmentModal } from "@/modules/ui/components/load-segment-modal";
import { SaveAsNewSegmentModal } from "@/modules/ui/components/save-as-new-segment-modal";
import { SegmentTitle } from "@/modules/ui/components/segment-title";
import { TargetingIndicator } from "@/modules/ui/components/targeting-indicator";
import { UpgradePlanNotice } from "@/modules/ui/components/upgrade-plan-notice";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { AlertCircle, CheckIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TBaseFilter, TSegment, TSegmentCreateInput, TSegmentUpdateInput } from "@formbricks/types/segment";
import { TSurvey } from "@formbricks/types/surveys/types";
import {
cloneBasicSegmentAction,
createBasicSegmentAction,
loadNewBasicSegmentAction,
resetBasicSegmentFiltersAction,
updateBasicSegmentAction,
} from "../actions";
interface TargetingCardProps {
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
environmentId: string;
attributeClasses: TAttributeClass[];
segments: TSegment[];
initialSegment?: TSegment;
isFormbricksCloud: boolean;
}
export const TargetingCard = ({
localSurvey,
setLocalSurvey,
environmentId,
attributeClasses,
segments,
initialSegment,
isFormbricksCloud,
}: TargetingCardProps) => {
const t = useTranslations();
const router = useRouter();
const [segment, setSegment] = useState<TSegment | null>(localSurvey.segment);
const [open, setOpen] = useState(false);
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
const [saveAsNewSegmentModalOpen, setSaveAsNewSegmentModalOpen] = useState(false);
const [isSegmentEditorOpen, setIsSegmentEditorOpen] = useState(!!localSurvey.segment?.isPrivate);
const [loadSegmentModalOpen, setLoadSegmentModalOpen] = useState(false);
const [resetAllFiltersModalOpen, setResetAllFiltersModalOpen] = useState(false);
const [segmentEditorViewOnly, setSegmentEditorViewOnly] = useState(true);
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 handleEditSegment = () => {
setIsSegmentEditorOpen(true);
setSegmentEditorViewOnly(false);
};
const handleCloneSegment = async () => {
if (!segment) return;
const cloneBasicSegmentResponse = await cloneBasicSegmentAction({
segmentId: segment.id,
surveyId: localSurvey.id,
});
if (cloneBasicSegmentResponse?.data) {
setSegment(cloneBasicSegmentResponse.data);
} else {
const errorMessage = getFormattedErrorMessage(cloneBasicSegmentResponse);
toast.error(errorMessage);
}
};
const handleLoadNewSegment = async (surveyId: string, segmentId: string) => {
const loadNewBasicSegmentResponse = await loadNewBasicSegmentAction({ surveyId, segmentId });
return loadNewBasicSegmentResponse?.data as TSurvey;
};
const handleSegmentUpdate = async (segmentId: string, data: TSegmentUpdateInput) => {
const updateBasicSegmentResponse = await updateBasicSegmentAction({ segmentId, data });
return updateBasicSegmentResponse?.data as TSegment;
};
const handleSegmentCreate = async (data: TSegmentCreateInput) => {
const createdSegment = await createBasicSegmentAction(data);
return createdSegment?.data as TSegment;
};
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
try {
if (!segment) throw new Error(t("environments.surveys.edit.invalid_segment"));
await updateBasicSegmentAction({ segmentId: segment?.id, data });
router.refresh();
toast.success(t("environments.surveys.edit.segment_saved_successfully"));
setIsSegmentEditorOpen(false);
setSegmentEditorViewOnly(true);
} catch (err) {
toast.error(err.message ?? t("environments.surveys.edit.error_saving_segment"));
}
};
const handleResetAllFilters = async () => {
try {
const resetBasicSegmentFiltersResponse = await resetBasicSegmentFiltersAction({
surveyId: localSurvey.id,
});
return resetBasicSegmentFiltersResponse?.data;
} catch (err) {
toast.error(t("environments.surveys.edit.error_resetting_filters"));
}
};
useEffect(() => {
if (!!segment && segment?.filters?.length > 0) {
setOpen(true);
}
}, [segment, segment?.filters?.length]);
useEffect(() => {
setLocalSurvey((localSurveyOld) => ({
...localSurveyOld,
segment: segment,
}));
}, [setLocalSurvey, segment]);
const isSegmentUsedInOtherSurveys = useMemo(
() => (localSurvey?.segment ? localSurvey.segment?.surveys?.length > 1 : false),
[localSurvey.segment]
);
const [parent] = useAutoAnimate();
return (
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className="w-full overflow-hidden rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
<div className="inline-flex px-4 py-6">
<div className="flex items-center pl-2 pr-5">
<CheckIcon
strokeWidth={3}
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
/>
</div>
<div>
<p className="font-semibold text-slate-800">{t("environments.surveys.edit.target_audience")}</p>
<p className="mt-1 text-sm text-slate-500">
{t("environments.surveys.edit.pre_segment_your_users_with_attributes_filters")}
</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="flex min-w-full flex-col overflow-auto" ref={parent}>
<hr className="text-slate-600" />
<div className="flex flex-col gap-5 p-6">
<TargetingIndicator segment={segment} />
<div className="filter-scrollbar flex flex-col gap-4 overflow-auto rounded-lg border border-slate-300 bg-slate-50 p-4">
<div className="flex w-full flex-col gap-2">
{isAdvancedSegment(segment?.filters ?? []) ? (
<div>
<SegmentTitle
title={localSurvey.segment?.title}
description={localSurvey.segment?.description}
isPrivate={localSurvey.segment?.isPrivate}
/>
<p className="text-sm italic text-slate-600">
{t(
"environments.surveys.edit.this_is_an_advanced_segment_please_upgrade_your_plan_to_edit_it"
)}
</p>
</div>
) : (
<>
{isSegmentEditorOpen ? (
<div className="flex w-full flex-col gap-2">
<div>
{!segment?.isPrivate ? (
<SegmentTitle
title={localSurvey.segment?.title}
description={localSurvey.segment?.description}
/>
) : (
<div className="mb-4">
<p className="text-sm font-semibold text-slate-800">
{t("environments.surveys.edit.send_survey_to_audience_who_match")}
</p>
</div>
)}
</div>
{!!segment?.filters?.length && (
<div className="w-full">
<BasicSegmentEditor
key={segment.filters.toString()}
group={segment.filters}
environmentId={environmentId}
segment={segment}
setSegment={setSegment}
attributeClasses={attributeClasses}
/>
</div>
)}
<div
className={cn(
"mt-3 flex items-center gap-2",
segment?.isPrivate && !segment?.filters?.length && "mt-0"
)}>
<Button variant="secondary" size="sm" onClick={() => setAddFilterModalOpen(true)}>
{t("common.add_filter")}
</Button>
{isSegmentEditorOpen && !segment?.isPrivate && (
<Button
variant="secondary"
size="sm"
onClick={() => {
handleSaveSegment({ filters: segment?.filters ?? [] });
}}>
{t("common.save_changes")}
</Button>
)}
{isSegmentEditorOpen && !segment?.isPrivate && (
<Button
variant="minimal"
size="sm"
className="flex items-center gap-2"
onClick={() => {
setIsSegmentEditorOpen(false);
setSegmentEditorViewOnly(true);
if (initialSegment) {
setSegment(initialSegment);
}
}}>
{t("common.cancel")}
</Button>
)}
</div>
</div>
) : (
<div className="flex flex-col gap-2 rounded-lg">
<SegmentTitle
title={localSurvey.segment?.title}
description={localSurvey.segment?.description}
isPrivate={localSurvey.segment?.isPrivate}
/>
{segmentEditorViewOnly && segment && (
<div className="opacity-60">
<BasicSegmentEditor
key={segment.filters.toString()}
group={segment.filters}
environmentId={environmentId}
segment={segment}
attributeClasses={attributeClasses}
setSegment={setSegment}
viewOnly
/>
</div>
)}
<div className="mt-3 flex items-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
setSegmentEditorViewOnly(!segmentEditorViewOnly);
}}>
{segmentEditorViewOnly ? t("common.hide_filters") : t("common.view_filters")}{" "}
{segmentEditorViewOnly ? (
<ChevronUpIcon className="ml-2 h-3 w-3" />
) : (
<ChevronDownIcon className="ml-2 h-3 w-3" />
)}
</Button>
{isSegmentUsedInOtherSurveys && (
<Button
variant="secondary"
size="sm"
onClick={() => {
handleCloneSegment();
}}>
{t("environments.surveys.edit.clone_edit_segment")}
</Button>
)}
{!isSegmentUsedInOtherSurveys && (
<Button
variant="secondary"
size="sm"
onClick={() => {
handleEditSegment();
}}>
{t("environments.surveys.edit.edit_segment")}
<PencilIcon className="ml-2 h-3 w-3" />
</Button>
)}
</div>
{isSegmentUsedInOtherSurveys && (
<p className="mt-1 flex items-center text-xs text-slate-500">
<AlertCircle className="mr-1 inline h-3 w-3" />
{t("environments.surveys.edit.this_segment_is_used_in_other_surveys_make_changes")}
<Link
href={`/environments/${environmentId}/segments`}
target="_blank"
className="ml-1 underline">
here.
</Link>
</p>
)}
</div>
)}
</>
)}
</div>
</div>
<div className="flex w-full gap-3">
<Button variant="secondary" size="sm" onClick={() => setLoadSegmentModalOpen(true)}>
{t("environments.surveys.edit.load_segment")}
</Button>
{!segment?.isPrivate && !!segment?.filters?.length && (
<Button variant="secondary" size="sm" onClick={() => setResetAllFiltersModalOpen(true)}>
{t("common.reset_all_filters")}
</Button>
)}
{isSegmentEditorOpen && !!segment?.filters?.length && (
<Button
variant="secondary"
size="sm"
className="flex items-center gap-2"
onClick={() => setSaveAsNewSegmentModalOpen(true)}>
{t("environments.surveys.edit.save_as_new_segment")}
</Button>
)}
</div>
<div className="-mt-1.5">
{isFormbricksCloud ? (
<UpgradePlanNotice
message={t("environments.surveys.edit.for_advanced_targeting_please")}
textForUrl={t("environments.surveys.edit.upgrade_to_the_scale_plan")}
url={`/environments/${environmentId}/settings/billing`}
/>
) : (
<UpgradePlanNotice
message={t("environments.surveys.edit.for_advanced_targeting_please")}
textForUrl={t("common.request_an_enterprise_license")}
url={`/environments/${environmentId}/settings/enterprise`}
/>
)}
</div>
</div>
{!!segment && (
<LoadSegmentModal
open={loadSegmentModalOpen}
setOpen={setLoadSegmentModalOpen}
surveyId={localSurvey.id}
currentSegment={segment}
segments={segments}
setSegment={setSegment}
setIsSegmentEditorOpen={setIsSegmentEditorOpen}
onSegmentLoad={handleLoadNewSegment}
/>
)}
<BasicAddFilterModal
onAddFilter={(filter) => {
handleAddFilterInGroup(filter);
}}
open={addFilterModalOpen}
setOpen={setAddFilterModalOpen}
attributeClasses={attributeClasses}
/>
{!!segment && (
<SaveAsNewSegmentModal
open={saveAsNewSegmentModalOpen}
setOpen={setSaveAsNewSegmentModalOpen}
localSurvey={localSurvey}
segment={segment}
setSegment={setSegment}
setIsSegmentEditorOpen={setIsSegmentEditorOpen}
onCreateSegment={handleSegmentCreate}
onUpdateSegment={handleSegmentUpdate}
/>
)}
<AlertDialog
headerText={t("common.are_you_sure")}
open={resetAllFiltersModalOpen}
setOpen={setResetAllFiltersModalOpen}
mainText={t("environments.surveys.edit.this_action_resets_all_filters_in_this_survey")}
declineBtnLabel={t("common.cancel")}
onDecline={() => {
setResetAllFiltersModalOpen(false);
}}
confirmBtnLabel={t("environments.surveys.edit.remove_all_filters")}
onConfirm={async () => {
const segment = await handleResetAllFilters();
if (segment) {
toast.success(t("common.filters_reset_successfully"));
router.refresh();
setSegment(segment);
setResetAllFiltersModalOpen(false);
}
}}
/>
<div>
<Alert className="flex items-center rounded-none bg-slate-50">
<AlertDescription className="ml-2">
<span className="mr-1 text-slate-600">
{t("environments.surveys.edit.user_targeting_is_currently_only_available_when")}
<Link
href="https://formbricks.com//docs/app-surveys/user-identification"
target="blank"
className="underline">
{t("environments.surveys.edit.identifying_users")}
</Link>{" "}
{t("environments.surveys.edit.with_the_formbricks_sdk")}
</span>
</AlertDescription>
</Alert>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.Root>
);
};

View File

@@ -0,0 +1,53 @@
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import * as Collapsible from "@radix-ui/react-collapsible";
import { LockIcon, UsersIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
export const TargetingLockedCard = () => {
const t = useTranslations();
const [open, setOpen] = useState(false);
return (
<Collapsible.Root
className="w-full overflow-hidden rounded-lg border border-slate-300 bg-white"
onOpenChange={setOpen}
open={open}>
<Collapsible.CollapsibleTrigger
asChild
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
<div className="inline-flex px-4 py-6">
<div className="flex items-center pl-2 pr-5">
<div className="rounded-full border border-slate-300 bg-slate-100 p-1">
<LockIcon className="h-4 w-4 text-slate-500" strokeWidth={3} />
</div>
</div>
<div>
<p className="font-semibold text-slate-800">{t("environments.segments.target_audience")}</p>
<p className="mt-1 text-sm text-slate-500">{t("environments.segments.pre_segment_users")}</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="min-w-full overflow-auto">
<hr className="text-slate-600" />
<div className="flex items-center justify-center">
<UpgradePrompt
icon={<UsersIcon className="h-6 w-6 text-slate-900" />}
title={t("environments.surveys.edit.unlock_targeting_title")}
description={t("environments.surveys.edit.unlock_targeting_description")}
buttons={[
{
text: t("common.start_free_trial"),
href: `https://formbricks.com/docs/self-hosting/license#30-day-trial-license-request`,
},
{
text: t("common.learn_more"),
href: `https://formbricks.com/docs/self-hosting/license#30-day-trial-license-request`,
},
]}
/>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.Root>
);
};

View File

@@ -6,7 +6,7 @@ import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils";
import { translate } from "@formbricks/lib/templates";
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TConditionGroup,
TLeftOperand,
@@ -127,7 +127,7 @@ export const getConditionValueOptions = (
export const replaceEndingCardHeadlineRecall = (
survey: TSurvey,
language: string,
attributeClasses: TAttributeClass[]
contactAttributeKeys: TContactAttributeKey[]
) => {
const modifiedSurvey = structuredClone(survey);
modifiedSurvey.endings.forEach((ending) => {
@@ -137,7 +137,7 @@ export const replaceEndingCardHeadlineRecall = (
modifiedSurvey,
false,
language,
attributeClasses
contactAttributeKeys
);
}
});

View File

@@ -1,7 +1,9 @@
import { getUserEmail } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/user";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contacts";
import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
import {
getAdvancedTargetingPermission,
getIsContactsEnabled,
getMultiLanguagePermission,
getSurveyFollowUpsPermission,
} from "@/modules/ee/license-check/lib/utils";
@@ -11,7 +13,6 @@ import { ErrorComponent } from "@/modules/ui/components/error-component";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import {
DEFAULT_LOCALE,
IS_FORMBRICKS_CLOUD,
@@ -25,7 +26,6 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSegments } from "@formbricks/lib/segment/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getUserLocale } from "@formbricks/lib/user/service";
import { SurveyEditor } from "./components/SurveyEditor";
@@ -47,7 +47,7 @@ const Page = async (props) => {
project,
environment,
actionClasses,
attributeClasses,
contactAttributeKeys,
responseCount,
organization,
session,
@@ -57,7 +57,7 @@ const Page = async (props) => {
getProjectByEnvironmentId(params.environmentId),
getEnvironment(params.environmentId),
getActionClasses(params.environmentId),
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
getContactAttributeKeys(params.environmentId),
getResponseCountBySurveyId(params.surveyId),
getOrganizationByEnvironmentId(params.environmentId),
getServerSession(authOptions),
@@ -86,7 +86,7 @@ const Page = async (props) => {
const isSurveyCreationDeletionDisabled = isMember && hasReadAccess;
const locale = session.user.id ? await getUserLocale(session.user.id) : undefined;
const isUserTargetingAllowed = await getAdvancedTargetingPermission(organization);
const isUserTargetingAllowed = await getIsContactsEnabled();
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
const isSurveyFollowUpsAllowed = await getSurveyFollowUpsPermission(organization);
@@ -96,7 +96,7 @@ const Page = async (props) => {
!survey ||
!environment ||
!actionClasses ||
!attributeClasses ||
!contactAttributeKeys ||
!project ||
!userEmail ||
isSurveyCreationDeletionDisabled
@@ -112,7 +112,7 @@ const Page = async (props) => {
project={project}
environment={environment}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
responseCount={responseCount}
membershipRole={currentUserMembership?.role}
projectPermission={projectPermission}

View File

@@ -1,54 +0,0 @@
"use server";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { z } from "zod";
import { getSegmentsByAttributeClassName } from "@formbricks/lib/segment/service";
import { ZAttributeClass } from "@formbricks/types/attribute-classes";
import { ZId } from "@formbricks/types/common";
const ZGetSegmentsByAttributeClassAction = z.object({
environmentId: ZId,
attributeClass: ZAttributeClass,
});
export const getSegmentsByAttributeClassAction = authenticatedActionClient
.schema(ZGetSegmentsByAttributeClassAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "read",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
const segments = await getSegmentsByAttributeClassName(
parsedInput.environmentId,
parsedInput.attributeClass.name
);
// segments is an array of segments, each segment has a survey array with objects with properties: id, name and status.
// We need the name of the surveys only and we need to filter out the surveys that are both in progress and not in progress.
const activeSurveys = segments
.map((segment) =>
segment.surveys.filter((survey) => survey.status === "inProgress").map((survey) => survey.name)
)
.flat();
const inactiveSurveys = segments
.map((segment) =>
segment.surveys.filter((survey) => survey.status !== "inProgress").map((survey) => survey.name)
)
.flat();
return { activeSurveys, inactiveSurveys };
});

View File

@@ -1,98 +0,0 @@
"use client";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { ErrorComponent } from "@/modules/ui/components/error-component";
import { Label } from "@/modules/ui/components/label";
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
import { TagIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { getSegmentsByAttributeClassAction } from "../actions";
interface EventActivityTabProps {
attributeClass: TAttributeClass;
}
export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps) => {
const [activeSurveys, setActiveSurveys] = useState<string[] | undefined>();
const [inactiveSurveys, setInactiveSurveys] = useState<string[] | undefined>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const t = useTranslations();
useEffect(() => {
setLoading(true);
const getSurveys = async () => {
setLoading(true);
const segmentsWithAttributeClassNameResponse = await getSegmentsByAttributeClassAction({
environmentId: attributeClass.environmentId,
attributeClass,
});
if (segmentsWithAttributeClassNameResponse?.data) {
setActiveSurveys(segmentsWithAttributeClassNameResponse.data.activeSurveys);
setInactiveSurveys(segmentsWithAttributeClassNameResponse.data.inactiveSurveys);
} else {
const errorMessage = getFormattedErrorMessage(segmentsWithAttributeClassNameResponse);
setError(new Error(errorMessage));
}
setLoading(false);
};
getSurveys();
}, [attributeClass, attributeClass.environmentId, attributeClass.id, attributeClass.name]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorComponent />;
return (
<div className="grid grid-cols-3 pb-2">
<div className="col-span-2 space-y-4 pr-6">
<div>
<Label className="text-slate-500">{t("common.active_surveys")}</Label>
{activeSurveys?.length === 0 && <p className="text-sm text-slate-900">-</p>}
{activeSurveys?.map((surveyName) => (
<p key={surveyName} className="text-sm text-slate-900">
{surveyName}
</p>
))}
</div>
<div>
<Label className="text-slate-500">{t("common.inactive_surveys")}</Label>
{inactiveSurveys?.length === 0 && <p className="text-sm text-slate-900">-</p>}
{inactiveSurveys?.map((surveyName) => (
<p key={surveyName} className="text-sm text-slate-900">
{surveyName}
</p>
))}
</div>
</div>
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
<div>
<Label className="text-xs font-normal text-slate-500">{t("common.created_at")}</Label>
<p className="text-xs text-slate-700">
{convertDateTimeStringShort(attributeClass.createdAt.toString())}
</p>
</div>{" "}
<div>
<Label className="text-xs font-normal text-slate-500">{t("common.updated_at")}</Label>
<p className="text-xs text-slate-700">
{convertDateTimeStringShort(attributeClass.updatedAt.toString())}
</p>
</div>
<div>
<Label className="block text-xs font-normal text-slate-500">{t("common.type")}</Label>
<div className="mt-1 flex items-center">
<div className="mr-1.5 h-4 w-4 text-slate-600">
<TagIcon className="h-4 w-4" />
</div>
<p className="text-sm text-slate-700">{capitalizeFirstLetter(attributeClass.type)}</p>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,92 +0,0 @@
"use client";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TUserLocale } from "@formbricks/types/user";
import { AttributeDetailModal } from "./AttributeDetailModal";
import { AttributeClassDataRow } from "./AttributeRowData";
interface AttributeClassesTableProps {
attributeClasses: TAttributeClass[];
locale: TUserLocale;
isReadOnly: boolean;
}
export const AttributeClassesTable = ({
attributeClasses,
locale,
isReadOnly,
}: AttributeClassesTableProps) => {
const [isAttributeDetailModalOpen, setAttributeDetailModalOpen] = useState(false);
const [activeAttributeClass, setActiveAttributeClass] = useState<TAttributeClass | null>(null);
const [showArchived, setShowArchived] = useState(false);
const t = useTranslations();
const displayedAttributeClasses = useMemo(() => {
return attributeClasses
? showArchived
? attributeClasses
: attributeClasses.filter((ac) => !ac.archived)
: [];
}, [showArchived, attributeClasses]);
const hasArchived = useMemo(() => {
return attributeClasses ? attributeClasses.some((ac) => ac.archived) : false;
}, [attributeClasses]);
const handleOpenAttributeDetailModalClick = (attributeClass: TAttributeClass) => {
setActiveAttributeClass(attributeClass);
setAttributeDetailModalOpen(true);
};
const toggleShowArchived = () => {
setShowArchived(!showArchived);
};
return (
<>
{hasArchived && (
<div className="my-4 flex items-center justify-end text-right">
<div className="flex items-center text-sm font-medium">
<Label htmlFor="showArchivedToggle" className="cursor-pointer">
{t("environments.attributes.show_archived")}
</Label>
<Switch
id="showArchivedToggle"
className="mx-3"
checked={showArchived}
onCheckedChange={toggleShowArchived}
/>
</div>
</div>
)}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-5 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6">{t("common.name")}</div>
<div className="hidden text-center sm:block">{t("common.created_at")}</div>
<div className="hidden text-center sm:block">{t("common.updated_at")}</div>
</div>
<div className="grid-cols-7">
{displayedAttributeClasses.map((attributeClass, index) => (
<button
onClick={() => handleOpenAttributeDetailModalClick(attributeClass)}
className="w-full cursor-default"
key={attributeClass.id}>
<AttributeClassDataRow attributeClass={attributeClass} key={index} locale={locale} />
</button>
))}
</div>
{activeAttributeClass && (
<AttributeDetailModal
open={isAttributeDetailModalOpen}
setOpen={setAttributeDetailModalOpen}
attributeClass={activeAttributeClass}
isReadOnly={isReadOnly}
/>
)}
</div>
</>
);
};

View File

@@ -1,49 +0,0 @@
"use client";
import { ModalWithTabs } from "@/modules/ui/components/modal-with-tabs";
import { TagIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { AttributeActivityTab } from "./AttributeActivityTab";
import { AttributeSettingsTab } from "./AttributeSettingsTab";
interface AttributeDetailModalProps {
open: boolean;
setOpen: (v: boolean) => void;
attributeClass: TAttributeClass;
isReadOnly: boolean;
}
export const AttributeDetailModal = ({
open,
setOpen,
attributeClass,
isReadOnly,
}: AttributeDetailModalProps) => {
const t = useTranslations();
const tabs = [
{
title: t("common.activity"),
children: <AttributeActivityTab attributeClass={attributeClass} />,
},
{
title: t("common.settings"),
children: (
<AttributeSettingsTab attributeClass={attributeClass} setOpen={setOpen} isReadOnly={isReadOnly} />
),
},
];
return (
<>
<ModalWithTabs
open={open}
setOpen={setOpen}
tabs={tabs}
icon={<TagIcon className="h-5 w-5" />}
label={attributeClass.name}
description={attributeClass.description || ""}
/>
</>
);
};

View File

@@ -1,31 +0,0 @@
import { Badge } from "@/modules/ui/components/badge";
import { TagIcon } from "lucide-react";
import { timeSince } from "@formbricks/lib/time";
export const AttributeClassDataRow = ({ attributeClass, locale }) => {
return (
<div className="m-2 grid h-16 cursor-pointer grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-5 flex items-center pl-6 text-sm sm:col-span-3">
<div className="flex items-center">
<TagIcon className="h-5 w-5 flex-shrink-0 text-slate-500" />
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
{attributeClass.name}
<span className="ml-2">
{attributeClass.archived && <Badge text="Archived" type="gray" size="tiny" />}
</span>
</div>
<div className="text-xs text-slate-400">{attributeClass.description}</div>
</div>
</div>
</div>
<div className="my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 md:block">
<div className="text-slate-900">{timeSince(attributeClass.createdAt.toString(), locale)}</div>
</div>
<div className="my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 md:block">
<div className="text-slate-900">{timeSince(attributeClass.updatedAt.toString(), locale)}</div>
</div>
</div>
);
};

View File

@@ -1,119 +0,0 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import type { AttributeClass } from "@prisma/client";
import { ArchiveIcon, ArchiveXIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { updateAttributeClass } from "@formbricks/lib/attributeClass/service";
interface AttributeSettingsTabProps {
attributeClass: AttributeClass;
setOpen: (v: boolean) => void;
isReadOnly: boolean;
}
export const AttributeSettingsTab = async ({
attributeClass,
setOpen,
isReadOnly,
}: AttributeSettingsTabProps) => {
const router = useRouter();
const t = useTranslations();
const { register, handleSubmit } = useForm({
defaultValues: { name: attributeClass.name, description: attributeClass.description },
});
const [isAttributeBeingSubmitted, setisAttributeBeingSubmitted] = useState(false);
const onSubmit = async (data) => {
setisAttributeBeingSubmitted(true);
setOpen(false);
await updateAttributeClass(attributeClass.id, data);
router.refresh();
setisAttributeBeingSubmitted(false);
};
const handleArchiveToggle = async () => {
setisAttributeBeingSubmitted(true);
const data = { archived: !attributeClass.archived };
await updateAttributeClass(attributeClass.id, data);
setisAttributeBeingSubmitted(false);
};
return (
<div>
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
<div>
<Label className="text-slate-600">{t("common.name")}</Label>
<Input
type="text"
placeholder={t("environments.attributes.ex_user_property")}
{...register("name", {
disabled: attributeClass.type === "automatic" || attributeClass.type === "code" ? true : false,
})}
/>
</div>
<div>
<Label className="text-slate-600">{t("common.description")}</Label>
<Input
type="text"
placeholder={t("environments.attributes.ex_user_property")}
{...register("description", {
disabled: attributeClass.type === "automatic" || isReadOnly ? true : false,
})}
/>
</div>
<div className="my-6">
<Label>{t("common.attribute_type")}</Label>
{attributeClass.type === "code" ? (
<p className="text-sm text-slate-600">
{t("environments.attributes.this_is_a_code_attribute_you_can_only_change_the_description")}
</p>
) : attributeClass.type === "automatic" ? (
<p className="text-sm text-slate-600">
{t(
"environments.attributes.this_attribute_was_added_automatically_you_cannot_make_changes_to_it"
)}
</p>
) : null}
</div>
<div className="flex justify-between border-t border-slate-200 pt-6">
<div className="flex items-center">
<Button
variant="secondary"
href="https://formbricks.com/docs/attributes/identify-users"
target="_blank">
{t("common.read_docs")}
</Button>
{attributeClass.type !== "automatic" && (
<Button
className="ml-3"
variant="secondary"
onClick={handleArchiveToggle}
StartIcon={attributeClass.archived ? ArchiveIcon : ArchiveXIcon}
startIconClassName="h-4 w-4"
disabled={isReadOnly}>
{attributeClass.archived ? (
<span>{t("common.unarchive")}</span>
) : (
<span>{t("common.archive")}</span>
)}
</Button>
)}
</div>
{!isReadOnly && attributeClass.type !== "automatic" && (
<div className="flex space-x-2">
<Button type="submit" loading={isAttributeBeingSubmitted}>
{t("common.save_changes")}
</Button>
</div>
)}
</div>
</form>
</div>
);
};

View File

@@ -1,54 +0,0 @@
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { TagIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
const Loading = async () => {
const t = await getTranslations();
return (
<>
<PageContentWrapper>
<PageHeader pageTitle={t("common.people")}>
<PersonSecondaryNavigation activeId="attributes" loading />
</PageHeader>
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-5 content-center border-b text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6">{t("common.name")}</div>
<div className="text-center">{t("common.created_at")}</div>
<div className="text-center">{t("common.updated_at")}</div>
</div>
<div className="w-full">
{[...Array(3)].map((_, index) => (
<div
key={index}
className="m-2 grid h-16 grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<TagIcon className="h-5 w-5 flex-shrink-0 animate-pulse text-slate-500" />
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="mt-1 text-xs text-slate-400">
<div className="h-2 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
</div>
</div>
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="m-4 h-4 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
))}
</div>
</div>
</PageContentWrapper>
</>
);
};
export default Loading;

View File

@@ -1,77 +0,0 @@
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { Button } from "@/modules/ui/components/button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { CircleHelpIcon } from "lucide-react";
import { Metadata } from "next";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { AttributeClassesTable } from "./components/AttributeClassesTable";
export const metadata: Metadata = {
title: "Attributes",
};
const Page = async (props) => {
const params = await props.params;
let attributeClasses = await getAttributeClasses(params.environmentId);
const t = await getTranslations();
const project = await getProjectByEnvironmentId(params.environmentId);
const locale = await findMatchingLocale();
if (!project) {
throw new Error(t("common.project_not_found"));
}
const [organization, session] = await Promise.all([
getOrganizationByEnvironmentId(params.environmentId),
getServerSession(authOptions),
]);
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
if (!session) {
throw new Error(t("common.session_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
const HowToAddAttributesButton = (
<Button
size="sm"
href="https://formbricks.com/docs/app-surveys/user-identification#setting-custom-user-attributes"
variant="secondary"
target="_blank"
EndIcon={CircleHelpIcon}>
{t("environments.attributes.how_to_add_attributes")}
</Button>
);
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.people")} cta={HowToAddAttributesButton}>
<PersonSecondaryNavigation activeId="attributes" environmentId={params.environmentId} />
</PageHeader>
<AttributeClassesTable attributeClasses={attributeClasses} locale={locale} isReadOnly={isReadOnly} />
</PageContentWrapper>
);
};
export default Page;

View File

@@ -1,91 +0,0 @@
import { BackIcon } from "@/modules/ui/components/icons";
import { ArrowDownUpIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
const Loading = () => {
const t = useTranslations();
return (
<div>
<main className="mx-auto px-4 sm:px-6 lg:px-8">
<div className="pointer-events-none animate-pulse cursor-not-allowed select-none">
<button className="inline-flex pt-5 text-sm text-slate-500">
<BackIcon className="mr-2 h-5 w-5" />
Back
</button>
</div>
<div className="flex items-baseline justify-between border-b border-slate-200 pb-6 pt-4">
<h1 className="ph-no-capture text-4xl font-bold tracking-tight text-slate-900">
<span className="animate-pulse rounded-full">{t("environments.people.fetching_user")}</span>
</h1>
<div className="flex items-center space-x-3">
<button className="pointer-events-none animate-pulse cursor-not-allowed select-none">
<TrashIcon className="h-5 w-5 text-slate-500 hover:text-red-700" />
</button>
</div>
</div>
<section className="pb-24 pt-6">
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
<div className="space-y-6">
<h2 className="text-lg font-bold text-slate-700">{t("common.attributes")}</h2>
<div>
<dt className="text-sm font-medium text-slate-500">{t("common.email")}</dt>
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
<span className="animate-pulse text-slate-300">{t("common.loading")}</span>
</dd>
</div>
<div>
<dt className="text-sm font-medium text-slate-500">{t("common.user_id")}</dt>
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
<span className="animate-pulse text-slate-300">{t("common.loading")}</span>
</dd>
</div>
<div>
<dt className="text-sm font-medium text-slate-500">
{t("environments.people.formbricks_id")}
</dt>
<dd className="mt-1 animate-pulse text-sm text-slate-300">{t("common.loading")}</dd>
</div>
<div>
<dt className="text-sm font-medium text-slate-500">{t("environments.people.sessions")}</dt>
<dd className="mt-1 animate-pulse text-sm text-slate-300">{t("common.loading")}</dd>
</div>
<div>
<dt className="text-sm font-medium text-slate-500">{t("common.responses")}</dt>
<dd className="mt-1 animate-pulse text-sm text-slate-300">{t("common.loading")}</dd>
</div>
</div>
<div className="md:col-span-3">
<div className="flex items-center justify-between pb-6">
<h2 className="text-lg font-bold text-slate-700">{t("common.responses")}</h2>
<div className="text-right">
<button className="hover:text-brand-dark pointer-events-none flex animate-pulse cursor-not-allowed select-none items-center px-1 text-slate-800">
<ArrowDownUpIcon className="inline h-4 w-4" />
</button>
</div>
</div>
<div className="group space-y-4 rounded-lg bg-white p-6">
<div className="flex items-center space-x-4">
<div className="h-12 w-12 flex-shrink-0 rounded-full bg-slate-100"></div>
<div className="h-6 w-full rounded-full bg-slate-100"></div>
</div>
<div className="space-y-4">
<div className="h-12 w-full rounded-full bg-slate-100"></div>
<div className="flex h-12 w-full items-center justify-center rounded-full bg-slate-50 text-sm text-slate-500 hover:bg-slate-100">
<span className="animate-pulse text-center">
{t("environments.people.loading_user_responses")}
</span>
</div>
<div className="h-12 w-full rounded-full bg-slate-50/50"></div>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
);
};
export default Loading;

View File

@@ -1,67 +0,0 @@
"use server";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import {
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromPersonId,
getProjectIdFromEnvironmentId,
getProjectIdFromPersonId,
} from "@/lib/utils/helper";
import { z } from "zod";
import { deletePerson, getPeople } from "@formbricks/lib/person/service";
import { ZId } from "@formbricks/types/common";
const ZGetPersonsAction = z.object({
environmentId: ZId,
offset: z.number().int().nonnegative(),
searchValue: z.string().optional(),
});
export const getPersonsAction = authenticatedActionClient
.schema(ZGetPersonsAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "read",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
return getPeople(parsedInput.environmentId, parsedInput.offset, parsedInput.searchValue);
});
const ZPersonDeleteAction = z.object({
personId: ZId,
});
export const deletePersonAction = authenticatedActionClient
.schema(ZPersonDeleteAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromPersonId(parsedInput.personId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromPersonId(parsedInput.personId),
},
],
});
return await deletePerson(parsedInput.personId);
});

View File

@@ -1,108 +0,0 @@
"use client";
import { getPersonsAction } from "@/app/(app)/environments/[environmentId]/(people)/people/actions";
import { PersonTable } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonTable";
import { debounce } from "lodash";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import React from "react";
import { TEnvironment } from "@formbricks/types/environment";
import { TPersonWithAttributes } from "@formbricks/types/people";
interface PersonDataViewProps {
environment: TEnvironment;
itemsPerPage: number;
isReadOnly: boolean;
}
export const PersonDataView = ({ environment, itemsPerPage, isReadOnly }: PersonDataViewProps) => {
const t = useTranslations();
const [persons, setPersons] = useState<TPersonWithAttributes[]>([]);
const [isDataLoaded, setIsDataLoaded] = useState<boolean>(false);
const [hasMore, setHasMore] = useState<boolean>(false);
const [loadingNextPage, setLoadingNextPage] = useState<boolean>(false);
const [searchValue, setSearchValue] = useState<string>("");
useEffect(() => {
const fetchData = async () => {
setIsDataLoaded(false);
try {
setHasMore(true);
const getPersonActionData = await getPersonsAction({
environmentId: environment.id,
offset: 0,
searchValue,
});
const personData = getPersonActionData?.data;
if (getPersonActionData?.data) {
setPersons(getPersonActionData.data);
}
if (personData && personData.length < itemsPerPage) {
setHasMore(false);
}
} catch (error) {
console.error(t("environments.people.error_fetching_people_data"), error);
} finally {
setIsDataLoaded(true);
}
};
const debouncedFetchData = debounce(fetchData, 300);
debouncedFetchData();
return () => {
debouncedFetchData.cancel();
};
}, [searchValue]);
const fetchNextPage = async () => {
if (hasMore && !loadingNextPage) {
setLoadingNextPage(true);
try {
const getPersonsActionData = await getPersonsAction({
environmentId: environment.id,
offset: persons.length,
searchValue,
});
const personData = getPersonsActionData?.data;
if (personData) {
setPersons((prevPersonsData) => [...prevPersonsData, ...personData]);
if (personData.length === 0 || personData.length < itemsPerPage) {
setHasMore(false);
}
}
} catch (error) {
console.error(t("environments.people.error_fetching_next_page_of_people_data"), error);
} finally {
setLoadingNextPage(false);
}
}
};
const deletePersons = (personIds: string[]) => {
setPersons((prevPersons) => prevPersons.filter((p) => !personIds.includes(p.id)));
};
const personTableData = persons.map((person) => ({
id: person.id,
userId: person.userId,
email: person.attributes.email,
createdAt: person.createdAt,
attributes: person.attributes,
personId: person.id,
}));
return (
<PersonTable
data={personTableData}
fetchNextPage={fetchNextPage}
hasMore={hasMore}
isDataLoaded={isDataLoaded}
deletePersons={deletePersons}
environmentId={environment.id}
searchValue={searchValue}
setSearchValue={setSearchValue}
isReadOnly={isReadOnly}
/>
);
};

View File

@@ -1,97 +0,0 @@
"use client";
import { getSelectionColumn } from "@/modules/ui/components/data-table";
import { HighlightedText } from "@/modules/ui/components/highlighted-text";
import { ColumnDef } from "@tanstack/react-table";
import { cn } from "@formbricks/lib/cn";
import { TPersonTableData } from "@formbricks/types/people";
export const generatePersonTableColumns = (
isExpanded: boolean,
searchValue: string,
t: (key: string) => string,
isReadOnly: boolean
): ColumnDef<TPersonTableData>[] => {
const dateColumn: ColumnDef<TPersonTableData> = {
accessorKey: "createdAt",
header: () => t("common.date"),
cell: ({ row }) => {
const isoDateString = row.original.createdAt;
const date = new Date(isoDateString);
const formattedDate = date.toLocaleString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
});
const formattedTime = date.toLocaleString(undefined, {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
return (
<div>
<p className="truncate text-slate-900">{formattedDate}</p>
<p className="truncate text-slate-900">{formattedTime}</p>
</div>
);
},
};
const userColumn: ColumnDef<TPersonTableData> = {
accessorKey: "user",
header: () => t("common.user"),
cell: ({ row }) => {
const personId = row.original.personId;
return <HighlightedText value={personId} searchValue={searchValue} />;
},
};
const userIdColumn: ColumnDef<TPersonTableData> = {
accessorKey: "userId",
header: () => t("common.user_id"),
cell: ({ row }) => {
const userId = row.original.userId;
return <HighlightedText value={userId} searchValue={searchValue} />;
},
};
const emailColumn: ColumnDef<TPersonTableData> = {
accessorKey: "email",
header: () => t("common.email"),
cell: ({ row }) => {
const email = row.original.attributes.email;
if (email) {
return <HighlightedText value={email} searchValue={searchValue} />;
}
},
};
const attributesColumn: ColumnDef<TPersonTableData> = {
accessorKey: "attributes",
header: () => t("common.attributes"),
cell: ({ row }) => {
const attributes = row.original.attributes;
// Handle cases where attributes are missing or empty
if (!attributes || Object.keys(attributes).length === 0) return null;
return (
<div className={cn(!isExpanded && "flex space-x-2")}>
{Object.entries(attributes).map(([key, value]) => (
<div key={key} className="flex space-x-2">
<div className="font-semibold">{key}</div> :{" "}
<HighlightedText value={value} searchValue={searchValue} />
</div>
))}
</div>
);
},
};
const columns = [dateColumn, userColumn, userIdColumn, emailColumn, attributesColumn];
return isReadOnly ? columns : [getSelectionColumn(), ...columns];
};

View File

@@ -1,48 +0,0 @@
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslations } from "next-intl/server";
const Loading = async () => {
const t = await getTranslations();
return (
<>
<PageContentWrapper>
<PageHeader pageTitle={t("common.people")}>
<PersonSecondaryNavigation activeId="people" loading />
</PageHeader>
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-7 content-center border-b text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6">{t("common.user")}</div>
<div className="col-span-2 text-center">{t("common.user_id")}</div>
<div className="col-span-2 text-center">{t("common.email")}</div>
</div>
<div className="w-full">
{[...Array(3)].map((_, index) => (
<div
key={index}
className="m-2 grid h-16 grid-cols-7 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="ph-no-capture h-10 w-10 flex-shrink-0 animate-pulse rounded-full bg-slate-200"></div>{" "}
<div className="ml-4">
<div className="ph-no-capture h-4 w-28 animate-pulse rounded-full bg-slate-200 font-medium text-slate-900"></div>
</div>{" "}
</div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="ph-no-capture m-12 h-4 animate-pulse rounded-full bg-slate-200 text-slate-900"></div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="ph-no-capture m-12 h-4 animate-pulse rounded-full bg-slate-200 text-slate-900"></div>
</div>
</div>
))}
</div>
</div>
</PageContentWrapper>
</>
);
};
export default Loading;

View File

@@ -1,73 +0,0 @@
import { PersonDataView } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonDataView";
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { Button } from "@/modules/ui/components/button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { CircleHelpIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session) {
throw new Error(t("common.session_not_found"));
}
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
const project = await getProjectByEnvironmentId(params.environmentId);
if (!project) {
throw new Error(t("common.project_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
const HowToAddPeopleButton = (
<Button
size="sm"
href="https://formbricks.com/docs/app-surveys/user-identification"
variant="secondary"
target="_blank"
EndIcon={CircleHelpIcon}>
{t("environments.people.how_to_add_people")}
</Button>
);
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.people")} cta={HowToAddPeopleButton}>
<PersonSecondaryNavigation activeId="people" environmentId={params.environmentId} />
</PageHeader>
<PersonDataView environment={environment} itemsPerPage={ITEMS_PER_PAGE} isReadOnly={isReadOnly} />
</PageContentWrapper>
);
};
export default Page;

View File

@@ -1,73 +0,0 @@
"use server";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromSegmentId, getProjectIdFromSegmentId } from "@/lib/utils/helper";
import { z } from "zod";
import { deleteSegment, updateSegment } from "@formbricks/lib/segment/service";
import { ZId } from "@formbricks/types/common";
import { ZSegmentFilters, ZSegmentUpdateInput } from "@formbricks/types/segment";
const ZDeleteBasicSegmentAction = z.object({
segmentId: ZId,
});
export const deleteBasicSegmentAction = authenticatedActionClient
.schema(ZDeleteBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
},
],
});
return await deleteSegment(parsedInput.segmentId);
});
const ZUpdateBasicSegmentAction = z.object({
segmentId: ZId,
data: ZSegmentUpdateInput,
});
export const updateBasicSegmentAction = authenticatedActionClient
.schema(ZUpdateBasicSegmentAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSegmentId(parsedInput.segmentId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromSegmentId(parsedInput.segmentId),
},
],
});
const { filters } = parsedInput.data;
if (filters) {
const parsedFilters = ZSegmentFilters.safeParse(filters);
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
throw new Error(errMsg);
}
}
return await updateSegment(parsedInput.segmentId, parsedInput.data);
});

View File

@@ -1,262 +0,0 @@
"use client";
import { createSegmentAction } from "@/modules/ee/advanced-targeting/lib/actions";
import { BasicAddFilterModal } from "@/modules/ui/components/basic-add-filter-modal";
import { BasicSegmentEditor } from "@/modules/ui/components/basic-segment-editor";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Modal } from "@/modules/ui/components/modal";
import { UpgradePlanNotice } from "@/modules/ui/components/upgrade-plan-notice";
import { FilterIcon, PlusIcon, UsersIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TBaseFilter, TSegment, ZSegmentFilters } from "@formbricks/types/segment";
type TCreateSegmentModalProps = {
environmentId: string;
attributeClasses: TAttributeClass[];
isFormbricksCloud: boolean;
};
export const BasicCreateSegmentModal = ({
environmentId,
attributeClasses,
isFormbricksCloud,
}: TCreateSegmentModalProps) => {
const t = useTranslations();
const router = useRouter();
const initialSegmentState = {
title: "",
description: "",
isPrivate: false,
filters: [],
environmentId,
id: "",
surveys: [],
createdAt: new Date(),
updatedAt: new Date(),
};
const [open, setOpen] = useState(false);
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
const [segment, setSegment] = useState<TSegment>(initialSegmentState);
const [isCreatingSegment, setIsCreatingSegment] = useState(false);
const handleResetState = () => {
setSegment(initialSegmentState);
setOpen(false);
};
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 handleCreateSegment = async () => {
if (!segment.title) {
toast.error("Title is required.");
return;
}
try {
setIsCreatingSegment(true);
await createSegmentAction({
title: segment.title,
description: segment.description ?? "",
isPrivate: segment.isPrivate,
filters: segment.filters,
environmentId,
surveyId: "",
});
setIsCreatingSegment(false);
toast.success(t("environments.segments.segment_created_successfully"));
} catch (err: any) {
// parse the segment filters to check if they are valid
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
if (!parsedFilters.success) {
toast.error(t("environments.segments.invalid_filters_please_check_the_filters_and_try_again"));
} else {
toast.error(t("common.something_went_wrong_please_try_again"));
}
setIsCreatingSegment(false);
return;
}
handleResetState();
setIsCreatingSegment(false);
router.refresh();
};
const isSaveDisabled = useMemo(() => {
// check if title is empty
if (!segment.title.trim()) {
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 (
<>
<Button size="sm" onClick={() => setOpen(true)} EndIcon={PlusIcon}>
{t("common.create_segment")}
</Button>
<Modal
open={open}
setOpen={() => {
handleResetState();
}}
noPadding
closeOnOutsideClick={false}
size="lg">
<div className="rounded-lg bg-slate-50">
<div className="rounded-t-lg bg-slate-100">
<div className="flex w-full items-center gap-4 p-6">
<div className="flex items-center space-x-2">
<div className="mr-1.5 h-6 w-6 text-slate-500">
<UsersIcon className="h-5 w-5" />
</div>
<div>
<h3 className="text-base font-medium">{t("common.create_segment")}</h3>
<p className="text-sm text-slate-600">
{t(
"environments.segments.segments_help_you_target_users_with_same_characteristics_easily"
)}
</p>
</div>
</div>
</div>
</div>
<div className="flex flex-col overflow-auto rounded-lg bg-white p-6">
<div className="flex w-full items-center gap-4">
<div className="flex w-1/2 flex-col gap-2">
<label className="text-sm font-medium text-slate-900">{t("common.title")}</label>
<div className="relative flex flex-col gap-1">
<Input
placeholder={t("environments.segments.ex_power_users")}
onChange={(e) => {
setSegment((prev) => ({
...prev,
title: e.target.value,
}));
}}
className="w-auto"
/>
</div>
</div>
<div className="flex w-1/2 flex-col gap-2">
<label className="text-sm font-medium text-slate-900">{t("common.description")}</label>
<Input
placeholder={t("environments.segments.ex_fully_activated_recurring_users")}
onChange={(e) => {
setSegment((prev) => ({
...prev,
description: e.target.value,
}));
}}
className="w-auto"
/>
</div>
</div>
<label className="my-4 text-sm font-medium text-slate-900">{t("common.targeting")}</label>
<div className="filter-scrollbar flex w-full flex-col gap-4 overflow-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
{segment?.filters?.length === 0 && (
<div className="-mb-2 flex items-center gap-1">
<FilterIcon className="h-5 w-5 text-slate-700" />
<h3 className="text-sm font-medium text-slate-700">
{t("environments.segments.add_your_first_filter_to_get_started")}
</h3>
</div>
)}
<BasicSegmentEditor
environmentId={environmentId}
segment={segment}
setSegment={setSegment}
group={segment.filters}
attributeClasses={attributeClasses}
/>
<Button
className="w-fit"
variant="secondary"
size="sm"
onClick={() => setAddFilterModalOpen(true)}>
{t("common.add_filter")}
</Button>
<BasicAddFilterModal
onAddFilter={(filter) => {
handleAddFilterInGroup(filter);
}}
open={addFilterModalOpen}
setOpen={setAddFilterModalOpen}
attributeClasses={attributeClasses}
/>
</div>
{isFormbricksCloud ? (
<UpgradePlanNotice
message={t("environments.segments.for_advanced_targeting_please")}
textForUrl={t("environments.segments.upgrade_your_plan")}
url={`/environments/${environmentId}/settings/billing`}
/>
) : (
<UpgradePlanNotice
message={t("environments.segments.for_advanced_targeting_please")}
textForUrl={t("common.request_an_enterprise_license")}
url={`/environments/${environmentId}/settings/enterprise`}
/>
)}
<div className="flex justify-end pt-4">
<div className="flex space-x-2">
<Button
type="button"
variant="minimal"
onClick={() => {
handleResetState();
}}>
{t("common.cancel")}
</Button>
<Button
type="submit"
loading={isCreatingSegment}
disabled={isSaveDisabled}
onClick={() => {
handleCreateSegment();
}}>
{t("common.create_segment")}
</Button>
</div>
</div>
</div>
</div>
</Modal>
</>
);
};

View File

@@ -1,279 +0,0 @@
"use client";
import { BasicAddFilterModal } from "@/modules/ui/components/basic-add-filter-modal";
import { BasicSegmentEditor } from "@/modules/ui/components/basic-segment-editor";
import { Button } from "@/modules/ui/components/button";
import { ConfirmDeleteSegmentModal } from "@/modules/ui/components/confirm-delete-segment-modal";
import { Input } from "@/modules/ui/components/input";
import { UpgradePlanNotice } from "@/modules/ui/components/upgrade-plan-notice";
import { FilterIcon, Trash2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TBaseFilter, TSegment, TSegmentWithSurveyNames, ZSegmentFilters } from "@formbricks/types/segment";
import { deleteBasicSegmentAction, updateBasicSegmentAction } from "../actions";
type TBasicSegmentSettingsTabProps = {
environmentId: string;
setOpen: (open: boolean) => void;
initialSegment: TSegmentWithSurveyNames;
attributeClasses: TAttributeClass[];
isFormbricksCloud: boolean;
isReadOnly: boolean;
};
export const BasicSegmentSettings = ({
environmentId,
initialSegment,
setOpen,
attributeClasses,
isFormbricksCloud,
isReadOnly,
}: TBasicSegmentSettingsTabProps) => {
const router = useRouter();
const t = useTranslations();
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
const [segment, setSegment] = useState<TSegment>(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(t("environments.segments.title_is_required"));
return;
}
try {
setIsUpdatingSegment(true);
await updateBasicSegmentAction({
segmentId: segment.id,
data: {
title: segment.title,
description: segment.description ?? "",
isPrivate: segment.isPrivate,
filters: segment.filters,
},
});
setIsUpdatingSegment(false);
toast.success(t("environments.segments.segment_updated_successfully"));
} catch (err: any) {
// parse the segment filters to check if they are valid
const parsedFilters = ZSegmentFilters.safeParse(segment.filters);
if (!parsedFilters.success) {
toast.error(t("environments.segments.invalid_filters_please_check_the_filters_and_try_again"));
} else {
toast.error(t("common.something_went_wrong_please_try_again"));
}
setIsUpdatingSegment(false);
return;
}
setIsUpdatingSegment(false);
handleResetState();
router.refresh();
};
const handleDeleteSegment = async () => {
try {
setIsDeletingSegment(true);
await deleteBasicSegmentAction({ segmentId: segment.id });
setIsDeletingSegment(false);
toast.success(t("environments.segments.segment_deleted_successfully"));
handleResetState();
} catch (err: any) {
toast.error(t("common.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]);
if (isAdvancedSegment(segment.filters)) {
return (
<p className="italic text-slate-600">
{t("environments.segments.advance_segment_cannot_be_edited_upgrade_your_plan")}
</p>
);
}
return (
<>
<div className="mb-4">
<div className="rounded-lg bg-slate-50">
<div className="flex flex-col overflow-auto rounded-lg bg-white">
<div className="flex w-full items-center gap-4">
<div className="flex w-1/2 flex-col gap-2">
<label className="text-sm font-medium text-slate-900">{t("common.title")}</label>
<div className="relative flex flex-col gap-1">
<Input
value={segment.title}
disabled={isReadOnly}
placeholder={t("environments.segments.ex_power_users")}
onChange={(e) => {
setSegment((prev) => ({
...prev,
title: e.target.value,
}));
}}
className="w-auto"
/>
</div>
</div>
<div className="flex w-1/2 flex-col gap-2">
<label className="text-sm font-medium text-slate-900">{t("common.description")}</label>
<div className="relative flex flex-col gap-1">
<Input
value={segment.description ?? ""}
disabled={isReadOnly}
placeholder={t("environments.segments.ex_fully_activated_recurring_users")}
onChange={(e) => {
setSegment((prev) => ({
...prev,
description: e.target.value,
}));
}}
className="w-auto"
/>
</div>
</div>
</div>
<label className="my-4 text-sm font-medium text-slate-900">{t("common.targeting")}</label>
<div className="filter-scrollbar flex max-h-96 w-full flex-col gap-4 overflow-auto rounded-lg border border-slate-200 bg-slate-50 p-4">
{segment?.filters?.length === 0 && (
<div className="-mb-2 flex items-center gap-1">
<FilterIcon className="h-5 w-5 text-slate-700" />
<h3 className="text-sm font-medium text-slate-700">
{t("environments.segments.add_your_first_filter_to_get_started")}
</h3>
</div>
)}
<BasicSegmentEditor
environmentId={environmentId}
segment={segment}
setSegment={setSegment}
group={segment.filters}
attributeClasses={attributeClasses}
viewOnly={isReadOnly}
/>
<div>
<Button
variant="secondary"
size="sm"
onClick={() => setAddFilterModalOpen(true)}
disabled={isReadOnly}>
{t("common.add_filter")}
</Button>
</div>
<BasicAddFilterModal
onAddFilter={(filter) => {
handleAddFilterInGroup(filter);
}}
open={addFilterModalOpen}
setOpen={setAddFilterModalOpen}
attributeClasses={attributeClasses}
/>
</div>
{isFormbricksCloud ? (
<UpgradePlanNotice
message={t("environments.segments.for_advanced_targeting_please")}
textForUrl={t("environments.segments.upgrade_your_plan")}
url={`/environments/${environmentId}/settings/billing`}
/>
) : (
<UpgradePlanNotice
message={t("environments.segments.for_advanced_targeting_please")}
textForUrl={t("common.request_an_enterprise_license")}
url={`/environments/${environmentId}/settings/enterprise`}
/>
)}
{!isReadOnly && (
<div className="flex w-full items-center justify-between pt-4">
<Button
type="button"
variant="warn"
loading={isDeletingSegment}
onClick={() => {
setIsDeleteSegmentModalOpen(true);
}}
EndIcon={Trash2}
endIconClassName="p-0.5">
{t("common.delete")}
</Button>
<Button
type="submit"
loading={isUpdatingSegment}
onClick={() => {
handleUpdateSegment();
}}
disabled={isSaveDisabled}>
{t("common.save_changes")}
</Button>
</div>
)}
{isDeleteSegmentModalOpen && (
<ConfirmDeleteSegmentModal
onDelete={handleDeleteSegment}
open={isDeleteSegmentModalOpen}
segment={initialSegment}
setOpen={setIsDeleteSegmentModalOpen}
/>
)}
</div>
</div>
</div>
</>
);
};

View File

@@ -1,97 +0,0 @@
import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation";
import { BasicCreateSegmentModal } from "@/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal";
import { SegmentTable } from "@/app/(app)/environments/[environmentId]/(people)/segments/components/SegmentTable";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { CreateSegmentModal } from "@/modules/ee/advanced-targeting/components/create-segment-modal";
import { getAdvancedTargetingPermission } from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSegments } from "@formbricks/lib/segment/service";
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const [environment, segments, attributeClasses, organization, project] = await Promise.all([
getEnvironment(params.environmentId),
getSegments(params.environmentId),
getAttributeClasses(params.environmentId, undefined, { skipArchived: true }),
getOrganizationByEnvironmentId(params.environmentId),
getProjectByEnvironmentId(params.environmentId),
]);
const session = await getServerSession(authOptions);
if (!session) {
throw new Error(t("common.session_not_found"));
}
if (!environment) {
throw new Error(t("common.environment_not_found"));
}
if (!project) {
throw new Error(t("common.project_not_found"));
}
if (!organization) {
throw new Error(t("common.organization_not_found"));
}
const isAdvancedTargetingAllowed = await getAdvancedTargetingPermission(organization);
if (!segments) {
throw new Error(t("environments.segments.failed_to_fetch_segments"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isMember } = getAccessFlags(currentUserMembership?.role);
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
const isReadOnly = isMember && hasReadAccess;
const filteredSegments = segments.filter((segment) => !segment.isPrivate);
const renderCreateSegmentButton = () =>
isAdvancedTargetingAllowed ? (
<CreateSegmentModal
environmentId={params.environmentId}
attributeClasses={attributeClasses}
segments={filteredSegments}
/>
) : (
<BasicCreateSegmentModal
attributeClasses={attributeClasses}
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
/>
);
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.people")} cta={!isReadOnly ? renderCreateSegmentButton() : undefined}>
<PersonSecondaryNavigation activeId="segments" environmentId={params.environmentId} />
</PageHeader>
<SegmentTable
segments={filteredSegments}
attributeClasses={attributeClasses}
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
isReadOnly={isReadOnly}
/>
</PageContentWrapper>
);
};
export default Page;

View File

@@ -62,7 +62,7 @@ interface NavigationProps {
organization: TOrganization;
projects: TProject[];
isMultiOrgEnabled: boolean;
isFormbricksCloud?: boolean;
isFormbricksCloud: boolean;
membershipRole?: TOrganizationRole;
organizationProjectsLimit: number;
isLicenseActive: boolean;
@@ -75,8 +75,8 @@ export const MainNavigation = ({
user,
projects,
isMultiOrgEnabled,
isFormbricksCloud = true,
membershipRole,
isFormbricksCloud,
organizationProjectsLimit,
isLicenseActive,
}: NavigationProps) => {
@@ -160,13 +160,10 @@ export const MainNavigation = ({
isHidden: false,
},
{
name: t("common.people"),
href: `/environments/${environment.id}/people`,
href: `/environments/${environment.id}/contacts`,
name: t("common.contacts"),
icon: UserIcon,
isActive:
pathname?.includes("/people") ||
pathname?.includes("/segments") ||
pathname?.includes("/attributes"),
isActive: pathname?.includes("/contacts") || pathname?.includes("/segments"),
},
{
name: t("common.actions"),
@@ -187,7 +184,7 @@ export const MainNavigation = ({
isActive: pathname?.includes("/project"),
},
],
[environment.id, pathname, isMember]
[t, environment.id, pathname]
);
const dropdownNavigation = [

View File

@@ -53,7 +53,7 @@ export const NavigationLink = ({
{children}
<span
className={cn(
"ml-2 transition-opacity duration-100",
"ml-2 flex transition-opacity duration-100",
isTextVisible ? "opacity-0" : "opacity-100"
)}>
{linkText}

View File

@@ -25,7 +25,7 @@ import { Controller, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TIntegrationItem } from "@formbricks/types/integration";
import {
TIntegrationAirtable,
@@ -46,7 +46,7 @@ type AddIntegrationModalProps = {
airtableArray: TIntegrationItem[];
surveys: TSurvey[];
airtableIntegration: TIntegrationAirtable;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
} & EditModeProps;
export type IntegrationModalInputs = {
@@ -79,7 +79,7 @@ export const AddIntegrationModal = ({
airtableIntegration,
isEditMode,
defaultData,
attributeClasses,
contactAttributeKeys,
}: AddIntegrationModalProps) => {
const t = useTranslations();
const router = useRouter();
@@ -323,7 +323,7 @@ export const AddIntegrationModal = ({
<Label htmlFor="Surveys">{t("common.questions")}</Label>
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
{replaceHeadlineRecall(selectedSurvey, "default", contactAttributeKeys)?.questions.map(
(question) => (
<Controller
key={question.id}

View File

@@ -5,7 +5,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
import airtableLogo from "@/images/airtableLogo.svg";
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
import { useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
@@ -20,7 +20,7 @@ interface AirtableWrapperProps {
environment: TEnvironment;
isEnabled: boolean;
webAppUrl: string;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -32,7 +32,7 @@ export const AirtableWrapper = ({
environment,
isEnabled,
webAppUrl,
attributeClasses,
contactAttributeKeys,
locale,
}: AirtableWrapperProps) => {
const [isConnected, setIsConnected] = useState(
@@ -55,7 +55,7 @@ export const AirtableWrapper = ({
airtableIntegration={airtableIntegration}
setIsConnected={setIsConnected}
surveys={surveys}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
) : (

View File

@@ -14,7 +14,7 @@ import { useTranslations } from "next-intl";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { timeSince } from "@formbricks/lib/time";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
@@ -28,7 +28,7 @@ interface ManageIntegrationProps {
setIsConnected: (data: boolean) => void;
surveys: TSurvey[];
airtableArray: TIntegrationItem[];
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -47,7 +47,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
setIsConnected,
surveys,
airtableArray,
attributeClasses,
contactAttributeKeys,
} = props;
const t = useTranslations();
const [isDeleting, setisDeleting] = useState(false);
@@ -179,7 +179,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
environmentId={environmentId}
surveys={surveys}
airtableIntegration={airtableIntegration}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
{...data}
/>
)}

View File

@@ -1,4 +1,5 @@
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
import { getContactAttributeKeys } from "@/app/(app)/environments/[environmentId]/integrations/lib/contact-attribute-key";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
@@ -9,7 +10,6 @@ import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getAirtableTables } from "@formbricks/lib/airtable/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
@@ -25,12 +25,12 @@ const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const isEnabled = !!AIRTABLE_CLIENT_ID;
const [session, surveys, integrations, environment, attributeClasses] = await Promise.all([
const [session, surveys, integrations, environment, contactAttributeKeys] = await Promise.all([
getServerSession(authOptions),
getSurveys(params.environmentId),
getIntegrations(params.environmentId),
getEnvironment(params.environmentId),
getAttributeClasses(params.environmentId),
getContactAttributeKeys(params.environmentId),
]);
if (!session) {
@@ -85,7 +85,7 @@ const Page = async (props) => {
surveys={surveys}
environment={environment}
webAppUrl={WEBAPP_URL}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -20,7 +20,7 @@ import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TIntegrationGoogleSheets,
TIntegrationGoogleSheetsConfigData,
@@ -35,7 +35,7 @@ interface AddIntegrationModalProps {
setOpen: (v: boolean) => void;
googleSheetIntegration: TIntegrationGoogleSheets;
selectedIntegration?: (TIntegrationGoogleSheetsConfigData & { index: number }) | null;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
}
export const AddIntegrationModal = ({
@@ -45,7 +45,7 @@ export const AddIntegrationModal = ({
setOpen,
googleSheetIntegration,
selectedIntegration,
attributeClasses,
contactAttributeKeys,
}: AddIntegrationModalProps) => {
const t = useTranslations();
const integrationData: TIntegrationGoogleSheetsConfigData = {
@@ -250,27 +250,29 @@ export const AddIntegrationModal = ({
<Label htmlFor="Surveys">{t("common.questions")}</Label>
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
(question) => (
<div key={question.id} className="my-1 flex items-center space-x-2">
<label htmlFor={question.id} className="flex cursor-pointer items-center">
<Checkbox
type="button"
id={question.id}
value={question.id}
className="bg-white"
checked={selectedQuestions.includes(question.id)}
onCheckedChange={() => {
handleCheckboxChange(question.id);
}}
/>
<span className="ml-2 w-[30rem] truncate">
{getLocalizedValue(question.headline, "default")}
</span>
</label>
</div>
)
)}
{replaceHeadlineRecall(
selectedSurvey,
"default",
contactAttributeKeys
)?.questions.map((question) => (
<div key={question.id} className="my-1 flex items-center space-x-2">
<label htmlFor={question.id} className="flex cursor-pointer items-center">
<Checkbox
type="button"
id={question.id}
value={question.id}
className="bg-white"
checked={selectedQuestions.includes(question.id)}
onCheckedChange={() => {
handleCheckboxChange(question.id);
}}
/>
<span className="ml-2 w-[30rem] truncate">
{getLocalizedValue(question.headline, "default")}
</span>
</label>
</div>
))}
</div>
</div>
</div>

View File

@@ -5,7 +5,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
import googleSheetLogo from "@/images/googleSheetsLogo.png";
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
import { useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import {
TIntegrationGoogleSheets,
@@ -21,7 +21,7 @@ interface GoogleSheetWrapperProps {
surveys: TSurvey[];
googleSheetIntegration?: TIntegrationGoogleSheets;
webAppUrl: string;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -31,7 +31,7 @@ export const GoogleSheetWrapper = ({
surveys,
googleSheetIntegration,
webAppUrl,
attributeClasses,
contactAttributeKeys,
locale,
}: GoogleSheetWrapperProps) => {
const [isConnected, setIsConnected] = useState(
@@ -61,7 +61,7 @@ export const GoogleSheetWrapper = ({
setOpen={setModalOpen}
googleSheetIntegration={googleSheetIntegration}
selectedIntegration={selectedIntegration}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
/>
<ManageIntegration
environment={environment}

View File

@@ -8,7 +8,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import {
GOOGLE_SHEETS_CLIENT_ID,
GOOGLE_SHEETS_CLIENT_SECRET,
@@ -23,17 +22,18 @@ import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
import { getContactAttributeKeys } from "../lib/contact-attribute-key";
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
const [session, surveys, integrations, environment, attributeClasses] = await Promise.all([
const [session, surveys, integrations, environment, contactAttributeKeys] = await Promise.all([
getServerSession(authOptions),
getSurveys(params.environmentId),
getIntegrations(params.environmentId),
getEnvironment(params.environmentId),
getAttributeClasses(params.environmentId),
getContactAttributeKeys(params.environmentId),
]);
if (!session) {
@@ -81,7 +81,7 @@ const Page = async (props) => {
surveys={surveys}
googleSheetIntegration={googleSheetIntegration}
webAppUrl={WEBAPP_URL}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -0,0 +1,20 @@
import { contactAttributeKeyCache } from "@/lib/cache/contact-attribute-key";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
export const getContactAttributeKeys = reactCache(
(environmentId: string): Promise<TContactAttributeKey[]> =>
cache(
async () => {
return await prisma.contactAttributeKey.findMany({
where: { environmentId },
});
},
[`getContactAttributeKeys-integrations-${environmentId}`],
{
tags: [contactAttributeKeyCache.tag.byEnvironmentId(environmentId)],
}
)()
);

View File

@@ -19,7 +19,7 @@ import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TIntegrationInput } from "@formbricks/types/integration";
import {
TIntegrationNotion,
@@ -37,7 +37,7 @@ interface AddIntegrationModalProps {
notionIntegration: TIntegrationNotion;
databases: TIntegrationNotionDatabase[];
selectedIntegration: (TIntegrationNotionConfigData & { index: number }) | null;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -49,7 +49,7 @@ export const AddIntegrationModal = ({
notionIntegration,
databases,
selectedIntegration,
attributeClasses,
contactAttributeKeys,
locale,
}: AddIntegrationModalProps) => {
const t = useTranslations();
@@ -116,7 +116,7 @@ export const AddIntegrationModal = ({
const questionItems = useMemo(() => {
const questions = selectedSurvey
? replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map((q) => ({
? replaceHeadlineRecall(selectedSurvey, "default", contactAttributeKeys)?.questions.map((q) => ({
id: q.id,
name: getLocalizedValue(q.headline, "default"),
type: q.type,

View File

@@ -5,7 +5,7 @@ import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/inte
import notionLogo from "@/images/notion.png";
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
import { useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import {
TIntegrationNotion,
@@ -23,7 +23,7 @@ interface NotionWrapperProps {
webAppUrl: string;
surveys: TSurvey[];
databasesArray: TIntegrationNotionDatabase[];
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -34,7 +34,7 @@ export const NotionWrapper = ({
webAppUrl,
surveys,
databasesArray,
attributeClasses,
contactAttributeKeys,
locale,
}: NotionWrapperProps) => {
const [isModalOpen, setModalOpen] = useState(false);
@@ -65,7 +65,7 @@ export const NotionWrapper = ({
notionIntegration={notionIntegration}
databases={databasesArray}
selectedIntegration={selectedIntegration}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<ManageIntegration

View File

@@ -8,7 +8,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import {
NOTION_AUTH_URL,
NOTION_OAUTH_CLIENT_ID,
@@ -25,6 +24,7 @@ import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
import { getContactAttributeKeys } from "../lib/contact-attribute-key";
const Page = async (props) => {
const params = await props.params;
@@ -35,12 +35,12 @@ const Page = async (props) => {
NOTION_AUTH_URL &&
NOTION_REDIRECT_URI
);
const [session, surveys, notionIntegration, environment, attributeClasses] = await Promise.all([
const [session, surveys, notionIntegration, environment, contactAttributeKeys] = await Promise.all([
getServerSession(authOptions),
getSurveys(params.environmentId),
getIntegrationByType(params.environmentId, "notion"),
getEnvironment(params.environmentId),
getAttributeClasses(params.environmentId),
getContactAttributeKeys(params.environmentId),
]);
if (!session) {
@@ -89,7 +89,7 @@ const Page = async (props) => {
notionIntegration={notionIntegration as TIntegrationNotion}
webAppUrl={WEBAPP_URL}
databasesArray={databasesArray}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</PageContentWrapper>

View File

@@ -13,7 +13,7 @@ import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TIntegrationItem } from "@formbricks/types/integration";
import {
TIntegrationSlack,
@@ -30,7 +30,7 @@ interface AddChannelMappingModalProps {
slackIntegration: TIntegrationSlack;
channels: TIntegrationItem[];
selectedIntegration?: (TIntegrationSlackConfigData & { index: number }) | null;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
}
export const AddChannelMappingModal = ({
@@ -41,7 +41,7 @@ export const AddChannelMappingModal = ({
channels,
slackIntegration,
selectedIntegration,
attributeClasses,
contactAttributeKeys,
}: AddChannelMappingModalProps) => {
const { handleSubmit } = useForm();
const t = useTranslations();
@@ -246,27 +246,27 @@ export const AddChannelMappingModal = ({
<Label htmlFor="Surveys">{t("common.questions")}</Label>
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions?.map(
(question) => (
<div key={question.id} className="my-1 flex items-center space-x-2">
<label htmlFor={question.id} className="flex cursor-pointer items-center">
<Checkbox
type="button"
id={question.id}
value={question.id}
className="bg-white"
checked={selectedQuestions.includes(question.id)}
onCheckedChange={() => {
handleCheckboxChange(question.id);
}}
/>
<span className="ml-2">
{getLocalizedValue(question.headline, "default")}
</span>
</label>
</div>
)
)}
{replaceHeadlineRecall(
selectedSurvey,
"default",
contactAttributeKeys
)?.questions?.map((question) => (
<div key={question.id} className="my-1 flex items-center space-x-2">
<label htmlFor={question.id} className="flex cursor-pointer items-center">
<Checkbox
type="button"
id={question.id}
value={question.id}
className="bg-white"
checked={selectedQuestions.includes(question.id)}
onCheckedChange={() => {
handleCheckboxChange(question.id);
}}
/>
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
</label>
</div>
))}
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
import slackLogo from "@/images/slacklogo.png";
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
import { useEffect, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
@@ -20,7 +20,7 @@ interface SlackWrapperProps {
surveys: TSurvey[];
slackIntegration?: TIntegrationSlack;
webAppUrl: string;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -30,7 +30,7 @@ export const SlackWrapper = ({
surveys,
slackIntegration,
webAppUrl,
attributeClasses,
contactAttributeKeys,
locale,
}: SlackWrapperProps) => {
const [isConnected, setIsConnected] = useState(slackIntegration ? slackIntegration.config?.key : false);
@@ -79,7 +79,7 @@ export const SlackWrapper = ({
channels={slackChannels}
slackIntegration={slackIntegration}
selectedIntegration={selectedIntegration}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
/>
<ManageIntegration
environment={environment}

View File

@@ -8,7 +8,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrationByType } from "@formbricks/lib/integration/service";
@@ -18,17 +17,19 @@ import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
import { getContactAttributeKeys } from "../lib/contact-attribute-key";
const Page = async (props) => {
const params = await props.params;
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
const t = await getTranslations();
const [session, surveys, slackIntegration, environment, attributeClasses] = await Promise.all([
const [session, surveys, slackIntegration, environment, contactAttributeKeys] = await Promise.all([
getServerSession(authOptions),
getSurveys(params.environmentId),
getIntegrationByType(params.environmentId, "slack"),
getEnvironment(params.environmentId),
getAttributeClasses(params.environmentId),
getContactAttributeKeys(params.environmentId),
]);
if (!session) {
@@ -73,7 +74,7 @@ const Page = async (props) => {
surveys={surveys}
slackIntegration={slackIntegration as TIntegrationSlack}
webAppUrl={WEBAPP_URL}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
</div>

View File

@@ -15,7 +15,10 @@ import { FormbricksClient } from "../../components/FormbricksClient";
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
import { PosthogIdentify } from "./components/PosthogIdentify";
export const EnvLayout = async (props) => {
const EnvLayout = async (props: {
params: Promise<{ environmentId: string }>;
children: React.ReactNode;
}) => {
const params = await props.params;
const { children } = props;

View File

@@ -6,7 +6,7 @@ import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
const Page = async (props) => {
const EnvironmentPage = async (props) => {
const params = await props.params;
const session = await getServerSession(authOptions);
const t = await getTranslations();
@@ -30,4 +30,4 @@ const Page = async (props) => {
return redirect(`/environments/${params.environmentId}/surveys`);
};
export default Page;
export default EnvironmentPage;

View File

@@ -93,8 +93,8 @@ const mapResponsesToTableData = (
),
verifiedEmail: typeof response.data["verifiedEmail"] === "string" ? response.data["verifiedEmail"] : "",
language: response.language,
person: response.person,
personAttributes: response.personAttributes,
person: response.contact,
contactAttributes: response.contactAttributes,
}));
};

View File

@@ -8,8 +8,8 @@ import { ColumnDef } from "@tanstack/react-table";
import { CircleHelpIcon, EyeOffIcon, MailIcon, TagIcon } from "lucide-react";
import Link from "next/link";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { processResponseData } from "@formbricks/lib/responses";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { getFormattedDate, getFormattedTime } from "@formbricks/lib/utils/datetime";
import { QUESTIONS_ICON_MAP, VARIABLES_ICON_MAP } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
@@ -224,7 +224,7 @@ export const generateResponseTableColumns = (
size: 275,
cell: ({ row }) => {
const personId = row.original.person
? getPersonIdentifier(row.original.person, row.original.personAttributes)
? getContactIdentifier(row.original.person, row.original.contactAttributes)
: t("common.anonymous");
return <p className="truncate text-slate-900">{personId}</p>;
},

View File

@@ -2,9 +2,9 @@ import { ArrayResponse } from "@/modules/ui/components/array-response";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { timeSince } from "@formbricks/lib/time";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummaryAddress } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
@@ -13,7 +13,7 @@ interface AddressSummaryProps {
questionSummary: TSurveyQuestionSummaryAddress;
environmentId: string;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -21,7 +21,7 @@ export const AddressSummary = ({
questionSummary,
environmentId,
survey,
attributeClasses,
contactAttributeKeys,
locale,
}: AddressSummaryProps) => {
const t = useTranslations();
@@ -30,7 +30,7 @@ export const AddressSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div>
@@ -46,15 +46,15 @@ export const AddressSummary = ({
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
<div className="pl-4 md:pl-6">
{response.person ? (
{response.contact ? (
<Link
className="ph-no-capture group flex items-center"
href={`/environments/${environmentId}/people/${response.person.id}`}>
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
<div className="hidden md:flex">
<PersonAvatar personId={response.person.id} />
<PersonAvatar personId={response.contact.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getContactIdentifier(response.contact, response.contactAttributes)}
</p>
</Link>
) : (

View File

@@ -1,7 +1,7 @@
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { InboxIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummaryCta } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { convertFloatToNDecimal } from "../lib/utils";
@@ -10,19 +10,20 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
interface CTASummaryProps {
questionSummary: TSurveyQuestionSummaryCta;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
export const CTASummary = ({ questionSummary, survey, attributeClasses, locale }: CTASummaryProps) => {
export const CTASummary = ({ questionSummary, survey, contactAttributeKeys, locale }: CTASummaryProps) => {
const t = useTranslations();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
survey={survey}
questionSummary={questionSummary}
showResponses={false}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
additionalInfo={
<>

View File

@@ -1,7 +1,7 @@
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { useTranslations } from "next-intl";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummaryCal } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
@@ -10,18 +10,19 @@ interface CalSummaryProps {
questionSummary: TSurveyQuestionSummaryCal;
environmentId: string;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
export const CalSummary = ({ questionSummary, survey, attributeClasses, locale }: CalSummaryProps) => {
export const CalSummary = ({ questionSummary, survey, contactAttributeKeys, locale }: CalSummaryProps) => {
const t = useTranslations();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">

View File

@@ -1,6 +1,6 @@
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { useTranslations } from "next-intl";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TI18nString,
TSurvey,
@@ -15,7 +15,7 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
interface ConsentSummaryProps {
questionSummary: TSurveyQuestionSummaryConsent;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
setFilter: (
questionId: TSurveyQuestionId,
label: TI18nString,
@@ -29,7 +29,7 @@ interface ConsentSummaryProps {
export const ConsentSummary = ({
questionSummary,
survey,
attributeClasses,
contactAttributeKeys,
setFilter,
locale,
}: ConsentSummaryProps) => {
@@ -51,7 +51,7 @@ export const ConsentSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">

View File

@@ -2,9 +2,9 @@ import { ArrayResponse } from "@/modules/ui/components/array-response";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { timeSince } from "@formbricks/lib/time";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummaryContactInfo } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
@@ -13,7 +13,7 @@ interface ContactInfoSummaryProps {
questionSummary: TSurveyQuestionSummaryContactInfo;
environmentId: string;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -21,7 +21,7 @@ export const ContactInfoSummary = ({
questionSummary,
environmentId,
survey,
attributeClasses,
contactAttributeKeys,
locale,
}: ContactInfoSummaryProps) => {
const t = useTranslations();
@@ -30,7 +30,7 @@ export const ContactInfoSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div>
@@ -46,15 +46,15 @@ export const ContactInfoSummary = ({
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
<div className="pl-4 md:pl-6">
{response.person ? (
{response.contact ? (
<Link
className="ph-no-capture group flex items-center"
href={`/environments/${environmentId}/people/${response.person.id}`}>
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
<div className="hidden md:flex">
<PersonAvatar personId={response.person.id} />
<PersonAvatar personId={response.contact.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getContactIdentifier(response.contact, response.contactAttributes)}
</p>
</Link>
) : (

View File

@@ -3,10 +3,10 @@ import { Button } from "@/modules/ui/components/button";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { timeSince } from "@formbricks/lib/time";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummaryDate } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
@@ -15,7 +15,7 @@ interface DateQuestionSummary {
questionSummary: TSurveyQuestionSummaryDate;
environmentId: string;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -23,7 +23,7 @@ export const DateQuestionSummary = ({
questionSummary,
environmentId,
survey,
attributeClasses,
contactAttributeKeys,
locale,
}: DateQuestionSummary) => {
const t = useTranslations();
@@ -41,7 +41,7 @@ export const DateQuestionSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div className="">
@@ -56,15 +56,15 @@ export const DateQuestionSummary = ({
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
<div className="pl-4 md:pl-6">
{response.person ? (
{response.contact ? (
<Link
className="ph-no-capture group flex items-center"
href={`/environments/${environmentId}/people/${response.person.id}`}>
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
<div className="hidden md:flex">
<PersonAvatar personId={response.person.id} />
<PersonAvatar personId={response.contact.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getContactIdentifier(response.contact, response.contactAttributes)}
</p>
</Link>
) : (

View File

@@ -4,10 +4,10 @@ import { DownloadIcon, FileIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
import { timeSince } from "@formbricks/lib/time";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummaryFileUpload } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
@@ -16,7 +16,7 @@ interface FileUploadSummaryProps {
questionSummary: TSurveyQuestionSummaryFileUpload;
environmentId: string;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -24,7 +24,7 @@ export const FileUploadSummary = ({
questionSummary,
environmentId,
survey,
attributeClasses,
contactAttributeKeys,
locale,
}: FileUploadSummaryProps) => {
const [visibleResponses, setVisibleResponses] = useState(10);
@@ -41,7 +41,7 @@ export const FileUploadSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div className="">
@@ -56,15 +56,15 @@ export const FileUploadSummary = ({
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
<div className="pl-4 md:pl-6">
{response.person ? (
{response.contact ? (
<Link
className="ph-no-capture group flex items-center"
href={`/environments/${environmentId}/people/${response.person.id}`}>
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
<div className="hidden md:flex">
<PersonAvatar personId={response.person.id} />
<PersonAvatar personId={response.contact.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getContactIdentifier(response.contact, response.contactAttributes)}
</p>
</Link>
) : (

View File

@@ -3,8 +3,8 @@ import { Button } from "@/modules/ui/components/button";
import { InboxIcon, Link, MessageSquareTextIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { timeSince } from "@formbricks/lib/time";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurveyQuestionSummaryHiddenFields } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -54,15 +54,15 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
key={response.value}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
<div className="pl-4 md:pl-6">
{response.person ? (
{response.contact ? (
<Link
className="ph-no-capture group flex items-center"
href={`/environments/${environment.id}/people/${response.person.id}`}>
href={`/environments/${environment.id}/contacts/${response.contact.id}`}>
<div className="hidden md:flex">
<PersonAvatar personId={response.person.id} />
<PersonAvatar personId={response.contact.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getContactIdentifier(response.contact, response.contactAttributes)}
</p>
</Link>
) : (

View File

@@ -1,6 +1,6 @@
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { useTranslations } from "next-intl";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TI18nString,
TSurvey,
@@ -14,7 +14,7 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
interface MatrixQuestionSummaryProps {
questionSummary: TSurveyQuestionSummaryMatrix;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
setFilter: (
questionId: TSurveyQuestionId,
label: TI18nString,
@@ -28,7 +28,7 @@ interface MatrixQuestionSummaryProps {
export const MatrixQuestionSummary = ({
questionSummary,
survey,
attributeClasses,
contactAttributeKeys,
setFilter,
locale,
}: MatrixQuestionSummaryProps) => {
@@ -55,7 +55,7 @@ export const MatrixQuestionSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div className="overflow-x-auto p-6">

View File

@@ -5,8 +5,8 @@ import { InboxIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TI18nString,
TSurvey,
@@ -24,7 +24,7 @@ interface MultipleChoiceSummaryProps {
environmentId: string;
surveyType: TSurveyType;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
setFilter: (
questionId: TSurveyQuestionId,
label: TI18nString,
@@ -40,7 +40,7 @@ export const MultipleChoiceSummary = ({
environmentId,
surveyType,
survey,
attributeClasses,
contactAttributeKeys,
setFilter,
locale,
}: MultipleChoiceSummaryProps) => {
@@ -73,7 +73,7 @@ export const MultipleChoiceSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
additionalInfo={
questionSummary.type === "multipleChoiceMulti" ? (
@@ -138,11 +138,11 @@ export const MultipleChoiceSummary = ({
<span>{otherValue.value}</span>
</div>
)}
{surveyType === "app" && otherValue.person && (
{surveyType === "app" && otherValue.contact && (
<Link
href={
otherValue.person.id
? `/environments/${environmentId}/people/${otherValue.person.id}`
otherValue.contact.id
? `/environments/${environmentId}/contacts/${otherValue.contact.id}`
: { pathname: null }
}
key={idx}
@@ -151,8 +151,10 @@ export const MultipleChoiceSummary = ({
<span>{otherValue.value}</span>
</div>
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
{otherValue.person.id && <PersonAvatar personId={otherValue.person.id} />}
<span>{getPersonIdentifier(otherValue.person, otherValue.personAttributes)}</span>
{otherValue.contact.id && <PersonAvatar personId={otherValue.contact.id} />}
<span>
{getContactIdentifier(otherValue.contact, otherValue.contactAttributes)}
</span>
</div>
</Link>
)}

View File

@@ -1,6 +1,6 @@
import { HalfCircle, ProgressBar } from "@/modules/ui/components/progress-bar";
import { useTranslations } from "next-intl";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TI18nString,
TSurvey,
@@ -15,7 +15,7 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
interface NPSSummaryProps {
questionSummary: TSurveyQuestionSummaryNps;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
setFilter: (
questionId: TSurveyQuestionId,
@@ -29,7 +29,7 @@ interface NPSSummaryProps {
export const NPSSummary = ({
questionSummary,
survey,
attributeClasses,
contactAttributeKeys,
setFilter,
locale,
}: NPSSummaryProps) => {
@@ -72,7 +72,7 @@ export const NPSSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">

View File

@@ -6,9 +6,9 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { timeSince } from "@formbricks/lib/time";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummaryOpenText } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
@@ -17,7 +17,7 @@ interface OpenTextSummaryProps {
questionSummary: TSurveyQuestionSummaryOpenText;
environmentId: string;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
isAIEnabled: boolean;
documentsPerPage?: number;
locale: TUserLocale;
@@ -27,7 +27,7 @@ export const OpenTextSummary = ({
questionSummary,
environmentId,
survey,
attributeClasses,
contactAttributeKeys,
isAIEnabled,
documentsPerPage,
locale,
@@ -64,7 +64,7 @@ export const OpenTextSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
additionalInfo={
isAIEnabled && questionSummary.insightsEnabled === false ? (
@@ -104,16 +104,16 @@ export const OpenTextSummary = ({
<TableBody>
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
<TableRow key={response.id}>
<TableCell width={180}>
{response.person ? (
<TableCell>
{response.contact ? (
<Link
className="ph-no-capture group flex items-center"
href={`/environments/${environmentId}/people/${response.person.id}`}>
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
<div className="hidden md:flex">
<PersonAvatar personId={response.person.id} />
<PersonAvatar personId={response.contact.id} />
</div>
<p className="ph-no-capture break-normal text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getContactIdentifier(response.contact, response.contactAttributes)}
</p>
</Link>
) : (

View File

@@ -2,7 +2,7 @@ import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { InboxIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TI18nString,
TSurvey,
@@ -17,7 +17,7 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
interface PictureChoiceSummaryProps {
questionSummary: TSurveyQuestionSummaryPictureSelection;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
setFilter: (
questionId: TSurveyQuestionId,
label: TI18nString,
@@ -31,7 +31,7 @@ interface PictureChoiceSummaryProps {
export const PictureChoiceSummary = ({
questionSummary,
survey,
attributeClasses,
contactAttributeKeys,
setFilter,
locale,
}: PictureChoiceSummaryProps) => {
@@ -42,7 +42,7 @@ export const PictureChoiceSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
additionalInfo={
questionSummary.question.allowMulti ? (

View File

@@ -3,7 +3,7 @@ import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummary } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
@@ -12,7 +12,7 @@ interface HeadProps {
showResponses?: boolean;
additionalInfo?: JSX.Element;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -21,7 +21,7 @@ export const QuestionSummaryHeader = ({
additionalInfo,
showResponses = true,
survey,
attributeClasses,
contactAttributeKeys,
locale,
}: HeadProps) => {
const questionType = getQuestionTypes(locale).find((type) => type.id === questionSummary.question.type);
@@ -50,9 +50,13 @@ export const QuestionSummaryHeader = ({
<div className={"align-center flex justify-between gap-4"}>
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">
{formatTextWithSlashes(
recallToHeadline(questionSummary.question.headline, survey, true, "default", attributeClasses)[
"default"
]
recallToHeadline(
questionSummary.question.headline,
survey,
true,
"default",
contactAttributeKeys
)["default"]
)}
</h3>
</div>

View File

@@ -1,5 +1,5 @@
import { useTranslations } from "next-intl";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TSurvey, TSurveyQuestionSummaryRanking, TSurveyType } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { convertFloatToNDecimal } from "../lib/utils";
@@ -9,7 +9,7 @@ interface RankingSummaryProps {
questionSummary: TSurveyQuestionSummaryRanking;
surveyType: TSurveyType;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
locale: TUserLocale;
}
@@ -17,7 +17,7 @@ export const RankingSummary = ({
questionSummary,
surveyType,
survey,
attributeClasses,
contactAttributeKeys,
locale,
}: RankingSummaryProps) => {
// sort by count and transform to array
@@ -31,7 +31,7 @@ export const RankingSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">

View File

@@ -4,7 +4,7 @@ import { RatingResponse } from "@/modules/ui/components/rating-response";
import { CircleSlash2, SmileIcon, StarIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import {
TI18nString,
TSurvey,
@@ -18,7 +18,7 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
interface RatingSummaryProps {
questionSummary: TSurveyQuestionSummaryRating;
survey: TSurvey;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
setFilter: (
questionId: TSurveyQuestionId,
label: TI18nString,
@@ -32,7 +32,7 @@ interface RatingSummaryProps {
export const RatingSummary = ({
questionSummary,
survey,
attributeClasses,
contactAttributeKeys,
setFilter,
locale,
}: RatingSummaryProps) => {
@@ -49,7 +49,7 @@ export const RatingSummary = ({
<QuestionSummaryHeader
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
additionalInfo={
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">

View File

@@ -26,7 +26,7 @@ import { SkeletonLoader } from "@/modules/ui/components/skeleton-loader";
import { useTranslations } from "next-intl";
import { toast } from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import { TI18nString, TSurveyQuestionId, TSurveySummary } from "@formbricks/types/surveys/types";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
@@ -40,7 +40,7 @@ interface SummaryListProps {
environment: TEnvironment;
survey: TSurvey;
totalResponseCount: number;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
isAIEnabled: boolean;
documentsPerPage?: number;
locale: TUserLocale;
@@ -52,7 +52,7 @@ export const SummaryList = ({
responseCount,
survey,
totalResponseCount,
attributeClasses,
contactAttributeKeys,
isAIEnabled,
documentsPerPage,
locale,
@@ -137,7 +137,7 @@ export const SummaryList = ({
questionSummary={questionSummary}
environmentId={environment.id}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
isAIEnabled={isAIEnabled}
documentsPerPage={documentsPerPage}
locale={locale}
@@ -155,7 +155,7 @@ export const SummaryList = ({
environmentId={environment.id}
surveyType={survey.type}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
setFilter={setFilter}
locale={locale}
/>
@@ -167,7 +167,7 @@ export const SummaryList = ({
key={questionSummary.question.id}
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
setFilter={setFilter}
locale={locale}
/>
@@ -179,7 +179,7 @@ export const SummaryList = ({
key={questionSummary.question.id}
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
);
@@ -190,7 +190,7 @@ export const SummaryList = ({
key={questionSummary.question.id}
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
setFilter={setFilter}
locale={locale}
/>
@@ -202,7 +202,7 @@ export const SummaryList = ({
key={questionSummary.question.id}
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
setFilter={setFilter}
locale={locale}
/>
@@ -214,7 +214,7 @@ export const SummaryList = ({
key={questionSummary.question.id}
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
setFilter={setFilter}
locale={locale}
/>
@@ -227,7 +227,7 @@ export const SummaryList = ({
questionSummary={questionSummary}
environmentId={environment.id}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
);
@@ -239,7 +239,7 @@ export const SummaryList = ({
questionSummary={questionSummary}
environmentId={environment.id}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
);
@@ -251,7 +251,7 @@ export const SummaryList = ({
questionSummary={questionSummary}
environmentId={environment.id}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
);
@@ -262,7 +262,7 @@ export const SummaryList = ({
key={questionSummary.question.id}
questionSummary={questionSummary}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
setFilter={setFilter}
locale={locale}
/>
@@ -275,7 +275,7 @@ export const SummaryList = ({
questionSummary={questionSummary}
environmentId={environment.id}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
);
@@ -287,7 +287,7 @@ export const SummaryList = ({
questionSummary={questionSummary}
surveyType={survey.type}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
);
@@ -309,7 +309,7 @@ export const SummaryList = ({
questionSummary={questionSummary}
environmentId={environment.id}
survey={survey}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
locale={locale}
/>
);

View File

@@ -18,7 +18,7 @@ import { useParams, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { useIntervalWhenFocused } from "@formbricks/lib/utils/hooks/useIntervalWhenFocused";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
import { TUser, TUserLocale } from "@formbricks/types/user";
@@ -47,7 +47,7 @@ interface SummaryPageProps {
webAppUrl: string;
user?: TUser;
totalResponseCount: number;
attributeClasses: TAttributeClass[];
contactAttributeKeys: TContactAttributeKey[];
isAIEnabled: boolean;
documentsPerPage?: number;
locale: TUserLocale;
@@ -60,7 +60,7 @@ export const SummaryPage = ({
surveyId,
webAppUrl,
totalResponseCount,
attributeClasses,
contactAttributeKeys,
isAIEnabled,
documentsPerPage,
locale,
@@ -154,8 +154,8 @@ export const SummaryPage = ({
);
const surveyMemoized = useMemo(() => {
return replaceHeadlineRecall(survey, "default", attributeClasses);
}, [survey, attributeClasses]);
return replaceHeadlineRecall(survey, "default", contactAttributeKeys);
}, [survey, contactAttributeKeys]);
useEffect(() => {
if (!searchParams?.get("referer")) {
@@ -185,7 +185,7 @@ export const SummaryPage = ({
survey={surveyMemoized}
environment={environment}
totalResponseCount={totalResponseCount}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
isAIEnabled={isAIEnabled}
documentsPerPage={documentsPerPage}
locale={locale}

View File

@@ -308,8 +308,8 @@ export const getQuestionSummary = async (
id: response.id,
updatedAt: response.updatedAt,
value: answer,
person: response.person,
personAttributes: response.personAttributes,
contact: response.contact,
contactAttributes: response.contactAttributes,
});
}
});
@@ -366,8 +366,8 @@ export const getQuestionSummary = async (
} else if (isOthersEnabled) {
otherValues.push({
value,
person: response.person,
personAttributes: response.personAttributes,
contact: response.contact,
contactAttributes: response.contactAttributes,
});
}
hasValidAnswer = true;
@@ -381,8 +381,8 @@ export const getQuestionSummary = async (
} else if (isOthersEnabled) {
otherValues.push({
value: answer,
person: response.person,
personAttributes: response.personAttributes,
contact: response.contact,
contactAttributes: response.contactAttributes,
});
}
hasValidAnswer = true;
@@ -650,8 +650,8 @@ export const getQuestionSummary = async (
id: response.id,
updatedAt: response.updatedAt,
value: answer,
person: response.person,
personAttributes: response.personAttributes,
contact: response.contact,
contactAttributes: response.contactAttributes,
});
}
});
@@ -675,8 +675,8 @@ export const getQuestionSummary = async (
id: response.id,
updatedAt: response.updatedAt,
value: answer,
person: response.person,
personAttributes: response.personAttributes,
contact: response.contact,
contactAttributes: response.contactAttributes,
});
}
});
@@ -791,8 +791,8 @@ export const getQuestionSummary = async (
id: response.id,
updatedAt: response.updatedAt,
value: answer,
person: response.person,
personAttributes: response.personAttributes,
contact: response.contact,
contactAttributes: response.contactAttributes,
});
}
});
@@ -868,8 +868,8 @@ export const getQuestionSummary = async (
values.push({
updatedAt: response.updatedAt,
value: answer,
person: response.person,
personAttributes: response.personAttributes,
contact: response.contact,
contactAttributes: response.contactAttributes,
});
}
});

View File

@@ -4,6 +4,7 @@ import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[s
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contacts";
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
@@ -12,7 +13,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import {
DEFAULT_LOCALE,
DOCUMENTS_PER_PAGE,
@@ -28,7 +28,7 @@ import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getUser } from "@formbricks/lib/user/service";
const Page = async (props) => {
const SurveyPage = async (props: { params: Promise<{ environmentId: string; surveyId: string }> }) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
@@ -42,10 +42,10 @@ const Page = async (props) => {
return notFound();
}
const [survey, environment, attributeClasses] = await Promise.all([
const [survey, environment, contactAttributeKeys] = await Promise.all([
getSurvey(params.surveyId),
getEnvironment(params.environmentId),
getAttributeClasses(params.environmentId),
getContactAttributeKeys(params.environmentId),
]);
if (!environment) {
throw new Error(t("common.environment_not_found"));
@@ -118,7 +118,7 @@ const Page = async (props) => {
webAppUrl={WEBAPP_URL}
user={user}
totalResponseCount={totalResponseCount}
attributeClasses={attributeClasses}
contactAttributeKeys={contactAttributeKeys}
isAIEnabled={isAIEnabled}
documentsPerPage={DOCUMENTS_PER_PAGE}
isReadOnly={isReadOnly}
@@ -128,4 +128,4 @@ const Page = async (props) => {
);
};
export default Page;
export default SurveyPage;

View File

@@ -72,7 +72,7 @@ export const getSurveyFilterDataAction = authenticatedActionClient
],
});
const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([
const [tags, { contactAttributes: attributes, meta, hiddenFields }] = await Promise.all([
getTagsByEnvironmentId(survey.environmentId),
getResponseFilteringValues(parsedInput.surveyId),
]);

View File

@@ -7,7 +7,7 @@ import { Button } from "@/modules/ui/components/button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { PlusIcon } from "lucide-react";
import { Metadata } from "next";
import { Metadata, NextPage } from "next";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
@@ -35,9 +35,10 @@ interface SurveyTemplateProps {
}>;
}
const Page = async (props: SurveyTemplateProps) => {
const searchParams = await props.searchParams;
const params = await props.params;
const SurveysPage = async ({ params: paramsProps, searchParams: searchParamsProps }: SurveyTemplateProps) => {
const searchParams = await searchParamsProps;
const params = await paramsProps;
const session = await getServerSession(authOptions);
const project = await getProjectByEnvironmentId(params.environmentId);
const organization = await getOrganizationByEnvironmentId(params.environmentId);
@@ -136,4 +137,4 @@ const Page = async (props: SurveyTemplateProps) => {
);
};
export default Page;
export default SurveysPage as NextPage;

View File

@@ -0,0 +1,3 @@
import { SingleContactPage } from "@/modules/ee/contacts/[contactId]/page";
export default SingleContactPage;

View File

@@ -0,0 +1,3 @@
import { ContactsPage } from "@/modules/ee/contacts/page";
export default ContactsPage;

View File

@@ -0,0 +1,3 @@
import EnvLayout from "../../../../(app)/environments/[environmentId]/layout";
export default EnvLayout;

View File

@@ -0,0 +1,3 @@
import { SegmentsPage } from "@/modules/ee/contacts/segments/page";
export default SegmentsPage;

View File

@@ -0,0 +1,48 @@
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId } from "@formbricks/types/common";
import { TContactAttributes } from "@formbricks/types/contact-attribute";
import { DatabaseError } from "@formbricks/types/errors";
export const getContactAttributes = reactCache((contactId: string) =>
cache(
async () => {
validateInputs([contactId, ZId]);
try {
const prismaAttributes = await prisma.contactAttribute.findMany({
where: {
contactId,
},
select: {
attributeKey: {
select: {
key: true,
},
},
value: true,
},
});
return prismaAttributes.reduce((acc, attr) => {
acc[attr.attributeKey.key] = attr.value;
return acc;
}, {}) as TContactAttributes;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getContactAttributes-insights-${contactId}`],
{
tags: [contactAttributeCache.tag.byContactId(contactId)],
}
)()
);

View File

@@ -6,7 +6,6 @@ import { Prisma } from "@prisma/client";
import { embed } from "ai";
import { prisma } from "@formbricks/database";
import { embeddingsModel } from "@formbricks/lib/aiModels";
import { getAttributes } from "@formbricks/lib/attribute/service";
import { getPromptText } from "@formbricks/lib/utils/ai";
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
import { validateInputs } from "@formbricks/lib/utils/validate";
@@ -25,6 +24,7 @@ import {
TSurveyQuestionTypeEnum,
ZSurveyQuestions,
} from "@formbricks/types/surveys/types";
import { getContactAttributes } from "./contact-attribute";
export const generateInsightsForSurveyResponsesConcept = async (
survey: Pick<TSurvey, "id" | "name" | "environmentId" | "questions">
@@ -82,7 +82,7 @@ export const generateInsightsForSurveyResponsesConcept = async (
id: true,
data: true,
variables: true,
personId: true,
contactId: true,
language: true,
},
take: batchSize,
@@ -104,7 +104,9 @@ export const generateInsightsForSurveyResponsesConcept = async (
const answersForDocumentCreationPromises = await Promise.all(
responsesWithOpenTextAnswers.map(async (response) => {
const attributes = response.personId ? await getAttributes(response.personId) : {};
const contactAttributes = response.contactId
? await getContactAttributes(response.contactId)
: {};
const responseEntries = openTextQuestionsWithInsights.map((question) => {
const responseText = response.data[question.id] as string;
if (!responseText) {
@@ -113,7 +115,7 @@ export const generateInsightsForSurveyResponsesConcept = async (
const headline = parseRecallInfo(
question.headline[response.language ?? "default"],
attributes,
contactAttributes,
response.data,
response.variables
);
@@ -242,7 +244,7 @@ export const generateInsightsForSurveyResponses = async (
id: true,
data: true,
variables: true,
personId: true,
contactId: true,
language: true,
},
take: batchSize,
@@ -258,7 +260,7 @@ export const generateInsightsForSurveyResponses = async (
const createDocumentPromises: Promise<TCreatedDocument | undefined>[] = [];
for (const response of responsesWithOpenTextAnswers) {
const attributes = response.personId ? await getAttributes(response.personId) : {};
const contactAttributes = response.contactId ? await getContactAttributes(response.contactId) : {};
for (const question of openTextQuestionsWithInsights) {
const responseText = response.data[question.id] as string;
@@ -268,7 +270,7 @@ export const generateInsightsForSurveyResponses = async (
const headline = parseRecallInfo(
question.headline[response.language ?? "default"],
attributes,
contactAttributes,
response.data,
response.variables
);

Some files were not shown because too many files have changed in this diff Show More