mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 00:49:42 -06:00
feat: add ability to copy surveys between different environments of different products (#2832)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
@@ -44,7 +44,6 @@ const Page = async ({ params }) => {
|
||||
getServerSession(authOptions),
|
||||
getSegments(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ const Page = async ({ params, searchParams }: LinkSurveyPageProps) => {
|
||||
if (isSingleUseSurvey) {
|
||||
try {
|
||||
singleUseResponse = singleUseId
|
||||
? (await getResponseBySingleUseId(survey.id, singleUseId)) ?? undefined
|
||||
? ((await getResponseBySingleUseId(survey.id, singleUseId)) ?? undefined)
|
||||
: undefined;
|
||||
} catch (error) {
|
||||
singleUseResponse = undefined;
|
||||
|
||||
@@ -3,7 +3,11 @@ import { type TActionClassNoCodeConfig } from "@formbricks/types/action-classes"
|
||||
import { type TIntegrationConfig } from "@formbricks/types/integration";
|
||||
import { type TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { type TProductConfig, type TProductStyling } from "@formbricks/types/product";
|
||||
import { type TResponseData, type TResponseMeta, type TResponsePersonAttributes } from "@formbricks/types/responses";
|
||||
import {
|
||||
type TResponseData,
|
||||
type TResponseMeta,
|
||||
type TResponsePersonAttributes,
|
||||
} from "@formbricks/types/responses";
|
||||
import { type TBaseFilters } from "@formbricks/types/segment";
|
||||
import {
|
||||
type TSurveyClosedMessage,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import "server-only";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { ZOptionalNumber } from "@formbricks/types/common";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { TEnvironment, ZId } from "@formbricks/types/environment";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TLegacySurvey } from "@formbricks/types/legacy-surveys";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSegment, ZSegmentFilters } from "@formbricks/types/segment";
|
||||
import {
|
||||
TSurvey,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
ZSurveyCreateInput,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { getActionsByPersonId } from "../action/service";
|
||||
import { actionClassCache } from "../actionClass/cache";
|
||||
import { getActionClasses } from "../actionClass/service";
|
||||
import { attributeCache } from "../attribute/cache";
|
||||
import { getAttributes } from "../attribute/service";
|
||||
@@ -24,6 +27,7 @@ import { cache } from "../cache";
|
||||
import { ITEMS_PER_PAGE } from "../constants";
|
||||
import { displayCache } from "../display/cache";
|
||||
import { getDisplaysByPersonId } from "../display/service";
|
||||
import { getEnvironment } from "../environment/service";
|
||||
import { reverseTranslateSurvey } from "../i18n/reverseTranslation";
|
||||
import { subscribeOrganizationMembersToSurveyResponses } from "../organization/service";
|
||||
import { personCache } from "../person/cache";
|
||||
@@ -92,6 +96,7 @@ export const selectSurvey = {
|
||||
alias: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
productId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -122,7 +127,7 @@ export const selectSurvey = {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} satisfies Prisma.SurveySelect;
|
||||
|
||||
const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: TActionClass[]) => {
|
||||
if (!triggers) return;
|
||||
@@ -712,114 +717,219 @@ export const createSurvey = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const duplicateSurvey = async (environmentId: string, surveyId: string, userId: string) => {
|
||||
validateInputs([environmentId, ZId], [surveyId, ZId]);
|
||||
export const copySurveyToOtherEnvironment = async (
|
||||
environmentId: string,
|
||||
surveyId: string,
|
||||
targetEnvironmentId: string,
|
||||
userId: string
|
||||
) => {
|
||||
validateInputs([environmentId, ZId], [surveyId, ZId], [targetEnvironmentId, ZId], [userId, ZId]);
|
||||
|
||||
try {
|
||||
const existingSurvey = await getSurvey(surveyId);
|
||||
const currentDate = new Date();
|
||||
if (!existingSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
const isSameEnvironment = environmentId === targetEnvironmentId;
|
||||
|
||||
// Fetch required resources
|
||||
const [existingEnvironment, existingProduct, existingSurvey] = await Promise.all([
|
||||
getEnvironment(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
getSurvey(surveyId),
|
||||
]);
|
||||
|
||||
if (!existingEnvironment) throw new ResourceNotFoundError("Environment", environmentId);
|
||||
if (!existingProduct) throw new ResourceNotFoundError("Product", environmentId);
|
||||
if (!existingSurvey) throw new ResourceNotFoundError("Survey", surveyId);
|
||||
|
||||
let targetEnvironment: TEnvironment | null = null;
|
||||
let targetProduct: TProduct | null = null;
|
||||
|
||||
if (isSameEnvironment) {
|
||||
targetEnvironment = existingEnvironment;
|
||||
targetProduct = existingProduct;
|
||||
} else {
|
||||
[targetEnvironment, targetProduct] = await Promise.all([
|
||||
getEnvironment(targetEnvironmentId),
|
||||
getProductByEnvironmentId(targetEnvironmentId),
|
||||
]);
|
||||
|
||||
if (!targetEnvironment) throw new ResourceNotFoundError("Environment", targetEnvironmentId);
|
||||
if (!targetProduct) throw new ResourceNotFoundError("Product", targetEnvironmentId);
|
||||
}
|
||||
|
||||
const defaultLanguageId = existingSurvey.languages.find((l) => l.default)?.language.id;
|
||||
const {
|
||||
environmentId: _,
|
||||
createdBy,
|
||||
id: existingSurveyId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
...restExistingSurvey
|
||||
} = existingSurvey;
|
||||
const hasLanguages = existingSurvey.languages && existingSurvey.languages.length > 0;
|
||||
|
||||
// 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
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
createdBy: undefined,
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
status: "draft",
|
||||
questions: structuredClone(existingSurvey.questions),
|
||||
endings: structuredClone(existingSurvey.endings),
|
||||
languages: {
|
||||
create: existingSurvey.languages?.map((surveyLanguage) => ({
|
||||
languageId: surveyLanguage.language.id,
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
})),
|
||||
},
|
||||
triggers: {
|
||||
create: existingSurvey.triggers.map((trigger) => ({
|
||||
actionClassId: trigger.actionClass.id,
|
||||
})),
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
creator: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage
|
||||
? structuredClone(existingSurvey.surveyClosedMessage)
|
||||
: Prisma.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ? structuredClone(existingSurvey.singleUse) : Prisma.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites
|
||||
? structuredClone(existingSurvey.productOverwrites)
|
||||
: Prisma.JsonNull,
|
||||
styling: existingSurvey.styling ? structuredClone(existingSurvey.styling) : Prisma.JsonNull,
|
||||
// we'll update the segment later
|
||||
segment: undefined,
|
||||
// Prepare survey data
|
||||
const surveyData: Prisma.SurveyCreateInput = {
|
||||
...restExistingSurvey,
|
||||
id: createId(),
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
type: existingSurvey.type,
|
||||
status: "draft",
|
||||
welcomeCard: structuredClone(existingSurvey.welcomeCard),
|
||||
questions: structuredClone(existingSurvey.questions),
|
||||
endings: structuredClone(existingSurvey.endings),
|
||||
hiddenFields: structuredClone(existingSurvey.hiddenFields),
|
||||
languages: hasLanguages
|
||||
? {
|
||||
create: existingSurvey.languages.map((surveyLanguage) => ({
|
||||
language: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
productId_code: { code: surveyLanguage.language.code, productId: targetProduct.id },
|
||||
},
|
||||
create: {
|
||||
code: surveyLanguage.language.code,
|
||||
alias: surveyLanguage.language.alias,
|
||||
productId: targetProduct.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
default: surveyLanguage.default,
|
||||
enabled: surveyLanguage.enabled,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
triggers: {
|
||||
create: existingSurvey.triggers.map((trigger): Prisma.SurveyTriggerCreateWithoutSurveyInput => {
|
||||
const baseActionClassData = {
|
||||
name: trigger.actionClass.name,
|
||||
environment: { connect: { id: targetEnvironmentId } },
|
||||
description: trigger.actionClass.description,
|
||||
type: trigger.actionClass.type,
|
||||
};
|
||||
|
||||
if (isSameEnvironment) {
|
||||
return {
|
||||
actionClass: { connect: { id: trigger.actionClass.id } },
|
||||
};
|
||||
} else if (trigger.actionClass.type === "code") {
|
||||
return {
|
||||
actionClass: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
key_environmentId: { key: trigger.actionClass.key!, environmentId: targetEnvironmentId },
|
||||
},
|
||||
create: {
|
||||
...baseActionClassData,
|
||||
key: trigger.actionClass.key,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
actionClass: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
name_environmentId: {
|
||||
name: trigger.actionClass.name,
|
||||
environmentId: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
...baseActionClassData,
|
||||
noCodeConfig: trigger.actionClass.noCodeConfig
|
||||
? structuredClone(trigger.actionClass.noCodeConfig)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
environment: {
|
||||
connect: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
creator: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage
|
||||
? structuredClone(existingSurvey.surveyClosedMessage)
|
||||
: Prisma.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ? structuredClone(existingSurvey.singleUse) : Prisma.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites
|
||||
? structuredClone(existingSurvey.productOverwrites)
|
||||
: Prisma.JsonNull,
|
||||
styling: existingSurvey.styling ? structuredClone(existingSurvey.styling) : Prisma.JsonNull,
|
||||
segment: undefined,
|
||||
};
|
||||
|
||||
// if the existing survey has an inline segment, we copy the filters and create a new inline segment and connect it to the new survey
|
||||
// Handle segment
|
||||
if (existingSurvey.segment) {
|
||||
if (existingSurvey.segment.isPrivate) {
|
||||
const newInlineSegment = await createSegment({
|
||||
environmentId,
|
||||
title: `${newSurvey.id}`,
|
||||
isPrivate: true,
|
||||
surveyId: newSurvey.id,
|
||||
filters: existingSurvey.segment.filters,
|
||||
});
|
||||
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
surveyData.segment = {
|
||||
create: {
|
||||
title: surveyData.id!,
|
||||
isPrivate: true,
|
||||
filters: existingSurvey.segment.filters,
|
||||
environment: { connect: { id: targetEnvironmentId } },
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: newInlineSegment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: newInlineSegment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
};
|
||||
} else if (isSameEnvironment) {
|
||||
surveyData.segment = { connect: { id: existingSurvey.segment.id } };
|
||||
} else {
|
||||
await prisma.survey.update({
|
||||
const existingSegmentInTargetEnvironment = await prisma.segment.findFirst({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: existingSurvey.segment.id,
|
||||
},
|
||||
},
|
||||
title: existingSurvey.segment.title,
|
||||
isPrivate: false,
|
||||
environmentId: targetEnvironmentId,
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: existingSurvey.segment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
surveyData.segment = {
|
||||
create: {
|
||||
title: existingSegmentInTargetEnvironment
|
||||
? `${existingSurvey.segment.title}-${Date.now()}`
|
||||
: existingSurvey.segment.title,
|
||||
isPrivate: false,
|
||||
filters: existingSurvey.segment.filters,
|
||||
environment: { connect: { id: targetEnvironmentId } },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const targetProductLanguageCodes = targetProduct.languages.map((language) => language.code);
|
||||
const newSurvey = await prisma.survey.create({
|
||||
data: surveyData,
|
||||
select: selectSurvey,
|
||||
});
|
||||
|
||||
// Identify newly created action classes
|
||||
const newActionClasses = newSurvey.triggers.map((trigger) => trigger.actionClass);
|
||||
|
||||
// Revalidate cache only for newly created action classes
|
||||
for (const actionClass of newActionClasses) {
|
||||
actionClassCache.revalidate({
|
||||
environmentId: actionClass.environmentId,
|
||||
name: actionClass.name,
|
||||
id: actionClass.id,
|
||||
});
|
||||
}
|
||||
|
||||
let newLanguageCreated = false;
|
||||
if (existingSurvey.languages && existingSurvey.languages.length > 0) {
|
||||
const targetLanguageCodes = newSurvey.languages.map((lang) => lang.language.code);
|
||||
newLanguageCreated = targetLanguageCodes.length > targetProductLanguageCodes.length;
|
||||
}
|
||||
|
||||
// Invalidate caches
|
||||
if (newLanguageCreated) {
|
||||
productCache.revalidate({ id: targetProduct.id, environmentId: targetEnvironmentId });
|
||||
}
|
||||
|
||||
surveyCache.revalidate({
|
||||
id: newSurvey.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
@@ -832,13 +942,19 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string, u
|
||||
});
|
||||
});
|
||||
|
||||
if (newSurvey.segment) {
|
||||
segmentCache.revalidate({
|
||||
id: newSurvey.segment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
|
||||
return newSurvey;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
console.error(error);
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import {
|
||||
@@ -87,6 +88,16 @@ export const mockDisplay = {
|
||||
status: null,
|
||||
};
|
||||
|
||||
export const mockEnvironment: TEnvironment = {
|
||||
id: mockId,
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
type: "production",
|
||||
productId: mockId,
|
||||
appSetupCompleted: false,
|
||||
websiteSetupCompleted: false,
|
||||
};
|
||||
|
||||
export const mockUser: TUser = {
|
||||
id: mockId,
|
||||
name: "mock User",
|
||||
|
||||
@@ -4,9 +4,9 @@ import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { testInputValidation } from "vitestSetup";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import {
|
||||
copySurveyToOtherEnvironment,
|
||||
createSurvey,
|
||||
deleteSurvey,
|
||||
duplicateSurvey,
|
||||
getSurvey,
|
||||
getSurveyCount,
|
||||
getSurveys,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
mockActionClass,
|
||||
mockAttributeClass,
|
||||
mockDisplay,
|
||||
mockEnvironment,
|
||||
mockId,
|
||||
mockOrganizationOutput,
|
||||
mockPrismaPerson,
|
||||
@@ -254,23 +255,34 @@ describe("Tests for duplicateSurvey", () => {
|
||||
it("Duplicates a survey successfully", async () => {
|
||||
prisma.survey.findUnique.mockResolvedValueOnce(mockSurveyOutput);
|
||||
prisma.survey.create.mockResolvedValueOnce(mockSurveyOutput);
|
||||
const createdSurvey = await duplicateSurvey(mockId, mockId, mockId);
|
||||
// @ts-expect-error
|
||||
prisma.environment.findUnique.mockResolvedValueOnce(mockEnvironment);
|
||||
// @ts-expect-error
|
||||
prisma.product.findFirst.mockResolvedValueOnce(mockProduct);
|
||||
prisma.actionClass.findFirst.mockResolvedValueOnce(mockActionClass);
|
||||
prisma.actionClass.create.mockResolvedValueOnce(mockActionClass);
|
||||
|
||||
const createdSurvey = await copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId, mockId);
|
||||
expect(createdSurvey).toEqual(mockSurveyOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(duplicateSurvey, "123#", "123#");
|
||||
testInputValidation(copySurveyToOtherEnvironment, "123#", "123#", "123#", "123#", "123#");
|
||||
|
||||
it("Throws ResourceNotFoundError if the survey does not exist", async () => {
|
||||
prisma.survey.findUnique.mockRejectedValueOnce(new ResourceNotFoundError("Survey", mockId));
|
||||
await expect(duplicateSurvey(mockId, mockId, mockId)).rejects.toThrow(ResourceNotFoundError);
|
||||
await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId, mockId)).rejects.toThrow(
|
||||
ResourceNotFoundError
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if there is an unknown error", async () => {
|
||||
const mockErrorMessage = "Unknown error occurred";
|
||||
prisma.survey.create.mockRejectedValue(new Error(mockErrorMessage));
|
||||
await expect(duplicateSurvey(mockId, mockId, mockId)).rejects.toThrow(Error);
|
||||
await expect(copySurveyToOtherEnvironment(mockId, mockId, mockId, mockId, mockId)).rejects.toThrow(
|
||||
Error
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1387,3 +1387,14 @@ 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>;
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
"use server";
|
||||
|
||||
import { Prisma as prismaClient } from "@prisma/client";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { segmentCache } from "@formbricks/lib/segment/cache";
|
||||
import { createSegment } from "@formbricks/lib/segment/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { deleteSurvey, duplicateSurvey, getSurvey, getSurveys } from "@formbricks/lib/survey/service";
|
||||
import {
|
||||
copySurveyToOtherEnvironment,
|
||||
deleteSurvey,
|
||||
getSurvey,
|
||||
getSurveys,
|
||||
} from "@formbricks/lib/survey/service";
|
||||
import { generateSurveySingleUseId } from "@formbricks/lib/utils/singleUseSurveys";
|
||||
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TSurveyFilterCriteria } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const getSurveyAction = async (surveyId: string) => {
|
||||
@@ -29,251 +26,50 @@ export const getSurveyAction = async (surveyId: string) => {
|
||||
return await getSurvey(surveyId);
|
||||
};
|
||||
|
||||
export const duplicateSurveyAction = async (environmentId: string, surveyId: string) => {
|
||||
export const copySurveyToOtherEnvironmentAction = async (
|
||||
environmentId: string,
|
||||
surveyId: string,
|
||||
targetEnvironmentId: 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 isSameEnvironment = environmentId === targetEnvironmentId;
|
||||
|
||||
const duplicatedSurvey = await duplicateSurvey(environmentId, surveyId, session.user.id);
|
||||
return duplicatedSurvey;
|
||||
// Optimize authorization checks
|
||||
if (isSameEnvironment) {
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
} else {
|
||||
const [sourceAccess, targetAccess] = await Promise.all([
|
||||
hasUserEnvironmentAccess(session.user.id, environmentId),
|
||||
hasUserEnvironmentAccess(session.user.id, targetEnvironmentId),
|
||||
]);
|
||||
if (!sourceAccess || !targetAccess) throw new AuthorizationError("Not authorized");
|
||||
}
|
||||
|
||||
const isAuthorizedForSurvey = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorizedForSurvey) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await copySurveyToOtherEnvironment(environmentId, surveyId, targetEnvironmentId, session.user.id);
|
||||
};
|
||||
|
||||
const ZCopyToOtherEnvironmentAction = z.object({
|
||||
environmentId: z.string(),
|
||||
surveyId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
});
|
||||
export const getProductsByEnvironmentIdAction = async (environmentId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
export const copyToOtherEnvironmentAction = authenticatedActionClient
|
||||
.schema(ZCopyToOtherEnvironmentAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
// check if user has access to source and target environments
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
rules: ["survey", "create"],
|
||||
});
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.targetEnvironmentId),
|
||||
rules: ["survey", "create"],
|
||||
});
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const existingSurvey = await prisma.survey.findFirst({
|
||||
where: {
|
||||
id: parsedInput.surveyId,
|
||||
environmentId: parsedInput.environmentId,
|
||||
},
|
||||
include: {
|
||||
triggers: {
|
||||
include: {
|
||||
actionClass: true,
|
||||
},
|
||||
},
|
||||
attributeFilters: {
|
||||
include: {
|
||||
attributeClass: true,
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
select: {
|
||||
default: true,
|
||||
enabled: true,
|
||||
language: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
segment: true,
|
||||
},
|
||||
});
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!existingSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
}
|
||||
if (!organization) {
|
||||
throw new Error("No organization found");
|
||||
}
|
||||
|
||||
let targetEnvironmentTriggers: string[] = [];
|
||||
// map the local triggers to the target environment
|
||||
for (const trigger of existingSurvey.triggers) {
|
||||
const targetEnvironmentTrigger = await prisma.actionClass.findFirst({
|
||||
where: {
|
||||
...(trigger.actionClass.type === "code"
|
||||
? { key: trigger.actionClass.key }
|
||||
: { name: trigger.actionClass.name }),
|
||||
environment: {
|
||||
id: parsedInput.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: parsedInput.targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
description: trigger.actionClass.description,
|
||||
type: trigger.actionClass.type,
|
||||
...(trigger.actionClass.type === "code"
|
||||
? { key: trigger.actionClass.key }
|
||||
: {
|
||||
noCodeConfig: trigger.actionClass.noCodeConfig
|
||||
? structuredClone(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: parsedInput.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: parsedInput.targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
targetEnvironmentAttributeFilters.push(newAttributeClass.id);
|
||||
} else {
|
||||
targetEnvironmentAttributeFilters.push(targetEnvironmentAttributeClass.id);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultLanguageId = existingSurvey.languages.find((l) => l.default)?.language.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
|
||||
createdBy: undefined,
|
||||
segmentId: undefined,
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
status: "draft",
|
||||
questions: structuredClone(existingSurvey.questions),
|
||||
endings: structuredClone(existingSurvey.endings),
|
||||
languages: {
|
||||
create: existingSurvey.languages?.map((surveyLanguage) => ({
|
||||
languageId: surveyLanguage.language.id,
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
})),
|
||||
},
|
||||
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: parsedInput.targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
creator: {
|
||||
connect: {
|
||||
id: ctx.user.id,
|
||||
},
|
||||
},
|
||||
isVerifyEmailEnabled: existingSurvey.isVerifyEmailEnabled,
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
|
||||
styling: existingSurvey.styling ?? prismaClient.JsonNull,
|
||||
segment: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// if the existing survey has an inline segment, we copy the filters and create a new inline segment and connect it to the new survey
|
||||
if (existingSurvey.segment) {
|
||||
if (existingSurvey.segment.isPrivate) {
|
||||
const newInlineSegment = await createSegment({
|
||||
environmentId: parsedInput.environmentId,
|
||||
title: `${newSurvey.id}`,
|
||||
isPrivate: true,
|
||||
surveyId: newSurvey.id,
|
||||
filters: existingSurvey.segment.filters,
|
||||
});
|
||||
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: newInlineSegment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: newInlineSegment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
} else {
|
||||
await prisma.survey.update({
|
||||
where: {
|
||||
id: newSurvey.id,
|
||||
},
|
||||
data: {
|
||||
segment: {
|
||||
connect: {
|
||||
id: existingSurvey.segment.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
segmentCache.revalidate({
|
||||
id: existingSurvey.segment.id,
|
||||
environmentId: newSurvey.environmentId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
surveyCache.revalidate({
|
||||
id: newSurvey.id,
|
||||
environmentId: parsedInput.targetEnvironmentId,
|
||||
});
|
||||
return newSurvey;
|
||||
});
|
||||
const products = await getProducts(organization.id);
|
||||
return products;
|
||||
};
|
||||
|
||||
export const deleteSurveyAction = async (surveyId: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
153
packages/ui/SurveysList/components/CopySurveyForm.tsx
Normal file
153
packages/ui/SurveysList/components/CopySurveyForm.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
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";
|
||||
|
||||
export const CopySurveyForm = ({
|
||||
defaultProducts,
|
||||
survey,
|
||||
onCancel,
|
||||
setOpen,
|
||||
}: {
|
||||
defaultProducts: TProduct[];
|
||||
survey: TSurvey;
|
||||
onCancel: () => void;
|
||||
setOpen: (value: boolean) => void;
|
||||
}) => {
|
||||
const form = useForm<TSurveyCopyFormData>({
|
||||
resolver: zodResolver(ZSurveyCopyFormValidation),
|
||||
defaultValues: {
|
||||
products: defaultProducts.map((product) => ({
|
||||
product: product.id,
|
||||
environments: [],
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const formFields = useFieldArray({
|
||||
name: "products",
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const onSubmit = async (data: TSurveyCopyFormData) => {
|
||||
const filteredData = data.products.filter((product) => product.environments.length > 0);
|
||||
|
||||
try {
|
||||
filteredData.map(async (product) => {
|
||||
product.environments.map(async (environment) => {
|
||||
await copySurveyToOtherEnvironmentAction(survey.environmentId, survey.id, environment);
|
||||
});
|
||||
});
|
||||
|
||||
toast.success("Survey copied successfully!");
|
||||
} catch (error) {
|
||||
toast.error("Failed to copy survey");
|
||||
} finally {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="relative flex h-full w-full flex-col gap-8 overflow-y-auto bg-white p-4">
|
||||
<div className="space-y-8 pb-12">
|
||||
{formFields.fields.map((field, productIndex) => {
|
||||
const product = defaultProducts.find((product) => product.id === field.product);
|
||||
const isDisabled = survey.type !== "link" && product?.config.channel !== survey.type;
|
||||
|
||||
return (
|
||||
<div key={product?.id}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<TooltipRenderer
|
||||
shouldRender={isDisabled}
|
||||
tooltipContent={
|
||||
<span>
|
||||
This product is not compatible with the survey type. Please select a different
|
||||
product.
|
||||
</span>
|
||||
}>
|
||||
<div className="w-fit">
|
||||
<p className="text-base font-semibold text-slate-900">
|
||||
{product?.name}
|
||||
{isDisabled && <span className="ml-2 mr-11 text-sm text-gray-500">(Disabled)</span>}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipRenderer>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{product?.environments.map((environment) => {
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`products.${productIndex}.environments`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<div className="flex items-center">
|
||||
<FormControl>
|
||||
<>
|
||||
<Checkbox
|
||||
{...field}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onCheckedChange={() => {
|
||||
if (field.value.includes(environment.id)) {
|
||||
field.onChange(
|
||||
field.value.filter((id: string) => id !== environment.id)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...field.value, environment.id]);
|
||||
}
|
||||
}}
|
||||
className="mr-2 h-4 w-4 appearance-none border-gray-300 checked:border-transparent checked:bg-slate-500 checked:after:bg-slate-500 checked:hover:bg-slate-500 focus:ring-2 focus:ring-slate-500 focus:ring-opacity-50"
|
||||
disabled={isDisabled}
|
||||
id={environment.id}
|
||||
/>
|
||||
<Label htmlFor={environment.id}>
|
||||
<p className="text-sm font-medium capitalize text-slate-900">
|
||||
{environment.type}
|
||||
</p>
|
||||
</Label>
|
||||
</>
|
||||
</FormControl>
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="fixed bottom-0 left-0 right-0 z-10 flex w-full justify-end space-x-2 bg-white">
|
||||
<div className="flex w-full justify-end pb-4 pr-4">
|
||||
<Button type="button" onClick={onCancel} variant="minimal">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button variant="primary" type="submit">
|
||||
Copy survey
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
37
packages/ui/SurveysList/components/CopySurveyModal.tsx
Normal file
37
packages/ui/SurveysList/components/CopySurveyModal.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { MousePointerClickIcon } from "lucide-react";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Modal } from "../../Modal";
|
||||
import SurveyCopyOptions from "./SurveyCopyOptions";
|
||||
|
||||
interface CopySurveyModalProps {
|
||||
open: boolean;
|
||||
setOpen: (value: boolean) => void;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export const CopySurveyModal = ({ open, setOpen, survey }: CopySurveyModalProps) => (
|
||||
<Modal open={open} setOpen={setOpen} noPadding restrictOverflow>
|
||||
<div className="flex h-full flex-col rounded-lg">
|
||||
<div className="fixed left-0 right-0 z-10 h-24 rounded-t-lg bg-slate-100">
|
||||
<div className="flex w-full items-center justify-between p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<MousePointerClickIcon className="h-6 w-6 text-slate-500" />
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">Copy Survey</div>
|
||||
<div className="text-sm text-slate-500">Copy this survey to another environment</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full max-h-[500px] overflow-auto pl-4 pt-24">
|
||||
<SurveyCopyOptions
|
||||
survey={survey}
|
||||
environmentId={survey.environmentId}
|
||||
onCancel={() => setOpen(false)}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
46
packages/ui/SurveysList/components/SurveyCopyOptions.tsx
Normal file
46
packages/ui/SurveysList/components/SurveyCopyOptions.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getProductsByEnvironmentIdAction } from "../actions";
|
||||
import { CopySurveyForm } from "./CopySurveyForm";
|
||||
|
||||
interface SurveyCopyOptionsProps {
|
||||
survey: TSurvey;
|
||||
environmentId: string;
|
||||
onCancel: () => void;
|
||||
setOpen: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const SurveyCopyOptions = ({ environmentId, survey, onCancel, setOpen }: SurveyCopyOptionsProps) => {
|
||||
const [products, setProducts] = useState<TProduct[]>([]);
|
||||
const [productLoading, setProductLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
const products = await getProductsByEnvironmentIdAction(environmentId);
|
||||
setProducts(products);
|
||||
} catch (error) {
|
||||
toast.error("Error fetching products");
|
||||
} finally {
|
||||
setProductLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProducts();
|
||||
}, [environmentId]);
|
||||
|
||||
if (productLoading) {
|
||||
return (
|
||||
<div className="relative flex h-full min-h-96 w-full items-center justify-center bg-white pb-12">
|
||||
<Loader2 className="animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <CopySurveyForm defaultProducts={products} survey={survey} onCancel={onCancel} setOpen={setOpen} />;
|
||||
};
|
||||
|
||||
export default SurveyCopyOptions;
|
||||
@@ -16,13 +16,8 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../../DropdownMenu";
|
||||
import { LoadingSpinner } from "../../LoadingSpinner";
|
||||
import {
|
||||
copyToOtherEnvironmentAction,
|
||||
deleteSurveyAction,
|
||||
duplicateSurveyAction,
|
||||
getSurveyAction,
|
||||
} from "../actions";
|
||||
import { copySurveyToOtherEnvironmentAction, deleteSurveyAction, getSurveyAction } from "../actions";
|
||||
import { CopySurveyModal } from "./CopySurveyModal";
|
||||
|
||||
interface SurveyDropDownMenuProps {
|
||||
environmentId: string;
|
||||
@@ -39,8 +34,6 @@ interface SurveyDropDownMenuProps {
|
||||
export const SurveyDropDownMenu = ({
|
||||
environmentId,
|
||||
survey,
|
||||
environment,
|
||||
otherEnvironment,
|
||||
webAppUrl,
|
||||
singleUseId,
|
||||
isSurveyCreationDeletionDisabled,
|
||||
@@ -50,6 +43,7 @@ export const SurveyDropDownMenu = ({
|
||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
|
||||
const [isCopyFormOpen, setIsCopyFormOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const surveyUrl = useMemo(() => webAppUrl + "/s/" + survey.id, [survey.id, webAppUrl]);
|
||||
@@ -71,7 +65,11 @@ export const SurveyDropDownMenu = ({
|
||||
const duplicateSurveyAndRefresh = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const duplicatedSurvey = await duplicateSurveyAction(environmentId, surveyId);
|
||||
const duplicatedSurvey = await copySurveyToOtherEnvironmentAction(
|
||||
environmentId,
|
||||
surveyId,
|
||||
environmentId
|
||||
);
|
||||
router.refresh();
|
||||
const transformedDuplicatedSurvey = await getSurveyAction(duplicatedSurvey.id);
|
||||
if (transformedDuplicatedSurvey) duplicateSurvey(transformedDuplicatedSurvey);
|
||||
@@ -82,33 +80,6 @@ export const SurveyDropDownMenu = ({
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const copyToOtherEnvironment = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// await copyToOtherEnvironmentAction(environmentId, surveyId, otherEnvironment.id);
|
||||
await copyToOtherEnvironmentAction({
|
||||
environmentId,
|
||||
surveyId,
|
||||
targetEnvironmentId: 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-slate-100">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
id={`${survey.name.toLowerCase().split(" ").join("-")}-survey-actions`}
|
||||
@@ -150,35 +121,20 @@ export const SurveyDropDownMenu = ({
|
||||
)}
|
||||
{!isSurveyCreationDeletionDisabled && (
|
||||
<>
|
||||
{environment.type === "development" ? (
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDropDownOpen(false);
|
||||
copyToOtherEnvironment(survey.id);
|
||||
}}>
|
||||
<ArrowUpFromLineIcon className="mr-2 h-4 w-4" />
|
||||
Copy to Prod
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
) : environment.type === "production" ? (
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDropDownOpen(false);
|
||||
copyToOtherEnvironment(survey.id);
|
||||
}}>
|
||||
<ArrowUpFromLineIcon className="mr-2 h-4 w-4" />
|
||||
Copy to Dev
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center"
|
||||
disabled={loading}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDropDownOpen(false);
|
||||
setIsCopyFormOpen(true);
|
||||
}}>
|
||||
<ArrowUpFromLineIcon className="mr-2 h-4 w-4" />
|
||||
Copy...
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{survey.type === "link" && survey.status !== "draft" && (
|
||||
@@ -245,6 +201,10 @@ export const SurveyDropDownMenu = ({
|
||||
text="Are you sure you want to delete this survey and all of its responses? This action cannot be undone."
|
||||
/>
|
||||
)}
|
||||
|
||||
{isCopyFormOpen && (
|
||||
<CopySurveyModal open={isCopyFormOpen} setOpen={setIsCopyFormOpen} survey={survey} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user