Compare commits

...

4 Commits

Author SHA1 Message Date
Cursor Agent
a7d8fc6ce6 Refactor: Improve import survey modal UI and text
Co-authored-by: johannes <johannes@formbricks.com>
2025-11-19 21:40:51 +00:00
Cursor Agent
fb019c8c4c Fix: Change background color of import survey modal
Co-authored-by: johannes <johannes@formbricks.com>
2025-11-19 21:38:03 +00:00
Cursor Agent
1d0ce7e5ce Refactor survey import to prepare survey data correctly
Co-authored-by: johannes <johannes@formbricks.com>
2025-11-19 21:37:09 +00:00
Cursor Agent
8b8fcf7539 feat: Add survey import and export functionality
Co-authored-by: johannes <johannes@formbricks.com>
2025-11-19 20:37:52 +00:00
8 changed files with 733 additions and 26 deletions

View File

@@ -1667,14 +1667,34 @@
"zip": "Zip"
},
"error_deleting_survey": "An error occured while deleting survey",
"export_survey": "Export survey",
"filter": {
"complete_and_partial_responses": "Complete and partial responses",
"complete_responses": "Complete responses",
"partial_responses": "Partial responses"
},
"import_error_invalid_json": "Invalid JSON file",
"import_error_validation": "Survey validation failed",
"import_survey": "Import Survey",
"import_survey_description": "Import a survey from a JSON file",
"import_survey_error": "Failed to import survey",
"import_survey_errors": "Errors",
"import_survey_file_label": "Select JSON file",
"import_survey_import": "Import Survey",
"import_survey_name_label": "Survey Name",
"import_survey_new_id": "New Survey ID",
"import_survey_success": "Survey imported successfully",
"import_survey_validate": "Validate Survey",
"import_survey_warnings": "Warnings",
"import_warning_follow_ups": "Survey follow-ups require an enterprise plan. Follow-ups will be removed.",
"import_warning_images": "Images detected in survey. You'll need to re-upload images after import.",
"import_warning_multi_language": "Multi-language surveys require an enterprise plan. Languages will be removed.",
"import_warning_recaptcha": "Spam protection requires an enterprise plan. reCAPTCHA will be disabled.",
"import_warning_segments": "Segment targeting will be removed. Configure targeting after import.",
"new_survey": "New Survey",
"no_surveys_created_yet": "No surveys created yet",
"open_options": "Open options",
"or_drag_and_drop_json": "or drag and drop a JSON file here",
"preview_survey_in_a_new_tab": "Preview survey in a new tab",
"read_only_user_not_allowed_to_create_survey_warning": "As a Read-Only user you are not allowed to create surveys. Please ask a user with write access to create a survey or a manager to upgrade your role.",
"relevance": "Relevance",
@@ -1918,6 +1938,8 @@
"survey_deleted_successfully": "Survey deleted successfully!",
"survey_duplicated_successfully": "Survey duplicated successfully.",
"survey_duplication_error": "Failed to duplicate the survey.",
"survey_export_error": "Failed to export survey",
"survey_exported_successfully": "Survey exported successfully",
"templates": {
"all_channels": "All channels",
"all_industries": "All industries",

View File

@@ -2,7 +2,12 @@
import { z } from "zod";
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
import { ZSurveyFilterCriteria } from "@formbricks/types/surveys/types";
import {
TSurveyCreateInput,
ZSurvey,
ZSurveyCreateInput,
ZSurveyFilterCriteria,
} from "@formbricks/types/surveys/types";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
@@ -15,7 +20,17 @@ import {
} from "@/lib/utils/helper";
import { generateSurveySingleUseIds } from "@/lib/utils/single-use-surveys";
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
import { checkMultiLanguagePermission } from "@/modules/ee/multi-language-surveys/lib/actions";
import { createSurvey } from "@/modules/survey/components/template-list/lib/survey";
import { getSurveyFollowUpsPermission } from "@/modules/survey/follow-ups/lib/utils";
import { checkSpamProtectionPermission } from "@/modules/survey/lib/permission";
import { getSurvey as getFullSurvey, getOrganizationBilling } from "@/modules/survey/lib/survey";
import { getProjectIdIfEnvironmentExists } from "@/modules/survey/list/lib/environment";
import {
detectImagesInSurvey,
getImportWarnings,
stripEnterpriseFeatures,
} from "@/modules/survey/list/lib/import-validation";
import { getUserProjects } from "@/modules/survey/list/lib/project";
import {
copySurveyToOtherEnvironment,
@@ -263,3 +278,200 @@ export const getSurveysAction = authenticatedActionClient
parsedInput.filterCriteria
);
});
const ZExportSurveyAction = z.object({
surveyId: z.string().cuid2(),
});
export const exportSurveyAction = authenticatedActionClient
.schema(ZExportSurveyAction)
.action(async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "read",
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
},
],
});
return await getFullSurvey(parsedInput.surveyId);
});
const ZValidateSurveyImportAction = z.object({
surveyData: z.record(z.any()),
environmentId: z.string().cuid2(),
});
export const validateSurveyImportAction = authenticatedActionClient
.schema(ZValidateSurveyImportAction)
.action(async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
const errors: string[] = [];
const warnings: string[] = [];
// Validate with Zod
const validationResult = ZSurveyCreateInput.safeParse(parsedInput.surveyData);
if (!validationResult.success) {
errors.push("import_error_validation");
}
// Check for images
const hasImages = detectImagesInSurvey(parsedInput.surveyData);
// Check permissions
let permissions = {
hasMultiLanguage: true,
hasFollowUps: true,
hasRecaptcha: true,
};
try {
await checkMultiLanguagePermission(organizationId);
} catch {
permissions.hasMultiLanguage = false;
}
try {
const organizationBillingData = await getOrganizationBilling(organizationId);
if (organizationBillingData) {
const isFollowUpsEnabled = await getSurveyFollowUpsPermission(organizationBillingData.plan);
if (!isFollowUpsEnabled) {
permissions.hasFollowUps = false;
}
}
} catch {
permissions.hasFollowUps = false;
}
try {
await checkSpamProtectionPermission(organizationId);
} catch {
permissions.hasRecaptcha = false;
}
// Get warnings
const importWarnings = getImportWarnings(parsedInput.surveyData, hasImages, permissions);
return {
valid: errors.length === 0,
errors,
warnings: importWarnings,
surveyName: parsedInput.surveyData?.name || "Imported Survey",
hasImages,
willStripFeatures: {
multiLanguage:
!permissions.hasMultiLanguage && (parsedInput.surveyData?.languages?.length > 1 || false),
followUps: !permissions.hasFollowUps && (parsedInput.surveyData?.followUps?.length > 0 || false),
recaptcha: !permissions.hasRecaptcha && (parsedInput.surveyData?.recaptcha?.enabled || false),
},
};
});
const ZImportSurveyAction = z.object({
surveyData: z.record(z.any()),
environmentId: z.string().cuid2(),
newName: z.string(),
});
export const importSurveyAction = authenticatedActionClient
.schema(ZImportSurveyAction)
.action(async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
{
type: "projectTeam",
minPermission: "readWrite",
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
},
],
});
// Re-validate
const validationResult = ZSurveyCreateInput.safeParse(parsedInput.surveyData);
if (!validationResult.success) {
throw new Error("Survey validation failed");
}
// Check permissions and strip features
let permissions = {
hasMultiLanguage: true,
hasFollowUps: true,
hasRecaptcha: true,
};
try {
await checkMultiLanguagePermission(organizationId);
} catch {
permissions.hasMultiLanguage = false;
}
try {
const organizationBillingData = await getOrganizationBilling(organizationId);
if (organizationBillingData) {
const isFollowUpsEnabled = await getSurveyFollowUpsPermission(organizationBillingData.plan);
if (!isFollowUpsEnabled) {
permissions.hasFollowUps = false;
}
}
} catch {
permissions.hasFollowUps = false;
}
try {
await checkSpamProtectionPermission(organizationId);
} catch {
permissions.hasRecaptcha = false;
}
// Prepare survey for import - strip fields that should be auto-generated
const { id, createdAt, updatedAt, ...surveyWithoutMetadata } = stripEnterpriseFeatures(
validationResult.data,
permissions
);
const importedSurvey: TSurveyCreateInput = {
...surveyWithoutMetadata,
name: parsedInput.newName,
segment: null,
environmentId: parsedInput.environmentId,
createdBy: ctx.user.id,
};
// Create the survey
const result = await createSurvey(parsedInput.environmentId, importedSurvey);
return result;
});

View File

@@ -0,0 +1,264 @@
"use client";
import { createId } from "@paralleldrive/cuid2";
import { FileTextIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useRef, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { logger } from "@formbricks/logger";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { importSurveyAction, validateSurveyImportAction } from "@/modules/survey/list/actions";
import { Alert } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
interface ImportSurveyModalProps {
environmentId: string;
open: boolean;
setOpen: (open: boolean) => void;
onSurveyImported?: () => void;
}
export const ImportSurveyModal = ({
environmentId,
open,
setOpen,
onSurveyImported,
}: ImportSurveyModalProps) => {
const { t } = useTranslation();
const router = useRouter();
const fileInputRef = useRef<HTMLInputElement>(null);
const [step, setStep] = useState<"upload" | "preview">("upload");
const [loading, setLoading] = useState(false);
const [surveyData, setSurveyData] = useState<any>(null);
const [surveyName, setSurveyName] = useState("");
const [newName, setNewName] = useState("");
const [newSurveyId, setNewSurveyId] = useState("");
const [errors, setErrors] = useState<string[]>([]);
const [warnings, setWarnings] = useState<string[]>([]);
const resetState = () => {
setStep("upload");
setSurveyData(null);
setSurveyName("");
setNewName("");
setNewSurveyId("");
setErrors([]);
setWarnings([]);
setLoading(false);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleClose = () => {
setOpen(false);
resetState();
};
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target?.files?.[0];
if (!file) return;
// Check file type
if (file.type !== "application/json" && !file.name.endsWith(".json")) {
toast.error(t("environments.surveys.import_error_invalid_json"));
return;
}
setLoading(true);
try {
const text = await file.text();
const parsed = JSON.parse(text);
setSurveyData(parsed);
setSurveyName(parsed?.name || "Imported Survey");
setNewName(`${parsed?.name || "Imported Survey"} (imported)`);
// Validate the survey data
const validationResult = await validateSurveyImportAction({
surveyData: parsed,
environmentId,
});
if (validationResult?.data) {
setErrors(validationResult.data.errors || []);
setWarnings(validationResult.data.warnings || []);
if (validationResult.data.errors.length === 0) {
// Generate a preview ID
const previewId = createId();
setNewSurveyId(previewId);
setStep("preview");
}
}
} catch (error) {
logger.error(error);
toast.error(t("environments.surveys.import_error_invalid_json"));
} finally {
setLoading(false);
}
};
const handleImport = async () => {
if (!surveyData || errors.length > 0) {
return;
}
setLoading(true);
try {
const result = await importSurveyAction({
surveyData,
environmentId,
newName,
});
if (result?.data) {
toast.success(t("environments.surveys.import_survey_success"));
onSurveyImported?.();
router.refresh();
handleClose();
} else {
const errorMessage = getFormattedErrorMessage(result);
toast.error(errorMessage || t("environments.surveys.import_survey_error"));
}
} catch (error) {
logger.error(error);
toast.error(t("environments.surveys.import_survey_error"));
} finally {
setLoading(false);
}
};
if (step === "upload") {
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="w-full max-w-md">
<DialogHeader>
<DialogTitle>{t("environments.surveys.import_survey")}</DialogTitle>
<DialogDescription>{t("environments.surveys.import_survey_description")}</DialogDescription>
</DialogHeader>
<DialogBody className="space-y-4">
<div>
<Label htmlFor="json-file">{t("environments.surveys.import_survey_file_label")}</Label>
<div className="mt-2 flex items-center justify-center rounded-lg border-2 border-dashed border-slate-300 bg-white p-8">
<div className="text-center">
<FileTextIcon className="mx-auto mb-4 h-12 w-12 text-slate-400" />
<input
ref={fileInputRef}
type="file"
id="json-file"
accept=".json"
onChange={handleFileSelect}
className="hidden"
disabled={loading}
/>
<Button
variant="secondary"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={loading}
loading={loading}>
{t("common.upload")}
</Button>
<p className="mt-2 text-sm text-slate-500">
{t("environments.surveys.or_drag_and_drop_json")}
</p>
</div>
</div>
</div>
</DialogBody>
<DialogFooter>
<Button variant="secondary" onClick={handleClose} disabled={loading}>
{t("common.cancel")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="w-full max-w-2xl">
<DialogHeader>
<DialogTitle>{t("environments.surveys.import_survey")}</DialogTitle>
<DialogDescription>{t("environments.surveys.import_survey_validate")}</DialogDescription>
</DialogHeader>
<DialogBody className="space-y-4">
{errors.length > 0 && (
<Alert variant="destructive" title={t("environments.surveys.import_survey_errors")}>
<ul className="space-y-1">
{errors.map((error) => (
<li key={error} className="text-sm">
{t(`environments.surveys.${error}`)}
</li>
))}
</ul>
</Alert>
)}
{warnings.length > 0 && (
<Alert variant="default" title={t("environments.surveys.import_survey_warnings")}>
<ul className="space-y-1">
{warnings.map((warning) => (
<li key={warning} className="text-sm">
{t(`environments.surveys.${warning}`)}
</li>
))}
</ul>
</Alert>
)}
<div className="space-y-4 rounded-lg border border-slate-200 bg-slate-50 p-4">
<div>
<Label htmlFor="survey-name">{t("environments.surveys.import_survey_name_label")}</Label>
<Input
id="survey-name"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder={surveyName}
disabled={loading}
className="mt-2"
/>
</div>
<div>
<Label>{t("environments.surveys.import_survey_new_id")}</Label>
<div className="mt-2">
<IdBadge id={newSurveyId} />
</div>
</div>
</div>
</DialogBody>
<DialogFooter>
<Button variant="secondary" onClick={() => setStep("upload")} disabled={loading}>
{t("common.back")}
</Button>
<Button
onClick={handleImport}
disabled={errors.length > 0 || loading || !newName}
loading={loading}>
{t("environments.surveys.import_survey_import")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -3,6 +3,7 @@
import {
ArrowUpFromLineIcon,
CopyIcon,
DownloadIcon,
EyeIcon,
LinkIcon,
MoreVertical,
@@ -24,6 +25,8 @@ import {
deleteSurveyAction,
getSurveyAction,
} from "@/modules/survey/list/actions";
import { exportSurveyAction } from "@/modules/survey/list/actions";
import { downloadSurveyJson } from "@/modules/survey/list/lib/download-survey";
import { TSurvey } from "@/modules/survey/list/types/surveys";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import {
@@ -125,6 +128,29 @@ export const SurveyDropDownMenu = ({
setIsCautionDialogOpen(true);
};
const handleExportSurvey = async (e: React.MouseEvent<HTMLButtonElement>) => {
try {
e.preventDefault();
setIsDropDownOpen(false);
setLoading(true);
const result = await exportSurveyAction({ surveyId: survey.id });
if (result?.data) {
const jsonString = JSON.stringify(result.data, null, 2);
downloadSurveyJson(survey.name, jsonString);
toast.success(t("environments.surveys.survey_exported_successfully"));
} else {
toast.error(t("environments.surveys.survey_export_error"));
}
} catch (error) {
logger.error(error);
toast.error(t("environments.surveys.survey_export_error"));
} finally {
setLoading(false);
}
};
return (
<div
id={`${survey.name.toLowerCase().split(" ").join("-")}-survey-actions`}
@@ -170,20 +196,33 @@ export const SurveyDropDownMenu = ({
</>
)}
{!isSurveyCreationDeletionDisabled && (
<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" />
{t("common.copy")}...
</button>
</DropdownMenuItem>
<>
<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" />
{t("common.copy")}...
</button>
</DropdownMenuItem>
<DropdownMenuItem>
<button
type="button"
className="flex w-full items-center"
disabled={loading}
onClick={handleExportSurvey}>
<DownloadIcon className="mr-2 h-4 w-4" />
{t("environments.surveys.export_survey")}
</button>
</DropdownMenuItem>
</>
)}
{survey.type === "link" && survey.status !== "draft" && (
<>

View File

@@ -0,0 +1,40 @@
"use client";
import { PlusIcon, UploadIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/modules/ui/components/button";
import { ImportSurveyModal } from "./import-survey-modal";
interface SurveysHeaderActionsProps {
environmentId: string;
}
export const SurveysHeaderActions = ({ environmentId }: SurveysHeaderActionsProps) => {
const { t } = useTranslation();
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
return (
<>
<div className="flex gap-2">
<Button size="sm" variant="secondary" onClick={() => setIsImportModalOpen(true)}>
<UploadIcon className="h-4 w-4" />
{t("environments.surveys.import_survey")}
</Button>
<Button size="sm" asChild>
<Link href={`/environments/${environmentId}/surveys/templates`}>
{t("environments.surveys.new_survey")}
<PlusIcon className="h-4 w-4" />
</Link>
</Button>
</div>
<ImportSurveyModal
environmentId={environmentId}
open={isImportModalOpen}
setOpen={setIsImportModalOpen}
/>
</>
);
};

View File

@@ -0,0 +1,28 @@
export const downloadSurveyJson = (surveyName: string, jsonContent: string): void => {
if (typeof window === "undefined" || typeof document === "undefined") {
throw new Error("downloadSurveyJson can only be used in a browser environment");
}
const trimmedName = (surveyName ?? "").trim();
const today = new Date().toISOString().slice(0, 10);
let normalizedFileName = trimmedName || `survey-${today}`;
if (!normalizedFileName.toLowerCase().endsWith(".json")) {
normalizedFileName = `${normalizedFileName}-export-${today}.json`;
}
const file = new File([jsonContent], normalizedFileName, {
type: "application/json;charset=utf-8",
});
const link = document.createElement("a");
let url: string | undefined;
url = URL.createObjectURL(file);
link.href = url;
link.download = normalizedFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};

View File

@@ -0,0 +1,108 @@
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
export const detectImagesInSurvey = (survey: any): boolean => {
if (survey?.questions) {
for (const question of survey.questions) {
// Check for image fields in various question types
if (question.imageUrl || question.videoUrl || question.fileUrl) {
return true;
}
// Check for images in options
if (question.options) {
for (const option of question.options) {
if (option.imageUrl) {
return true;
}
}
}
}
}
// Check welcome card
if (survey?.welcomeCard?.fileUrl) {
return true;
}
// Check endings
if (survey?.endings) {
for (const ending of survey.endings) {
if (ending.imageUrl || ending.videoUrl) {
return true;
}
}
}
return false;
};
export const stripEnterpriseFeatures = (
survey: any,
permissions: {
hasMultiLanguage: boolean;
hasFollowUps: boolean;
hasRecaptcha: boolean;
}
): any => {
const cleanedSurvey = { ...survey };
// Strip multi-language if not permitted
if (!permissions.hasMultiLanguage) {
cleanedSurvey.languages = [
{
language: {
code: "en",
alias: "English",
},
default: true,
enabled: true,
},
];
cleanedSurvey.showLanguageSwitch = false;
}
// Strip follow-ups if not permitted
if (!permissions.hasFollowUps) {
cleanedSurvey.followUps = [];
}
// Strip recaptcha if not permitted
if (!permissions.hasRecaptcha) {
cleanedSurvey.recaptcha = null;
}
return cleanedSurvey;
};
export const getImportWarnings = (
survey: any,
hasImages: boolean,
permissions: {
hasMultiLanguage: boolean;
hasFollowUps: boolean;
hasRecaptcha: boolean;
}
): string[] => {
const warnings: string[] = [];
if (!permissions.hasMultiLanguage && survey?.languages?.length > 1) {
warnings.push("import_warning_multi_language");
}
if (!permissions.hasFollowUps && survey?.followUps?.length) {
warnings.push("import_warning_follow_ups");
}
if (!permissions.hasRecaptcha && survey?.recaptcha?.enabled) {
warnings.push("import_warning_recaptcha");
}
if (hasImages) {
warnings.push("import_warning_images");
}
if (survey?.segment) {
warnings.push("import_warning_segments");
}
return warnings;
};

View File

@@ -9,6 +9,7 @@ import { getTranslate } from "@/lingodotdev/server";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { getProjectWithTeamIdsByEnvironmentId } from "@/modules/survey/lib/project";
import { SurveysList } from "@/modules/survey/list/components/survey-list";
import { SurveysHeaderActions } from "@/modules/survey/list/components/surveys-header-actions";
import { getSurveyCount } from "@/modules/survey/list/lib/survey";
import { TemplateContainerWithPreview } from "@/modules/survey/templates/components/template-container";
import { Button } from "@/modules/ui/components/button";
@@ -46,16 +47,6 @@ export const SurveysPage = async ({ params: paramsProps }: SurveyTemplateProps)
const currentProjectChannel = project.config.channel ?? null;
const locale = (await getUserLocale(session.user.id)) ?? DEFAULT_LOCALE;
const CreateSurveyButton = () => {
return (
<Button size="sm" asChild>
<Link href={`/environments/${environment.id}/surveys/templates`}>
{t("environments.surveys.new_survey")}
<PlusIcon />
</Link>
</Button>
);
};
const projectWithRequiredProps = {
...project,
@@ -77,7 +68,10 @@ export const SurveysPage = async ({ params: paramsProps }: SurveyTemplateProps)
if (surveyCount > 0) {
content = (
<>
<PageHeader pageTitle={t("common.surveys")} cta={isReadOnly ? <></> : <CreateSurveyButton />} />
<PageHeader
pageTitle={t("common.surveys")}
cta={isReadOnly ? <></> : <SurveysHeaderActions environmentId={environment.id} />}
/>
<SurveysList
environmentId={environment.id}
isReadOnly={isReadOnly}