feat: custom data fetching for surveys table (#3318)

This commit is contained in:
Dhruwang Jariwala
2024-10-10 20:06:20 +05:30
committed by GitHub
parent a6a815c014
commit abc4c7f156
22 changed files with 485 additions and 424 deletions

View File

@@ -1,5 +1,6 @@
"use server";
import { getSurvey, getSurveys } from "@/app/(app)/environments/[environmentId]/surveys/lib/surveys";
import { z } from "zod";
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
@@ -8,12 +9,7 @@ import {
getOrganizationIdFromSurveyId,
} from "@formbricks/lib/organization/utils";
import { getProducts } from "@formbricks/lib/product/service";
import {
copySurveyToOtherEnvironment,
deleteSurvey,
getSurvey,
getSurveys,
} from "@formbricks/lib/survey/service";
import { copySurveyToOtherEnvironment, deleteSurvey } from "@formbricks/lib/survey/service";
import { generateSurveySingleUseId } from "@formbricks/lib/utils/singleUseSurveys";
import { ZId } from "@formbricks/types/common";
import { ZSurveyFilterCriteria } from "@formbricks/types/surveys/types";

View File

@@ -1,14 +1,20 @@
"use client";
import { copySurveyToOtherEnvironmentAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import {
TSurveyCopyFormData,
ZSurveyCopyFormValidation,
} from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFieldArray, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyCopyFormData, ZSurveyCopyFormValidation } from "@formbricks/types/surveys/types";
import { Button } from "../../Button";
import { Checkbox } from "../../Checkbox";
import { FormControl, FormField, FormItem, FormProvider } from "../../Form";
import { Label } from "../../Label";
import { TooltipRenderer } from "../../Tooltip";
import { copySurveyToOtherEnvironmentAction } from "../actions";
import { Button } from "@formbricks/ui/components/Button";
import { Checkbox } from "@formbricks/ui/components/Checkbox";
import { FormControl, FormField, FormItem, FormProvider } from "@formbricks/ui/components/Form";
import { Label } from "@formbricks/ui/components/Label";
import { TooltipRenderer } from "@formbricks/ui/components/Tooltip";
export const CopySurveyForm = ({
defaultProducts,

View File

@@ -1,6 +1,8 @@
"use client";
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import { MousePointerClickIcon } from "lucide-react";
import { TSurvey } from "@formbricks/types/surveys/types";
import { Modal } from "../../Modal";
import { Modal } from "@formbricks/ui/components/Modal";
import SurveyCopyOptions from "./SurveyCopyOptions";
interface CopySurveyModalProps {

View File

@@ -1,5 +1,7 @@
"use client";
import { TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types";
import { DropdownMenuItem } from "../../DropdownMenu";
import { DropdownMenuItem } from "@formbricks/ui/components/DropdownMenu";
interface SortOptionProps {
option: TSortOption;

View File

@@ -0,0 +1,124 @@
"use client";
import { generateSingleUseIdAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
import { SurveyTypeIndicator } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyTypeIndicator";
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { cn } from "@formbricks/lib/cn";
import { convertDateString, timeSince } from "@formbricks/lib/time";
import { TEnvironment } from "@formbricks/types/environment";
import { SurveyStatusIndicator } from "@formbricks/ui/components/SurveyStatusIndicator";
import { SurveyDropDownMenu } from "./SurveyDropdownMenu";
interface SurveyCardProps {
survey: TSurvey;
environment: TEnvironment;
otherEnvironment: TEnvironment;
isViewer: boolean;
WEBAPP_URL: string;
duplicateSurvey: (survey: TSurvey) => void;
deleteSurvey: (surveyId: string) => void;
}
export const SurveyCard = ({
survey,
environment,
otherEnvironment,
isViewer,
WEBAPP_URL,
deleteSurvey,
duplicateSurvey,
}: SurveyCardProps) => {
const isSurveyCreationDeletionDisabled = isViewer;
const surveyStatusLabel = useMemo(() => {
if (survey.status === "inProgress") return "In Progress";
else if (survey.status === "scheduled") return "Scheduled";
else if (survey.status === "completed") return "Completed";
else if (survey.status === "draft") return "Draft";
else if (survey.status === "paused") return "Paused";
}, [survey]);
const [singleUseId, setSingleUseId] = useState<string | undefined>();
useEffect(() => {
const fetchSingleUseId = async () => {
if (survey.singleUse?.enabled) {
const generateSingleUseIdResponse = await generateSingleUseIdAction({
surveyId: survey.id,
isEncrypted: !!survey.singleUse?.isEncrypted,
});
if (generateSingleUseIdResponse?.data) {
setSingleUseId(generateSingleUseIdResponse.data);
} else {
const errorMessage = getFormattedErrorMessage(generateSingleUseIdResponse);
toast.error(errorMessage);
}
} else {
setSingleUseId(undefined);
}
};
fetchSingleUseId();
}, [survey]);
const linkHref = useMemo(() => {
return survey.status === "draft"
? `/environments/${environment.id}/surveys/${survey.id}/edit`
: `/environments/${environment.id}/surveys/${survey.id}/summary`;
}, [survey.status, survey.id, environment.id]);
return (
<Link
href={linkHref}
key={survey.id}
className="relative grid w-full grid-cols-8 place-items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition-all ease-in-out hover:scale-[101%]">
<div className="col-span-1 flex max-w-full items-center justify-self-start text-sm font-medium text-slate-900">
<div className="w-full truncate">{survey.name}</div>
</div>
<div
className={cn(
"col-span-1 flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
surveyStatusLabel === "Scheduled" && "bg-slate-200",
surveyStatusLabel === "In Progress" && "bg-emerald-50",
surveyStatusLabel === "Completed" && "bg-slate-200",
surveyStatusLabel === "Draft" && "bg-slate-100",
surveyStatusLabel === "Paused" && "bg-slate-100"
)}>
<SurveyStatusIndicator status={survey.status} /> {surveyStatusLabel}{" "}
</div>
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{survey.responseCount}
</div>
<div className="col-span-1 flex justify-between">
<SurveyTypeIndicator type={survey.type} />
</div>
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{convertDateString(survey.createdAt.toString())}
</div>
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{timeSince(survey.updatedAt.toString())}
</div>
<div className="col-span-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{survey.creator ? survey.creator.name : "-"}
</div>
<div className="col-span-1 place-self-end">
<SurveyDropDownMenu
survey={survey}
key={`surveys-${survey.id}`}
environmentId={environment.id}
environment={environment}
otherEnvironment={otherEnvironment!}
webAppUrl={WEBAPP_URL}
singleUseId={singleUseId}
isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled}
duplicateSurvey={duplicateSurvey}
deleteSurvey={deleteSurvey}
/>
</div>
</Link>
);
};

View File

@@ -1,10 +1,12 @@
"use client";
import { getProductsByEnvironmentIdAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { TProduct } from "@formbricks/types/product";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getProductsByEnvironmentIdAction } from "../actions";
import { CopySurveyForm } from "./CopySurveyForm";
interface SurveyCopyOptionsProps {

View File

@@ -1,5 +1,11 @@
"use client";
import {
copySurveyToOtherEnvironmentAction,
deleteSurveyAction,
} from "@/app/(app)/environments/[environmentId]/surveys/actions";
import { getSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import { ArrowUpFromLineIcon, CopyIcon, EyeIcon, LinkIcon, SquarePenIcon, TrashIcon } from "lucide-react";
import { MoreVertical } from "lucide-react";
import Link from "next/link";
@@ -8,16 +14,14 @@ import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TSurvey } from "@formbricks/types/surveys/types";
import { DeleteDialog } from "../../DeleteDialog";
import { DeleteDialog } from "@formbricks/ui/components/DeleteDialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../../DropdownMenu";
import { copySurveyToOtherEnvironmentAction, deleteSurveyAction, getSurveyAction } from "../actions";
} from "@formbricks/ui/components/DropdownMenu";
import { CopySurveyModal } from "./CopySurveyModal";
interface SurveyDropDownMenuProps {
@@ -49,11 +53,11 @@ export const SurveyDropDownMenu = ({
const surveyUrl = useMemo(() => webAppUrl + "/s/" + survey.id, [survey.id, webAppUrl]);
const handleDeleteSurvey = async (survey: TSurvey) => {
const handleDeleteSurvey = async (surveyId: string) => {
setLoading(true);
try {
await deleteSurveyAction({ surveyId: survey.id });
deleteSurvey(survey.id);
await deleteSurveyAction({ surveyId });
deleteSurvey(surveyId);
router.refresh();
setDeleteDialogOpen(false);
toast.success("Survey deleted successfully.");
@@ -206,7 +210,7 @@ export const SurveyDropDownMenu = ({
deleteWhat="Survey"
open={isDeleteDialogOpen}
setOpen={setDeleteDialogOpen}
onDelete={() => handleDeleteSurvey(survey)}
onDelete={() => handleDeleteSurvey(survey.id)}
text="Are you sure you want to delete this survey and all of its responses? This action cannot be undone."
/>
)}

View File

@@ -1,7 +1,14 @@
"use client";
import { ChevronDownIcon } from "lucide-react";
import { TFilterOption } from "@formbricks/types/surveys/types";
import { Checkbox } from "../../Checkbox";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../DropdownMenu";
import { Checkbox } from "@formbricks/ui/components/Checkbox";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
interface SurveyFilterDropdownProps {
title: string;

View File

@@ -1,20 +1,22 @@
import { ChevronDownIcon, Equal, Grid2X2, X } from "lucide-react";
"use client";
import { SortOption } from "@/app/(app)/environments/[environmentId]/surveys/components/SortOption";
import { initialFilters } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyList";
import { ChevronDownIcon, X } from "lucide-react";
import { useState } from "react";
import { useDebounce } from "react-use";
import { FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS } from "@formbricks/lib/localStorage";
import { TProductConfigChannel } from "@formbricks/types/product";
import { TFilterOption, TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types";
import { initialFilters } from "..";
import { Button } from "../../Button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "../../DropdownMenu";
import { SearchBar } from "../../SearchBar";
import { TooltipRenderer } from "../../Tooltip";
import { SortOption } from "./SortOption";
import { Button } from "@formbricks/ui/components/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
import { SearchBar } from "@formbricks/ui/components/SearchBar";
import { SurveyFilterDropdown } from "./SurveyFilterDropdown";
interface SurveyFilterProps {
orientation: string;
setOrientation: (orientation: string) => void;
surveyFilters: TSurveyFilters;
setSurveyFilters: React.Dispatch<React.SetStateAction<TSurveyFilters>>;
currentProductChannel: TProductConfigChannel;
@@ -52,13 +54,7 @@ const sortOptions: TSortOption[] = [
},
];
const getToolTipContent = (orientation: string) => {
return <div>{orientation} View</div>;
};
export const SurveyFilters = ({
orientation,
setOrientation,
surveyFilters,
setSurveyFilters,
currentProductChannel,
@@ -73,16 +69,7 @@ export const SurveyFilters = ({
const typeOptions: TFilterOption[] = [
{ label: "Link", value: "link" },
{ label: "App", value: "app" },
{ label: "Website", value: "website" },
].filter((option) => {
if (currentProductChannel === "website") {
return option.value !== "app";
} else if (currentProductChannel === "app") {
return option.value !== "website";
} else {
return option;
}
});
];
const toggleDropdown = (id: string) => {
setDropdownOpenStates(new Map(dropdownOpenStates).set(id, !dropdownOpenStates.get(id)));
@@ -128,11 +115,6 @@ export const SurveyFilters = ({
setSurveyFilters((prev) => ({ ...prev, sortBy: option.value }));
};
const handleOrientationChange = (value: string) => {
setOrientation(value);
localStorage.setItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS, value);
};
return (
<div className="flex justify-between">
<div className="flex space-x-2">
@@ -193,32 +175,6 @@ export const SurveyFilters = ({
)}
</div>
<div className="flex space-x-2">
<TooltipRenderer
shouldRender={true}
tooltipContent={getToolTipContent("List")}
className="bg-slate-900 text-white">
<div
className={`flex h-8 w-8 items-center justify-center rounded-lg border p-1 ${
orientation === "list" ? "bg-slate-900 text-white" : "bg-white"
}`}
onClick={() => handleOrientationChange("list")}>
<Equal className="h-5 w-5" />
</div>
</TooltipRenderer>
<TooltipRenderer
shouldRender={true}
tooltipContent={getToolTipContent("Grid")}
className="bg-slate-900 text-white">
<div
className={`flex h-8 w-8 items-center justify-center rounded-lg border p-1 ${
orientation === "grid" ? "bg-slate-900 text-white" : "bg-white"
}`}
onClick={() => handleOrientationChange("grid")}>
<Grid2X2 className="h-5 w-5" />
</div>
</TooltipRenderer>
<DropdownMenu>
<DropdownMenuTrigger
asChild

View File

@@ -1,20 +1,18 @@
"use client";
import { getSurveysAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
import { getFormattedFilters } from "@/app/(app)/environments/[environmentId]/surveys/lib/utils";
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
FORMBRICKS_SURVEYS_FILTERS_KEY_LS,
FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS,
} from "@formbricks/lib/localStorage";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { TEnvironment } from "@formbricks/types/environment";
import { wrapThrows } from "@formbricks/types/error-handlers";
import { TProductConfigChannel } from "@formbricks/types/product";
import { TSurvey, TSurveyFilters } from "@formbricks/types/surveys/types";
import { Button } from "../Button";
import { getSurveysAction } from "./actions";
import { SurveyCard } from "./components/SurveyCard";
import { SurveyFilters } from "./components/SurveyFilters";
import { SurveyLoading } from "./components/SurveyLoading";
import { getFormattedFilters } from "./utils";
import { TSurveyFilters } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { SurveyCard } from "./SurveyCard";
import { SurveyFilters } from "./SurveyFilters";
import { SurveyLoading } from "./SurveyLoading";
interface SurveysListProps {
environment: TEnvironment;
@@ -52,18 +50,8 @@ export const SurveysList = ({
const filters = useMemo(() => getFormattedFilters(surveyFilters, userId), [surveyFilters, userId]);
const [orientation, setOrientation] = useState("");
useEffect(() => {
if (typeof window !== "undefined") {
const orientationFromLocalStorage = localStorage.getItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS);
if (orientationFromLocalStorage) {
setOrientation(orientationFromLocalStorage);
} else {
setOrientation("grid");
localStorage.setItem(FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS, "grid");
}
const savedFilters = localStorage.getItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
if (savedFilters) {
const surveyParseResult = wrapThrows(() => JSON.parse(savedFilters))();
@@ -142,59 +130,37 @@ export const SurveysList = ({
return (
<div className="space-y-6">
<SurveyFilters
orientation={orientation}
setOrientation={setOrientation}
surveyFilters={surveyFilters}
setSurveyFilters={setSurveyFilters}
currentProductChannel={currentProductChannel}
/>
{surveys.length > 0 ? (
<div>
{orientation === "list" && (
<div className="flex-col space-y-3">
<div className="mt-6 grid w-full grid-cols-8 place-items-center gap-3 px-6 text-sm text-slate-800">
<div className="col-span-4 place-self-start">Name</div>
<div className="col-span-4 grid w-full grid-cols-5 place-items-center">
<div className="col-span-2">Created at</div>
<div className="col-span-2">Updated at</div>
</div>
</div>
{surveys.map((survey) => {
return (
<SurveyCard
key={survey.id}
survey={survey}
environment={environment}
otherEnvironment={otherEnvironment}
isViewer={isViewer}
WEBAPP_URL={WEBAPP_URL}
orientation={orientation}
duplicateSurvey={handleDuplicateSurvey}
deleteSurvey={handleDeleteSurvey}
/>
);
})}
<div className="flex-col space-y-3">
<div className="mt-6 grid w-full grid-cols-8 place-items-center gap-3 px-6 text-sm text-slate-800">
<div className="col-span-1 place-self-start">Name</div>
<div className="col-span-1">Status</div>
<div className="col-span-1">Responses</div>
<div className="col-span-1">Type</div>
<div className="col-span-1">Created at</div>
<div className="col-span-1">Updated at</div>
<div className="col-span-1">Created by</div>
</div>
)}
{orientation === "grid" && (
<div className="grid grid-cols-2 place-content-stretch gap-4 lg:grid-cols-3 2xl:grid-cols-5">
{surveys.map((survey) => {
return (
<SurveyCard
key={survey.id}
survey={survey}
environment={environment}
otherEnvironment={otherEnvironment}
isViewer={isViewer}
WEBAPP_URL={WEBAPP_URL}
orientation={orientation}
duplicateSurvey={handleDuplicateSurvey}
deleteSurvey={handleDeleteSurvey}
/>
);
})}
</div>
)}
{surveys.map((survey) => {
return (
<SurveyCard
key={survey.id}
survey={survey}
environment={environment}
otherEnvironment={otherEnvironment}
isViewer={isViewer}
WEBAPP_URL={WEBAPP_URL}
duplicateSurvey={handleDuplicateSurvey}
deleteSurvey={handleDeleteSurvey}
/>
);
})}
</div>
{hasMore && (
<div className="flex justify-center py-5">

View File

@@ -0,0 +1,21 @@
import { Code, HelpCircle, Link2Icon } from "lucide-react";
interface SurveyTypeIndicatorProps {
type: string;
}
const surveyTypeMapping = {
app: { icon: Code, label: "App" },
link: { icon: Link2Icon, label: "Link" },
};
export const SurveyTypeIndicator = ({ type }: SurveyTypeIndicatorProps) => {
const { icon: Icon, label } = surveyTypeMapping[type] || { icon: HelpCircle, label: "Unknown" };
return (
<div className="flex items-center space-x-2 text-sm text-slate-600">
<Icon className="h-4 w-4" />
<span>{label}</span>
</div>
);
};

View File

@@ -0,0 +1,208 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { responseCache } from "@formbricks/lib/response/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getInProgressSurveyCount } from "@formbricks/lib/survey/service";
import { buildOrderByClause, buildWhereClause } from "@formbricks/lib/survey/utils";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZOptionalNumber } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
import { TSurveyFilterCriteria } from "@formbricks/types/surveys/types";
import { TSurvey } from "../types/surveys";
export const surveySelect: Prisma.SurveySelect = {
id: true,
createdAt: true,
updatedAt: true,
name: true,
type: true,
creator: {
select: {
name: true,
},
},
status: true,
singleUse: true,
environmentId: true,
_count: {
select: { responses: true },
},
};
export const getSurveys = reactCache(
(
environmentId: string,
limit?: number,
offset?: number,
filterCriteria?: TSurveyFilterCriteria
): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
try {
if (filterCriteria?.sortBy === "relevance") {
// Call the sortByRelevance function
return await getSurveysSortedByRelevance(environmentId, limit, offset ?? 0, filterCriteria);
}
// Fetch surveys normally with pagination and include response count
const surveysPrisma = await prisma.survey.findMany({
where: {
environmentId,
...buildWhereClause(filterCriteria),
},
select: surveySelect,
orderBy: buildOrderByClause(filterCriteria?.sortBy),
take: limit,
skip: offset,
});
return surveysPrisma.map((survey) => {
return {
...survey,
responseCount: survey._count.responses,
};
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[`surveyList-getSurveys-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
{
tags: [
surveyCache.tag.byEnvironmentId(environmentId),
responseCache.tag.byEnvironmentId(environmentId),
],
}
)()
);
export const getSurveysSortedByRelevance = reactCache(
(
environmentId: string,
limit?: number,
offset?: number,
filterCriteria?: TSurveyFilterCriteria
): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
try {
let surveys: TSurvey[] = [];
const inProgressSurveyCount = await getInProgressSurveyCount(environmentId, filterCriteria);
// Fetch surveys that are in progress first
const inProgressSurveys =
offset && offset > inProgressSurveyCount
? []
: await prisma.survey.findMany({
where: {
environmentId,
status: "inProgress",
...buildWhereClause(filterCriteria),
},
select: surveySelect,
orderBy: buildOrderByClause("updatedAt"),
take: limit,
skip: offset,
});
surveys = inProgressSurveys.map((survey) => {
return {
...survey,
responseCount: survey._count.responses,
};
});
// Determine if additional surveys are needed
if (offset !== undefined && limit && inProgressSurveys.length < limit) {
const remainingLimit = limit - inProgressSurveys.length;
const newOffset = Math.max(0, offset - inProgressSurveyCount);
const additionalSurveys = await prisma.survey.findMany({
where: {
environmentId,
status: { not: "inProgress" },
...buildWhereClause(filterCriteria),
},
select: surveySelect,
orderBy: buildOrderByClause("updatedAt"),
take: remainingLimit,
skip: newOffset,
});
surveys = [
...surveys,
...additionalSurveys.map((survey) => {
return {
...survey,
responseCount: survey._count.responses,
};
}),
];
}
return surveys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[
`surveyList-getSurveysSortedByRelevance-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`,
],
{
tags: [
surveyCache.tag.byEnvironmentId(environmentId),
responseCache.tag.byEnvironmentId(environmentId),
],
}
)()
);
export const getSurvey = reactCache(
(surveyId: string): Promise<TSurvey | null> =>
cache(
async () => {
validateInputs([surveyId, ZId]);
let surveyPrisma;
try {
surveyPrisma = await prisma.survey.findUnique({
where: {
id: surveyId,
},
select: surveySelect,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
if (!surveyPrisma) {
return null;
}
return { ...surveyPrisma, responseCount: surveyPrisma?._count.responses };
},
[`surveyList-getSurvey-${surveyId}`],
{
tags: [surveyCache.tag.byId(surveyId), responseCache.tag.bySurveyId(surveyId)],
}
)()
);

View File

@@ -1,6 +1,6 @@
import { SurveyLoading } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyLoading";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { SurveyLoading } from "@formbricks/ui/components/SurveysList/components/SurveyLoading";
const Loading = () => {
return (

View File

@@ -1,3 +1,4 @@
import { SurveysList } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyList";
import { PlusIcon } from "lucide-react";
import { Metadata } from "next";
import { getServerSession } from "next-auth";
@@ -14,7 +15,6 @@ import { TTemplateRole } from "@formbricks/types/templates";
import { Button } from "@formbricks/ui/components/Button";
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/components/PageHeader";
import { SurveysList } from "@formbricks/ui/components/SurveysList";
import { TemplateList } from "@formbricks/ui/components/TemplateList";
export const metadata: Metadata = {

View File

@@ -0,0 +1,37 @@
import { z } from "zod";
import { ZSurveyStatus } from "@formbricks/types/surveys/types";
export const ZSurvey = z.object({
id: z.string(),
name: z.string(),
environmentId: z.string(),
type: z.enum(["link", "app", "website", "web"]), //we can replace this with ZSurveyType after we remove "web" from schema
status: ZSurveyStatus,
createdAt: z.date(),
updatedAt: z.date(),
responseCount: z.number(),
creator: z
.object({
name: z.string(),
})
.nullable(),
singleUse: z
.object({
enabled: z.boolean(),
isEncrypted: z.boolean(),
})
.nullable(),
});
export type TSurvey = z.infer<typeof ZSurvey>;
export const ZSurveyCopyFormValidation = z.object({
products: z.array(
z.object({
product: z.string(),
environments: z.array(z.string()),
})
),
});
export type TSurveyCopyFormData = z.infer<typeof ZSurveyCopyFormValidation>;

View File

@@ -1,4 +1,3 @@
import { type TFormbricksApp } from "@formbricks/js-core";
import { loadFormbricksToProxy } from "./lib/load-formbricks";

View File

@@ -1,3 +1,2 @@
export const FORMBRICKS_SURVEYS_ORIENTATION_KEY_LS = "formbricks-surveys-orientation";
export const FORMBRICKS_SURVEYS_FILTERS_KEY_LS = "formbricks-surveys-filters";
export const FORMBRICKS_ENVIRONMENT_ID_LS = "formbricks-environment-id";

View File

@@ -277,74 +277,6 @@ export const getSurveysByActionClassId = reactCache(
)()
);
export const getSurveysSortedByRelevance = reactCache(
(
environmentId: string,
limit?: number,
offset?: number,
filterCriteria?: TSurveyFilterCriteria
): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
try {
let surveys: TSurvey[] = [];
const inProgressSurveyCount = await getInProgressSurveyCount(environmentId, filterCriteria);
// Fetch surveys that are in progress first
const inProgressSurveys =
offset && offset > inProgressSurveyCount
? []
: await prisma.survey.findMany({
where: {
environmentId,
status: "inProgress",
...buildWhereClause(filterCriteria),
},
select: selectSurvey,
orderBy: buildOrderByClause("updatedAt"),
take: limit,
skip: offset,
});
surveys = inProgressSurveys.map(transformPrismaSurvey);
// Determine if additional surveys are needed
if (offset !== undefined && limit && inProgressSurveys.length < limit) {
const remainingLimit = limit - inProgressSurveys.length;
const newOffset = Math.max(0, offset - inProgressSurveyCount);
const additionalSurveys = await prisma.survey.findMany({
where: {
environmentId,
status: { not: "inProgress" },
...buildWhereClause(filterCriteria),
},
select: selectSurvey,
orderBy: buildOrderByClause("updatedAt"),
take: remainingLimit,
skip: newOffset,
});
surveys = [...surveys, ...additionalSurveys.map(transformPrismaSurvey)];
}
return surveys;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getSurveysSortedByRelevance-${environmentId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
{
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
}
)()
);
export const getSurveys = reactCache(
(
environmentId: string,
@@ -357,12 +289,6 @@ export const getSurveys = reactCache(
validateInputs([environmentId, ZId], [limit, ZOptionalNumber], [offset, ZOptionalNumber]);
try {
if (filterCriteria?.sortBy === "relevance") {
// Call the sortByRelevance function
return await getSurveysSortedByRelevance(environmentId, limit, offset ?? 0, filterCriteria);
}
// Fetch surveys normally with pagination
const surveysPrisma = await prisma.survey.findMany({
where: {
environmentId,

View File

@@ -2477,14 +2477,3 @@ export const ZSurveyRecallItem = z.object({
});
export type TSurveyRecallItem = z.infer<typeof ZSurveyRecallItem>;
export const ZSurveyCopyFormValidation = z.object({
products: z.array(
z.object({
product: z.string(),
environments: z.array(z.string()),
})
),
});
export type TSurveyCopyFormData = z.infer<typeof ZSurveyCopyFormValidation>;

View File

@@ -1,183 +0,0 @@
import { Code, Link2Icon } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { cn } from "@formbricks/lib/cn";
import { convertDateString, timeSince } from "@formbricks/lib/time";
import { TEnvironment } from "@formbricks/types/environment";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys/types";
import { SurveyStatusIndicator } from "../../SurveyStatusIndicator";
import { generateSingleUseIdAction } from "../actions";
import { SurveyDropDownMenu } from "./SurveyDropdownMenu";
interface SurveyCardProps {
survey: TSurvey;
environment: TEnvironment;
otherEnvironment: TEnvironment;
isViewer: boolean;
WEBAPP_URL: string;
orientation: string;
duplicateSurvey: (survey: TSurvey) => void;
deleteSurvey: (surveyId: string) => void;
}
export const SurveyCard = ({
survey,
environment,
otherEnvironment,
isViewer,
WEBAPP_URL,
orientation,
deleteSurvey,
duplicateSurvey,
}: SurveyCardProps) => {
const isSurveyCreationDeletionDisabled = isViewer;
const surveyStatusLabel = useMemo(() => {
if (survey.status === "inProgress") return "In Progress";
else if (survey.status === "scheduled") return "Scheduled";
else if (survey.status === "completed") return "Completed";
else if (survey.status === "draft") return "Draft";
else if (survey.status === "paused") return "Paused";
}, [survey]);
const [singleUseId, setSingleUseId] = useState<string | undefined>();
useEffect(() => {
const fetchSingleUseId = async () => {
if (survey.singleUse?.enabled) {
const generateSingleUseIdResponse = await generateSingleUseIdAction({
surveyId: survey.id,
isEncrypted: survey.singleUse?.isEncrypted ? true : false,
});
if (generateSingleUseIdResponse?.data) {
setSingleUseId(generateSingleUseIdResponse.data);
} else {
const errorMessage = getFormattedErrorMessage(generateSingleUseIdResponse);
toast.error(errorMessage);
}
} else {
setSingleUseId(undefined);
}
};
fetchSingleUseId();
}, [survey]);
const linkHref = useMemo(() => {
return survey.status === "draft"
? `/environments/${environment.id}/surveys/${survey.id}/edit`
: `/environments/${environment.id}/surveys/${survey.id}/summary`;
}, [survey.status, survey.id, environment.id]);
const SurveyTypeIndicator = ({ type }: { type: TSurveyType }) => (
<div className="flex items-center space-x-2 text-sm text-slate-600">
{type === "app" && (
<>
<Code className="h-4 w-4" />
<span>App</span>
</>
)}
{type === "link" && (
<>
<Link2Icon className="h-4 w-4" />
<span> Link</span>
</>
)}
</div>
);
const renderGridContent = () => {
return (
<Link
href={linkHref}
key={survey.id}
className="relative col-span-1 flex h-44 flex-col justify-between rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition-all ease-in-out hover:scale-105">
<div className="flex justify-between">
<SurveyTypeIndicator type={survey.type} />
<SurveyDropDownMenu
survey={survey}
key={`surveys-${survey.id}`}
environmentId={environment.id}
environment={environment}
otherEnvironment={otherEnvironment!}
webAppUrl={WEBAPP_URL}
singleUseId={singleUseId}
isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled}
duplicateSurvey={duplicateSurvey}
deleteSurvey={deleteSurvey}
/>
</div>
<div>
<div className="text-md font-medium text-slate-900">{survey.name}</div>
<div
className={cn(
"mt-3 flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-xs text-slate-800",
surveyStatusLabel === "Scheduled" && "bg-slate-200",
surveyStatusLabel === "In Progress" && "bg-emerald-50",
surveyStatusLabel === "Completed" && "bg-slate-200",
surveyStatusLabel === "Draft" && "bg-slate-100",
surveyStatusLabel === "Paused" && "bg-slate-100"
)}>
<SurveyStatusIndicator status={survey.status} /> {surveyStatusLabel}
</div>
</div>
</Link>
);
};
const renderListContent = () => {
return (
<Link
href={linkHref}
key={survey.id}
className="relative grid w-full grid-cols-8 place-items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition-all ease-in-out hover:scale-[101%]">
<div className="col-span-2 flex max-w-full items-center justify-self-start text-sm font-medium text-slate-900">
<div className="w-full truncate">{survey.name}</div>
</div>
<div
className={cn(
"flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
surveyStatusLabel === "Scheduled" && "bg-slate-200",
surveyStatusLabel === "In Progress" && "bg-emerald-50",
surveyStatusLabel === "Completed" && "bg-slate-200",
surveyStatusLabel === "Draft" && "bg-slate-100",
surveyStatusLabel === "Paused" && "bg-slate-100"
)}>
<SurveyStatusIndicator status={survey.status} /> {surveyStatusLabel}{" "}
</div>
<div className="flex justify-between">
<SurveyTypeIndicator type={survey.type} />
</div>
<div className="col-span-4 grid w-full grid-cols-5 place-items-center">
<div className="col-span-2 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{convertDateString(survey.createdAt.toString())}
</div>
<div className="col-span-2 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
{timeSince(survey.updatedAt.toString())}
</div>
<div className="place-self-end">
<SurveyDropDownMenu
survey={survey}
key={`surveys-${survey.id}`}
environmentId={environment.id}
environment={environment}
otherEnvironment={otherEnvironment!}
webAppUrl={WEBAPP_URL}
singleUseId={singleUseId}
isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled}
duplicateSurvey={duplicateSurvey}
deleteSurvey={deleteSurvey}
/>
</div>
</div>
</Link>
);
};
if (orientation === "grid") {
return renderGridContent();
} else return renderListContent();
};