mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Compare commits
1 Commits
cursor/imp
...
feat/czech
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acd6d7185e |
@@ -17,7 +17,8 @@
|
||||
"zh-Hans-CN",
|
||||
"zh-Hant-TW",
|
||||
"nl-NL",
|
||||
"es-ES"
|
||||
"es-ES",
|
||||
"cs-CZ"
|
||||
]
|
||||
},
|
||||
"version": 1.8
|
||||
|
||||
@@ -176,6 +176,7 @@ export const AVAILABLE_LOCALES: TUserLocale[] = [
|
||||
"ja-JP",
|
||||
"zh-Hans-CN",
|
||||
"es-ES",
|
||||
"cs-CZ",
|
||||
];
|
||||
|
||||
// Billing constants
|
||||
|
||||
@@ -139,6 +139,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "英语(美国)",
|
||||
"nl-NL": "Engels (VS)",
|
||||
"es-ES": "Inglés (EE.UU.)",
|
||||
"cs-CZ": "Angličtina (USA)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -155,6 +156,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "德语",
|
||||
"nl-NL": "Duits",
|
||||
"es-ES": "Alemán",
|
||||
"cs-CZ": "Němčina",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -171,6 +173,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "葡萄牙语(巴西)",
|
||||
"nl-NL": "Portugees (Brazilië)",
|
||||
"es-ES": "Portugués (Brasil)",
|
||||
"cs-CZ": "Portugalština (Brazílie)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -187,6 +190,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "法语",
|
||||
"nl-NL": "Frans",
|
||||
"es-ES": "Francés",
|
||||
"cs-CZ": "Francouzština",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -203,6 +207,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "繁体中文",
|
||||
"nl-NL": "Chinees (Traditioneel)",
|
||||
"es-ES": "Chino (Tradicional)",
|
||||
"cs-CZ": "Čínština (tradiční)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -219,6 +224,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "葡萄牙语(葡萄牙)",
|
||||
"nl-NL": "Portugees (Portugal)",
|
||||
"es-ES": "Portugués (Portugal)",
|
||||
"cs-CZ": "Portugalština (Portugalsko)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -235,6 +241,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "罗马尼亚语",
|
||||
"nl-NL": "Roemeens",
|
||||
"es-ES": "Rumano",
|
||||
"cs-CZ": "Rumunština",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -251,6 +258,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "日语",
|
||||
"nl-NL": "Japans",
|
||||
"es-ES": "Japonés",
|
||||
"cs-CZ": "Japonština",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -267,6 +275,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "简体中文",
|
||||
"nl-NL": "Chinees (Vereenvoudigd)",
|
||||
"es-ES": "Chino (Simplificado)",
|
||||
"cs-CZ": "Čínština (zjednodušená)",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -283,6 +292,7 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "荷兰语",
|
||||
"nl-NL": "Nederlands",
|
||||
"es-ES": "Neerlandés",
|
||||
"cs-CZ": "Holandština",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -299,6 +309,24 @@ export const appLanguages = [
|
||||
"zh-Hans-CN": "西班牙语",
|
||||
"nl-NL": "Spaans",
|
||||
"es-ES": "Español",
|
||||
"cs-CZ": "Španělština",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "cs-CZ",
|
||||
label: {
|
||||
"en-US": "Czech",
|
||||
"de-DE": "Tschechisch",
|
||||
"pt-BR": "Tcheco",
|
||||
"fr-FR": "Tchèque",
|
||||
"zh-Hant-TW": "捷克語",
|
||||
"pt-PT": "Checo",
|
||||
"ro-RO": "Cehă",
|
||||
"ja-JP": "チェコ語",
|
||||
"zh-Hans-CN": "捷克语",
|
||||
"nl-NL": "Tsjechisch",
|
||||
"es-ES": "Checo",
|
||||
"cs-CZ": "Čeština",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { formatDistance, intlFormat } from "date-fns";
|
||||
import { de, enUS, es, fr, ja, nl, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
|
||||
import { cs, de, enUS, es, fr, ja, nl, pt, ptBR, ro, zhCN, zhTW } from "date-fns/locale";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
export const convertDateString = (dateString: string | null) => {
|
||||
@@ -105,6 +105,8 @@ const getLocaleForTimeSince = (locale: TUserLocale) => {
|
||||
return zhCN;
|
||||
case "es-ES":
|
||||
return es;
|
||||
case "cs-CZ":
|
||||
return cs;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
2929
apps/web/locales/cs-CZ.json
Normal file
2929
apps/web/locales/cs-CZ.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1667,34 +1667,14 @@
|
||||
"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",
|
||||
@@ -1938,8 +1918,6 @@
|
||||
"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",
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import {
|
||||
TSurveyCreateInput,
|
||||
ZSurvey,
|
||||
ZSurveyCreateInput,
|
||||
ZSurveyFilterCriteria,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { 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";
|
||||
@@ -20,17 +15,7 @@ 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,
|
||||
@@ -278,200 +263,3 @@ 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;
|
||||
});
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
import {
|
||||
ArrowUpFromLineIcon,
|
||||
CopyIcon,
|
||||
DownloadIcon,
|
||||
EyeIcon,
|
||||
LinkIcon,
|
||||
MoreVertical,
|
||||
@@ -25,8 +24,6 @@ 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 {
|
||||
@@ -128,29 +125,6 @@ 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`}
|
||||
@@ -196,33 +170,20 @@ 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={handleExportSurvey}>
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
{t("environments.surveys.export_survey")}
|
||||
</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>
|
||||
)}
|
||||
{survey.type === "link" && survey.status !== "draft" && (
|
||||
<>
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
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);
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -9,7 +9,6 @@ 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";
|
||||
@@ -47,6 +46,16 @@ 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,
|
||||
@@ -68,10 +77,7 @@ export const SurveysPage = async ({ params: paramsProps }: SurveyTemplateProps)
|
||||
if (surveyCount > 0) {
|
||||
content = (
|
||||
<>
|
||||
<PageHeader
|
||||
pageTitle={t("common.surveys")}
|
||||
cta={isReadOnly ? <></> : <SurveysHeaderActions environmentId={environment.id} />}
|
||||
/>
|
||||
<PageHeader pageTitle={t("common.surveys")} cta={isReadOnly ? <></> : <CreateSurveyButton />} />
|
||||
<SurveysList
|
||||
environmentId={environment.id}
|
||||
isReadOnly={isReadOnly}
|
||||
|
||||
@@ -223,6 +223,7 @@ vi.mock("@/lib/constants", () => ({
|
||||
"ja-JP",
|
||||
"zh-Hans-CN",
|
||||
"es-ES",
|
||||
"cs-CZ",
|
||||
],
|
||||
DEFAULT_LOCALE: "en-US",
|
||||
BREVO_API_KEY: "mock-brevo-api-key",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ export const ZUserLocale = z.enum([
|
||||
"ja-JP",
|
||||
"zh-Hans-CN",
|
||||
"es-ES",
|
||||
"cs-CZ",
|
||||
]);
|
||||
|
||||
export type TUserLocale = z.infer<typeof ZUserLocale>;
|
||||
|
||||
Reference in New Issue
Block a user