mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-05 02:52:50 -05:00
feat: Formbricks App Redesign (#2581)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com> Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import FormbricksClient from "@/app/(app)/components/FormbricksClient";
|
||||
import PosthogIdentify from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
||||
import ToasterClient from "@formbricks/ui/ToasterClient";
|
||||
|
||||
export default async function EnvLayout({ children, params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
||||
if (!hasAccess) {
|
||||
throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
|
||||
const team = await getTeamByEnvironmentId(params.environmentId);
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResponseFilterProvider>
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
environmentId={params.environmentId}
|
||||
teamId={team.id}
|
||||
teamName={team.name}
|
||||
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
|
||||
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
|
||||
userTargetingBillingStatus={team.billing.features.userTargeting.status}
|
||||
/>
|
||||
<FormbricksClient session={session} />
|
||||
<ToasterClient />
|
||||
<div className="flex h-screen flex-col">
|
||||
<DevEnvironmentBanner environment={environment} />
|
||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||
</div>
|
||||
</ResponseFilterProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -61,7 +61,7 @@ export const deleteSurveyAction = async (surveyId: string) => {
|
||||
await deleteSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const refetchProduct = async (productId: string): Promise<TProduct | null> => {
|
||||
export const refetchProductAction = async (productId: string): Promise<TProduct | null> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { CreateNewActionTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/CreateNewActionTab";
|
||||
import { SavedActionsTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SavedActionsTab";
|
||||
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
|
||||
|
||||
import { CreateNewActionTab } from "./CreateNewActionTab";
|
||||
import { SavedActionsTab } from "./SavedActionsTab";
|
||||
|
||||
interface AddActionModalProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import LogicEditor from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor";
|
||||
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import LogicEditor from "./LogicEditor";
|
||||
import UpdateQuestionId from "./UpdateQuestionId";
|
||||
|
||||
interface AdvancedSettingsProps {
|
||||
+3
-3
@@ -23,7 +23,7 @@ interface BackgroundStylingCardProps {
|
||||
isUnsplashConfigured: boolean;
|
||||
}
|
||||
|
||||
export default function BackgroundStylingCard({
|
||||
export const BackgroundStylingCard = ({
|
||||
open,
|
||||
setOpen,
|
||||
styling,
|
||||
@@ -33,7 +33,7 @@ export default function BackgroundStylingCard({
|
||||
disabled,
|
||||
environmentId,
|
||||
isUnsplashConfigured,
|
||||
}: BackgroundStylingCardProps) {
|
||||
}: BackgroundStylingCardProps) => {
|
||||
const { bgType, brightness } = styling?.background ?? {};
|
||||
|
||||
const handleBgChange = (color: string, type: TSurveyBackgroundBgType) => {
|
||||
@@ -150,4 +150,4 @@ export default function BackgroundStylingCard({
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
};
|
||||
+1
-3
@@ -27,7 +27,7 @@ type CardStylingSettingsProps = {
|
||||
localProduct: TProduct;
|
||||
};
|
||||
|
||||
const CardStylingSettings = ({
|
||||
export const CardStylingSettings = ({
|
||||
setStyling,
|
||||
styling,
|
||||
isSettingsPage = false,
|
||||
@@ -300,5 +300,3 @@ const CardStylingSettings = ({
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardStylingSettings;
|
||||
+1
-3
@@ -21,7 +21,7 @@ type FormStylingSettingsProps = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const FormStylingSettings = ({
|
||||
export const FormStylingSettings = ({
|
||||
styling,
|
||||
setStyling,
|
||||
open,
|
||||
@@ -201,5 +201,3 @@ const FormStylingSettings = ({
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormStylingSettings;
|
||||
+2
-1
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { validateId } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { FC, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -13,6 +12,8 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { Tag } from "@formbricks/ui/Tag";
|
||||
|
||||
import { validateId } from "../lib/validation";
|
||||
|
||||
interface HiddenFieldsCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
+1
-1
@@ -182,7 +182,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey, environment
|
||||
</p>
|
||||
<p className="text-xs font-normal">
|
||||
<Link
|
||||
href={`/environments/${environment.id}/settings/setup`}
|
||||
href={`/environments/${environment.id}/product/setup`}
|
||||
className="underline hover:text-amber-900"
|
||||
target="_blank">
|
||||
Connect Formbricks
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { HelpCircle, TrashIcon } from "lucide-react";
|
||||
import { ChevronDown, SplitIcon } from "lucide-react";
|
||||
import { CornerDownRightIcon, MoveDownIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { BsArrowDown, BsArrowReturnRight } from "react-icons/bs";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
@@ -269,7 +269,7 @@ export default function LogicEditor({
|
||||
<div className="mt-2 space-y-3">
|
||||
{question?.logic?.map((logic, logicIdx) => (
|
||||
<div key={logicIdx} className="flex items-center space-x-2 space-y-1 text-xs xl:text-sm">
|
||||
<BsArrowReturnRight className="h-4 w-4" />
|
||||
<CornerDownRightIcon className="h-4 w-4" />
|
||||
<p className="text-slate-800">If this answer</p>
|
||||
|
||||
<Select value={logic.condition} onValueChange={(e) => updateLogic(logicIdx, { condition: e })}>
|
||||
@@ -385,7 +385,7 @@ export default function LogicEditor({
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-wrap items-center space-x-2 py-1 text-sm">
|
||||
<BsArrowDown className="h-4 w-4" />
|
||||
<MoveDownIcon className="h-4 w-4" />
|
||||
<p className="text-slate-700">All other answers will continue to the next question</p>
|
||||
</div>
|
||||
</div>
|
||||
+2
-1
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { isLabelValidForAllLanguages } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -11,6 +10,8 @@ import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
|
||||
interface MatrixQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyMatrixQuestion;
|
||||
+2
-1
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { isLabelValidForAllLanguages } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -20,6 +19,8 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyMultipleChoiceSingleQuestion;
|
||||
+1
-2
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { getPlacementStyle } from "@/app/lib/preview";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TPlacement } from "@formbricks/types/common";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
|
||||
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
||||
|
||||
const placements = [
|
||||
+15
-15
@@ -1,19 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { AddressQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddressQuestionForm";
|
||||
import { AdvancedSettings } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AdvancedSettings";
|
||||
import { CTAQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/CTAQuestionForm";
|
||||
import { CalQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/CalQuestionForm";
|
||||
import { ConsentQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConsentQuestionForm";
|
||||
import { DateQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/DateQuestionForm";
|
||||
import { FileUploadQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/FileUploadQuestionForm";
|
||||
import { MatrixQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MatrixQuestionForm";
|
||||
import { MultipleChoiceMultiForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceMultiForm";
|
||||
import { MultipleChoiceSingleForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceSingleForm";
|
||||
import { NPSQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/NPSQuestionForm";
|
||||
import { OpenQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/OpenQuestionForm";
|
||||
import { PictureSelectionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm";
|
||||
import { RatingQuestionForm } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/RatingQuestionForm";
|
||||
import { getTSurveyQuestionTypeName } from "@/app/lib/questions";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import {
|
||||
@@ -44,7 +30,21 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
import QuestionDropdown from "./QuestionMenu";
|
||||
import { AddressQuestionForm } from "./AddressQuestionForm";
|
||||
import { AdvancedSettings } from "./AdvancedSettings";
|
||||
import { CTAQuestionForm } from "./CTAQuestionForm";
|
||||
import { CalQuestionForm } from "./CalQuestionForm";
|
||||
import { ConsentQuestionForm } from "./ConsentQuestionForm";
|
||||
import { DateQuestionForm } from "./DateQuestionForm";
|
||||
import { FileUploadQuestionForm } from "./FileUploadQuestionForm";
|
||||
import { MatrixQuestionForm } from "./MatrixQuestionForm";
|
||||
import { MultipleChoiceMultiForm } from "./MultipleChoiceMultiForm";
|
||||
import { MultipleChoiceSingleForm } from "./MultipleChoiceSingleForm";
|
||||
import { NPSQuestionForm } from "./NPSQuestionForm";
|
||||
import { OpenQuestionForm } from "./OpenQuestionForm";
|
||||
import { PictureSelectionForm } from "./PictureSelectionForm";
|
||||
import { QuestionDropdown } from "./QuestionMenu";
|
||||
import { RatingQuestionForm } from "./RatingQuestionForm";
|
||||
|
||||
interface QuestionCardProps {
|
||||
localSurvey: TSurvey;
|
||||
+3
-3
@@ -10,13 +10,13 @@ interface QuestionDropdownProps {
|
||||
moveQuestion: (questionIdx: number, up: boolean) => void;
|
||||
}
|
||||
|
||||
export default function QuestionActions({
|
||||
export const QuestionDropdown = ({
|
||||
questionIdx,
|
||||
lastQuestion,
|
||||
duplicateQuestion,
|
||||
deleteQuestion,
|
||||
moveQuestion,
|
||||
}: QuestionDropdownProps) {
|
||||
}: QuestionDropdownProps) => {
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
<ArrowUpIcon
|
||||
@@ -57,4 +57,4 @@ export default function QuestionActions({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
+2
-6
@@ -1,11 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import HiddenFieldsCard from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard";
|
||||
import {
|
||||
isCardValid,
|
||||
validateQuestion,
|
||||
validateSurveyQuestionsInBatch,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { DragDropContext } from "react-beautiful-dnd";
|
||||
@@ -18,9 +12,11 @@ import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/u
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import { isCardValid, validateQuestion, validateSurveyQuestionsInBatch } from "../lib/validation";
|
||||
import AddQuestionButton from "./AddQuestionButton";
|
||||
import EditThankYouCard from "./EditThankYouCard";
|
||||
import EditWelcomeCard from "./EditWelcomeCard";
|
||||
import HiddenFieldsCard from "./HiddenFieldsCard";
|
||||
import QuestionCard from "./QuestionCard";
|
||||
import { StrictModeDroppable } from "./StrictModeDroppable";
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ export default function RecontactOptionsCard({
|
||||
This setting overwrites your{" "}
|
||||
<Link
|
||||
className="decoration-brand-dark underline"
|
||||
href={`/environments/${environmentId}/settings/product`}
|
||||
href={`/environments/${environmentId}/product/general`}
|
||||
target="_blank">
|
||||
waiting period
|
||||
</Link>
|
||||
+1
-2
@@ -1,5 +1,3 @@
|
||||
import SurveyPlacementCard from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyPlacementCard";
|
||||
|
||||
import { AdvancedTargetingCard } from "@formbricks/ee/advancedTargeting/components/AdvancedTargetingCard";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
@@ -11,6 +9,7 @@ import { TSurvey } from "@formbricks/types/surveys";
|
||||
import HowToSendCard from "./HowToSendCard";
|
||||
import RecontactOptionsCard from "./RecontactOptionsCard";
|
||||
import ResponseOptionsCard from "./ResponseOptionsCard";
|
||||
import SurveyPlacementCard from "./SurveyPlacementCard";
|
||||
import TargetingCard from "./TargetingCard";
|
||||
import WhenToSendCard from "./WhenToSendCard";
|
||||
|
||||
+4
-3
@@ -1,6 +1,3 @@
|
||||
import BackgroundStylingCard from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/BackgroundStylingCard";
|
||||
import CardStylingSettings from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/CardStylingSettings";
|
||||
import FormStylingSettings from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/FormStylingSettings";
|
||||
import { RotateCcwIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
@@ -13,6 +10,10 @@ import { AlertDialog } from "@formbricks/ui/AlertDialog";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
import { BackgroundStylingCard } from "./BackgroundStylingCard";
|
||||
import { CardStylingSettings } from "./CardStylingSettings";
|
||||
import { FormStylingSettings } from "./FormStylingSettings";
|
||||
|
||||
type StylingViewProps = {
|
||||
environment: TEnvironment;
|
||||
product: TProduct;
|
||||
+11
-10
@@ -1,13 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { refetchProduct } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { LoadingSkeleton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton";
|
||||
import { QuestionsAudienceTabs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs";
|
||||
import { QuestionsView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView";
|
||||
import { SettingsView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView";
|
||||
import { StylingView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingView";
|
||||
import { SurveyMenuBar } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar";
|
||||
import { PreviewSurvey } from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { extractLanguageCodes, getEnabledLanguages } from "@formbricks/lib/i18n/utils";
|
||||
@@ -20,6 +12,15 @@ import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys";
|
||||
import { PreviewSurvey } from "@formbricks/ui/PreviewSurvey";
|
||||
|
||||
import { refetchProductAction } from "../actions";
|
||||
import { LoadingSkeleton } from "./LoadingSkeleton";
|
||||
import { QuestionsAudienceTabs } from "./QuestionsStylingSettingsTabs";
|
||||
import { QuestionsView } from "./QuestionsView";
|
||||
import { SettingsView } from "./SettingsView";
|
||||
import { StylingView } from "./StylingView";
|
||||
import { SurveyMenuBar } from "./SurveyMenuBar";
|
||||
|
||||
interface SurveyEditorProps {
|
||||
survey: TSurvey;
|
||||
@@ -64,7 +65,7 @@ export default function SurveyEditor({
|
||||
const [localStylingChanges, setLocalStylingChanges] = useState<TSurveyStyling | null>(null);
|
||||
|
||||
const fetchLatestProduct = useCallback(async () => {
|
||||
const latestProduct = await refetchProduct(localProduct.id);
|
||||
const latestProduct = await refetchProductAction(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
}
|
||||
@@ -91,7 +92,7 @@ export default function SurveyEditor({
|
||||
const listener = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
const fetchLatestProduct = async () => {
|
||||
const latestProduct = await refetchProduct(localProduct.id);
|
||||
const latestProduct = await refetchProductAction(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
}
|
||||
+2
-9
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||
import { isSurveyValid } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
|
||||
import { isEqual } from "lodash";
|
||||
import { AlertTriangleIcon, ArrowLeftIcon, SettingsIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -19,6 +18,7 @@ import { Input } from "@formbricks/ui/Input";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
|
||||
|
||||
import { updateSurveyAction } from "../actions";
|
||||
import { isSurveyValid } from "../lib/validation";
|
||||
|
||||
interface SurveyMenuBarProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -53,7 +53,7 @@ export const SurveyMenuBar = ({
|
||||
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [isSurveyPublishing, setIsSurveyPublishing] = useState(false);
|
||||
const [isSurveySaving, setIsSurveySaving] = useState(false);
|
||||
const cautionText = "This survey received responses, make changes with caution.";
|
||||
const cautionText = "This survey received responses.";
|
||||
|
||||
const faultyQuestions: string[] = [];
|
||||
|
||||
@@ -216,13 +216,6 @@ export const SurveyMenuBar = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{environment?.type === "development" && (
|
||||
<nav className="top-0 z-10 w-full border-b border-slate-200 bg-white">
|
||||
<div className="h-6 w-full bg-[#A33700] p-0.5 text-center text-sm text-white">
|
||||
You're in development mode. Use it to test surveys, actions and attributes.
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
<div className="border-b border-slate-200 bg-white px-5 py-3 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<Button
|
||||
+2
-1
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Placement from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/Placement";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -11,6 +10,8 @@ import { TSurvey, TSurveyProductOverwrites } from "@formbricks/types/surveys";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
import Placement from "./Placement";
|
||||
|
||||
interface SurveyPlacementCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
+2
-1
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { validateId } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/lib/validation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
@@ -9,6 +8,8 @@ import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
|
||||
import { validateId } from "../lib/validation";
|
||||
|
||||
interface UpdateQuestionIdProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyQuestion;
|
||||
+2
-1
@@ -131,7 +131,8 @@ export default function WhenToSendCard({
|
||||
className="w-full rounded-lg border border-slate-300 bg-white">
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
||||
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50"
|
||||
id="whenToSendCardTrigger">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
{containsEmptyTriggers ? (
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { LoadingSkeleton } from "./components/LoadingSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export const minimalSurvey: TSurvey = {
|
||||
id: "someUniqueId1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
name: "Minimal Survey",
|
||||
type: "app",
|
||||
environmentId: "someEnvId1",
|
||||
createdBy: null,
|
||||
status: "draft",
|
||||
displayOption: "displayOnce",
|
||||
autoClose: null,
|
||||
triggers: [],
|
||||
redirectUrl: null,
|
||||
recontactDays: null,
|
||||
welcomeCard: {
|
||||
enabled: false,
|
||||
headline: { default: "Welcome!" },
|
||||
html: { default: "Thanks for providing your feedback - let's go!" },
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
},
|
||||
questions: [],
|
||||
thankYouCard: {
|
||||
enabled: false,
|
||||
},
|
||||
hiddenFields: {
|
||||
enabled: false,
|
||||
},
|
||||
delay: 0, // No delay
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
surveyClosedMessage: {
|
||||
enabled: false,
|
||||
},
|
||||
productOverwrites: null,
|
||||
singleUse: null,
|
||||
styling: null,
|
||||
resultShareKey: null,
|
||||
segment: null,
|
||||
languages: [],
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
export const BackButton = () => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
StartIcon={ArrowLeftIcon}
|
||||
onClick={() => {
|
||||
router.back();
|
||||
}}>
|
||||
Back
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { BackButton } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/BackButton";
|
||||
|
||||
export const MenuBar = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-slate-200 bg-white px-5 py-3 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<BackButton />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+10
-16
@@ -1,17 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { PreviewSurvey } from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey";
|
||||
import { TemplateList } from "@/app/(app)/environments/[environmentId]/surveys/templates/TemplateList";
|
||||
import { replacePresetPlaceholders } from "@/app/lib/templates";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MenuBar } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/MenuBar";
|
||||
import { useState } from "react";
|
||||
|
||||
import type { TEnvironment } from "@formbricks/types/environment";
|
||||
import type { TProduct } from "@formbricks/types/product";
|
||||
import type { TTemplate } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { PreviewSurvey } from "@formbricks/ui/PreviewSurvey";
|
||||
import { SearchBox } from "@formbricks/ui/SearchBox";
|
||||
import { TemplateList } from "@formbricks/ui/TemplateList";
|
||||
|
||||
import { minimalSurvey, templates } from "./templates";
|
||||
import { minimalSurvey } from "../../lib/minimalSurvey";
|
||||
|
||||
type TemplateContainerWithPreviewProps = {
|
||||
environmentId: string;
|
||||
@@ -29,21 +29,15 @@ export default function TemplateContainerWithPreview({
|
||||
const [activeTemplate, setActiveTemplate] = useState<TTemplate | null>(null);
|
||||
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
||||
const [templateSearch, setTemplateSearch] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (product && templates?.length) {
|
||||
const newTemplate = replacePresetPlaceholders(templates[0], product);
|
||||
setActiveTemplate(newTemplate);
|
||||
setActiveQuestionId(newTemplate.preset.questions[0].id);
|
||||
}
|
||||
}, [product]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col ">
|
||||
<div className="flex h-full flex-col">
|
||||
<MenuBar />
|
||||
<div className="relative z-0 flex flex-1 overflow-hidden">
|
||||
<div className="flex-1 flex-col overflow-auto bg-slate-50">
|
||||
<div className="flex flex-col items-center justify-between md:flex-row md:items-start">
|
||||
<h1 className="ml-6 mt-6 text-2xl font-bold text-slate-800">Create a new survey</h1>
|
||||
<div className="ml-6 mt-6 px-6">
|
||||
<div className="ml-6 mt-6 flex flex-col items-center justify-between md:flex-row md:items-start">
|
||||
<h1 className="text-2xl font-bold text-slate-800">Create a new survey</h1>
|
||||
<div className="px-6">
|
||||
<SearchBox
|
||||
autoFocus
|
||||
value={templateSearch ?? ""}
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
|
||||
import TemplateContainerWithPreview from "./TemplateContainer";
|
||||
import TemplateContainerWithPreview from "./components/TemplateContainer";
|
||||
|
||||
export default async function SurveyTemplatesPage({ params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Confetti } from "@formbricks/ui/Confetti";
|
||||
import { ContentWrapper } from "@formbricks/ui/ContentWrapper";
|
||||
|
||||
interface ConfirmationPageProps {
|
||||
environmentId: string;
|
||||
@@ -15,25 +14,24 @@ export default function ConfirmationPage({ environmentId }: ConfirmationPageProp
|
||||
useEffect(() => {
|
||||
setShowConfetti(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
{showConfetti && <Confetti />}
|
||||
<ContentWrapper>
|
||||
<div className="mx-auto max-w-sm py-8 sm:px-6 lg:px-8">
|
||||
<div className="my-6 sm:flex-auto">
|
||||
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
|
||||
<p className="mt-2 text-center text-sm text-slate-700">
|
||||
Thanks a lot for upgrading your Formbricks subscription.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="w-full justify-center"
|
||||
href={`/environments/${environmentId}/settings/billing`}>
|
||||
Back to billing overview
|
||||
</Button>
|
||||
<div className="mx-auto max-w-sm py-8 sm:px-6 lg:px-8">
|
||||
<div className="my-6 sm:flex-auto">
|
||||
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
|
||||
<p className="mt-2 text-center text-sm text-slate-700">
|
||||
Thanks a lot for upgrading your Formbricks subscription.
|
||||
</p>
|
||||
</div>
|
||||
</ContentWrapper>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="w-full justify-center"
|
||||
href={`/environments/${environmentId}/settings/billing`}>
|
||||
Back to billing overview
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import ConfirmationPage from "./components/ConfirmationPage";
|
||||
import ConfirmationPage from "@/app/(app)/billing-confirmation/components/ConfirmationPage";
|
||||
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default function BillingConfirmation({ searchParams }) {
|
||||
const { environmentId } = searchParams;
|
||||
|
||||
return <ConfirmationPage environmentId={environmentId?.toString()} />;
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<ConfirmationPage environmentId={environmentId?.toString()} />
|
||||
</PageContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-9
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
@@ -9,7 +8,6 @@ import { Switch } from "@formbricks/ui/Switch";
|
||||
import { AttributeDetailModal } from "./AttributeDetailModal";
|
||||
import { AttributeClassDataRow } from "./AttributeRowData";
|
||||
import { AttributeTableHeading } from "./AttributeTableHeading";
|
||||
import { HowToAddAttributesButton } from "./HowToAddAttributesButton";
|
||||
import { UploadAttributesModal } from "./UploadAttributesModal";
|
||||
|
||||
interface AttributeClassesTableProps {
|
||||
@@ -45,16 +43,15 @@ export const AttributeClassesTable = ({ attributeClasses }: AttributeClassesTabl
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
{hasArchived && (
|
||||
{hasArchived && (
|
||||
<div className="my-4 flex items-center justify-end text-right">
|
||||
<div className="flex items-center text-sm font-medium">
|
||||
Show archived
|
||||
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
|
||||
</div>
|
||||
)}
|
||||
<HowToAddAttributesButton />
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<AttributeTableHeading />
|
||||
<div className="grid-cols-7">
|
||||
{displayedAttributeClasses.map((attributeClass, index) => (
|
||||
+2
-4
@@ -5,12 +5,10 @@ import { Badge } from "@formbricks/ui/Badge";
|
||||
|
||||
export const AttributeClassDataRow = ({ attributeClass }) => {
|
||||
return (
|
||||
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
|
||||
<div 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-5 flex items-center pl-6 text-sm sm:col-span-3">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<TagIcon className="h-8 w-8 flex-shrink-0 text-slate-500" />
|
||||
</div>
|
||||
<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}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
export const AttributeTableHeading = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<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 ">Name</div>
|
||||
<div className="hidden text-center sm:block">Created</div>
|
||||
<div className="hidden text-center sm:block">Last Updated</div>
|
||||
+1
-14
@@ -1,21 +1,8 @@
|
||||
import { HelpCircleIcon, TagIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { TagIcon } from "lucide-react";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="pointer-events-none animate-pulse cursor-not-allowed select-none">
|
||||
<HelpCircleIcon className="mr-2 h-4 w-4" />
|
||||
Loading Attributes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
|
||||
import { CircleHelpIcon } from "lucide-react";
|
||||
import { Metadata } from "next";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
import { AttributeClassesTable } from "./components/AttributeClassesTable";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Attributes",
|
||||
};
|
||||
|
||||
export default async function AttributesPage({ params }) {
|
||||
let attributeClasses = await getAttributeClasses(params.environmentId);
|
||||
|
||||
const HowToAddAttributesButton = (
|
||||
<Button
|
||||
size="sm"
|
||||
href="https://formbricks.com/docs/app-surveys/user-identification#setting-custom-user-attributes"
|
||||
variant="secondary"
|
||||
target="_blank"
|
||||
EndIcon={CircleHelpIcon}>
|
||||
How to add attributes
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="People" cta={HowToAddAttributesButton}>
|
||||
<PeopleSecondaryNavigation activeId="attributes" environmentId={params.environmentId} />
|
||||
</PageHeader>
|
||||
<AttributeClassesTable attributeClasses={attributeClasses} />
|
||||
</PageContentWrapper>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityTimeline";
|
||||
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivityTimeline";
|
||||
|
||||
import { getActionsByPersonId } from "@formbricks/lib/action/service";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
+9
-5
@@ -1,21 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/actions";
|
||||
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/actions";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
|
||||
|
||||
interface DeletePersonButtonProps {
|
||||
environmentId: string;
|
||||
personId: string;
|
||||
membershipRole?: TMembershipRole;
|
||||
isViewer: boolean;
|
||||
}
|
||||
|
||||
export function DeletePersonButton({ environmentId, personId }: DeletePersonButtonProps) {
|
||||
export const DeletePersonButton = ({ environmentId, personId, isViewer }: DeletePersonButtonProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -34,6 +33,11 @@ export function DeletePersonButton({ environmentId, personId }: DeletePersonButt
|
||||
setIsDeletingPerson(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isViewer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -51,4 +55,4 @@ export function DeletePersonButton({ environmentId, personId }: DeletePersonButt
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponseTimeline";
|
||||
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import ResponseFeed from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponsesFeed";
|
||||
import ResponseFeed from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed";
|
||||
import { ArrowDownUpIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
ActivityItemIcon,
|
||||
ActivityItemPopover,
|
||||
} from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityItemComponents";
|
||||
} from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivityItemComponents";
|
||||
import { ArrowDownUpIcon } from "lucide-react";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import ActivitySection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivitySection";
|
||||
import AttributesSection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/AttributesSection";
|
||||
import { DeletePersonButton } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton";
|
||||
import ResponseSection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
export default async function PersonPage({ params }) {
|
||||
const [environment, environmentTags, product, session, team, person, attributes] = await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getTagsByEnvironmentId(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
getTeamByEnvironmentId(params.environmentId),
|
||||
getPerson(params.personId),
|
||||
getAttributes(params.personId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
const { isViewer } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const getDeletePersonButton = () => {
|
||||
return (
|
||||
<DeletePersonButton environmentId={environment.id} personId={params.personId} isViewer={isViewer} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={getPersonIdentifier(person, attributes)} cta={getDeletePersonButton()} />
|
||||
<section className="pb-24 pt-6">
|
||||
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
|
||||
<AttributesSection personId={params.personId} />
|
||||
<ResponseSection
|
||||
environment={environment}
|
||||
personId={params.personId}
|
||||
environmentTags={environmentTags}
|
||||
/>
|
||||
<ActivitySection environmentId={params.environmentId} personId={params.personId} />
|
||||
</div>
|
||||
</section>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation";
|
||||
|
||||
interface PeopleSegmentsTabsProps {
|
||||
activeId: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const PeopleSecondaryNavigation = ({ activeId, environmentId }: PeopleSegmentsTabsProps) => {
|
||||
const navigation = [
|
||||
{
|
||||
id: "people",
|
||||
label: "People",
|
||||
href: `/environments/${environmentId}/people`,
|
||||
},
|
||||
{
|
||||
id: "segments",
|
||||
label: "Segments",
|
||||
href: `/environments/${environmentId}/segments`,
|
||||
},
|
||||
{
|
||||
id: "attributes",
|
||||
label: "Attributes",
|
||||
href: `/environments/${environmentId}/attributes`,
|
||||
},
|
||||
];
|
||||
|
||||
return <SecondaryNavigation navigation={navigation} activeId={activeId} />;
|
||||
};
|
||||
+1
-1
@@ -2,7 +2,7 @@ import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { PersonAvatar } from "@formbricks/ui/Avatars";
|
||||
|
||||
-8
@@ -1,14 +1,6 @@
|
||||
import HowToAddPeopleButton from "@/app/(app)/environments/[environmentId]/components/HowToAddPeopleButton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
<HowToAddPeopleButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6">User</div>
|
||||
+25
-11
@@ -1,10 +1,14 @@
|
||||
import HowToAddPeopleButton from "@/app/(app)/environments/[environmentId]/components/HowToAddPeopleButton";
|
||||
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
|
||||
import { CircleHelpIcon } from "lucide-react";
|
||||
|
||||
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
import { Pagination } from "@formbricks/ui/Pagination";
|
||||
|
||||
import { PersonCard } from "./components/PersonCard";
|
||||
@@ -36,28 +40,38 @@ export default async function PeoplePage({
|
||||
people = await getPeople(params.environmentId, pageNumber);
|
||||
}
|
||||
|
||||
const HowToAddPeopleButton = (
|
||||
<Button
|
||||
size="sm"
|
||||
href="https://formbricks.com/docs/app-surveys/user-identification"
|
||||
variant="secondary"
|
||||
target="_blank"
|
||||
EndIcon={CircleHelpIcon}>
|
||||
How to add people
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
<HowToAddPeopleButton />
|
||||
</div>
|
||||
</div>
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="People" cta={HowToAddPeopleButton}>
|
||||
<PeopleSecondaryNavigation activeId="people" environmentId={params.environmentId} />
|
||||
</PageHeader>
|
||||
{people.length === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environment={environment}
|
||||
emptyMessage="Your users will appear here as soon as they use your app ⏲️"
|
||||
noWidgetRequired={true}
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<div className="grid h-12 grid-cols-7 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">User</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">User ID</div>
|
||||
<div className="col-span-2 hidden text-center sm:block">Email</div>
|
||||
</div>
|
||||
{people.map((person) => (
|
||||
<PersonCard person={person} />
|
||||
<PersonCard person={person} key={person.id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -69,6 +83,6 @@ export default async function PeoplePage({
|
||||
itemsPerPage={ITEMS_PER_PAGE}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { deleteSegment, getSegment, updateSegment } from "@formbricks/lib/segment/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
|
||||
export const deleteBasicSegmentAction = async (environmentId: string, segmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const foundSegment = await getSegment(segmentId);
|
||||
|
||||
if (!foundSegment) {
|
||||
throw new Error(`Segment with id ${segmentId} not found`);
|
||||
}
|
||||
|
||||
return await deleteSegment(segmentId);
|
||||
};
|
||||
|
||||
export const updateBasicSegmentAction = async (
|
||||
environmentId: string,
|
||||
segmentId: string,
|
||||
data: TSegmentUpdateInput
|
||||
) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!environmentAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const { filters } = 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(segmentId, data);
|
||||
};
|
||||
+5
-8
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { UsersIcon } from "lucide-react";
|
||||
import { FilterIcon } from "lucide-react";
|
||||
import { FilterIcon, PlusIcon, UsersIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -118,11 +117,9 @@ const BasicCreateSegmentModal = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button variant="darkCTA" onClick={() => setOpen(true)}>
|
||||
Create Segment
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="darkCTA" size="sm" onClick={() => setOpen(true)} EndIcon={PlusIcon}>
|
||||
Create segment
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -249,7 +246,7 @@ const BasicCreateSegmentModal = ({
|
||||
onClick={() => {
|
||||
handleCreateSegment();
|
||||
}}>
|
||||
Create Segment
|
||||
Create segment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
+2
-4
@@ -1,9 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
deleteBasicSegmentAction,
|
||||
updateBasicSegmentAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { FilterIcon, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -20,6 +16,8 @@ import BasicSegmentEditor from "@formbricks/ui/Targeting/BasicSegmentEditor";
|
||||
import ConfirmDeleteSegmentModal from "@formbricks/ui/Targeting/ConfirmDeleteSegmentModal";
|
||||
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
|
||||
|
||||
import { deleteBasicSegmentAction, updateBasicSegmentAction } from "../actions";
|
||||
|
||||
type TBasicSegmentSettingsTabProps = {
|
||||
environmentId: string;
|
||||
setOpen: (open: boolean) => void;
|
||||
+27
-19
@@ -1,5 +1,6 @@
|
||||
import BasicCreateSegmentModal from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/segments/components/BasicCreateSegmentModal";
|
||||
import SegmentTable from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/segments/components/SegmentTable";
|
||||
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
|
||||
import BasicCreateSegmentModal from "@/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal";
|
||||
import SegmentTable from "@/app/(app)/environments/[environmentId]/(people)/segments/components/SegmentTable";
|
||||
|
||||
import CreateSegmentModal from "@formbricks/ee/advancedTargeting/components/CreateSegmentModal";
|
||||
import { ACTIONS_TO_EXCLUDE } from "@formbricks/ee/advancedTargeting/lib/constants";
|
||||
@@ -11,6 +12,8 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getSegments } from "@formbricks/lib/segment/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
|
||||
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
export default async function SegmentsPage({ params }) {
|
||||
const [environment, segments, attributeClasses, actionClassesFromServer, team] = await Promise.all([
|
||||
@@ -49,28 +52,33 @@ export default async function SegmentsPage({ params }) {
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAdvancedTargetingAllowed ? (
|
||||
<CreateSegmentModal
|
||||
environmentId={params.environmentId}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
segments={filteredSegments}
|
||||
/>
|
||||
) : (
|
||||
<BasicCreateSegmentModal
|
||||
attributeClasses={attributeClasses}
|
||||
environmentId={params.environmentId}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
)}
|
||||
const renderCreateSegmentButton = () =>
|
||||
isAdvancedTargetingAllowed ? (
|
||||
<CreateSegmentModal
|
||||
environmentId={params.environmentId}
|
||||
actionClasses={actionClasses}
|
||||
attributeClasses={attributeClasses}
|
||||
segments={filteredSegments}
|
||||
/>
|
||||
) : (
|
||||
<BasicCreateSegmentModal
|
||||
attributeClasses={attributeClasses}
|
||||
environmentId={params.environmentId}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle="People" cta={renderCreateSegmentButton()}>
|
||||
<PeopleSecondaryNavigation activeId="segments" environmentId={params.environmentId} />
|
||||
</PageHeader>
|
||||
{filteredSegments.length === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="table"
|
||||
environment={environment}
|
||||
emptyMessage="No segments yet. Add your first one to get started."
|
||||
noWidgetRequired={true}
|
||||
/>
|
||||
) : (
|
||||
<SegmentTable
|
||||
@@ -80,6 +88,6 @@ export default async function SegmentsPage({ params }) {
|
||||
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
}
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import SurveyNavBarName from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/attributes/components/SurveyNavBarName";
|
||||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
|
||||
interface SecondNavbarProps {
|
||||
tabs: { id: string; label: string; href: string; icon?: React.ReactNode }[];
|
||||
activeId: string;
|
||||
surveyId?: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export default async function SecondNavbar({
|
||||
tabs,
|
||||
activeId,
|
||||
surveyId,
|
||||
environmentId,
|
||||
...props
|
||||
}: SecondNavbarProps) {
|
||||
const product = await getProductByEnvironmentId(environmentId!);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
let survey;
|
||||
if (surveyId) {
|
||||
survey = await getSurvey(surveyId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<div className="grid h-14 w-full grid-cols-3 items-center justify-items-stretch border-b bg-white px-4">
|
||||
<div className="justify-self-start">
|
||||
{survey && environmentId && (
|
||||
<SurveyNavBarName surveyName={survey.name} productName={product.name} />
|
||||
)}
|
||||
</div>{" "}
|
||||
<nav className="flex h-full items-center space-x-4 justify-self-center" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<Link
|
||||
key={tab.id}
|
||||
href={tab.href}
|
||||
className={cn(
|
||||
tab.id === activeId
|
||||
? " border-brand-dark border-b-2 font-semibold text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-700",
|
||||
"flex h-full items-center px-3 text-sm font-medium"
|
||||
)}
|
||||
aria-current={tab.id === activeId ? "page" : undefined}>
|
||||
{tab.icon && <div className="mr-2 h-5 w-5">{tab.icon}</div>}
|
||||
{tab.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<div className="justify-self-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user