mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-29 18:00:26 -06:00
feat: add pretty URL UI components for surveys (#6969)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com> Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -144,6 +144,12 @@ export const OrganizationBreadcrumb = ({
|
||||
href: `/environments/${currentEnvironmentId}/settings/api-keys`,
|
||||
hidden: !isOwnerOrManager,
|
||||
},
|
||||
{
|
||||
id: "domain",
|
||||
label: t("common.domain"),
|
||||
href: `/environments/${currentEnvironmentId}/settings/domain`,
|
||||
hidden: isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
id: "billing",
|
||||
label: t("common.billing"),
|
||||
|
||||
@@ -47,6 +47,13 @@ export const OrganizationSettingsNavbar = ({
|
||||
current: pathname?.includes("/api-keys"),
|
||||
hidden: !isOwner,
|
||||
},
|
||||
{
|
||||
id: "domain",
|
||||
label: t("common.domain"),
|
||||
href: `/environments/${environmentId}/settings/domain`,
|
||||
current: pathname?.includes("/domain"),
|
||||
hidden: isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
id: "billing",
|
||||
label: t("common.billing"),
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyStatus } from "@formbricks/types/surveys/types";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||
|
||||
interface SurveyWithSlug {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string | null;
|
||||
status: TSurveyStatus;
|
||||
environment: {
|
||||
id: string;
|
||||
type: "production" | "development";
|
||||
project: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface PrettyUrlsTableProps {
|
||||
surveys: SurveyWithSlug[];
|
||||
}
|
||||
|
||||
export const PrettyUrlsTable = ({ surveys }: PrettyUrlsTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getEnvironmentBadgeColor = (type: string) => {
|
||||
return type === "production" ? "bg-green-100 text-green-800" : "bg-blue-100 text-blue-800";
|
||||
};
|
||||
|
||||
const tableHeaders = [
|
||||
{
|
||||
label: t("environments.settings.domain.survey_name"),
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
label: t("environments.settings.domain.project"),
|
||||
key: "project",
|
||||
},
|
||||
{
|
||||
label: t("environments.settings.domain.pretty_url"),
|
||||
key: "slug",
|
||||
},
|
||||
{
|
||||
label: t("common.environment"),
|
||||
key: "environment",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-100">
|
||||
{tableHeaders.map((header) => (
|
||||
<TableHead key={header.key} className="font-medium text-slate-500">
|
||||
{header.label}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="[&_tr:last-child]:border-b">
|
||||
{surveys.length === 0 && (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={4} className="text-center text-slate-500">
|
||||
{t("environments.settings.domain.no_pretty_urls")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{surveys.map((survey) => (
|
||||
<TableRow key={survey.id} className="border-slate-200 hover:bg-transparent">
|
||||
<TableCell className="font-medium">
|
||||
<Link
|
||||
href={`/environments/${survey.environment.id}/surveys/${survey.id}/summary`}
|
||||
className="text-slate-900 hover:text-slate-700 hover:underline">
|
||||
{survey.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>{survey.environment.project.name}</TableCell>
|
||||
<TableCell>
|
||||
<IdBadge id={survey.slug ?? ""} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
className={`rounded px-2 py-1 text-xs font-medium ${getEnvironmentBadgeColor(survey.environment.type)}`}>
|
||||
{survey.environment.type === "production"
|
||||
? t("common.production")
|
||||
: t("common.development")}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { getSurveysWithSlugsByOrganizationId } from "@/modules/survey/lib/slug";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { SettingsCard } from "../../components/SettingsCard";
|
||||
import { OrganizationSettingsNavbar } from "../components/OrganizationSettingsNavbar";
|
||||
import { PrettyUrlsTable } from "./components/pretty-urls-table";
|
||||
|
||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
const result = await getSurveysWithSlugsByOrganizationId(organization.id);
|
||||
if (!result.ok) {
|
||||
throw new Error(t("common.something_went_wrong"));
|
||||
}
|
||||
|
||||
const surveys = result.data;
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
|
||||
<OrganizationSettingsNavbar
|
||||
environmentId={params.environmentId}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
activeId="domain"
|
||||
/>
|
||||
</PageHeader>
|
||||
|
||||
<SettingsCard
|
||||
title={t("environments.settings.domain.title")}
|
||||
description={t("environments.settings.domain.description")}>
|
||||
<PrettyUrlsTable surveys={surveys} />
|
||||
</SettingsCard>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -3,7 +3,7 @@
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import {
|
||||
Code2Icon,
|
||||
LinkIcon,
|
||||
Link2Icon,
|
||||
MailIcon,
|
||||
QrCodeIcon,
|
||||
Settings,
|
||||
@@ -22,6 +22,7 @@ import { DynamicPopupTab } from "@/app/(app)/environments/[environmentId]/survey
|
||||
import { EmailTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/email-tab";
|
||||
import { LinkSettingsTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/link-settings-tab";
|
||||
import { PersonalLinksTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/personal-links-tab";
|
||||
import { PrettyUrlTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/pretty-url-tab";
|
||||
import { QRCodeTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/qr-code-tab";
|
||||
import { SocialMediaTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/social-media-tab";
|
||||
import { TabContainer } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/tab-container";
|
||||
@@ -80,13 +81,13 @@ export const ShareSurveyModal = ({
|
||||
componentType: React.ComponentType<unknown>;
|
||||
componentProps: unknown;
|
||||
disabled?: boolean;
|
||||
}[] = useMemo(
|
||||
() => [
|
||||
}[] = useMemo(() => {
|
||||
const tabs = [
|
||||
{
|
||||
id: ShareViaType.ANON_LINKS,
|
||||
type: LinkTabsType.SHARE_VIA,
|
||||
label: t("environments.surveys.share.anonymous_links.nav_title"),
|
||||
icon: LinkIcon,
|
||||
icon: Link2Icon,
|
||||
title: t("environments.surveys.share.anonymous_links.nav_title"),
|
||||
description: t("environments.surveys.share.anonymous_links.description"),
|
||||
componentType: AnonymousLinksTab,
|
||||
@@ -180,22 +181,33 @@ export const ShareSurveyModal = ({
|
||||
componentType: LinkSettingsTab,
|
||||
componentProps: { isReadOnly, locale: user.locale, isStorageConfigured },
|
||||
},
|
||||
],
|
||||
[
|
||||
t,
|
||||
survey,
|
||||
publicDomain,
|
||||
user.locale,
|
||||
surveyUrl,
|
||||
isReadOnly,
|
||||
environmentId,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
isFormbricksCloud,
|
||||
email,
|
||||
isStorageConfigured,
|
||||
]
|
||||
);
|
||||
{
|
||||
id: ShareSettingsType.PRETTY_URL,
|
||||
type: LinkTabsType.SHARE_SETTING,
|
||||
label: t("environments.surveys.share.pretty_url.title"),
|
||||
icon: Link2Icon,
|
||||
title: t("environments.surveys.share.pretty_url.title"),
|
||||
description: t("environments.surveys.share.pretty_url.description"),
|
||||
componentType: PrettyUrlTab,
|
||||
componentProps: { publicDomain, isReadOnly },
|
||||
},
|
||||
];
|
||||
|
||||
return isFormbricksCloud ? tabs.filter((tab) => tab.id !== ShareSettingsType.PRETTY_URL) : tabs;
|
||||
}, [
|
||||
t,
|
||||
survey,
|
||||
publicDomain,
|
||||
user.locale,
|
||||
surveyUrl,
|
||||
isReadOnly,
|
||||
environmentId,
|
||||
segments,
|
||||
isContactsEnabled,
|
||||
isFormbricksCloud,
|
||||
email,
|
||||
isStorageConfigured,
|
||||
]);
|
||||
|
||||
const getDefaultActiveId = useCallback(() => {
|
||||
if (survey.type !== "link") {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
|
||||
interface PrettyUrlInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
publicDomain: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const PrettyUrlInput = ({ value, onChange, publicDomain, disabled = false }: PrettyUrlInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center overflow-hidden rounded-md border border-slate-300 bg-white">
|
||||
<span className="flex-shrink-0 border-r border-slate-300 bg-slate-50 px-3 py-2 text-sm text-slate-600">
|
||||
{publicDomain}/p/
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value.toLowerCase().replaceAll(/[^a-z0-9-]/g, ""))}
|
||||
placeholder={t("environments.surveys.share.pretty_url.slug_placeholder")}
|
||||
disabled={disabled}
|
||||
className="border-0 bg-white focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import { Copy, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/context/survey-context";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { removeSurveySlugAction, updateSurveySlugAction } from "@/modules/survey/slug/actions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormProvider,
|
||||
} from "@/modules/ui/components/form";
|
||||
import { PrettyUrlInput } from "./pretty-url-input";
|
||||
|
||||
interface PrettyUrlTabProps {
|
||||
publicDomain: string;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
interface PrettyUrlFormData {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export const PrettyUrlTab = ({ publicDomain, isReadOnly = false }: PrettyUrlTabProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { survey } = useSurvey();
|
||||
const [isEditing, setIsEditing] = useState(!survey.slug);
|
||||
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Initialize form with current values - memoize to prevent re-initialization
|
||||
const initialFormData = useMemo(() => {
|
||||
return {
|
||||
slug: survey.slug || "",
|
||||
};
|
||||
}, [survey.slug]);
|
||||
|
||||
const form = useForm<PrettyUrlFormData>({
|
||||
defaultValues: initialFormData,
|
||||
});
|
||||
|
||||
const { handleSubmit, reset } = form;
|
||||
|
||||
// Sync isEditing state and form with survey.slug changes
|
||||
useEffect(() => {
|
||||
setIsEditing(!survey.slug);
|
||||
reset({ slug: survey.slug || "" });
|
||||
}, [survey.slug, reset]);
|
||||
|
||||
const onSubmit = async (data: PrettyUrlFormData) => {
|
||||
if (!data.slug.trim()) {
|
||||
toast.error(t("environments.surveys.share.pretty_url.slug_required"));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const result = await updateSurveySlugAction({
|
||||
surveyId: survey.id,
|
||||
slug: data.slug,
|
||||
});
|
||||
|
||||
if (result?.data) {
|
||||
if (result.data.ok) {
|
||||
toast.success(t("environments.surveys.share.pretty_url.save_success"));
|
||||
router.refresh();
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
toast.error(result.data.error.message);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(result);
|
||||
toast.error(errorMessage || "Failed to update slug");
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
reset({ slug: survey.slug || "" });
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleRemove = async () => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const result = await removeSurveySlugAction({ surveyId: survey.id });
|
||||
|
||||
if (result?.data) {
|
||||
if (result.data.ok) {
|
||||
setShowRemoveDialog(false);
|
||||
reset({ slug: "" });
|
||||
router.refresh();
|
||||
setIsEditing(true);
|
||||
toast.success(t("environments.surveys.share.pretty_url.remove_success"));
|
||||
} else {
|
||||
toast.error(result.data.error.message);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(result);
|
||||
toast.error(errorMessage || "Failed to remove slug");
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const handleCopyUrl = () => {
|
||||
if (!survey.slug) return;
|
||||
const prettyUrl = `${publicDomain}/p/${survey.slug}`;
|
||||
navigator.clipboard.writeText(prettyUrl);
|
||||
toast.success(t("common.copied_to_clipboard"));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-1">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("environments.surveys.share.pretty_url.slug_label")}</FormLabel>
|
||||
<FormControl>
|
||||
<PrettyUrlInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
publicDomain={publicDomain}
|
||||
disabled={isReadOnly || !isEditing}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>{t("environments.surveys.share.pretty_url.slug_help")}</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button type="submit" disabled={isReadOnly || isSubmitting}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
{survey.slug && (
|
||||
<Button type="button" variant="secondary" onClick={handleCancel} disabled={isSubmitting}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button type="button" variant="secondary" onClick={handleEdit} disabled={isReadOnly}>
|
||||
{t("common.edit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{survey.slug && !isEditing && (
|
||||
<>
|
||||
<Button type="button" variant="default" onClick={handleCopyUrl} disabled={isReadOnly}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
{t("common.copy")} URL
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setShowRemoveDialog(true)}
|
||||
disabled={isReadOnly}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{t("common.remove")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
<DeleteDialog
|
||||
open={showRemoveDialog}
|
||||
setOpen={setShowRemoveDialog}
|
||||
deleteWhat={t("environments.surveys.share.pretty_url.title")}
|
||||
onDelete={handleRemove}
|
||||
text={t("environments.surveys.share.pretty_url.remove_description")}></DeleteDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ export enum ShareViaType {
|
||||
|
||||
export enum ShareSettingsType {
|
||||
LINK_SETTINGS = "link-settings",
|
||||
PRETTY_URL = "pretty-url",
|
||||
}
|
||||
|
||||
export enum LinkTabsType {
|
||||
|
||||
@@ -4915,5 +4915,6 @@ export const previewSurvey = (projectName: string, t: TFunction): TSurvey => {
|
||||
isBackButtonHidden: false,
|
||||
metadata: {},
|
||||
questions: [], // Required for build-time type checking (Zod defaults to [] at runtime)
|
||||
slug: null,
|
||||
};
|
||||
};
|
||||
|
||||
5
apps/web/app/p/[slug]/not-found.tsx
Normal file
5
apps/web/app/p/[slug]/not-found.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LinkSurveyNotFound } from "@/modules/survey/link/not-found";
|
||||
|
||||
export default function NotFound() {
|
||||
return <LinkSurveyNotFound />;
|
||||
}
|
||||
34
apps/web/app/p/[slug]/page.tsx
Normal file
34
apps/web/app/p/[slug]/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getSurveyBySlug } from "@/modules/survey/lib/slug";
|
||||
|
||||
interface PrettyUrlPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}
|
||||
|
||||
export default async function PrettyUrlPage(props: PrettyUrlPageProps) {
|
||||
const { slug } = await props.params;
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const result = await getSurveyBySlug(slug);
|
||||
if (!result.ok || !result.data) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const survey = result.data;
|
||||
|
||||
// Preserve query params (suId, lang, etc.)
|
||||
const queryString = new URLSearchParams(
|
||||
Object.entries(searchParams).filter(([_, v]) => v !== undefined) as [string, string][]
|
||||
).toString();
|
||||
|
||||
const baseUrl = `/s/${survey.id}`;
|
||||
const redirectUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
|
||||
redirect(redirectUrl);
|
||||
}
|
||||
@@ -160,6 +160,7 @@ checksums:
|
||||
common/delete: 8bcf303dd10a645b5baacb02b47d72c9
|
||||
common/description: e17686a22ffad04cc7bb70524ed4478b
|
||||
common/dev_env: e650911d5e19ba256358e0cda154c005
|
||||
common/development: 85211dbb918bda7a6e87649dcfc1b17a
|
||||
common/development_environment_banner: 90aa859c1f43a9c2d9fbc758853d0c20
|
||||
common/disable: 81b754fd7962e0bd9b6ba87f3972e7fc
|
||||
common/disallow: 144b91c92ed07f29ff362fa9edc2e4d0
|
||||
@@ -167,6 +168,7 @@ checksums:
|
||||
common/dismissed: f0e21b3fe28726c577a7238a63cc29c2
|
||||
common/docs: 1563fcb5ddb5037b0709ccd3dd384a92
|
||||
common/documentation: 1563fcb5ddb5037b0709ccd3dd384a92
|
||||
common/domain: 402d46965eacc3af4c5df92e53e95712
|
||||
common/download: 56b7d0834952b39ee394b44bd8179178
|
||||
common/draft: e8a92958ad300aacfe46c2bf6644927e
|
||||
common/duplicate: 27756566785c2b8463e21582c4bb619b
|
||||
@@ -176,6 +178,7 @@ checksums:
|
||||
common/ending_card: 16d30d3a36472159da8c2dbd374dfe22
|
||||
common/enter_url: 468c2276d0f2cb971ff5a47a20fa4b97
|
||||
common/enterprise_license: e81bf506f47968870c7bd07245648a0d
|
||||
common/environment: 0844e8dc1485339c8de066dc0a9bb6a1
|
||||
common/environment_not_found: 4d7610bdb55a8b5e6131bb5b08ce04c5
|
||||
common/environment_notice: 0a860e3fa89407726dd8a2083a6b7fd5
|
||||
common/error: 3c95bcb32c2104b99a46f5b3dd015248
|
||||
@@ -294,6 +297,7 @@ checksums:
|
||||
common/preview_survey: 7409e9c118e3e5d5f2a86201c2b354f2
|
||||
common/privacy: 7459744a63ef8af4e517a09024bd7c08
|
||||
common/product_manager: dfeadc96e6d3de22a884ee97974b505e
|
||||
common/production: 226e0ce83b49700bc1b1c08c4c3ed23a
|
||||
common/profile: d7878693f91303a438852d617f6d35df
|
||||
common/profile_id: 0ef1286cce9d47b148e9a09deccb6655
|
||||
common/progress: dd0200d5849ebb7d64c15098ae91d229
|
||||
@@ -954,6 +958,12 @@ checksums:
|
||||
environments/settings/billing/upgrade: 63c3b52882e0d779859307d672c178c2
|
||||
environments/settings/billing/uptime_sla_99: 25ca4060e575e1a7eee47fceb5576d7c
|
||||
environments/settings/billing/website_surveys: f4d176cc66ffcc2abf44c0d5da1642e3
|
||||
environments/settings/domain/description: f0b4d8c96da816f793cf1f4fdfaade34
|
||||
environments/settings/domain/no_pretty_urls: 2a83d8e399d325fcd4e2db14e45b1a86
|
||||
environments/settings/domain/pretty_url: 10a4b387b6df844245fc842be29527e3
|
||||
environments/settings/domain/project: e13002ec4570f3fcc2f050f5ce974294
|
||||
environments/settings/domain/survey_name: 169f24df0e2f42254c5458f32eb7bf00
|
||||
environments/settings/domain/title: 2c3a2951a31218e8e73f07cda69ed5ff
|
||||
environments/settings/enterprise/audit_logs: c7efb33d09676938d07651774b067bf6
|
||||
environments/settings/enterprise/coming_soon: ee2b0671e00972773210c5be5a9ccb89
|
||||
environments/settings/enterprise/contacts_and_segments: 5795d9e89c0c10e1ddbbc6fc65b8d90f
|
||||
@@ -1722,6 +1732,15 @@ checksums:
|
||||
environments/surveys/share/personal_links/upgrade_prompt_description: 09f394f5adf2d5a47352ceb80b43598b
|
||||
environments/surveys/share/personal_links/upgrade_prompt_title: 8068b8c5b27268ec4d804a9c7d153a8f
|
||||
environments/surveys/share/personal_links/work_with_segments: 45030e61077c88ae60f011b40566d5d1
|
||||
environments/surveys/share/pretty_url/description: 67173cfc7010ab044428a22f5a4bdaa2
|
||||
environments/surveys/share/pretty_url/remove_description: 7fb0afcb17447101ed4aa6aaffc6e136
|
||||
environments/surveys/share/pretty_url/remove_success: a4a08113d3d4c53889da7c12a0674473
|
||||
environments/surveys/share/pretty_url/save_success: e73e6e0adc93e0db723c765fbef7d82a
|
||||
environments/surveys/share/pretty_url/slug_help: 3c81237bfb50f41b9e0525e9a2754b9d
|
||||
environments/surveys/share/pretty_url/slug_label: b1cc25b6bf5032c75420aa862362808c
|
||||
environments/surveys/share/pretty_url/slug_placeholder: e9266ae26c4993ef285a5b1a927e52a5
|
||||
environments/surveys/share/pretty_url/slug_required: 9f1c28ff24a3507588c7c3710ae0b073
|
||||
environments/surveys/share/pretty_url/title: 10a4b387b6df844245fc842be29527e3
|
||||
environments/surveys/share/send_email/copy_embed_code: 04833baca1ed64dee8fc230462965d2d
|
||||
environments/surveys/share/send_email/description: 875935d1d039fc5dd2de7b2a25008796
|
||||
environments/surveys/share/send_email/email_preview_tab: 84458b39d3fd3bebb92de0e81d41b7da
|
||||
|
||||
@@ -267,6 +267,7 @@ export const mockSyncSurveyOutput: SurveyMock = {
|
||||
variables: [],
|
||||
showLanguageSwitch: null,
|
||||
metadata: {},
|
||||
slug: null,
|
||||
};
|
||||
|
||||
export const mockSurveyOutput: SurveyMock = {
|
||||
@@ -290,6 +291,7 @@ export const mockSurveyOutput: SurveyMock = {
|
||||
variables: [],
|
||||
showLanguageSwitch: null,
|
||||
...baseSurveyProperties,
|
||||
slug: null,
|
||||
};
|
||||
|
||||
export const createSurveyInput: TSurveyCreateInput = {
|
||||
@@ -319,6 +321,7 @@ export const updateSurveyInput: TSurvey = {
|
||||
followUps: [],
|
||||
...baseSurveyProperties,
|
||||
...commonMockProperties,
|
||||
slug: null,
|
||||
};
|
||||
|
||||
export const mockTransformedSurveyOutput = {
|
||||
|
||||
@@ -108,6 +108,7 @@ export const selectSurvey = {
|
||||
},
|
||||
},
|
||||
followUps: true,
|
||||
slug: true,
|
||||
} satisfies Prisma.SurveySelect;
|
||||
|
||||
export const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => {
|
||||
|
||||
@@ -84,9 +84,12 @@ describe("Helper Utilities", () => {
|
||||
validationErrors: {
|
||||
name: { _errors: ["Name is required"] },
|
||||
email: { _errors: ["Email is invalid"] },
|
||||
password: { _errors: ["is too short"] },
|
||||
},
|
||||
};
|
||||
expect(getFormattedErrorMessage(result)).toBe("nameName is required\nemailEmail is invalid");
|
||||
expect(getFormattedErrorMessage(result)).toBe(
|
||||
"Name is required\nEmail is invalid\npassword: is too short"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns empty string for undefined errors", () => {
|
||||
|
||||
@@ -27,7 +27,12 @@ export const getFormattedErrorMessage = (result): string => {
|
||||
message = Object.keys(errors || {})
|
||||
.map((key) => {
|
||||
if (key === "_errors") return errors[key].join(", ");
|
||||
return `${key ? `${key}` : ""}${errors?.[key]?._errors?.join(", ")}`;
|
||||
const fieldError = errors?.[key]?._errors?.join(", ");
|
||||
if (key && fieldError?.toLowerCase().startsWith(key.toLowerCase())) {
|
||||
return fieldError;
|
||||
}
|
||||
const keyPrefix = key ? `${key}: ` : "";
|
||||
return `${keyPrefix}${fieldError}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Löschen",
|
||||
"description": "Beschreibung",
|
||||
"dev_env": "Entwicklungsumgebung",
|
||||
"development": "Entwicklung",
|
||||
"development_environment_banner": "Du bist in einer Entwicklungsumgebung. Richte sie ein, um Umfragen, Aktionen und Attribute zu testen.",
|
||||
"disable": "Deaktivieren",
|
||||
"disallow": "Nicht erlauben",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Entlassen",
|
||||
"docs": "Dokumentation",
|
||||
"documentation": "Dokumentation",
|
||||
"domain": "Domain",
|
||||
"download": "Herunterladen",
|
||||
"draft": "Entwurf",
|
||||
"duplicate": "Duplikat",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Abschluss-Karte",
|
||||
"enter_url": "URL eingeben",
|
||||
"enterprise_license": "Enterprise Lizenz",
|
||||
"environment": "Umgebung",
|
||||
"environment_not_found": "Umgebung nicht gefunden",
|
||||
"environment_notice": "Du befindest dich derzeit in der {environment}-Umgebung.",
|
||||
"error": "Fehler",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Umfragevorschau",
|
||||
"privacy": "Datenschutz",
|
||||
"product_manager": "Produktmanager",
|
||||
"production": "Produktion",
|
||||
"profile": "Profil",
|
||||
"profile_id": "Profil-ID",
|
||||
"progress": "Fortschritt",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "Betriebszeit SLA (99%)",
|
||||
"website_surveys": "Website-Umfragen"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Übersicht aller Umfragen mit Pretty URLs in Ihrer Organisation",
|
||||
"no_pretty_urls": "Noch keine Umfragen mit Pretty URLs konfiguriert.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Projekt",
|
||||
"survey_name": "Umfragename",
|
||||
"title": "Pretty URLs"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Audit Logs",
|
||||
"coming_soon": "Kommt bald",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Verwende persönliche Links mit einem höheren Plan",
|
||||
"work_with_segments": "Persönliche Links funktionieren mit Segmenten."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Erstellen Sie eine individuelle, einprägsame URL für Ihre Umfrage",
|
||||
"remove_description": "Ihre Umfrage bleibt über die ursprüngliche URL weiterhin erreichbar.",
|
||||
"remove_success": "Pretty URL erfolgreich entfernt",
|
||||
"save_success": "Pretty URL erfolgreich gespeichert",
|
||||
"slug_help": "Verwenden Sie nur Kleinbuchstaben, Zahlen und Bindestriche.",
|
||||
"slug_label": "Individueller Slug",
|
||||
"slug_placeholder": "kunden-feedback",
|
||||
"slug_required": "Bitte geben Sie einen gültigen Slug ein",
|
||||
"title": "Pretty URL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Einbettungscode kopieren",
|
||||
"description": "Binden Sie Ihre Umfrage in eine E-Mail ein, um Antworten von Ihrem Publikum zu erhalten.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Delete",
|
||||
"description": "Description",
|
||||
"dev_env": "Dev Environment",
|
||||
"development": "Development",
|
||||
"development_environment_banner": "You're in a development environment. Set it up to test surveys, actions and attributes.",
|
||||
"disable": "Disable",
|
||||
"disallow": "Don't allow",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Dismissed",
|
||||
"docs": "Documentation",
|
||||
"documentation": "Documentation",
|
||||
"domain": "Domain",
|
||||
"download": "Download",
|
||||
"draft": "Draft",
|
||||
"duplicate": "Duplicate",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Ending card",
|
||||
"enter_url": "Enter URL",
|
||||
"enterprise_license": "Enterprise License",
|
||||
"environment": "Environment",
|
||||
"environment_not_found": "Environment not found",
|
||||
"environment_notice": "You're currently in the {environment} environment.",
|
||||
"error": "Error",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Preview Survey",
|
||||
"privacy": "Privacy Policy",
|
||||
"product_manager": "Product Manager",
|
||||
"production": "Production",
|
||||
"profile": "Profile",
|
||||
"profile_id": "Profile ID",
|
||||
"progress": "Progress",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "Uptime SLA (99%)",
|
||||
"website_surveys": "Website Surveys"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Overview of all surveys using Pretty URLs across your organization",
|
||||
"no_pretty_urls": "No surveys with Pretty URLs configured yet.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Project",
|
||||
"survey_name": "Survey Name",
|
||||
"title": "Pretty URLs"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Audit Logs",
|
||||
"coming_soon": "Coming soon",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Use personal links with a higher plan",
|
||||
"work_with_segments": "Personal links work with segments."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Create a custom, memorable URL for your survey",
|
||||
"remove_description": "Your survey will still be accessible via its original URL.",
|
||||
"remove_success": "Pretty URL removed successfully",
|
||||
"save_success": "Pretty URL saved successfully",
|
||||
"slug_help": "Use lowercase letters, numbers, and hyphens only.",
|
||||
"slug_label": "Custom Slug",
|
||||
"slug_placeholder": "customer-feedback",
|
||||
"slug_required": "Please enter a valid slug",
|
||||
"title": "Pretty URL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Copy embed code",
|
||||
"description": "Embed your survey in an email to get responses from your audience.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Eliminar",
|
||||
"description": "Descripción",
|
||||
"dev_env": "Entorno de desarrollo",
|
||||
"development": "Desarrollo",
|
||||
"development_environment_banner": "Estás en un entorno de desarrollo. Configúralo para probar encuestas, acciones y atributos.",
|
||||
"disable": "Desactivar",
|
||||
"disallow": "No permitir",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Descartado",
|
||||
"docs": "Documentación",
|
||||
"documentation": "Documentación",
|
||||
"domain": "Dominio",
|
||||
"download": "Descargar",
|
||||
"draft": "Borrador",
|
||||
"duplicate": "Duplicar",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Tarjeta final",
|
||||
"enter_url": "Introducir URL",
|
||||
"enterprise_license": "Licencia empresarial",
|
||||
"environment": "Entorno",
|
||||
"environment_not_found": "Entorno no encontrado",
|
||||
"environment_notice": "Actualmente estás en el entorno {environment}.",
|
||||
"error": "Error",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Vista previa de la encuesta",
|
||||
"privacy": "Política de privacidad",
|
||||
"product_manager": "Gestor de producto",
|
||||
"production": "Producción",
|
||||
"profile": "Perfil",
|
||||
"profile_id": "ID de perfil",
|
||||
"progress": "Progreso",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "Acuerdo de nivel de servicio de tiempo de actividad (99 %)",
|
||||
"website_surveys": "Encuestas de sitio web"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Resumen de todas las encuestas que utilizan URL bonitas en tu organización",
|
||||
"no_pretty_urls": "Aún no hay encuestas con URL bonitas configuradas.",
|
||||
"pretty_url": "URL bonita",
|
||||
"project": "Proyecto",
|
||||
"survey_name": "Nombre de la encuesta",
|
||||
"title": "URL bonitas"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Registros de auditoría",
|
||||
"coming_soon": "Próximamente",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Usa enlaces personales con un plan superior",
|
||||
"work_with_segments": "Los enlaces personales funcionan con segmentos."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Crea una URL personalizada y memorable para tu encuesta",
|
||||
"remove_description": "Tu encuesta seguirá siendo accesible a través de su URL original.",
|
||||
"remove_success": "URL bonita eliminada correctamente",
|
||||
"save_success": "URL bonita guardada correctamente",
|
||||
"slug_help": "Usa solo letras minúsculas, números y guiones.",
|
||||
"slug_label": "Slug personalizado",
|
||||
"slug_placeholder": "comentarios-clientes",
|
||||
"slug_required": "Por favor, introduce un slug válido",
|
||||
"title": "URL bonita"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Copiar código de inserción",
|
||||
"description": "Inserta tu encuesta en un correo electrónico para obtener respuestas de tu audiencia.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Supprimer",
|
||||
"description": "Description",
|
||||
"dev_env": "Environnement de développement",
|
||||
"development": "Développement",
|
||||
"development_environment_banner": "Vous êtes dans un environnement de développement. Configurez-le pour tester des enquêtes, des actions et des attributs.",
|
||||
"disable": "Désactiver",
|
||||
"disallow": "Ne pas autoriser",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Rejeté",
|
||||
"docs": "Documentation",
|
||||
"documentation": "Documentation",
|
||||
"domain": "Domaine",
|
||||
"download": "Télécharger",
|
||||
"draft": "Brouillon",
|
||||
"duplicate": "Dupliquer",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Carte de fin",
|
||||
"enter_url": "Saisir l'URL",
|
||||
"enterprise_license": "Licence d'entreprise",
|
||||
"environment": "Environnement",
|
||||
"environment_not_found": "Environnement non trouvé",
|
||||
"environment_notice": "Vous êtes actuellement dans l'environnement {environment}.",
|
||||
"error": "Erreur",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Aperçu de l'enquête",
|
||||
"privacy": "Politique de confidentialité",
|
||||
"product_manager": "Chef de produit",
|
||||
"production": "Production",
|
||||
"profile": "Profil",
|
||||
"profile_id": "Identifiant de profil",
|
||||
"progress": "Progression",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "Disponibilité de 99 %",
|
||||
"website_surveys": "Sondages de site web"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Aperçu de toutes les enquêtes utilisant des URL personnalisées dans votre organisation",
|
||||
"no_pretty_urls": "Aucune enquête avec URL personnalisée configurée pour le moment.",
|
||||
"pretty_url": "URL personnalisée",
|
||||
"project": "Projet",
|
||||
"survey_name": "Nom de l'enquête",
|
||||
"title": "URL personnalisées"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Journaux d'audit",
|
||||
"coming_soon": "À venir bientôt",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Utilisez des liens personnels avec un plan supérieur",
|
||||
"work_with_segments": "Les liens personnels fonctionnent avec les segments."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Créez une URL personnalisée et mémorable pour votre enquête",
|
||||
"remove_description": "Votre enquête restera accessible via son URL d'origine.",
|
||||
"remove_success": "URL personnalisée supprimée avec succès",
|
||||
"save_success": "URL personnalisée enregistrée avec succès",
|
||||
"slug_help": "Utilisez uniquement des lettres minuscules, des chiffres et des tirets.",
|
||||
"slug_label": "Slug personnalisé",
|
||||
"slug_placeholder": "retour-client",
|
||||
"slug_required": "Veuillez saisir un slug valide",
|
||||
"title": "URL personnalisée"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Copier le code d'intégration",
|
||||
"description": "Intégrez votre sondage dans un email pour obtenir des réponses de votre audience.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "削除",
|
||||
"description": "説明",
|
||||
"dev_env": "開発環境",
|
||||
"development": "開発",
|
||||
"development_environment_banner": "あなたは開発環境にいます。フォーム、アクション、属性をテストするように設定してください。",
|
||||
"disable": "無効にする",
|
||||
"disallow": "許可しない",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "非表示",
|
||||
"docs": "ドキュメント",
|
||||
"documentation": "ドキュメント",
|
||||
"domain": "ドメイン",
|
||||
"download": "ダウンロード",
|
||||
"draft": "下書き",
|
||||
"duplicate": "複製",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "終了カード",
|
||||
"enter_url": "URLを入力",
|
||||
"enterprise_license": "エンタープライズライセンス",
|
||||
"environment": "環境",
|
||||
"environment_not_found": "環境が見つかりません",
|
||||
"environment_notice": "現在、{environment} 環境にいます。",
|
||||
"error": "エラー",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "フォームをプレビュー",
|
||||
"privacy": "プライバシーポリシー",
|
||||
"product_manager": "プロダクトマネージャー",
|
||||
"production": "本番",
|
||||
"profile": "プロフィール",
|
||||
"profile_id": "プロフィールID",
|
||||
"progress": "進捗",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "稼働率SLA (99%)",
|
||||
"website_surveys": "ウェブサイトフォーム"
|
||||
},
|
||||
"domain": {
|
||||
"description": "組織全体でカスタムURLを使用しているすべてのフォームの概要",
|
||||
"no_pretty_urls": "カスタムURLが設定されたフォームはまだありません。",
|
||||
"pretty_url": "カスタムURL",
|
||||
"project": "プロジェクト",
|
||||
"survey_name": "フォーム名",
|
||||
"title": "カスタムURL"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "監査ログ",
|
||||
"coming_soon": "近日公開",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "上位プランで個人リンクを使用",
|
||||
"work_with_segments": "個人リンクはセグメントで機能します。"
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "フォーム用のカスタムで覚えやすいURLを作成",
|
||||
"remove_description": "フォームは元のURLから引き続きアクセス可能です。",
|
||||
"remove_success": "カスタムURLを削除しました",
|
||||
"save_success": "カスタムURLを保存しました",
|
||||
"slug_help": "小文字のアルファベット、数字、ハイフンのみ使用できます。",
|
||||
"slug_label": "カスタムスラッグ",
|
||||
"slug_placeholder": "customer-feedback",
|
||||
"slug_required": "有効なスラッグを入力してください",
|
||||
"title": "カスタムURL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "埋め込みコードをコピー",
|
||||
"description": "メールにフォームを埋め込んで、オーディエンスから回答を得ます。",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Verwijderen",
|
||||
"description": "Beschrijving",
|
||||
"dev_env": "Ontwikkelomgeving",
|
||||
"development": "Ontwikkeling",
|
||||
"development_environment_banner": "Je bevindt je in een ontwikkelomgeving. Stel het in om enquêtes, acties en attributen te testen.",
|
||||
"disable": "Uitzetten",
|
||||
"disallow": "Niet toestaan",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Afgewezen",
|
||||
"docs": "Documentatie",
|
||||
"documentation": "Documentatie",
|
||||
"domain": "Domein",
|
||||
"download": "Downloaden",
|
||||
"draft": "Voorlopige versie",
|
||||
"duplicate": "Duplicaat",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Einde kaart",
|
||||
"enter_url": "URL invoeren",
|
||||
"enterprise_license": "Enterprise-licentie",
|
||||
"environment": "Omgeving",
|
||||
"environment_not_found": "Omgeving niet gevonden",
|
||||
"environment_notice": "U bevindt zich momenteel in de {environment}-omgeving.",
|
||||
"error": "Fout",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Voorbeeld van enquête",
|
||||
"privacy": "Privacybeleid",
|
||||
"product_manager": "Productmanager",
|
||||
"production": "Productie",
|
||||
"profile": "Profiel",
|
||||
"profile_id": "Profiel-ID",
|
||||
"progress": "Voortgang",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "Uptime-SLA (99%)",
|
||||
"website_surveys": "Website-enquêtes"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Overzicht van alle enquêtes die Pretty URL's gebruiken binnen je organisatie",
|
||||
"no_pretty_urls": "Nog geen enquêtes met Pretty URL's geconfigureerd.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Project",
|
||||
"survey_name": "Enquêtenaam",
|
||||
"title": "Pretty URL's"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Auditlogboeken",
|
||||
"coming_soon": "Binnenkort beschikbaar",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Gebruik persoonlijke links met een hoger plan",
|
||||
"work_with_segments": "Persoonlijke links werken met segmenten."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Maak een aangepaste, memorabele URL voor je enquête",
|
||||
"remove_description": "Je enquête blijft toegankelijk via de oorspronkelijke URL.",
|
||||
"remove_success": "Pretty URL succesvol verwijderd",
|
||||
"save_success": "Pretty URL succesvol opgeslagen",
|
||||
"slug_help": "Gebruik alleen kleine letters, cijfers en koppeltekens.",
|
||||
"slug_label": "Aangepaste slug",
|
||||
"slug_placeholder": "klantfeedback",
|
||||
"slug_required": "Voer een geldige slug in",
|
||||
"title": "Pretty URL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Kopieer de insluitcode",
|
||||
"description": "Sluit uw enquête in een e-mail in om reacties van uw publiek te krijgen.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Apagar",
|
||||
"description": "Descrição",
|
||||
"dev_env": "Ambiente de Desenvolvimento",
|
||||
"development": "Desenvolvimento",
|
||||
"development_environment_banner": "Você está em um ambiente de desenvolvimento. Configure-o para testar pesquisas, ações e atributos.",
|
||||
"disable": "desativar",
|
||||
"disallow": "Não permita",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Dispensado",
|
||||
"docs": "Documentação",
|
||||
"documentation": "Documentação",
|
||||
"domain": "Domínio",
|
||||
"download": "baixar",
|
||||
"draft": "Rascunho",
|
||||
"duplicate": "Duplicar",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Cartão de encerramento",
|
||||
"enter_url": "Inserir URL",
|
||||
"enterprise_license": "Licença Empresarial",
|
||||
"environment": "Ambiente",
|
||||
"environment_not_found": "Ambiente não encontrado",
|
||||
"environment_notice": "Você está atualmente no ambiente {environment}.",
|
||||
"error": "Erro",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Prévia da Pesquisa",
|
||||
"privacy": "Política de Privacidade",
|
||||
"product_manager": "Gerente de Produto",
|
||||
"production": "Produção",
|
||||
"profile": "Perfil",
|
||||
"profile_id": "ID de Perfil",
|
||||
"progress": "Progresso",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "Tempo de atividade SLA (99%)",
|
||||
"website_surveys": "Pesquisas de Site"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Visão geral de todas as pesquisas usando URLs amigáveis em sua organização",
|
||||
"no_pretty_urls": "Nenhuma pesquisa com URLs amigáveis configuradas ainda.",
|
||||
"pretty_url": "URL amigável",
|
||||
"project": "Projeto",
|
||||
"survey_name": "Nome da Pesquisa",
|
||||
"title": "URLs amigáveis"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Registros de Auditoria",
|
||||
"coming_soon": "Em breve",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Use links pessoais com um plano superior",
|
||||
"work_with_segments": "Links pessoais funcionam com segmentos."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Crie uma URL personalizada e memorável para sua pesquisa",
|
||||
"remove_description": "Sua pesquisa ainda estará acessível através da URL original.",
|
||||
"remove_success": "URL amigável removida com sucesso",
|
||||
"save_success": "URL amigável salva com sucesso",
|
||||
"slug_help": "Use apenas letras minúsculas, números e hífens.",
|
||||
"slug_label": "Slug personalizado",
|
||||
"slug_placeholder": "feedback-do-cliente",
|
||||
"slug_required": "Por favor, insira um slug válido",
|
||||
"title": "URL amigável"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Copiar código incorporado",
|
||||
"description": "Incorpore sua pesquisa em um e-mail para obter respostas do seu público.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Eliminar",
|
||||
"description": "Descrição",
|
||||
"dev_env": "Ambiente de Desenvolvimento",
|
||||
"development": "Desenvolvimento",
|
||||
"development_environment_banner": "Está num ambiente de desenvolvimento. Configure-o para testar inquéritos, ações e atributos.",
|
||||
"disable": "Desativar",
|
||||
"disallow": "Não permitir",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Dispensado",
|
||||
"docs": "Documentação",
|
||||
"documentation": "Documentação",
|
||||
"domain": "Domínio",
|
||||
"download": "Transferir",
|
||||
"draft": "Rascunho",
|
||||
"duplicate": "Duplicar",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Cartão de encerramento",
|
||||
"enter_url": "Introduzir URL",
|
||||
"enterprise_license": "Licença Enterprise",
|
||||
"environment": "Ambiente",
|
||||
"environment_not_found": "Ambiente não encontrado",
|
||||
"environment_notice": "Está atualmente no ambiente {environment}.",
|
||||
"error": "Erro",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Pré-visualização do inquérito",
|
||||
"privacy": "Política de Privacidade",
|
||||
"product_manager": "Gestor de Produto",
|
||||
"production": "Produção",
|
||||
"profile": "Perfil",
|
||||
"profile_id": "ID do Perfil",
|
||||
"progress": "Progresso",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "SLA de Tempo de Atividade (99%)",
|
||||
"website_surveys": "Inquéritos (site)"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Visão geral de todos os inquéritos que utilizam URLs amigáveis na sua organização",
|
||||
"no_pretty_urls": "Ainda não existem inquéritos com URLs amigáveis configurados.",
|
||||
"pretty_url": "URL amigável",
|
||||
"project": "Projeto",
|
||||
"survey_name": "Nome do inquérito",
|
||||
"title": "URLs amigáveis"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Registos de Auditoria",
|
||||
"coming_soon": "Em breve",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Utilize links pessoais com um plano superior",
|
||||
"work_with_segments": "Os links pessoais funcionam com segmentos."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Crie um URL personalizado e memorável para o seu inquérito",
|
||||
"remove_description": "O seu inquérito continuará acessível através do URL original.",
|
||||
"remove_success": "URL amigável removido com sucesso",
|
||||
"save_success": "URL amigável guardado com sucesso",
|
||||
"slug_help": "Use apenas letras minúsculas, números e hífenes.",
|
||||
"slug_label": "Slug personalizado",
|
||||
"slug_placeholder": "feedback-cliente",
|
||||
"slug_required": "Por favor, introduza um slug válido",
|
||||
"title": "URL amigável"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Copiar código de incorporação",
|
||||
"description": "Incorpora o teu inquérito num email para obter respostas do teu público.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Șterge",
|
||||
"description": "Descriere",
|
||||
"dev_env": "Mediu de dezvoltare",
|
||||
"development": "Dezvoltare",
|
||||
"development_environment_banner": "Ești într-un mediu de dezvoltare. Configurează-l pentru a testa sondaje, acțiuni și atribute.",
|
||||
"disable": "Dezactivează",
|
||||
"disallow": "Nu permite",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Respins",
|
||||
"docs": "Documentație",
|
||||
"documentation": "Documentație",
|
||||
"domain": "Domeniu",
|
||||
"download": "Descărcare",
|
||||
"draft": "Schiță",
|
||||
"duplicate": "Duplicități",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Cardul de finalizare",
|
||||
"enter_url": "Introduceți URL-ul",
|
||||
"enterprise_license": "Licență Întreprindere",
|
||||
"environment": "Mediu",
|
||||
"environment_not_found": "Mediul nu a fost găsit",
|
||||
"environment_notice": "Te afli în prezent în mediul {environment}",
|
||||
"error": "Eroare",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Previzualizare Chestionar",
|
||||
"privacy": "Politica de Confidențialitate",
|
||||
"product_manager": "Manager de Produs",
|
||||
"production": "Producție",
|
||||
"profile": "Profil",
|
||||
"profile_id": "ID Profil",
|
||||
"progress": "Progres",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "Disponibilitate SLA (99%)",
|
||||
"website_surveys": "Sondaje ale site-ului"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Prezentare generală a tuturor chestionarelor care folosesc Pretty URLs în organizația dvs.",
|
||||
"no_pretty_urls": "Nu există încă chestionare cu Pretty URLs configurate.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Proiect",
|
||||
"survey_name": "Nume chestionar",
|
||||
"title": "URL-uri frumoase"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Jurnale de audit",
|
||||
"coming_soon": "În curând",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Folosește linkuri personale cu un plan superior",
|
||||
"work_with_segments": "Linkuri personale funcționează cu segmente."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Creează o adresă URL personalizată și ușor de reținut pentru chestionarul tău",
|
||||
"remove_description": "Chestionarul va fi în continuare accesibil prin URL-ul original.",
|
||||
"remove_success": "Pretty URL a fost eliminat cu succes",
|
||||
"save_success": "Pretty URL a fost salvat cu succes",
|
||||
"slug_help": "Folosiți doar litere mici, cifre și cratime.",
|
||||
"slug_label": "Slug personalizat",
|
||||
"slug_placeholder": "customer-feedback",
|
||||
"slug_required": "Vă rugăm să introduceți un slug valid",
|
||||
"title": "Pretty URL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Copiază codul de inserare",
|
||||
"description": "Inserați sondajul dvs. într-un e-mail pentru a obține răspunsuri de la audiența dvs.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Удалить",
|
||||
"description": "Описание",
|
||||
"dev_env": "Dev Environment",
|
||||
"development": "Разработка",
|
||||
"development_environment_banner": "Вы находитесь в среде разработки. Настройте её для тестирования опросов, действий и атрибутов.",
|
||||
"disable": "Отключить",
|
||||
"disallow": "Не разрешать",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Отклонено",
|
||||
"docs": "Документация",
|
||||
"documentation": "Документация",
|
||||
"domain": "Домен",
|
||||
"download": "Скачать",
|
||||
"draft": "Черновик",
|
||||
"duplicate": "Дублировать",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Завершающая карточка",
|
||||
"enter_url": "Введите URL",
|
||||
"enterprise_license": "Корпоративная лицензия",
|
||||
"environment": "Окружение",
|
||||
"environment_not_found": "Среда не найдена",
|
||||
"environment_notice": "В данный момент вы находитесь в среде {environment}.",
|
||||
"error": "Ошибка",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Предпросмотр опроса",
|
||||
"privacy": "Политика конфиденциальности",
|
||||
"product_manager": "Менеджер продукта",
|
||||
"production": "Продакшн",
|
||||
"profile": "Профиль",
|
||||
"profile_id": "ID профиля",
|
||||
"progress": "Прогресс",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "SLA по времени безотказной работы (99%)",
|
||||
"website_surveys": "Опросы на сайте"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Обзор всех опросов с Pretty URL в вашей организации",
|
||||
"no_pretty_urls": "Пока не настроено ни одного опроса с Pretty URL.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Проект",
|
||||
"survey_name": "Название опроса",
|
||||
"title": "Pretty URL"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Журналы аудита",
|
||||
"coming_soon": "Скоро будет",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Используйте персональные ссылки на более высоком тарифе",
|
||||
"work_with_segments": "Персональные ссылки работают с сегментами."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Создайте индивидуальный, запоминающийся URL для вашего опроса",
|
||||
"remove_description": "Ваш опрос по-прежнему будет доступен по исходному URL.",
|
||||
"remove_success": "Pretty URL успешно удалён",
|
||||
"save_success": "Pretty URL успешно сохранён",
|
||||
"slug_help": "Используйте только строчные буквы, цифры и дефисы.",
|
||||
"slug_label": "Пользовательский слаг",
|
||||
"slug_placeholder": "customer-feedback",
|
||||
"slug_required": "Пожалуйста, введите корректный слаг",
|
||||
"title": "Pretty URL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Скопировать код для вставки",
|
||||
"description": "Вставьте ваш опрос в электронное письмо, чтобы получить ответы от вашей аудитории.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "Ta bort",
|
||||
"description": "Beskrivning",
|
||||
"dev_env": "Utvecklingsmiljö",
|
||||
"development": "Utveckling",
|
||||
"development_environment_banner": "Du är i en utvecklingsmiljö. Konfigurera den för att testa enkäter, åtgärder och attribut.",
|
||||
"disable": "Inaktivera",
|
||||
"disallow": "Tillåt inte",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "Avvisad",
|
||||
"docs": "Dokumentation",
|
||||
"documentation": "Dokumentation",
|
||||
"domain": "Domän",
|
||||
"download": "Ladda ner",
|
||||
"draft": "Utkast",
|
||||
"duplicate": "Duplicera",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "Avslutningskort",
|
||||
"enter_url": "Ange URL",
|
||||
"enterprise_license": "Företagslicens",
|
||||
"environment": "Miljö",
|
||||
"environment_not_found": "Miljö hittades inte",
|
||||
"environment_notice": "Du är för närvarande i {environment}-miljön.",
|
||||
"error": "Fel",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "Förhandsgranska enkät",
|
||||
"privacy": "Integritetspolicy",
|
||||
"product_manager": "Produktchef",
|
||||
"production": "Produktion",
|
||||
"profile": "Profil",
|
||||
"profile_id": "Profil-ID",
|
||||
"progress": "Framsteg",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "Drifttids-SLA (99%)",
|
||||
"website_surveys": "Webbplatsenkäter"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Översikt över alla enkäter som använder Pretty URLs i din organisation",
|
||||
"no_pretty_urls": "Inga enkäter med Pretty URLs har konfigurerats ännu.",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "Projekt",
|
||||
"survey_name": "Enkätnamn",
|
||||
"title": "Pretty URL:er"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "Granskningsloggar",
|
||||
"coming_soon": "Kommer snart",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "Använd personliga länkar med en högre plan",
|
||||
"work_with_segments": "Personliga länkar fungerar med segment."
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "Skapa en anpassad, minnesvärd URL för din enkät",
|
||||
"remove_description": "Din enkät kommer fortfarande att vara tillgänglig via sin ursprungliga URL.",
|
||||
"remove_success": "Pretty URL har tagits bort",
|
||||
"save_success": "Pretty URL har sparats",
|
||||
"slug_help": "Använd endast små bokstäver, siffror och bindestreck.",
|
||||
"slug_label": "Anpassad slug",
|
||||
"slug_placeholder": "customer-feedback",
|
||||
"slug_required": "Ange en giltig slug",
|
||||
"title": "Pretty URL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "Kopiera inbäddningskod",
|
||||
"description": "Bädda in din enkät i ett e-postmeddelande för att få svar från din målgrupp.",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "删除",
|
||||
"description": "描述",
|
||||
"dev_env": "开发 环境",
|
||||
"development": "开发环境",
|
||||
"development_environment_banner": "您 在 开发 环境 中。 设置 以 测试 调查 , 操作 和 属性。",
|
||||
"disable": "禁用",
|
||||
"disallow": "不允许",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "忽略",
|
||||
"docs": "文档",
|
||||
"documentation": "文档",
|
||||
"domain": "域名",
|
||||
"download": "下载",
|
||||
"draft": "草稿",
|
||||
"duplicate": "复制",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "结尾卡片",
|
||||
"enter_url": "输入 URL",
|
||||
"enterprise_license": "企业 许可证",
|
||||
"environment": "环境",
|
||||
"environment_not_found": "环境 未找到",
|
||||
"environment_notice": "你 目前 位于 {environment} 环境。",
|
||||
"error": "错误",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "预览 Survey",
|
||||
"privacy": "隐私政策",
|
||||
"product_manager": "产品经理",
|
||||
"production": "生产环境",
|
||||
"profile": "资料",
|
||||
"profile_id": "资料 ID",
|
||||
"progress": "进度",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "正常运行时间 SLA (99%)",
|
||||
"website_surveys": "网站 调查"
|
||||
},
|
||||
"domain": {
|
||||
"description": "概览组织内所有使用美化 URL 的调查问卷",
|
||||
"no_pretty_urls": "尚未配置任何美化 URL 的调查问卷。",
|
||||
"pretty_url": "美化 URL",
|
||||
"project": "项目",
|
||||
"survey_name": "调查名称",
|
||||
"title": "美化 URL"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "审计日志",
|
||||
"coming_soon": "即将推出",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "在更高的计划中使用 个人 链接",
|
||||
"work_with_segments": "个人链接与片段一起工作。"
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "为您的调查创建一个自定义且易记的 URL",
|
||||
"remove_description": "您的调查仍可通过原始 URL 访问。",
|
||||
"remove_success": "美化 URL 移除成功",
|
||||
"save_success": "美化 URL 保存成功",
|
||||
"slug_help": "仅可使用小写字母、数字和连字符。",
|
||||
"slug_label": "自定义短链",
|
||||
"slug_placeholder": "customer-feedback",
|
||||
"slug_required": "请输入有效的短链",
|
||||
"title": "美化 URL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "复制 嵌入代码",
|
||||
"description": "将你的调查嵌入到电子邮件中,以获取你所在受众的回复。",
|
||||
|
||||
@@ -187,6 +187,7 @@
|
||||
"delete": "刪除",
|
||||
"description": "描述",
|
||||
"dev_env": "開發環境",
|
||||
"development": "開發",
|
||||
"development_environment_banner": "您正在開發環境中。設定它以測試問卷、操作和屬性。",
|
||||
"disable": "停用",
|
||||
"disallow": "不允許",
|
||||
@@ -194,6 +195,7 @@
|
||||
"dismissed": "已關閉",
|
||||
"docs": "文件",
|
||||
"documentation": "文件",
|
||||
"domain": "網域",
|
||||
"download": "下載",
|
||||
"draft": "草稿",
|
||||
"duplicate": "複製",
|
||||
@@ -203,6 +205,7 @@
|
||||
"ending_card": "結尾卡片",
|
||||
"enter_url": "輸入 URL",
|
||||
"enterprise_license": "企業授權",
|
||||
"environment": "環境",
|
||||
"environment_not_found": "找不到環境",
|
||||
"environment_notice": "您目前在 '{'environment'}' 環境中。",
|
||||
"error": "錯誤",
|
||||
@@ -321,6 +324,7 @@
|
||||
"preview_survey": "預覽問卷",
|
||||
"privacy": "隱私權政策",
|
||||
"product_manager": "產品經理",
|
||||
"production": "正式環境",
|
||||
"profile": "個人資料",
|
||||
"profile_id": "個人資料 ID",
|
||||
"progress": "進度",
|
||||
@@ -1026,6 +1030,14 @@
|
||||
"uptime_sla_99": "正常運作時間 SLA (99%)",
|
||||
"website_surveys": "網站問卷"
|
||||
},
|
||||
"domain": {
|
||||
"description": "檢視貴組織內所有使用 Pretty URL 的問卷",
|
||||
"no_pretty_urls": "尚未設定任何使用 Pretty URL 的問卷。",
|
||||
"pretty_url": "Pretty URL",
|
||||
"project": "專案",
|
||||
"survey_name": "問卷名稱",
|
||||
"title": "美化網址"
|
||||
},
|
||||
"enterprise": {
|
||||
"audit_logs": "稽核記錄",
|
||||
"coming_soon": "即將推出",
|
||||
@@ -1825,6 +1837,17 @@
|
||||
"upgrade_prompt_title": "使用 個人 連結 與 更高 的 計劃",
|
||||
"work_with_segments": "個人 連結 可 與 分段 一起 使用"
|
||||
},
|
||||
"pretty_url": {
|
||||
"description": "為您的問卷建立自訂且易記的網址",
|
||||
"remove_description": "您的問卷仍可透過原始網址存取。",
|
||||
"remove_success": "Pretty URL 已成功移除",
|
||||
"save_success": "Pretty URL 已成功儲存",
|
||||
"slug_help": "僅可使用小寫字母、數字和連字號。",
|
||||
"slug_label": "自訂網址代稱",
|
||||
"slug_placeholder": "customer-feedback",
|
||||
"slug_required": "請輸入有效的代稱",
|
||||
"title": "Pretty URL"
|
||||
},
|
||||
"send_email": {
|
||||
"copy_embed_code": "複製嵌入程式碼",
|
||||
"description": "將 你的 調查 嵌入 在 電子郵件 中 以 獲得 觀眾 的 回應。",
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MembersView } from "@/modules/organization/settings/teams/components/me
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
|
||||
export const TeamsPage = async (props) => {
|
||||
export const TeamsPage = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
|
||||
160
apps/web/modules/survey/lib/slug.test.ts
Normal file
160
apps/web/modules/survey/lib/slug.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError, ResourceNotFoundError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import { getSurveyBySlug, getSurveysWithSlugsByOrganizationId, updateSurveySlug } from "./slug";
|
||||
|
||||
// Mock prisma
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
survey: {
|
||||
findUnique: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Slug Library Tests", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getSurveyBySlug", () => {
|
||||
test("should return survey when found", async () => {
|
||||
const mockSurvey = { id: "survey_123", environmentId: "env_123", status: "inProgress" };
|
||||
vi.mocked(prisma.survey.findUnique).mockResolvedValueOnce(mockSurvey as any);
|
||||
|
||||
const result = await getSurveyBySlug("test-slug");
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data).toEqual(mockSurvey);
|
||||
}
|
||||
expect(prisma.survey.findUnique).toHaveBeenCalledWith({
|
||||
where: { slug: "test-slug" },
|
||||
select: { id: true, environmentId: true, status: true },
|
||||
});
|
||||
});
|
||||
|
||||
test("should return null when survey not found", async () => {
|
||||
vi.mocked(prisma.survey.findUnique).mockResolvedValueOnce(null);
|
||||
|
||||
const result = await getSurveyBySlug("nonexistent");
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test("should return DatabaseError when prisma fails", async () => {
|
||||
vi.mocked(prisma.survey.findUnique).mockRejectedValueOnce(new Error("DB error"));
|
||||
|
||||
const result = await getSurveyBySlug("error-slug");
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toBeInstanceOf(DatabaseError);
|
||||
expect(result.error.message).toBe("DB error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateSurveySlug", () => {
|
||||
test("should update slug successfully", async () => {
|
||||
const mockResult = { id: "survey_123", slug: "new-slug" };
|
||||
vi.mocked(prisma.survey.update).mockResolvedValueOnce(mockResult as any);
|
||||
|
||||
const result = await updateSurveySlug("survey_123", "new-slug");
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data).toEqual(mockResult);
|
||||
}
|
||||
expect(prisma.survey.update).toHaveBeenCalledWith({
|
||||
where: { id: "survey_123" },
|
||||
data: { slug: "new-slug" },
|
||||
select: { id: true, slug: true },
|
||||
});
|
||||
});
|
||||
|
||||
test("should return UniqueConstraintError on P2002", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Conflict", {
|
||||
code: "P2002",
|
||||
clientVersion: "0.0.1",
|
||||
});
|
||||
vi.mocked(prisma.survey.update).mockRejectedValueOnce(prismaError);
|
||||
|
||||
const result = await updateSurveySlug("survey_123", "taken-slug");
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toBeInstanceOf(UniqueConstraintError);
|
||||
}
|
||||
});
|
||||
|
||||
test("should return ResourceNotFoundError on P2025", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Not found", {
|
||||
code: "P2025",
|
||||
clientVersion: "0.0.1",
|
||||
});
|
||||
vi.mocked(prisma.survey.update).mockRejectedValueOnce(prismaError);
|
||||
|
||||
const result = await updateSurveySlug("nonexistent", "new-slug");
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toBeInstanceOf(ResourceNotFoundError);
|
||||
}
|
||||
});
|
||||
|
||||
test("should return DatabaseError on other prisma errors", async () => {
|
||||
vi.mocked(prisma.survey.update).mockRejectedValueOnce(new Error("Unexpected error"));
|
||||
|
||||
const result = await updateSurveySlug("survey_123", "new-slug");
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toBeInstanceOf(DatabaseError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSurveysWithSlugsByOrganizationId", () => {
|
||||
test("should return surveys with slugs", async () => {
|
||||
const mockSurveys = [
|
||||
{
|
||||
id: "survey_1",
|
||||
name: "Survey 1",
|
||||
slug: "slug-1",
|
||||
status: "inProgress",
|
||||
createdAt: new Date(),
|
||||
environment: {
|
||||
id: "env_1",
|
||||
type: "production",
|
||||
project: { id: "proj_1", name: "Project 1" },
|
||||
},
|
||||
},
|
||||
];
|
||||
vi.mocked(prisma.survey.findMany).mockResolvedValueOnce(mockSurveys as any);
|
||||
|
||||
const result = await getSurveysWithSlugsByOrganizationId("org_123");
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data).toEqual(mockSurveys);
|
||||
}
|
||||
expect(prisma.survey.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
slug: { not: null },
|
||||
environment: { project: { organizationId: "org_123" } },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("should return DatabaseError when prisma fails", async () => {
|
||||
vi.mocked(prisma.survey.findMany).mockRejectedValueOnce(new Error("DB error"));
|
||||
|
||||
const result = await getSurveysWithSlugsByOrganizationId("org_123");
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error).toBeInstanceOf(DatabaseError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
106
apps/web/modules/survey/lib/slug.ts
Normal file
106
apps/web/modules/survey/lib/slug.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import { DatabaseError, ResourceNotFoundError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import { TSurveyStatus } from "@formbricks/types/surveys/types";
|
||||
|
||||
export interface TSurveyBySlug {
|
||||
id: string;
|
||||
environmentId: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface TSurveyWithSlug {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string | null;
|
||||
status: TSurveyStatus;
|
||||
createdAt: Date;
|
||||
environment: {
|
||||
id: string;
|
||||
type: "production" | "development";
|
||||
project: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Find a survey by its slug
|
||||
export const getSurveyBySlug = reactCache(
|
||||
async (slug: string): Promise<Result<TSurveyBySlug | null, DatabaseError>> => {
|
||||
try {
|
||||
const survey = await prisma.survey.findUnique({
|
||||
where: { slug },
|
||||
select: { id: true, environmentId: true, status: true },
|
||||
});
|
||||
return ok(survey);
|
||||
} catch (error) {
|
||||
return err(new DatabaseError(error.message));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Update a survey's slug
|
||||
export const updateSurveySlug = async (
|
||||
surveyId: string,
|
||||
slug: string | null
|
||||
): Promise<
|
||||
Result<{ id: string; slug: string | null }, UniqueConstraintError | DatabaseError | ResourceNotFoundError>
|
||||
> => {
|
||||
try {
|
||||
const result = await prisma.survey.update({
|
||||
where: { id: surveyId },
|
||||
data: { slug },
|
||||
select: { id: true, slug: true },
|
||||
});
|
||||
return ok(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
return err(new UniqueConstraintError("Survey with this slug already exists"));
|
||||
}
|
||||
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
|
||||
return err(new ResourceNotFoundError("Survey", surveyId));
|
||||
}
|
||||
|
||||
return err(new DatabaseError(error.message));
|
||||
}
|
||||
};
|
||||
|
||||
// Get all surveys with slugs for an organization (for Domain settings page)
|
||||
export const getSurveysWithSlugsByOrganizationId = reactCache(
|
||||
async (organizationId: string): Promise<Result<TSurveyWithSlug[], DatabaseError>> => {
|
||||
try {
|
||||
const surveys = await prisma.survey.findMany({
|
||||
where: {
|
||||
slug: { not: null },
|
||||
environment: {
|
||||
project: { organizationId },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
environment: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
project: {
|
||||
select: { id: true, name: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return ok(surveys);
|
||||
} catch (error) {
|
||||
return err(new DatabaseError(error.message));
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -39,6 +39,7 @@ export const selectSurvey = {
|
||||
recaptcha: true,
|
||||
isBackButtonHidden: true,
|
||||
metadata: true,
|
||||
slug: true,
|
||||
languages: {
|
||||
select: {
|
||||
default: true,
|
||||
|
||||
67
apps/web/modules/survey/slug/actions.ts
Normal file
67
apps/web/modules/survey/slug/actions.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZSurveySlug } from "@formbricks/types/surveys/types";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||
import { updateSurveySlug } from "@/modules/survey/lib/slug";
|
||||
|
||||
const ZUpdateSurveySlugAction = z.object({
|
||||
surveyId: z.string().cuid2(),
|
||||
slug: ZSurveySlug,
|
||||
});
|
||||
|
||||
export const updateSurveySlugAction = authenticatedActionClient
|
||||
.schema(ZUpdateSurveySlugAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
throw new OperationNotAllowedError("Pretty URLs are only available on self-hosted instances");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{ type: "organization", roles: ["owner", "manager"] },
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return await updateSurveySlug(parsedInput.surveyId, parsedInput.slug);
|
||||
});
|
||||
|
||||
const ZRemoveSurveySlugAction = z.object({
|
||||
surveyId: z.string().cuid2(),
|
||||
});
|
||||
|
||||
export const removeSurveySlugAction = authenticatedActionClient
|
||||
.schema(ZRemoveSurveySlugAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
throw new OperationNotAllowedError("Pretty URLs are only available on self-hosted instances");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{ type: "organization", roles: ["owner", "manager"] },
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return await updateSurveySlug(parsedInput.surveyId, null);
|
||||
});
|
||||
@@ -42,4 +42,5 @@ export const getMinimalSurvey = (t: TFunction): TSurvey => ({
|
||||
followUps: [],
|
||||
isBackButtonHidden: false,
|
||||
metadata: {},
|
||||
slug: null,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[slug]` on the table `Survey` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Survey" ADD COLUMN "slug" TEXT;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Survey_slug_key" ON "public"."Survey"("slug");
|
||||
@@ -388,6 +388,8 @@ model Survey {
|
||||
/// [SurveyLinkMetadata]
|
||||
metadata Json @default("{}")
|
||||
|
||||
slug String? @unique
|
||||
|
||||
@@index([environmentId, updatedAt])
|
||||
@@index([segmentId])
|
||||
}
|
||||
|
||||
@@ -218,6 +218,12 @@ export const ZSurveyVariables = z.array(ZSurveyVariable);
|
||||
export type TSurveyVariable = z.infer<typeof ZSurveyVariable>;
|
||||
export type TSurveyVariables = z.infer<typeof ZSurveyVariables>;
|
||||
|
||||
export const ZSurveySlug = z
|
||||
.string()
|
||||
.regex(/^[a-z0-9-]+$/, "Slug can only contain lowercase letters, numbers, and hyphens");
|
||||
|
||||
export type TSurveySlug = z.infer<typeof ZSurveySlug>;
|
||||
|
||||
export const ZSurveyProjectOverwrites = z.object({
|
||||
brandColor: ZColor.nullish(),
|
||||
highlightBorderColor: ZColor.nullish(),
|
||||
@@ -893,6 +899,7 @@ export const ZSurvey = z
|
||||
displayPercentage: z.number().min(0.01).max(100).nullable(),
|
||||
languages: z.array(ZSurveyLanguage),
|
||||
metadata: ZSurveyMetadata,
|
||||
slug: ZSurveySlug.nullable(),
|
||||
})
|
||||
.superRefine((survey, ctx) => {
|
||||
const { questions, blocks, languages, welcomeCard, endings, isBackButtonHidden } = survey;
|
||||
|
||||
1
pnpm-lock.yaml
generated
1
pnpm-lock.yaml
generated
@@ -8478,6 +8478,7 @@ packages:
|
||||
next@15.5.9:
|
||||
resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
|
||||
Reference in New Issue
Block a user