Skip to content

Commit

Permalink
Refactor Survey Overview Page
Browse files Browse the repository at this point in the history
Refactor Survey Overview Page
  • Loading branch information
jobenjada committed Jul 20, 2023
2 parents 7475df1 + feadefa commit cf953db
Show file tree
Hide file tree
Showing 16 changed files with 749 additions and 311 deletions.
176 changes: 175 additions & 1 deletion apps/web/app/(app)/environments/[environmentId]/actions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"use server";

import { prisma } from "@formbricks/database";
import { Team } from "@prisma/client";
import { ResourceNotFoundError } from "@formbricks/errors";
import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey";
import { QuestionType } from "@formbricks/types/questions";
import { createId } from "@paralleldrive/cuid2";
import { Team } from "@prisma/client";
import { Prisma as prismaClient } from "@prisma/client/";

export async function createTeam(teamName: string, ownerUserId: string): Promise<Team> {
const newTeam = await prisma.team.create({
Expand Down Expand Up @@ -1372,3 +1375,174 @@ export async function addDemoData(teamId: string): Promise<void> {
InterviewPromptResults.displays
);
}

export async function duplicateSurveyAction(environmentId: string, surveyId: string) {
const existingSurvey = await getSurvey(surveyId);

if (!existingSurvey) {
throw new ResourceNotFoundError("Survey", surveyId);
}

// 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: existingSurvey.triggers.map((trigger) => ({
eventClassId: trigger.id,
})),
},
attributeFilters: {
create: existingSurvey.attributeFilters.map((attributeFilter) => ({
attributeClassId: attributeFilter.attributeClassId,
condition: attributeFilter.condition,
value: attributeFilter.value,
})),
},
environment: {
connect: {
id: environmentId,
},
},
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
},
});
return newSurvey;
}

export async function copyToOtherEnvironmentAction(
environmentId: string,
surveyId: string,
targetEnvironmentId: string
) {
const existingSurvey = await prisma.survey.findFirst({
where: {
id: surveyId,
environmentId,
},
include: {
triggers: {
include: {
eventClass: 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.eventClass.findFirst({
where: {
name: trigger.eventClass.name,
environment: {
id: targetEnvironmentId,
},
},
});
if (!targetEnvironmentTrigger) {
// if the trigger does not exist in the target environment, create it
const newTrigger = await prisma.eventClass.create({
data: {
name: trigger.eventClass.name,
environment: {
connect: {
id: targetEnvironmentId,
},
},
description: trigger.eventClass.description,
type: trigger.eventClass.type,
noCodeConfig: trigger.eventClass.noCodeConfig
? JSON.parse(JSON.stringify(trigger.eventClass.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((eventClassId) => ({
eventClassId: eventClassId,
})),
},
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,
},
});
return newSurvey;
}

export const deleteSurveyAction = async (surveyId: string) => {
await deleteSurvey(surveyId);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"use client";

import {
copyToOtherEnvironmentAction,
deleteSurveyAction,
duplicateSurveyAction,
} from "@/app/(app)/environments/[environmentId]/actions";
import DeleteDialog from "@/components/shared/DeleteDialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/shared/DropdownMenu";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import type { TEnvironment } from "@formbricks/types/v1/environment";
import type { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
import {
ArrowUpOnSquareStackIcon,
DocumentDuplicateIcon,
EllipsisHorizontalIcon,
EyeIcon,
LinkIcon,
PencilSquareIcon,
TrashIcon,
} from "@heroicons/react/24/solid";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";

interface SurveyDropDownMenuProps {
environmentId: string;
survey: TSurveyWithAnalytics;
environment: TEnvironment;
otherEnvironment: TEnvironment;
}

export default function SurveyDropDownMenu({
environmentId,
survey,
environment,
otherEnvironment,
}: SurveyDropDownMenuProps) {
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [loading, setLoading] = useState(false);
const router = useRouter();

const handleDeleteSurvey = async (survey) => {
setLoading(true);
try {
await deleteSurveyAction(survey.id);
router.refresh();
setDeleteDialogOpen(false);
toast.success("Survey deleted successfully.");
} catch (error) {
toast.error("An error occured while deleting survey");
}
setLoading(false);
};

const duplicateSurveyAndRefresh = async (surveyId) => {
setLoading(true);
try {
await duplicateSurveyAction(environmentId, surveyId);
router.refresh();
toast.success("Survey duplicated successfully.");
} catch (error) {
toast.error("Failed to duplicate the survey.");
}
setLoading(false);
};

const copyToOtherEnvironment = async (surveyId) => {
setLoading(true);
try {
await copyToOtherEnvironmentAction(environmentId, surveyId, otherEnvironment.id);
if (otherEnvironment.type === "production") {
toast.success("Survey copied to production env.");
} else if (otherEnvironment.type === "development") {
toast.success("Survey copied to development env.");
}
router.replace(`/environments/${otherEnvironment.id}`);
} catch (error) {
toast.error(`Failed to copy to ${otherEnvironment.type}`);
}
setLoading(false);
};
if (loading) {
return (
<div className="opacity-0.2 absolute left-0 top-0 h-full w-full bg-gray-100">
<LoadingSpinner />
</div>
);
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger className="z-10 cursor-pointer" asChild>
<div>
<span className="sr-only">Open options</span>
<EllipsisHorizontalIcon className="h-5 w-5" aria-hidden="true" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40">
<DropdownMenuGroup>
<DropdownMenuItem>
<Link
className="flex w-full items-center"
href={`/environments/${environmentId}/surveys/${survey.id}/edit`}>
<PencilSquareIcon className="mr-2 h-4 w-4" />
Edit
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<button
className="flex w-full items-center"
onClick={async () => {
duplicateSurveyAndRefresh(survey.id);
}}>
<DocumentDuplicateIcon className="mr-2 h-4 w-4" />
Duplicate
</button>
</DropdownMenuItem>
{environment.type === "development" ? (
<DropdownMenuItem>
<button
className="flex w-full items-center"
onClick={() => {
copyToOtherEnvironment(survey.id);
}}>
<ArrowUpOnSquareStackIcon className="mr-2 h-4 w-4" />
Copy to Prod
</button>
</DropdownMenuItem>
) : environment.type === "production" ? (
<DropdownMenuItem>
<button
className="flex w-full items-center"
onClick={() => {
copyToOtherEnvironment(survey.id);
}}>
<ArrowUpOnSquareStackIcon className="mr-2 h-4 w-4" />
Copy to Dev
</button>
</DropdownMenuItem>
) : null}
{survey.type === "link" && survey.status !== "draft" && (
<>
<DropdownMenuItem>
<Link
className="flex w-full items-center"
href={`/s/${survey.id}?preview=true`}
target="_blank">
<EyeIcon className="mr-2 h-4 w-4" />
Preview Survey
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<button
className="flex w-full items-center"
onClick={() => {
navigator.clipboard.writeText(
`${window.location.protocol}//${window.location.host}/s/${survey.id}`
);
toast.success("Copied link to clipboard");
}}>
<LinkIcon className="mr-2 h-4 w-4" />
Copy Link
</button>
</DropdownMenuItem>
</>
)}
<DropdownMenuItem>
<button
className="flex w-full items-center"
onClick={() => {
setDeleteDialogOpen(true);
}}>
<TrashIcon className="mr-2 h-4 w-4" />
Delete
</button>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

<DeleteDialog
deleteWhat="Survey"
open={isDeleteDialogOpen}
setOpen={setDeleteDialogOpen}
onDelete={() => handleDeleteSurvey(survey)}
text="Are you sure you want to delete this survey and all of its responses? This action cannot be undone."
/>
</>
);
}

1 comment on commit cf953db

@vercel
Copy link

@vercel vercel bot commented on cf953db Jul 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

formbricks-com – ./apps/formbricks-com

formbricks-com-formbricks.vercel.app
formbricks-com-git-main-formbricks.vercel.app
formbricks-com.vercel.app
formbricks.com
www.formbricks.com

Please sign in to comment.