mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
feat: contacts revamp (#3399)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -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 |
@@ -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" },
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 || ""}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -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>;
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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={
|
||||
<>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { SingleContactPage } from "@/modules/ee/contacts/[contactId]/page";
|
||||
|
||||
export default SingleContactPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ContactsPage } from "@/modules/ee/contacts/page";
|
||||
|
||||
export default ContactsPage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import EnvLayout from "../../../../(app)/environments/[environmentId]/layout";
|
||||
|
||||
export default EnvLayout;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { SegmentsPage } from "@/modules/ee/contacts/segments/page";
|
||||
|
||||
export default SegmentsPage;
|
||||
@@ -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)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -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
Reference in New Issue
Block a user