From 1402f4a48b856de89cebcf6cea8bbf00989f2e71 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Thu, 1 Feb 2024 13:21:21 +0530 Subject: [PATCH] chore: Tweaked survey list (#1978) Co-authored-by: Johannes Co-authored-by: Matthias Nannt --- .../environments/[environmentId]/actions.ts | 185 ----------- .../components/SurveyStatusDropdown.tsx | 5 +- .../[environmentId]/surveys/actions.ts | 2 +- .../surveys/components/SurveyList.tsx | 143 -------- .../[environmentId]/surveys/page.tsx | 60 +++- .../surveys/templates/actions.ts | 2 +- .../surveys/templates/templates.ts | 1 + apps/web/app/globals.css | 8 + apps/web/playwright/js.spec.ts | 6 +- .../migration.sql | 5 + packages/database/schema.prisma | 3 + packages/lib/survey/service.ts | 21 +- packages/lib/survey/tests/survey.mock.ts | 2 + packages/lib/survey/tests/survey.unit.ts | 6 +- packages/lib/user/service.ts | 18 + packages/lib/utils/singleUseSurveys.ts | 40 +++ packages/types/surveys.ts | 1 + packages/ui/Button/index.tsx | 2 +- packages/ui/SurveyStatusIndicator/index.tsx | 29 +- packages/ui/SurveysList/actions.ts | 211 ++++++++++++ .../ui/SurveysList/components/SurveyCard.tsx | 156 +++++++++ .../components/SurveyDropdownMenu.tsx | 66 ++-- .../SurveysList/components/SurveyFilters.tsx | 307 ++++++++++++++++++ packages/ui/SurveysList/index.tsx | 101 ++++++ packages/ui/Tooltip/index.tsx | 23 ++ 25 files changed, 1011 insertions(+), 392 deletions(-) delete mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyList.tsx create mode 100644 packages/database/migrations/20240130103957_add_created_by_to_survey/migration.sql create mode 100644 packages/lib/utils/singleUseSurveys.ts create mode 100644 packages/ui/SurveysList/actions.ts create mode 100644 packages/ui/SurveysList/components/SurveyCard.tsx rename apps/web/app/(app)/environments/[environmentId]/surveys/components/SurveyDropDownMenu.tsx => packages/ui/SurveysList/components/SurveyDropdownMenu.tsx (78%) create mode 100644 packages/ui/SurveysList/components/SurveyFilters.tsx create mode 100644 packages/ui/SurveysList/index.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts index 80f5df948c..abdeabb675 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts @@ -1,19 +1,14 @@ "use server"; import { Team } from "@prisma/client"; -import { Prisma as prismaClient } from "@prisma/client/"; import { getServerSession } from "next-auth"; -import { prisma } from "@formbricks/database"; import { authOptions } from "@formbricks/lib/authOptions"; import { SHORT_URL_BASE, WEBAPP_URL } from "@formbricks/lib/constants"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { createMembership } from "@formbricks/lib/membership/service"; import { createProduct } from "@formbricks/lib/product/service"; import { createShortUrl } from "@formbricks/lib/shortUrl/service"; -import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth"; -import { surveyCache } from "@formbricks/lib/survey/cache"; -import { deleteSurvey, duplicateSurvey, getSurvey } from "@formbricks/lib/survey/service"; import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service"; import { updateUser } from "@formbricks/lib/user/service"; import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors"; @@ -67,186 +62,6 @@ export async function createTeamAction(teamName: string): Promise { return newTeam; } -export async function duplicateSurveyAction(environmentId: string, surveyId: string) { - const session = await getServerSession(authOptions); - if (!session) throw new AuthorizationError("Not authorized"); - - const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId); - if (!isAuthorized) throw new AuthorizationError("Not authorized"); - - const duplicatedSurvey = await duplicateSurvey(environmentId, surveyId); - return duplicatedSurvey; -} - -export async function copyToOtherEnvironmentAction( - environmentId: string, - surveyId: string, - targetEnvironmentId: string -) { - const session = await getServerSession(authOptions); - if (!session) throw new AuthorizationError("Not authorized"); - - const isAuthorizedToAccessSourceEnvironment = await hasUserEnvironmentAccess( - session.user.id, - environmentId - ); - if (!isAuthorizedToAccessSourceEnvironment) throw new AuthorizationError("Not authorized"); - - const isAuthorizedToAccessTargetEnvironment = await hasUserEnvironmentAccess( - session.user.id, - targetEnvironmentId - ); - if (!isAuthorizedToAccessTargetEnvironment) throw new AuthorizationError("Not authorized"); - - const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId); - if (!isAuthorized) throw new AuthorizationError("Not authorized"); - - const existingSurvey = await prisma.survey.findFirst({ - where: { - id: surveyId, - environmentId, - }, - include: { - triggers: { - include: { - actionClass: true, - }, - }, - attributeFilters: { - include: { - attributeClass: true, - }, - }, - }, - }); - - if (!existingSurvey) { - throw new ResourceNotFoundError("Survey", surveyId); - } - - let targetEnvironmentTriggers: string[] = []; - // map the local triggers to the target environment - for (const trigger of existingSurvey.triggers) { - const targetEnvironmentTrigger = await prisma.actionClass.findFirst({ - where: { - name: trigger.actionClass.name, - environment: { - id: targetEnvironmentId, - }, - }, - }); - if (!targetEnvironmentTrigger) { - // if the trigger does not exist in the target environment, create it - const newTrigger = await prisma.actionClass.create({ - data: { - name: trigger.actionClass.name, - environment: { - connect: { - id: targetEnvironmentId, - }, - }, - description: trigger.actionClass.description, - type: trigger.actionClass.type, - noCodeConfig: trigger.actionClass.noCodeConfig - ? JSON.parse(JSON.stringify(trigger.actionClass.noCodeConfig)) - : undefined, - }, - }); - targetEnvironmentTriggers.push(newTrigger.id); - } else { - targetEnvironmentTriggers.push(targetEnvironmentTrigger.id); - } - } - - let targetEnvironmentAttributeFilters: string[] = []; - // map the local attributeFilters to the target env - for (const attributeFilter of existingSurvey.attributeFilters) { - // check if attributeClass exists in target env. - // if not, create it - const targetEnvironmentAttributeClass = await prisma.attributeClass.findFirst({ - where: { - name: attributeFilter.attributeClass.name, - environment: { - id: targetEnvironmentId, - }, - }, - }); - if (!targetEnvironmentAttributeClass) { - const newAttributeClass = await prisma.attributeClass.create({ - data: { - name: attributeFilter.attributeClass.name, - description: attributeFilter.attributeClass.description, - type: attributeFilter.attributeClass.type, - environment: { - connect: { - id: targetEnvironmentId, - }, - }, - }, - }); - targetEnvironmentAttributeFilters.push(newAttributeClass.id); - } else { - targetEnvironmentAttributeFilters.push(targetEnvironmentAttributeClass.id); - } - } - - // create new survey with the data of the existing survey - const newSurvey = await prisma.survey.create({ - data: { - ...existingSurvey, - id: undefined, // id is auto-generated - environmentId: undefined, // environmentId is set below - name: `${existingSurvey.name} (copy)`, - status: "draft", - questions: JSON.parse(JSON.stringify(existingSurvey.questions)), - thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)), - triggers: { - create: targetEnvironmentTriggers.map((actionClassId) => ({ - actionClassId: actionClassId, - })), - }, - attributeFilters: { - create: existingSurvey.attributeFilters.map((attributeFilter, idx) => ({ - attributeClassId: targetEnvironmentAttributeFilters[idx], - condition: attributeFilter.condition, - value: attributeFilter.value, - })), - }, - environment: { - connect: { - id: targetEnvironmentId, - }, - }, - surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull, - singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull, - productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull, - verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull, - styling: existingSurvey.styling ?? prismaClient.JsonNull, - }, - }); - - surveyCache.revalidate({ - id: newSurvey.id, - environmentId: targetEnvironmentId, - }); - return newSurvey; -} - -export const deleteSurveyAction = async (surveyId: string) => { - const session = await getServerSession(authOptions); - if (!session) throw new AuthorizationError("Not authorized"); - - const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId); - if (!isAuthorized) throw new AuthorizationError("Not authorized"); - - const survey = await getSurvey(surveyId); - - const { hasDeleteAccess } = await verifyUserRoleAccess(survey!.environmentId, session.user.id); - if (!hasDeleteAccess) throw new AuthorizationError("Not authorized"); - - await deleteSurvey(surveyId); -}; - export const createProductAction = async (environmentId: string, productName: string) => { const session = await getServerSession(authOptions); if (!session) throw new AuthorizationError("Not authorized"); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.tsx index 4a24849f2f..d555022aa2 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown.tsx @@ -27,10 +27,7 @@ export default function SurveyStatusDropdown({ <> {survey.status === "draft" ? (
- {(survey.type === "link" || environment.widgetSetupCompleted) && ( - - )} - {survey.status === "draft" &&

Draft

} +

Draft

) : ( setSearchTerm(e.target.value)} + /> + + +
+ +
+
+ +
+
+ +
+ {(createdByFilter.length > 0 || statusFilters.length > 0 || typeFilters.length > 0) && ( + + )} + +
+ +
setOrientation("list")}> + +
+
+ + +
setOrientation("grid")}> + +
+
+ + + +
+
+ Sort by: {sortBy.label} + +
+
+
+ + {sortOptions.map(renderSortOption)} + +
+
+ + ); +} diff --git a/packages/ui/SurveysList/index.tsx b/packages/ui/SurveysList/index.tsx new file mode 100644 index 0000000000..10e93c77f9 --- /dev/null +++ b/packages/ui/SurveysList/index.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { PlusIcon } from "lucide-react"; +import { useState } from "react"; + +import { TEnvironment } from "@formbricks/types/environment"; +import { TSurvey } from "@formbricks/types/surveys"; + +import { Button } from "../v2/Button"; +import SurveyCard from "./components/SurveyCard"; +import SurveyFilters from "./components/SurveyFilters"; + +interface SurveysListProps { + environment: TEnvironment; + surveys: TSurvey[]; + otherEnvironment: TEnvironment; + isViewer: boolean; + WEBAPP_URL: string; + userId: string; +} + +export default function SurveysList({ + environment, + surveys, + otherEnvironment, + isViewer, + WEBAPP_URL, + userId, +}: SurveysListProps) { + const [filteredSurveys, setFilteredSurveys] = useState(surveys); + const [orientation, setOrientation] = useState("grid"); + return ( +
+
+

Surveys

+ +
+ + {filteredSurveys.length > 0 ? ( +
+ {orientation === "list" && ( +
+
+
Name
+
+
Created at
+
Updated at
+
+
+ {filteredSurveys.map((survey) => { + return ( + + ); + })} +
+ )} + {orientation === "grid" && ( +
+ {filteredSurveys.map((survey) => { + return ( + + ); + })} +
+ )} +
+ ) : ( +
+ 🕵️ + +
No surveys found
+
+ )} +
+ ); +} diff --git a/packages/ui/Tooltip/index.tsx b/packages/ui/Tooltip/index.tsx index afb2dc3b72..338269287f 100644 --- a/packages/ui/Tooltip/index.tsx +++ b/packages/ui/Tooltip/index.tsx @@ -2,6 +2,7 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as React from "react"; +import { ReactNode } from "react"; import { cn } from "@formbricks/lib/cn"; @@ -31,3 +32,25 @@ const TooltipContent: React.ComponentType TooltipContent.displayName = TooltipPrimitive.Content.displayName; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; + +interface TooltipRendererProps { + shouldRender: boolean; + tooltipContent: ReactNode; + children: ReactNode; + className?: string; +} +export function TooltipRenderer(props: TooltipRendererProps) { + const { children, shouldRender, tooltipContent, className } = props; + if (shouldRender) { + return ( + + + {children} + {tooltipContent} + + + ); + } + + return <>{children}; +}