mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-06 11:20:56 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6b777db6f | |||
| 34c587342c | |||
| 5a0b421153 | |||
| f6fab9a996 | |||
| fe33527da8 |
@@ -12,18 +12,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^5.0.1",
|
||||
"@storybook/addon-a11y": "10.2.15",
|
||||
"@storybook/addon-links": "10.2.15",
|
||||
"@storybook/addon-onboarding": "10.2.15",
|
||||
"@storybook/react-vite": "10.2.15",
|
||||
"@storybook/addon-a11y": "10.2.14",
|
||||
"@storybook/addon-links": "10.2.14",
|
||||
"@storybook/addon-onboarding": "10.2.14",
|
||||
"@storybook/react-vite": "10.2.14",
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"@tailwindcss/vite": "4.2.1",
|
||||
"@typescript-eslint/parser": "8.56.1",
|
||||
"@vitejs/plugin-react": "5.1.4",
|
||||
"eslint-plugin-react-refresh": "0.4.26",
|
||||
"eslint-plugin-storybook": "10.2.14",
|
||||
"storybook": "10.2.15",
|
||||
"storybook": "10.2.14",
|
||||
"vite": "7.3.1",
|
||||
"@storybook/addon-docs": "10.2.15"
|
||||
"@storybook/addon-docs": "10.2.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZOrganizationTeam = z.object({
|
||||
id: z.cuid2(),
|
||||
id: z.string().cuid2(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const ZCreateProjectAction = z.object({
|
||||
data: ZProjectUpdateInput,
|
||||
});
|
||||
|
||||
export const createProjectAction = authenticatedActionClient.inputSchema(ZCreateProjectAction).action(
|
||||
export const createProjectAction = authenticatedActionClient.schema(ZCreateProjectAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"project",
|
||||
@@ -97,7 +97,7 @@ const ZGetOrganizationsForSwitcherAction = z.object({
|
||||
* Called on-demand when user opens the organization switcher.
|
||||
*/
|
||||
export const getOrganizationsForSwitcherAction = authenticatedActionClient
|
||||
.inputSchema(ZGetOrganizationsForSwitcherAction)
|
||||
.schema(ZGetOrganizationsForSwitcherAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -122,7 +122,7 @@ const ZGetProjectsForSwitcherAction = z.object({
|
||||
* Called on-demand when user opens the project switcher.
|
||||
*/
|
||||
export const getProjectsForSwitcherAction = authenticatedActionClient
|
||||
.inputSchema(ZGetProjectsForSwitcherAction)
|
||||
.schema(ZGetProjectsForSwitcherAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
RocketIcon,
|
||||
UserCircleIcon,
|
||||
UserIcon,
|
||||
WorkflowIcon,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -115,13 +114,6 @@ export const MainNavigation = ({
|
||||
pathname?.includes("/segments") ||
|
||||
pathname?.includes("/attributes"),
|
||||
},
|
||||
{
|
||||
name: t("common.workflows"),
|
||||
href: `/environments/${environment.id}/workflows`,
|
||||
icon: WorkflowIcon,
|
||||
isActive: pathname?.includes("/workflows"),
|
||||
isHidden: !isFormbricksCloud,
|
||||
},
|
||||
{
|
||||
name: t("common.configuration"),
|
||||
href: `/environments/${environment.id}/workspace/general`,
|
||||
@@ -129,7 +121,7 @@ export const MainNavigation = ({
|
||||
isActive: pathname?.includes("/project"),
|
||||
},
|
||||
],
|
||||
[t, environment.id, pathname, isFormbricksCloud]
|
||||
[t, environment.id, pathname]
|
||||
);
|
||||
|
||||
const dropdownNavigation = [
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const ZUpdateNotificationSettingsAction = z.object({
|
||||
});
|
||||
|
||||
export const updateNotificationSettingsAction = authenticatedActionClient
|
||||
.inputSchema(ZUpdateNotificationSettingsAction)
|
||||
.schema(ZUpdateNotificationSettingsAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ async function handleEmailUpdate({
|
||||
return payload;
|
||||
}
|
||||
|
||||
export const updateUserAction = authenticatedActionClient.inputSchema(ZUserPersonalInfoUpdateInput).action(
|
||||
export const updateUserAction = authenticatedActionClient.schema(ZUserPersonalInfoUpdateInput).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"user",
|
||||
|
||||
+25
-33
@@ -17,7 +17,7 @@ const ZUpdateOrganizationNameAction = z.object({
|
||||
});
|
||||
|
||||
export const updateOrganizationNameAction = authenticatedActionClient
|
||||
.inputSchema(ZUpdateOrganizationNameAction)
|
||||
.schema(ZUpdateOrganizationNameAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
@@ -55,36 +55,28 @@ const ZDeleteOrganizationAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const deleteOrganizationAction = authenticatedActionClient
|
||||
.inputSchema(ZDeleteOrganizationAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
}) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled) throw new OperationNotAllowedError("Organization deletion disabled");
|
||||
export const deleteOrganizationAction = authenticatedActionClient.schema(ZDeleteOrganizationAction).action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"organization",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled) throw new OperationNotAllowedError("Organization deletion disabled");
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner"],
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = parsedInput.organizationId;
|
||||
const oldObject = await getOrganization(parsedInput.organizationId);
|
||||
ctx.auditLoggingCtx.oldObject = oldObject;
|
||||
return await deleteOrganization(parsedInput.organizationId);
|
||||
}
|
||||
)
|
||||
);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: parsedInput.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner"],
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = parsedInput.organizationId;
|
||||
const oldObject = await getOrganization(parsedInput.organizationId);
|
||||
ctx.auditLoggingCtx.oldObject = oldObject;
|
||||
return await deleteOrganization(parsedInput.organizationId);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
+6
-6
@@ -23,7 +23,7 @@ const ZGetResponsesAction = z.object({
|
||||
});
|
||||
|
||||
export const getResponsesAction = authenticatedActionClient
|
||||
.inputSchema(ZGetResponsesAction)
|
||||
.schema(ZGetResponsesAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -57,7 +57,7 @@ const ZGetSurveySummaryAction = z.object({
|
||||
});
|
||||
|
||||
export const getSurveySummaryAction = authenticatedActionClient
|
||||
.inputSchema(ZGetSurveySummaryAction)
|
||||
.schema(ZGetSurveySummaryAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -85,7 +85,7 @@ const ZGetResponseCountAction = z.object({
|
||||
});
|
||||
|
||||
export const getResponseCountAction = authenticatedActionClient
|
||||
.inputSchema(ZGetResponseCountAction)
|
||||
.schema(ZGetResponseCountAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -110,12 +110,12 @@ export const getResponseCountAction = authenticatedActionClient
|
||||
|
||||
const ZGetDisplaysWithContactAction = z.object({
|
||||
surveyId: ZId,
|
||||
limit: z.int().min(1).max(100),
|
||||
offset: z.int().nonnegative(),
|
||||
limit: z.number().int().min(1).max(100),
|
||||
offset: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export const getDisplaysWithContactAction = authenticatedActionClient
|
||||
.inputSchema(ZGetDisplaysWithContactAction)
|
||||
.schema(ZGetDisplaysWithContactAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
+5
-5
@@ -22,7 +22,7 @@ const ZSendEmbedSurveyPreviewEmailAction = z.object({
|
||||
});
|
||||
|
||||
export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient
|
||||
.inputSchema(ZSendEmbedSurveyPreviewEmailAction)
|
||||
.schema(ZSendEmbedSurveyPreviewEmailAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId);
|
||||
const organizationLogoUrl = await getOrganizationLogoUrl(organizationId);
|
||||
@@ -69,7 +69,7 @@ const ZResetSurveyAction = z.object({
|
||||
projectId: ZId,
|
||||
});
|
||||
|
||||
export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action(
|
||||
export const resetSurveyAction = authenticatedActionClient.schema(ZResetSurveyAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"survey",
|
||||
@@ -123,7 +123,7 @@ const ZGetEmailHtmlAction = z.object({
|
||||
});
|
||||
|
||||
export const getEmailHtmlAction = authenticatedActionClient
|
||||
.inputSchema(ZGetEmailHtmlAction)
|
||||
.schema(ZGetEmailHtmlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -152,7 +152,7 @@ const ZGeneratePersonalLinksAction = z.object({
|
||||
});
|
||||
|
||||
export const generatePersonalLinksAction = authenticatedActionClient
|
||||
.inputSchema(ZGeneratePersonalLinksAction)
|
||||
.schema(ZGeneratePersonalLinksAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const isContactsEnabled = await getIsContactsEnabled();
|
||||
if (!isContactsEnabled) {
|
||||
@@ -231,7 +231,7 @@ const ZUpdateSingleUseLinksAction = z.object({
|
||||
});
|
||||
|
||||
export const updateSingleUseLinksAction = authenticatedActionClient
|
||||
.inputSchema(ZUpdateSingleUseLinksAction)
|
||||
.schema(ZUpdateSingleUseLinksAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
+1
-1
@@ -1095,7 +1095,7 @@ export const getResponsesForSummary = reactCache(
|
||||
[limit, ZOptionalNumber],
|
||||
[offset, ZOptionalNumber],
|
||||
[filterCriteria, ZResponseFilterCriteria.optional()],
|
||||
[cursor, z.cuid2().optional()]
|
||||
[cursor, z.string().cuid2().optional()]
|
||||
);
|
||||
|
||||
const queryLimit = limit ?? RESPONSES_PER_PAGE;
|
||||
|
||||
@@ -28,7 +28,7 @@ const ZGetResponsesDownloadUrlAction = z.object({
|
||||
});
|
||||
|
||||
export const getResponsesDownloadUrlAction = authenticatedActionClient
|
||||
.inputSchema(ZGetResponsesDownloadUrlAction)
|
||||
.schema(ZGetResponsesDownloadUrlAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -58,7 +58,7 @@ const ZGetSurveyFilterDataAction = z.object({
|
||||
});
|
||||
|
||||
export const getSurveyFilterDataAction = authenticatedActionClient
|
||||
.inputSchema(ZGetSurveyFilterDataAction)
|
||||
.schema(ZGetSurveyFilterDataAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
|
||||
@@ -121,7 +121,7 @@ const checkSurveyFollowUpsPermission = async (organizationId: string): Promise<v
|
||||
}
|
||||
};
|
||||
|
||||
export const updateSurveyAction = authenticatedActionClient.inputSchema(ZSurvey).action(
|
||||
export const updateSurveyAction = authenticatedActionClient.schema(ZSurvey).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"survey",
|
||||
|
||||
-208
@@ -1,208 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CheckCircle2, Sparkles } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
const FORMBRICKS_HOST = "https://app.formbricks.com";
|
||||
const SURVEY_ID = "cr9r4b2r73x6hlmn5aa2ha44";
|
||||
const ENVIRONMENT_ID = "cmk41i8bi92bdad01svi74dec";
|
||||
|
||||
interface WorkflowsPageProps {
|
||||
userEmail: string;
|
||||
organizationName: string;
|
||||
billingPlan: string;
|
||||
}
|
||||
|
||||
type Step = "prompt" | "followup" | "thankyou";
|
||||
|
||||
export const WorkflowsPage = ({ userEmail, organizationName, billingPlan }: WorkflowsPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [step, setStep] = useState<Step>("prompt");
|
||||
const [promptValue, setPromptValue] = useState("");
|
||||
const [detailsValue, setDetailsValue] = useState("");
|
||||
const [responseId, setResponseId] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleGenerateWorkflow = async () => {
|
||||
if (promptValue.trim().length < 100 || isSubmitting) return;
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${FORMBRICKS_HOST}/api/v2/client/${ENVIRONMENT_ID}/responses`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
surveyId: SURVEY_ID,
|
||||
finished: false,
|
||||
data: {
|
||||
workflow: promptValue.trim(),
|
||||
useremail: userEmail,
|
||||
orgname: organizationName,
|
||||
billingplan: billingPlan,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setResponseId(json.data?.id ?? null);
|
||||
}
|
||||
|
||||
setStep("followup");
|
||||
} catch {
|
||||
setStep("followup");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitFeedback = async () => {
|
||||
if (isSubmitting) return;
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (responseId) {
|
||||
try {
|
||||
await fetch(`${FORMBRICKS_HOST}/api/v1/client/${ENVIRONMENT_ID}/responses/${responseId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
finished: true,
|
||||
data: {
|
||||
details: detailsValue.trim(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
setStep("thankyou");
|
||||
};
|
||||
|
||||
const handleSkipFeedback = async () => {
|
||||
if (!responseId) {
|
||||
setStep("thankyou");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(`${FORMBRICKS_HOST}/api/v1/client/${ENVIRONMENT_ID}/responses/${responseId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
finished: true,
|
||||
data: {},
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
|
||||
setStep("thankyou");
|
||||
};
|
||||
|
||||
if (step === "prompt") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center px-4 pt-[15vh]">
|
||||
<div className="w-full max-w-2xl space-y-8">
|
||||
<div className="space-y-3 text-center">
|
||||
<div className="from-brand-light to-brand-dark mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br shadow-md">
|
||||
<Sparkles className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-slate-800">{t("workflows.heading")}</h1>
|
||||
<p className="text-lg text-slate-500">{t("workflows.subheading")}</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={promptValue}
|
||||
onChange={(e) => setPromptValue(e.target.value)}
|
||||
placeholder={t("workflows.placeholder")}
|
||||
rows={5}
|
||||
className="focus:border-brand-dark focus:ring-brand-light/20 w-full resize-none rounded-xl border border-slate-200 bg-white px-5 py-4 text-base text-slate-800 shadow-sm transition-all placeholder:text-slate-400 focus:outline-none focus:ring-2"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
handleGenerateWorkflow();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span
|
||||
className={`text-xs ${promptValue.trim().length >= 100 ? "text-slate-400" : "text-amber-500"}`}>
|
||||
{promptValue.trim().length} / 100
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleGenerateWorkflow}
|
||||
disabled={promptValue.trim().length < 100 || isSubmitting}
|
||||
loading={isSubmitting}
|
||||
size="lg">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{t("workflows.generate_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === "followup") {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center px-4 pt-[15vh]">
|
||||
<div className="w-full max-w-2xl space-y-8">
|
||||
<div className="space-y-3 text-center">
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-slate-100">
|
||||
<Sparkles className="text-brand-dark h-6 w-6" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-slate-800">
|
||||
{t("workflows.coming_soon_title")}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-md text-base text-slate-500">
|
||||
{t("workflows.coming_soon_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||
<label className="text-md mb-2 block font-medium text-slate-700">
|
||||
{t("workflows.follow_up_label")}
|
||||
</label>
|
||||
<textarea
|
||||
value={detailsValue}
|
||||
onChange={(e) => setDetailsValue(e.target.value)}
|
||||
placeholder={t("workflows.follow_up_placeholder")}
|
||||
rows={4}
|
||||
className="focus:border-brand-dark focus:ring-brand-light/20 w-full resize-none rounded-lg border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-800 transition-all placeholder:text-slate-400 focus:bg-white focus:outline-none focus:ring-2"
|
||||
/>
|
||||
<div className="mt-4 flex items-center justify-end gap-3">
|
||||
<Button variant="ghost" onClick={handleSkipFeedback} className="text-slate-500">
|
||||
{t("common.skip")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitFeedback}
|
||||
disabled={!detailsValue.trim() || isSubmitting}
|
||||
loading={isSubmitting}>
|
||||
{t("workflows.submit_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center px-4 pt-[15vh]">
|
||||
<div className="w-full max-w-md space-y-6 text-center">
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-green-50">
|
||||
<CheckCircle2 className="h-8 w-8 text-green-500" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">{t("workflows.thank_you_title")}</h1>
|
||||
<p className="text-base text-slate-500">{t("workflows.thank_you_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { WorkflowsPage } from "./components/workflows-page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Workflows",
|
||||
};
|
||||
|
||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const params = await props.params;
|
||||
|
||||
if (!IS_FORMBRICKS_CLOUD) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const { session, organization, isBilling } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
if (isBilling) {
|
||||
return redirect(`/environments/${params.environmentId}/settings/billing`);
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowsPage
|
||||
userEmail={user.email}
|
||||
organizationName={organization.name}
|
||||
billingPlan={organization.billing.plan}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -21,7 +21,7 @@ const ZCreateOrUpdateIntegrationAction = z.object({
|
||||
});
|
||||
|
||||
export const createOrUpdateIntegrationAction = authenticatedActionClient
|
||||
.inputSchema(ZCreateOrUpdateIntegrationAction)
|
||||
.schema(ZCreateOrUpdateIntegrationAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"createdUpdated",
|
||||
@@ -67,7 +67,7 @@ const ZDeleteIntegrationAction = z.object({
|
||||
integrationId: ZId,
|
||||
});
|
||||
|
||||
export const deleteIntegrationAction = authenticatedActionClient.inputSchema(ZDeleteIntegrationAction).action(
|
||||
export const deleteIntegrationAction = authenticatedActionClient.schema(ZDeleteIntegrationAction).action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"integration",
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ const ZValidateGoogleSheetsConnectionAction = z.object({
|
||||
});
|
||||
|
||||
export const validateGoogleSheetsConnectionAction = authenticatedActionClient
|
||||
.inputSchema(ZValidateGoogleSheetsConnectionAction)
|
||||
.schema(ZValidateGoogleSheetsConnectionAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -51,7 +51,7 @@ const ZGetSpreadsheetNameByIdAction = z.object({
|
||||
});
|
||||
|
||||
export const getSpreadsheetNameByIdAction = authenticatedActionClient
|
||||
.inputSchema(ZGetSpreadsheetNameByIdAction)
|
||||
.schema(ZGetSpreadsheetNameByIdAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const ZGetSlackChannelsAction = z.object({
|
||||
});
|
||||
|
||||
export const getSlackChannelsAction = authenticatedActionClient
|
||||
.inputSchema(ZGetSlackChannelsAction)
|
||||
.schema(ZGetSlackChannelsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -50,7 +50,7 @@ export const GET = withV1ApiWrapper({
|
||||
{
|
||||
environmentId: params.environmentId,
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
validationError: cuidValidation.error.errors[0]?.message,
|
||||
},
|
||||
"Invalid CUID v1 format detected"
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
|
||||
export const deleteSurvey = async (surveyId: string) => {
|
||||
validateInputs([surveyId, z.cuid2()]);
|
||||
validateInputs([surveyId, z.string().cuid2()]);
|
||||
|
||||
try {
|
||||
const deletedSurvey = await prisma.survey.delete({
|
||||
|
||||
@@ -101,9 +101,7 @@ describe("verifyRecaptchaToken", () => {
|
||||
},
|
||||
signal: {},
|
||||
};
|
||||
vi.spyOn(global, "AbortController").mockImplementation(function AbortController() {
|
||||
return abortController as any;
|
||||
});
|
||||
vi.spyOn(global, "AbortController").mockImplementation(() => abortController as any);
|
||||
(global.fetch as any).mockImplementation(() => new Promise(() => {}));
|
||||
verifyRecaptchaToken("token", 0.5);
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as cuid2 from "@paralleldrive/cuid2";
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as crypto from "@/lib/crypto";
|
||||
import { generateSurveySingleUseId, validateSurveySingleUseId } from "./singleUseSurveys";
|
||||
@@ -20,6 +20,10 @@ vi.mock("@paralleldrive/cuid2", () => {
|
||||
const isCuidMock = vi.fn();
|
||||
|
||||
return {
|
||||
default: {
|
||||
createId: createIdMock,
|
||||
isCuid: isCuidMock,
|
||||
},
|
||||
createId: createIdMock,
|
||||
isCuid: isCuidMock,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createId, isCuid } from "@paralleldrive/cuid2";
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import { ENCRYPTION_KEY } from "@/lib/constants";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "@/lib/crypto";
|
||||
|
||||
// generate encrypted single use id for the survey
|
||||
export const generateSurveySingleUseId = (isEncrypted: boolean): string => {
|
||||
const cuid = createId();
|
||||
const cuid = cuid2.createId();
|
||||
if (!isEncrypted) {
|
||||
return cuid;
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export const validateSurveySingleUseId = (surveySingleUseId: string): string | u
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isCuid(decryptedCuid)) {
|
||||
if (cuid2.isCuid(decryptedCuid)) {
|
||||
return decryptedCuid;
|
||||
} else {
|
||||
return undefined;
|
||||
|
||||
+210
-210
File diff suppressed because it is too large
Load Diff
@@ -14,39 +14,31 @@ const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
.inputSchema(ZCreateOrganizationAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
}) => {
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
export const createOrganizationAction = authenticatedActionClient.schema(ZCreateOrganizationAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"organization",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
}
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = newOrganization.id;
|
||||
ctx.auditLoggingCtx.newObject = newOrganization;
|
||||
|
||||
return newOrganization;
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = newOrganization.id;
|
||||
ctx.auditLoggingCtx.newObject = newOrganization;
|
||||
|
||||
return newOrganization;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
+20
-22
@@ -148,11 +148,12 @@ checksums:
|
||||
common/copy: 627c00d2c850b9b45f7341a6ac01b6bb
|
||||
common/copy_code: 704c13d9bc01caad29a1cf3179baa111
|
||||
common/copy_link: 57a37acfe6d7ed71d00fbbc8079fbb35
|
||||
common/count_attributes: 042fba9baffef5afe2c24f13d4f50697
|
||||
common/count_contacts: b1c413a4b06961b71b6aeee95d6775d7
|
||||
common/count_members: 8cabb9805075f20e3977b919b3b2fdc5
|
||||
common/count_responses: 690118a456c01c5b4d437ae82b50b131
|
||||
common/count_selections: c0f581d21468af2f46dad171921f71ba
|
||||
common/count_attributes: 48805e836a9b50f9635ad00fed953058
|
||||
common/count_contacts: 9f71d503455264f1eec1ae58894cf143
|
||||
common/count_members: 31ce64ca63fdf95e02ab5543b6e2f717
|
||||
common/count_questions: a7a34376a01eda781381fe7544541293
|
||||
common/count_responses: 437e022825c7a08481d8f7e56926742d
|
||||
common/count_selections: a1ec41682b9a7d8601c3905dfba34e16
|
||||
common/create_new_organization: 51dae7b33143686ee218abf5bea764a5
|
||||
common/create_segment: 9d8291cd4d778b53b73bbc84fd91c181
|
||||
common/create_survey: 1cfbba08d34876566d84b2960054a987
|
||||
@@ -251,7 +252,6 @@ checksums:
|
||||
common/look_and_feel: 9125503712626d495cedec7a79f1418c
|
||||
common/manage: a3d40c0267b81ae53c9598eaeb05087d
|
||||
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||
common/member: 1606dc30b369856b9dba1fe9aec425d2
|
||||
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
|
||||
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
|
||||
@@ -359,8 +359,6 @@ checksums:
|
||||
common/select_teams: ae5d451929846ae6367562bc671a1af9
|
||||
common/selected: 9f09e059ba20c88ed34e2b4e8e032d56
|
||||
common/selected_questions: beffe92d5272d99a0022f004e6a6ad73
|
||||
common/selection: 25b570dc6339916a7aada2142aca0cd1
|
||||
common/selections: 82f0681bf0208e25d7efedc23c556b8f
|
||||
common/send_test_email: 2fd3ea40199b9589132ac826a5b0f3f5
|
||||
common/session_not_found: e9622df3170dbfd9636403bb0c22295b
|
||||
common/settings: 8df6777277469c1fd88cc18dde2f1cc3
|
||||
@@ -369,7 +367,6 @@ checksums:
|
||||
common/show_response_count: 609e5dc7c074d57e711a728fa2f8eb79
|
||||
common/shown: 63e4ffb245c05e04b636446c3dbdd8df
|
||||
common/size: 227fadeeff951e041ff42031a11a4626
|
||||
common/skip: b7f28dfa2f58b80b149bb82b392d0291
|
||||
common/skipped: d496f0f667e1b4364b954db71335d4ef
|
||||
common/skips: 99de7579122a3fa6ec5e2a47f3fd8b34
|
||||
common/some_files_failed_to_upload: a0e26efeb29ae905257ecf93b112dff0
|
||||
@@ -439,7 +436,6 @@ checksums:
|
||||
common/website_survey: 17513d25a07b6361768a15ec622b021b
|
||||
common/weeks: 545de30df4f44d3f6d1d344af6a10815
|
||||
common/welcome_card: 76081ebd5b2e35da9b0f080323704ae7
|
||||
common/workflows: b0c9c8615a9ba7d9cb73e767290a7f72
|
||||
common/workspace_configuration: d0a5812d6a97d7724d565b1017c34387
|
||||
common/workspace_created_successfully: bf401ae83da954f1db48724e2a8e40f1
|
||||
common/workspace_creation_description: aea2f480ba0c54c5cabac72c9c900ddf
|
||||
@@ -1200,11 +1196,13 @@ checksums:
|
||||
environments/surveys/edit/adjust_survey_closed_message: ae6f38c9daf08656362bd84459a312fa
|
||||
environments/surveys/edit/adjust_survey_closed_message_description: e906aebd9af6451a2a39c73287927299
|
||||
environments/surveys/edit/adjust_the_theme_in_the: bccdafda8af5871513266f668b55d690
|
||||
environments/surveys/edit/all_are_true: 05d02c5afac857da530b73dcf18dd8e4
|
||||
environments/surveys/edit/all_other_answers_will_continue_to: 9a5d09eea42ff5fd1c18cc58a14dcabd
|
||||
environments/surveys/edit/allow_multi_select: 7b4b83f7a0205e2a0a8971671a69a174
|
||||
environments/surveys/edit/allow_multiple_files: dbd99f9d1026e4f7c5a5d03f71ba379d
|
||||
environments/surveys/edit/allow_users_to_select_more_than_one_image: d683e0b538d1366400292a771f3fbd08
|
||||
environments/surveys/edit/and_launch_surveys_in_your_website_or_app: a3edcdb4aea792a27d90aad1930f001a
|
||||
environments/surveys/edit/any_is_true: 32c9f3998984fd32a2b5bc53f2d97429
|
||||
environments/surveys/edit/animation: 66a18eacfb92fc9fc9db188d2dde4f81
|
||||
environments/surveys/edit/app_survey_description: bdfacfce478e97f70b700a1382dfa687
|
||||
environments/surveys/edit/assign: e80715ab64bf7cf463abb3a9fd1ad516
|
||||
@@ -1496,6 +1494,7 @@ checksums:
|
||||
environments/surveys/edit/question_deleted: ecdeb22b81ae2d732656a7742c1eec7b
|
||||
environments/surveys/edit/question_duplicated: 3f02439fd0a8b818bc84c1b1b473898c
|
||||
environments/surveys/edit/question_id_updated: e8d94dbefcbad00c7464b3d1fb0ee81a
|
||||
environments/surveys/edit/question_number: 742636e9d2d5dcc7ee6ca1b3016bcee7
|
||||
environments/surveys/edit/question_used_in_logic_warning_text: ec78767a7cf335222d41b98cb5baa6be
|
||||
environments/surveys/edit/question_used_in_logic_warning_title: 4bb8528cdc3b8649c194487067737f6d
|
||||
environments/surveys/edit/question_used_in_quota: ceb5e88f6916e4863e589c6be030bb3b
|
||||
@@ -1685,6 +1684,7 @@ checksums:
|
||||
environments/surveys/edit/waiting_time_across_surveys: 6873c18d51830e2cadef67cce6a2c95c
|
||||
environments/surveys/edit/waiting_time_across_surveys_description: 6edafaeb3ccd8cadde81175776636c8e
|
||||
environments/surveys/edit/welcome_message: 986a434e3895c8ee0b267df95cc40051
|
||||
environments/surveys/edit/when: a40ad3eed1b75e76226290eeb9bb20cd
|
||||
environments/surveys/edit/without_a_filter_all_of_your_users_can_be_surveyed: 451990569c61f25d01044cc45b1ce122
|
||||
environments/surveys/edit/you_have_not_created_a_segment_yet: c6658bd1cee9c5c957c675db044708dd
|
||||
environments/surveys/edit/your_description_here_recall_information_with: 60f73a3cc9bdb9afea2166a7db8fd618
|
||||
@@ -2248,6 +2248,16 @@ checksums:
|
||||
templates/alignment_and_engagement_survey_question_4_headline: e36be56ce8aad1d0ca04939bea4e39b7
|
||||
templates/alignment_and_engagement_survey_question_4_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
|
||||
templates/back: f541015a827e37cb3b1234e56bc2aa3c
|
||||
templates/block_1: 5e1b4dce0cb70662441b663507a69454
|
||||
templates/block_2: f50d8aab8b44f168a2ab00526d4f9a2c
|
||||
templates/block_3: 78d84f8e4763a95710543c5368ce8a41
|
||||
templates/block_4: 2c346374f245a6821940c061b855ac69
|
||||
templates/block_5: 975abfc66e8e377478ff691a040dda0b
|
||||
templates/block_6: 2bd10f1edb210243c5ab459c59e02d30
|
||||
templates/block_7: 13f0f680c09c96081e125123ad2f6786
|
||||
templates/block_8: 1be1b18e159e8c8d11d2fb1082ea5d98
|
||||
templates/block_9: 2da3894d05e4415fa043ba18d11d60e2
|
||||
templates/block_10: 09a42e99b34b45700e734730acfe37ed
|
||||
templates/book_interview: 1cc9c72d1c088b28e5dfa5ec7d7b78c4
|
||||
templates/build_product_roadmap_description: 6ca163ed3b0095cedcbc11822a0d502a
|
||||
templates/build_product_roadmap_name: 8c216b183c3539c0340ce87465a391cc
|
||||
@@ -2455,7 +2465,6 @@ checksums:
|
||||
templates/csat_survey_question_3_headline: 25974b7f1692cad41908fe305830b6c0
|
||||
templates/csat_survey_question_3_placeholder: 37ee9c84f3777b9220d4faec1e1c78ee
|
||||
templates/cta_description: bc94a2ddc965b286a8677b0642696c7e
|
||||
templates/custom_survey_block_1_name: 5e1b4dce0cb70662441b663507a69454
|
||||
templates/custom_survey_description: 0492afdea2ef1bd683eaf48a2bad2caa
|
||||
templates/custom_survey_name: 6fc756927ca9ea22c26368cccd64a67e
|
||||
templates/custom_survey_question_1_headline: 0abf9d41e0b5c5567c3833fd63048398
|
||||
@@ -3109,14 +3118,3 @@ checksums:
|
||||
templates/usability_question_9_headline: 5850229e97ae97698ce90b330ea49682
|
||||
templates/usability_rating_description: 8c4f3818fe830ae544611f816265f1a1
|
||||
templates/usability_score_name: 5cbf1172d24dfcb17d979dff6dfdf7e2
|
||||
workflows/coming_soon_description: 1e0621d287924d84fb539afab7372b23
|
||||
workflows/coming_soon_title: d79be80559c70c828cf20811d2ed5039
|
||||
workflows/follow_up_label: 8cafe669370271035aeac8e8cab0f123
|
||||
workflows/follow_up_placeholder: 0c26f9e4f82429acb2ac7525a3e8f24e
|
||||
workflows/generate_button: b194b6172a49af8374a19dd2cf39cfdc
|
||||
workflows/heading: a98a6b14d3e955f38cc16386df9a4111
|
||||
workflows/placeholder: 0d24da3af3b860b8f943c83efdeef227
|
||||
workflows/subheading: ebf5e3b3aeb85e13e843358cc5476f42
|
||||
workflows/submit_button: 7a062f2de02ce60b1d73e510ff1ca094
|
||||
workflows/thank_you_description: 842579609c6bf16a1d6c57a333fd5125
|
||||
workflows/thank_you_title: 07edd8c50685a52c0969d711df26d768
|
||||
|
||||
@@ -159,7 +159,7 @@ export const BREVO_LIST_ID = env.BREVO_LIST_ID;
|
||||
export const UNSPLASH_ACCESS_KEY = env.UNSPLASH_ACCESS_KEY;
|
||||
export const UNSPLASH_ALLOWED_DOMAINS = ["api.unsplash.com"];
|
||||
|
||||
export const STRIPE_API_VERSION = "2026-02-25.clover";
|
||||
export const STRIPE_API_VERSION = "2024-06-20";
|
||||
|
||||
// Maximum number of attribute classes allowed:
|
||||
export const MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT = 150;
|
||||
|
||||
@@ -71,8 +71,8 @@ export const getDisplaysBySurveyIdWithContact = reactCache(
|
||||
async (surveyId: string, limit?: number, offset?: number): Promise<TDisplayWithContact[]> => {
|
||||
validateInputs(
|
||||
[surveyId, ZId],
|
||||
[limit, z.int().min(1).optional()],
|
||||
[offset, z.int().nonnegative().optional()]
|
||||
[limit, z.number().int().min(1).optional()],
|
||||
[offset, z.number().int().nonnegative().optional()]
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
+14
-10
@@ -14,7 +14,7 @@ export const env = createEnv({
|
||||
CRON_SECRET: z.string().optional(),
|
||||
BREVO_API_KEY: z.string().optional(),
|
||||
BREVO_LIST_ID: z.string().optional(),
|
||||
DATABASE_URL: z.url(),
|
||||
DATABASE_URL: z.string().url(),
|
||||
DEBUG: z.enum(["1", "0"]).optional(),
|
||||
AUTH_DEFAULT_TEAM_ID: z.string().optional(),
|
||||
AUTH_SKIP_INVITE_FOR_SSO: z.enum(["1", "0"]).optional(),
|
||||
@@ -23,7 +23,7 @@ export const env = createEnv({
|
||||
EMAIL_VERIFICATION_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
ENCRYPTION_KEY: z.string(),
|
||||
ENTERPRISE_LICENSE_KEY: z.string().optional(),
|
||||
ENVIRONMENT: z.enum(["production", "staging"]).prefault("production"),
|
||||
ENVIRONMENT: z.enum(["production", "staging"]).default("production"),
|
||||
GITHUB_ID: z.string().optional(),
|
||||
GITHUB_SECRET: z.string().optional(),
|
||||
GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
@@ -31,20 +31,21 @@ export const env = createEnv({
|
||||
GOOGLE_SHEETS_CLIENT_ID: z.string().optional(),
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: z.string().optional(),
|
||||
GOOGLE_SHEETS_REDIRECT_URL: z.string().optional(),
|
||||
HTTP_PROXY: z.url().optional(),
|
||||
HTTPS_PROXY: z.url().optional(),
|
||||
HTTP_PROXY: z.string().url().optional(),
|
||||
HTTPS_PROXY: z.string().url().optional(),
|
||||
IMPRINT_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
IMPRINT_ADDRESS: z.string().optional(),
|
||||
INVITE_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
CHATWOOT_WEBSITE_TOKEN: z.string().optional(),
|
||||
CHATWOOT_BASE_URL: z.url().optional(),
|
||||
CHATWOOT_BASE_URL: z.string().url().optional(),
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
LOG_LEVEL: z.enum(["debug", "info", "warn", "error", "fatal"]).optional(),
|
||||
MAIL_FROM: z.email().optional(),
|
||||
NEXTAUTH_URL: z.url().optional(),
|
||||
MAIL_FROM: z.string().email().optional(),
|
||||
NEXTAUTH_URL: z.string().url().optional(),
|
||||
NEXTAUTH_SECRET: z.string().optional(),
|
||||
MAIL_FROM_NAME: z.string().optional(),
|
||||
NOTION_OAUTH_CLIENT_ID: z.string().optional(),
|
||||
@@ -57,9 +58,10 @@ export const env = createEnv({
|
||||
REDIS_URL:
|
||||
process.env.NODE_ENV === "test"
|
||||
? z.string().optional()
|
||||
: z.url("REDIS_URL is required for caching, rate limiting, and audit logging"),
|
||||
: z.string().url("REDIS_URL is required for caching, rate limiting, and audit logging"),
|
||||
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
PRIVACY_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
@@ -84,6 +86,7 @@ export const env = createEnv({
|
||||
STRIPE_SECRET_KEY: z.string().optional(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
||||
PUBLIC_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.refine(
|
||||
(url) => {
|
||||
@@ -95,11 +98,12 @@ export const env = createEnv({
|
||||
}
|
||||
},
|
||||
{
|
||||
error: "PUBLIC_URL must be a valid URL with a proper host (e.g., https://example.com)",
|
||||
message: "PUBLIC_URL must be a valid URL with a proper host (e.g., https://example.com)",
|
||||
}
|
||||
)
|
||||
.optional(),
|
||||
TERMS_URL: z
|
||||
.string()
|
||||
.url()
|
||||
.optional()
|
||||
.or(z.string().refine((str) => str === "")),
|
||||
@@ -108,7 +112,7 @@ export const env = createEnv({
|
||||
RECAPTCHA_SITE_KEY: z.string().optional(),
|
||||
RECAPTCHA_SECRET_KEY: z.string().optional(),
|
||||
VERCEL_URL: z.string().optional(),
|
||||
WEBAPP_URL: z.url().optional(),
|
||||
WEBAPP_URL: z.string().url().optional(),
|
||||
UNSPLASH_ACCESS_KEY: z.string().optional(),
|
||||
|
||||
NODE_ENV: z.enum(["development", "production", "test"]).optional(),
|
||||
|
||||
@@ -267,7 +267,7 @@ export const getResponses = reactCache(
|
||||
[limit, ZOptionalNumber],
|
||||
[offset, ZOptionalNumber],
|
||||
[filterCriteria, ZResponseFilterCriteria.optional()],
|
||||
[cursor, z.cuid2().optional()]
|
||||
[cursor, z.string().cuid2().optional()]
|
||||
);
|
||||
|
||||
limit = limit ?? RESPONSES_PER_PAGE;
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
getOrganizationByEnvironmentId,
|
||||
subscribeOrganizationMembersToSurveyResponses,
|
||||
} from "@/lib/organization/service";
|
||||
import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger";
|
||||
import { getActionClasses } from "../actionClass/service";
|
||||
import { ITEMS_PER_PAGE } from "../constants";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
@@ -23,6 +22,15 @@ import {
|
||||
validateMediaAndPrepareBlocks,
|
||||
} from "./utils";
|
||||
|
||||
interface TriggerUpdate {
|
||||
create?: Array<{ actionClassId: string }>;
|
||||
deleteMany?: {
|
||||
actionClassId: {
|
||||
in: string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const selectSurvey = {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
@@ -106,32 +114,19 @@ export const selectSurvey = {
|
||||
slug: true,
|
||||
} satisfies Prisma.SurveySelect;
|
||||
|
||||
const getTriggerIds = (triggers: TSurvey["triggers"]): string[] | null => {
|
||||
if (!triggers) return null;
|
||||
if (!Array.isArray(triggers)) {
|
||||
throw new InvalidInputError("Invalid trigger id");
|
||||
}
|
||||
|
||||
return triggers.map((trigger) => {
|
||||
const actionClassId = trigger?.actionClass?.id;
|
||||
if (typeof actionClassId !== "string") {
|
||||
throw new InvalidInputError("Invalid trigger id");
|
||||
}
|
||||
return actionClassId;
|
||||
});
|
||||
};
|
||||
|
||||
export const checkTriggersValidity = (triggers: TSurvey["triggers"], actionClasses: ActionClass[]) => {
|
||||
const triggerIds = getTriggerIds(triggers);
|
||||
if (!triggerIds) return;
|
||||
if (!triggers) return;
|
||||
|
||||
// check if all the triggers are valid
|
||||
triggerIds.forEach((triggerId) => {
|
||||
if (!actionClasses.find((actionClass) => actionClass.id === triggerId)) {
|
||||
triggers.forEach((trigger) => {
|
||||
if (!actionClasses.find((actionClass) => actionClass.id === trigger.actionClass.id)) {
|
||||
throw new InvalidInputError("Invalid trigger id");
|
||||
}
|
||||
});
|
||||
|
||||
// check if all the triggers are unique
|
||||
const triggerIds = triggers.map((trigger) => trigger.actionClass.id);
|
||||
|
||||
if (new Set(triggerIds).size !== triggerIds.length) {
|
||||
throw new InvalidInputError("Duplicate trigger id");
|
||||
}
|
||||
@@ -142,33 +137,36 @@ export const handleTriggerUpdates = (
|
||||
currentTriggers: TSurvey["triggers"],
|
||||
actionClasses: ActionClass[]
|
||||
) => {
|
||||
const updatedTriggerIds = getTriggerIds(updatedTriggers);
|
||||
if (!updatedTriggerIds) return {};
|
||||
|
||||
if (!updatedTriggers) return {};
|
||||
checkTriggersValidity(updatedTriggers, actionClasses);
|
||||
|
||||
const currentTriggerIds = getTriggerIds(currentTriggers) ?? [];
|
||||
const currentTriggerIds = currentTriggers.map((trigger) => trigger.actionClass.id);
|
||||
const updatedTriggerIds = updatedTriggers.map((trigger) => trigger.actionClass.id);
|
||||
|
||||
// added triggers are triggers that are not in the current triggers and are there in the new triggers
|
||||
const addedTriggerIds = updatedTriggerIds.filter((triggerId) => !currentTriggerIds.includes(triggerId));
|
||||
const addedTriggers = updatedTriggers.filter(
|
||||
(trigger) => !currentTriggerIds.includes(trigger.actionClass.id)
|
||||
);
|
||||
|
||||
// deleted triggers are triggers that are not in the new triggers and are there in the current triggers
|
||||
const deletedTriggerIds = currentTriggerIds.filter((triggerId) => !updatedTriggerIds.includes(triggerId));
|
||||
const deletedTriggers = currentTriggers.filter(
|
||||
(trigger) => !updatedTriggerIds.includes(trigger.actionClass.id)
|
||||
);
|
||||
|
||||
// Construct the triggers update object
|
||||
const triggersUpdate: TriggerUpdate = {};
|
||||
|
||||
if (addedTriggerIds.length > 0) {
|
||||
triggersUpdate.create = addedTriggerIds.map((triggerId) => ({
|
||||
actionClassId: triggerId,
|
||||
if (addedTriggers.length > 0) {
|
||||
triggersUpdate.create = addedTriggers.map((trigger) => ({
|
||||
actionClassId: trigger.actionClass.id,
|
||||
}));
|
||||
}
|
||||
|
||||
if (deletedTriggerIds.length > 0) {
|
||||
if (deletedTriggers.length > 0) {
|
||||
// disconnect the public triggers from the survey
|
||||
triggersUpdate.deleteMany = {
|
||||
actionClassId: {
|
||||
in: deletedTriggerIds,
|
||||
in: deletedTriggers.map((trigger) => trigger.actionClass.id),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -602,16 +600,21 @@ export const createSurvey = async (
|
||||
);
|
||||
|
||||
try {
|
||||
const { createdBy, languages, ...restSurveyBody } = parsedSurveyBody;
|
||||
const { createdBy, ...restSurveyBody } = parsedSurveyBody;
|
||||
|
||||
// empty languages array
|
||||
if (!restSurveyBody.languages?.length) {
|
||||
delete restSurveyBody.languages;
|
||||
}
|
||||
|
||||
const actionClasses = await getActionClasses(parsedEnvironmentId);
|
||||
|
||||
// @ts-expect-error
|
||||
let data: Omit<Prisma.SurveyCreateInput, "environment"> = {
|
||||
...restSurveyBody,
|
||||
// @ts-expect-error - languages would be undefined in case of empty array
|
||||
languages: languages?.length ? languages : undefined,
|
||||
// TODO: Create with attributeFilters
|
||||
triggers: restSurveyBody.triggers
|
||||
? // @ts-expect-error - triggers' createdAt and updatedAt are actually dates
|
||||
handleTriggerUpdates(restSurveyBody.triggers, [], actionClasses)
|
||||
? handleTriggerUpdates(restSurveyBody.triggers, [], actionClasses)
|
||||
: undefined,
|
||||
attributeFilters: undefined,
|
||||
};
|
||||
@@ -780,13 +783,15 @@ export const loadNewSegmentInSurvey = async (surveyId: string, newSegmentId: str
|
||||
};
|
||||
}
|
||||
|
||||
const modifiedSurvey = {
|
||||
...prismaSurvey,
|
||||
// TODO: Fix this, this happens because the survey type "web" is no longer in the zod types but its required in the schema for migration
|
||||
// @ts-expect-error
|
||||
const modifiedSurvey: TSurvey = {
|
||||
...prismaSurvey, // Properties from prismaSurvey
|
||||
segment: surveySegment,
|
||||
customHeadScriptsMode: prismaSurvey.customHeadScriptsMode,
|
||||
};
|
||||
|
||||
return modifiedSurvey as TSurvey;
|
||||
return modifiedSurvey;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
|
||||
@@ -52,7 +52,7 @@ export const getUser = reactCache(async (id: string): Promise<TUser | null> => {
|
||||
});
|
||||
|
||||
export const getUserByEmail = reactCache(async (email: string): Promise<TUser | null> => {
|
||||
validateInputs([email, z.email()]);
|
||||
validateInputs([email, z.string().email()]);
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findFirst({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as cuid2 from "@paralleldrive/cuid2";
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as crypto from "@/lib/crypto";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import cuid2 from "@paralleldrive/cuid2";
|
||||
import { symmetricEncrypt } from "@/lib/crypto";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
// generate encrypted single use id for the survey
|
||||
export const generateSurveySingleUseId = (isEncrypted: boolean): string => {
|
||||
const cuid = createId();
|
||||
const cuid = cuid2.createId();
|
||||
if (!isEncrypted) {
|
||||
return cuid;
|
||||
}
|
||||
|
||||
+16
-17
@@ -178,6 +178,7 @@
|
||||
"count_attributes": "{count, plural, one {{count} Attribut} other {{count} Attribute}}",
|
||||
"count_contacts": "{count, plural, one {{count} Kontakt} other {{count} Kontakte}}",
|
||||
"count_members": "{count, plural, one {{count} Mitglied} other {{count} Mitglieder}}",
|
||||
"count_questions": "{count, plural, one {{count} Frage} other {{count} Fragen}}",
|
||||
"count_responses": "{count, plural, one {{count} Antwort} other {{count} Antworten}}",
|
||||
"count_selections": "{count, plural, one {{count} Auswahl} other {{count} Auswahlen}}",
|
||||
"create_new_organization": "Neue Organisation erstellen",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Antwortanzahl anzeigen",
|
||||
"shown": "Angezeigt",
|
||||
"size": "Größe",
|
||||
"skip": "Überspringen",
|
||||
"skipped": "Übersprungen",
|
||||
"skips": "Übersprungen",
|
||||
"some_files_failed_to_upload": "Einige Dateien konnten nicht hochgeladen werden",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Website-Umfrage",
|
||||
"weeks": "Wochen",
|
||||
"welcome_card": "Willkommenskarte",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Projektkonfiguration",
|
||||
"workspace_created_successfully": "Projekt erfolgreich erstellt",
|
||||
"workspace_creation_description": "Organisieren Sie Umfragen in Projekten für eine bessere Zugriffskontrolle.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "'Umfrage geschlossen'-Nachricht anpassen",
|
||||
"adjust_survey_closed_message_description": "Ändere die Nachricht, die Besucher sehen, wenn die Umfrage geschlossen ist.",
|
||||
"adjust_the_theme_in_the": "Passe das Thema an in den",
|
||||
"all_are_true": "alle sind wahr",
|
||||
"all_other_answers_will_continue_to": "Alle anderen Antworten werden weiterhin",
|
||||
"allow_multi_select": "Mehrfachauswahl erlauben",
|
||||
"allow_multiple_files": "Mehrere Dateien zulassen",
|
||||
"allow_users_to_select_more_than_one_image": "Erlaube Nutzern, mehr als ein Bild auszuwählen",
|
||||
"and_launch_surveys_in_your_website_or_app": "und Umfragen auf deiner Website oder App starten.",
|
||||
"animation": "Animation",
|
||||
"any_is_true": "mindestens eine ist wahr",
|
||||
"app_survey_description": "Bette eine Umfrage in deine Web-App oder Website ein, um Antworten zu sammeln.",
|
||||
"assign": "Zuweisen =",
|
||||
"audience": "Publikum",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Option {choiceIndex}",
|
||||
"option_used_in_logic_error": "Diese Option wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
|
||||
"optional": "Optional",
|
||||
"options": "Optionen",
|
||||
"options": "Optionen*",
|
||||
"options_used_in_logic_bulk_error": "Die folgenden Optionen werden in der Logik verwendet: {questionIndexes}. Bitte entferne sie zuerst aus der Logik.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Styling für diese Umfrage überschreiben.",
|
||||
"overwrite_global_waiting_time": "Benutzerdefinierte Abkühlphase festlegen",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Frage gelöscht.",
|
||||
"question_duplicated": "Frage dupliziert.",
|
||||
"question_id_updated": "Frage-ID aktualisiert",
|
||||
"question_number": "Frage {number}",
|
||||
"question_used_in_logic_warning_text": "Elemente aus diesem Block werden in einer Logikregel verwendet. Möchten Sie ihn wirklich löschen?",
|
||||
"question_used_in_logic_warning_title": "Logikinkonsistenz",
|
||||
"question_used_in_quota": "Diese Frage wird in der “{quotaName}” Quote verwendet",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Abkühlphase (umfrageübergreifend)",
|
||||
"waiting_time_across_surveys_description": "Um Umfragemüdigkeit zu vermeiden, wähle aus, wie diese Umfrage mit der workspace-weiten Abkühlphase interagiert.",
|
||||
"welcome_message": "Willkommensnachricht",
|
||||
"when": "Wenn",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Ohne Filter können alle deine Nutzer befragt werden.",
|
||||
"you_have_not_created_a_segment_yet": "Du hast noch keinen Segment erstellt.",
|
||||
"your_description_here_recall_information_with": "Deine Beschreibung hier. Informationen abrufen mit @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Wie kann das Unternehmen seine Vision und strategische Ausrichtung verbessern?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Tippe deine Antwort hier...",
|
||||
"back": "Zurück",
|
||||
"block_1": "Block 1",
|
||||
"block_10": "Block 10",
|
||||
"block_2": "Block 2",
|
||||
"block_3": "Block 3",
|
||||
"block_4": "Block 4",
|
||||
"block_5": "Block 5",
|
||||
"block_6": "Block 6",
|
||||
"block_7": "Block 7",
|
||||
"block_8": "Block 8",
|
||||
"block_9": "Block 9",
|
||||
"book_interview": "Interview buchen",
|
||||
"build_product_roadmap_description": "Finde die EINE Sache heraus, die deine Nutzer am meisten wollen, und baue sie.",
|
||||
"build_product_roadmap_name": "Produkt Roadmap erstellen",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ugh, sorry! Können wir irgendwas tun, um deine Erfahrung zu verbessern?",
|
||||
"csat_survey_question_3_placeholder": "Tippe deine Antwort hier...",
|
||||
"cta_description": "Information anzeigen und Benutzer auffordern, eine bestimmte Aktion auszuführen",
|
||||
"custom_survey_block_1_name": "Block 1",
|
||||
"custom_survey_description": "Erstelle eine Umfrage ohne Vorlage.",
|
||||
"custom_survey_name": "Eigene Umfrage erstellen",
|
||||
"custom_survey_question_1_headline": "Was möchtest Du wissen?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Ich fühlte mich beim Benutzen des Systems sicher.",
|
||||
"usability_rating_description": "Bewerte die wahrgenommene Benutzerfreundlichkeit, indem du die Nutzer bittest, ihre Erfahrung mit deinem Produkt mittels eines standardisierten 10-Fragen-Fragebogens zu bewerten.",
|
||||
"usability_score_name": "System Usability Score Survey (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Danke, dass du deine Workflow-Idee mit uns geteilt hast! Wir arbeiten gerade an diesem Feature und dein Feedback hilft uns dabei, genau das zu entwickeln, was du brauchst.",
|
||||
"coming_soon_title": "Wir sind fast da!",
|
||||
"follow_up_label": "Gibt es noch etwas, das du hinzufügen möchtest?",
|
||||
"follow_up_placeholder": "Welche spezifischen Aufgaben möchtest du automatisieren? Gibt es Tools oder Integrationen, die du gerne einbinden würdest?",
|
||||
"generate_button": "Workflow generieren",
|
||||
"heading": "Welchen Workflow möchtest du erstellen?",
|
||||
"placeholder": "Beschreibe den Workflow, den du generieren möchtest...",
|
||||
"subheading": "Generiere deinen Workflow in Sekunden.",
|
||||
"submit_button": "Details hinzufügen",
|
||||
"thank_you_description": "Dein Input hilft uns dabei, das Workflows-Feature zu entwickeln, das du wirklich brauchst. Wir halten dich über unsere Fortschritte auf dem Laufenden.",
|
||||
"thank_you_title": "Danke für dein Feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-17
@@ -178,6 +178,7 @@
|
||||
"count_attributes": "{count, plural, one {{count} attribute} other {{count} attributes}}",
|
||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
|
||||
"count_members": "{count, plural, one {{count} member} other {{count} members}}",
|
||||
"count_questions": "{count, plural, one {{count} question} other {{count} questions}}",
|
||||
"count_responses": "{count, plural, one {{count} response} other {{count} responses}}",
|
||||
"count_selections": "{count, plural, one {{count} selection} other {{count} selections}}",
|
||||
"create_new_organization": "Create new organization",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Show response count",
|
||||
"shown": "Shown",
|
||||
"size": "Size",
|
||||
"skip": "Skip",
|
||||
"skipped": "Skipped",
|
||||
"skips": "Skips",
|
||||
"some_files_failed_to_upload": "Some files failed to upload",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Website Survey",
|
||||
"weeks": "weeks",
|
||||
"welcome_card": "Welcome card",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Workspace Configuration",
|
||||
"workspace_created_successfully": "Workspace created successfully",
|
||||
"workspace_creation_description": "Organize surveys in workspaces for better access control.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Adjust “Survey Closed” message",
|
||||
"adjust_survey_closed_message_description": "Change the message visitors see when the survey is closed.",
|
||||
"adjust_the_theme_in_the": "Adjust the theme in the",
|
||||
"all_are_true": "all are true",
|
||||
"all_other_answers_will_continue_to": "All other answers will continue to",
|
||||
"allow_multi_select": "Allow multi-select",
|
||||
"allow_multiple_files": "Allow multiple files",
|
||||
"allow_users_to_select_more_than_one_image": "Allow users to select more than one image",
|
||||
"and_launch_surveys_in_your_website_or_app": "and launch surveys in your website or app.",
|
||||
"animation": "Animation",
|
||||
"any_is_true": "any is true",
|
||||
"app_survey_description": "Embed a survey in your web app or website to collect responses.",
|
||||
"assign": "Assign =",
|
||||
"audience": "Audience",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Option {choiceIndex}",
|
||||
"option_used_in_logic_error": "This option is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||
"optional": "Optional",
|
||||
"options": "Options",
|
||||
"options": "Options*",
|
||||
"options_used_in_logic_bulk_error": "The following options are used in logic: {questionIndexes}. Please remove them from logic first.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.",
|
||||
"overwrite_global_waiting_time": "Set custom Cooldown Period",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Question deleted.",
|
||||
"question_duplicated": "Question duplicated.",
|
||||
"question_id_updated": "Question ID updated",
|
||||
"question_number": "Question {number}",
|
||||
"question_used_in_logic_warning_text": "Elements from this block are used in a logic rule, are you sure you want to delete it?",
|
||||
"question_used_in_logic_warning_title": "Logic Inconsistency",
|
||||
"question_used_in_quota": "This question is being used in “{quotaName}” quota",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Cooldown Period (across surveys)",
|
||||
"waiting_time_across_surveys_description": "To prevent survey fatigue, choose how this survey interacts with the workspace-wide Cooldown Period.",
|
||||
"welcome_message": "Welcome message",
|
||||
"when": "When",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Without a filter, all of your users can be surveyed.",
|
||||
"you_have_not_created_a_segment_yet": "You have not created a segment yet",
|
||||
"your_description_here_recall_information_with": "Your description here. Recall information with @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "How can the company improve its vision and strategy alignment?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Type your answer here…",
|
||||
"back": "Back",
|
||||
"block_1": "Block 1",
|
||||
"block_10": "Block 10",
|
||||
"block_2": "Block 2",
|
||||
"block_3": "Block 3",
|
||||
"block_4": "Block 4",
|
||||
"block_5": "Block 5",
|
||||
"block_6": "Block 6",
|
||||
"block_7": "Block 7",
|
||||
"block_8": "Block 8",
|
||||
"block_9": "Block 9",
|
||||
"book_interview": "Book interview",
|
||||
"build_product_roadmap_description": "Identify the ONE thing your users want the most and build it.",
|
||||
"build_product_roadmap_name": "Build Product Roadmap",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ugh, sorry! Is there anything we can do to improve your experience?",
|
||||
"csat_survey_question_3_placeholder": "Type your answer here…",
|
||||
"cta_description": "Display information and prompt users to take a specific action",
|
||||
"custom_survey_block_1_name": "Block 1",
|
||||
"custom_survey_description": "Create a survey without template.",
|
||||
"custom_survey_name": "Start from scratch",
|
||||
"custom_survey_question_1_headline": "What would you like to know?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "I felt confident while using the system.",
|
||||
"usability_rating_description": "Measure perceived usability by asking users to rate their experience with your product using a standardized 10-question survey.",
|
||||
"usability_score_name": "System Usability Score (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Thank you for sharing your workflow idea with us! We are currently designing this feature and your feedback will help us build exactly what you need.",
|
||||
"coming_soon_title": "We are almost there!",
|
||||
"follow_up_label": "Is there anything else you'd like to add?",
|
||||
"follow_up_placeholder": "What specific tasks would you like to automate? Any tools or integrations you'd want included?",
|
||||
"generate_button": "Generate workflow",
|
||||
"heading": "What workflow do you want to create?",
|
||||
"placeholder": "Describe the workflow you want to generate...",
|
||||
"subheading": "Generate your workflow in seconds.",
|
||||
"submit_button": "Add details",
|
||||
"thank_you_description": "Your input helps us build the Workflows feature you actually need. We'll keep you posted on our progress.",
|
||||
"thank_you_title": "Thank you for your feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-17
@@ -178,6 +178,7 @@
|
||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
|
||||
"count_members": "{count, plural, one {{count} miembro} other {{count} miembros}}",
|
||||
"count_questions": "{count, plural, one {{count} pregunta} other {{count} preguntas}}",
|
||||
"count_responses": "{count, plural, one {{count} respuesta} other {{count} respuestas}}",
|
||||
"count_selections": "{count, plural, one {{count} selección} other {{count} selecciones}}",
|
||||
"create_new_organization": "Crear organización nueva",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Mostrar recuento de respuestas",
|
||||
"shown": "Mostrado",
|
||||
"size": "Tamaño",
|
||||
"skip": "Omitir",
|
||||
"skipped": "Omitido",
|
||||
"skips": "Omisiones",
|
||||
"some_files_failed_to_upload": "Algunos archivos no se han podido subir",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Encuesta de sitio web",
|
||||
"weeks": "semanas",
|
||||
"welcome_card": "Tarjeta de bienvenida",
|
||||
"workflows": "Flujos de trabajo",
|
||||
"workspace_configuration": "Configuración del proyecto",
|
||||
"workspace_created_successfully": "Proyecto creado correctamente",
|
||||
"workspace_creation_description": "Organiza las encuestas en proyectos para un mejor control de acceso.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensaje 'Encuesta cerrada'",
|
||||
"adjust_survey_closed_message_description": "Cambiar el mensaje que ven los visitantes cuando la encuesta está cerrada.",
|
||||
"adjust_the_theme_in_the": "Ajustar el tema en el",
|
||||
"all_are_true": "todas son verdaderas",
|
||||
"all_other_answers_will_continue_to": "Todas las demás respuestas continuarán",
|
||||
"allow_multi_select": "Permitir selección múltiple",
|
||||
"allow_multiple_files": "Permitir múltiples archivos",
|
||||
"allow_users_to_select_more_than_one_image": "Permitir a los usuarios seleccionar más de una imagen",
|
||||
"and_launch_surveys_in_your_website_or_app": "y lanzar encuestas en tu sitio web o aplicación.",
|
||||
"animation": "Animación",
|
||||
"any_is_true": "alguna es verdadera",
|
||||
"app_survey_description": "Integra una encuesta en tu aplicación web o sitio web para recopilar respuestas.",
|
||||
"assign": "Asignar =",
|
||||
"audience": "Audiencia",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Opción {choiceIndex}",
|
||||
"option_used_in_logic_error": "Esta opción se utiliza en la lógica de la pregunta {questionIndex}. Por favor, elimínala de la lógica primero.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opciones",
|
||||
"options": "Opciones*",
|
||||
"options_used_in_logic_bulk_error": "Las siguientes opciones se utilizan en la lógica: {questionIndexes}. Por favor, elimínalas de la lógica primero.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Anular el tema con estilos individuales para esta encuesta.",
|
||||
"overwrite_global_waiting_time": "Establecer periodo de espera personalizado",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Pregunta eliminada.",
|
||||
"question_duplicated": "Pregunta duplicada.",
|
||||
"question_id_updated": "ID de pregunta actualizado",
|
||||
"question_number": "Pregunta {number}",
|
||||
"question_used_in_logic_warning_text": "Los elementos de este bloque se usan en una regla de lógica, ¿estás seguro de que quieres eliminarlo?",
|
||||
"question_used_in_logic_warning_title": "Inconsistencia de lógica",
|
||||
"question_used_in_quota": "Esta pregunta se está utilizando en la cuota “{quotaName}”",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Periodo de espera (entre encuestas)",
|
||||
"waiting_time_across_surveys_description": "Para evitar la fatiga de encuestas, elige cómo interactúa esta encuesta con el periodo de espera general del espacio de trabajo.",
|
||||
"welcome_message": "Mensaje de bienvenida",
|
||||
"when": "Cuando",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sin un filtro, todos tus usuarios pueden ser encuestados.",
|
||||
"you_have_not_created_a_segment_yet": "Aún no has creado un segmento",
|
||||
"your_description_here_recall_information_with": "Tu descripción aquí. Recupera información con @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "¿Cómo puede mejorar la empresa su alineación de visión y estrategia?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Escribe tu respuesta aquí...",
|
||||
"back": "Atrás",
|
||||
"block_1": "Bloque 1",
|
||||
"block_10": "Bloque 10",
|
||||
"block_2": "Bloque 2",
|
||||
"block_3": "Bloque 3",
|
||||
"block_4": "Bloque 4",
|
||||
"block_5": "Bloque 5",
|
||||
"block_6": "Bloque 6",
|
||||
"block_7": "Bloque 7",
|
||||
"block_8": "Bloque 8",
|
||||
"block_9": "Bloque 9",
|
||||
"book_interview": "Reservar entrevista",
|
||||
"build_product_roadmap_description": "Identifica lo ÚNICO que tus usuarios desean más y constrúyelo.",
|
||||
"build_product_roadmap_name": "Crear hoja de ruta del producto",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Vaya, ¡lo sentimos! ¿Hay algo que podamos hacer para mejorar tu experiencia?",
|
||||
"csat_survey_question_3_placeholder": "Escribe tu respuesta aquí...",
|
||||
"cta_description": "Muestra información y anima a los usuarios a realizar una acción específica",
|
||||
"custom_survey_block_1_name": "Bloque 1",
|
||||
"custom_survey_description": "Crea una encuesta sin plantilla.",
|
||||
"custom_survey_name": "Empezar desde cero",
|
||||
"custom_survey_question_1_headline": "¿Qué te gustaría saber?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Me sentí seguro mientras usaba el sistema.",
|
||||
"usability_rating_description": "Mide la usabilidad percibida pidiendo a los usuarios que valoren su experiencia con tu producto mediante una encuesta estandarizada de 10 preguntas.",
|
||||
"usability_score_name": "Puntuación de usabilidad del sistema (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "¡Gracias por compartir tu idea de flujo de trabajo con nosotros! Actualmente estamos diseñando esta funcionalidad y tus comentarios nos ayudarán a construir exactamente lo que necesitas.",
|
||||
"coming_soon_title": "¡Ya casi estamos!",
|
||||
"follow_up_label": "¿Hay algo más que te gustaría añadir?",
|
||||
"follow_up_placeholder": "¿Qué tareas específicas te gustaría automatizar? ¿Alguna herramienta o integración que quisieras incluir?",
|
||||
"generate_button": "Generar flujo de trabajo",
|
||||
"heading": "¿Qué flujo de trabajo quieres crear?",
|
||||
"placeholder": "Describe el flujo de trabajo que quieres generar...",
|
||||
"subheading": "Genera tu flujo de trabajo en segundos.",
|
||||
"submit_button": "Añadir detalles",
|
||||
"thank_you_description": "Tu aportación nos ayuda a construir la funcionalidad de flujos de trabajo que realmente necesitas. Te mantendremos informado sobre nuestro progreso.",
|
||||
"thank_you_title": "¡Gracias por tus comentarios!"
|
||||
}
|
||||
}
|
||||
|
||||
+17
-18
@@ -176,8 +176,9 @@
|
||||
"copy_code": "Copier le code",
|
||||
"copy_link": "Copier le lien",
|
||||
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attributs}}",
|
||||
"count_contacts": "{count, plural, one {# contact} other {# contacts} }",
|
||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
|
||||
"count_members": "{count, plural, one {{count} membre} other {{count} membres}}",
|
||||
"count_questions": "{count, plural, one {{count} question} other {{count} questions}}",
|
||||
"count_responses": "{count, plural, other {# réponses}}",
|
||||
"count_selections": "{count, plural, one {{count} sélection} other {{count} sélections}}",
|
||||
"create_new_organization": "Créer une nouvelle organisation",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Afficher le nombre de réponses",
|
||||
"shown": "Montré",
|
||||
"size": "Taille",
|
||||
"skip": "Ignorer",
|
||||
"skipped": "Passé",
|
||||
"skips": "Sauter",
|
||||
"some_files_failed_to_upload": "Certains fichiers n'ont pas pu être téléchargés",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Sondage de site web",
|
||||
"weeks": "semaines",
|
||||
"welcome_card": "Carte de bienvenue",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Configuration du projet",
|
||||
"workspace_created_successfully": "Projet créé avec succès",
|
||||
"workspace_creation_description": "Organisez les enquêtes dans des projets pour un meilleur contrôle d'accès.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajuster le message \"Sondage fermé\"",
|
||||
"adjust_survey_closed_message_description": "Modifiez le message que les visiteurs voient lorsque l'enquête est fermée.",
|
||||
"adjust_the_theme_in_the": "Ajustez le thème dans le",
|
||||
"all_are_true": "toutes sont vraies",
|
||||
"all_other_answers_will_continue_to": "Toutes les autres réponses continueront à",
|
||||
"allow_multi_select": "Autoriser la sélection multiple",
|
||||
"allow_multiple_files": "Autoriser plusieurs fichiers",
|
||||
"allow_users_to_select_more_than_one_image": "Permettre aux utilisateurs de sélectionner plusieurs images",
|
||||
"and_launch_surveys_in_your_website_or_app": "et lancez des enquêtes sur votre site web ou votre application.",
|
||||
"animation": "Animation",
|
||||
"any_is_true": "au moins une est vraie",
|
||||
"app_survey_description": "Intégrez une enquête dans votre application web ou votre site web pour collecter des réponses.",
|
||||
"assign": "Attribuer =",
|
||||
"audience": "Public",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Option {choiceIndex}",
|
||||
"option_used_in_logic_error": "Cette option est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
|
||||
"optional": "Optionnel",
|
||||
"options": "Options",
|
||||
"options": "Options*",
|
||||
"options_used_in_logic_bulk_error": "Les options suivantes sont utilisées dans la logique : {questionIndexes}. Veuillez d'abord les supprimer de la logique.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Override the theme with individual styles for this survey.",
|
||||
"overwrite_global_waiting_time": "Définir une période de refroidissement personnalisée",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Question supprimée.",
|
||||
"question_duplicated": "Question dupliquée.",
|
||||
"question_id_updated": "ID de la question mis à jour",
|
||||
"question_number": "Question {number}",
|
||||
"question_used_in_logic_warning_text": "Des éléments de ce bloc sont utilisés dans une règle logique, êtes-vous sûr de vouloir le supprimer ?",
|
||||
"question_used_in_logic_warning_title": "Incohérence de logique",
|
||||
"question_used_in_quota": "Cette question est utilisée dans le quota “{quotaName}”",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Période de refroidissement (entre les sondages)",
|
||||
"waiting_time_across_surveys_description": "Pour éviter la fatigue liée aux sondages, choisissez comment ce sondage interagit avec la période de refroidissement globale de l'espace de travail.",
|
||||
"welcome_message": "Message de bienvenue",
|
||||
"when": "Quand",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sans filtre, tous vos utilisateurs peuvent être sondés.",
|
||||
"you_have_not_created_a_segment_yet": "Tu n'as pas encore créé de segment.",
|
||||
"your_description_here_recall_information_with": "Votre description ici. Rappelez-vous des informations avec @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Comment l'entreprise peut-elle améliorer l'alignement de sa vision et de sa stratégie ?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Entrez votre réponse ici...",
|
||||
"back": "Retour",
|
||||
"block_1": "Bloc 1",
|
||||
"block_10": "Bloc 10",
|
||||
"block_2": "Bloc 2",
|
||||
"block_3": "Bloc 3",
|
||||
"block_4": "Bloc 4",
|
||||
"block_5": "Bloc 5",
|
||||
"block_6": "Bloc 6",
|
||||
"block_7": "Bloc 7",
|
||||
"block_8": "Bloc 8",
|
||||
"block_9": "Bloc 9",
|
||||
"book_interview": "Réserver un entretien",
|
||||
"build_product_roadmap_description": "Identifiez la chose UNIQUE que vos utilisateurs désirent le plus et construisez-la.",
|
||||
"build_product_roadmap_name": "Élaborer la feuille de route du produit",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ah, désolé ! Y a-t-il quelque chose que nous puissions faire pour améliorer votre expérience ?",
|
||||
"csat_survey_question_3_placeholder": "Entrez votre réponse ici...",
|
||||
"cta_description": "Afficher des informations et inciter les utilisateurs à effectuer une action spécifique",
|
||||
"custom_survey_block_1_name": "Bloc 1",
|
||||
"custom_survey_description": "Créez une enquête sans utiliser de modèle.",
|
||||
"custom_survey_name": "Tout créer moi-même",
|
||||
"custom_survey_question_1_headline": "Que voudriez-vous savoir ?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Je me suis senti confiant en utilisant le système.",
|
||||
"usability_rating_description": "Mesurez la convivialité perçue en demandant aux utilisateurs d'évaluer leur expérience avec votre produit via un sondage standardisé de 10 questions.",
|
||||
"usability_score_name": "Score d'Utilisabilité du Système (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Merci d'avoir partagé votre idée de workflow avec nous ! Nous concevons actuellement cette fonctionnalité et vos retours nous aideront à créer exactement ce dont vous avez besoin.",
|
||||
"coming_soon_title": "Nous y sommes presque !",
|
||||
"follow_up_label": "Y a-t-il autre chose que vous aimeriez ajouter ?",
|
||||
"follow_up_placeholder": "Quelles tâches spécifiques aimeriez-vous automatiser ? Des outils ou intégrations que vous souhaiteriez inclure ?",
|
||||
"generate_button": "Générer le workflow",
|
||||
"heading": "Quel workflow souhaitez-vous créer ?",
|
||||
"placeholder": "Décrivez le workflow que vous souhaitez générer...",
|
||||
"subheading": "Générez votre workflow en quelques secondes.",
|
||||
"submit_button": "Ajouter des détails",
|
||||
"thank_you_description": "Votre contribution nous aide à créer la fonctionnalité Workflows dont vous avez réellement besoin. Nous vous tiendrons informé de nos progrès.",
|
||||
"thank_you_title": "Merci pour vos retours !"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-17
@@ -178,6 +178,7 @@
|
||||
"count_attributes": "{count, plural, one {{count} attribútum} other {{count} attribútum}}",
|
||||
"count_contacts": "{count, plural, one {{count} partner} other {{count} partner}}",
|
||||
"count_members": "{count, plural, one {{count} tag} other {{count} tag}}",
|
||||
"count_questions": "{count, plural, one {{count} kérdés} other {{count} kérdés}}",
|
||||
"count_responses": "{count, plural, one {{count} válasz} other {{count} válasz}}",
|
||||
"count_selections": "{count, plural, one {{count} kijelölés} other {{count} kijelölés}}",
|
||||
"create_new_organization": "Új szervezet létrehozása",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Válaszok számának megjelenítése",
|
||||
"shown": "Megjelenítve",
|
||||
"size": "Méret",
|
||||
"skip": "Kihagyás",
|
||||
"skipped": "Kihagyva",
|
||||
"skips": "Kihagyja",
|
||||
"some_files_failed_to_upload": "Néhány fájlt nem sikerült feltölteni",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Webhely kérdőív",
|
||||
"weeks": "hetek",
|
||||
"welcome_card": "Üdvözlő kártya",
|
||||
"workflows": "Munkafolyamatok",
|
||||
"workspace_configuration": "Munkaterület beállítása",
|
||||
"workspace_created_successfully": "A munkaterület sikeresen létrehozva",
|
||||
"workspace_creation_description": "Kérdőívek munkaterületekre szervezése a jobb hozzáférés-vezérlés érdekében.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "A „Kérdőív lezárva” üzenet módosítása",
|
||||
"adjust_survey_closed_message_description": "Annak az üzenetnek a megváltoztatása, amelyet a látogatók akkor látnak, amikor a kérdőív lezárul.",
|
||||
"adjust_the_theme_in_the": "A téma beállítása ebben:",
|
||||
"all_are_true": "az összes igaz",
|
||||
"all_other_answers_will_continue_to": "Az összes többi válasz továbbra is",
|
||||
"allow_multi_select": "Több választás engedélyezése",
|
||||
"allow_multiple_files": "Több fájl engedélyezése",
|
||||
"allow_users_to_select_more_than_one_image": "Lehetővé tétel a felhasználóknak, hogy egynél több képet válasszanak ki",
|
||||
"and_launch_surveys_in_your_website_or_app": "és kérdőívek indítása a webhelyén vagy az alkalmazásában.",
|
||||
"animation": "Animáció",
|
||||
"any_is_true": "bármelyik igaz",
|
||||
"app_survey_description": "Egy kérdőív beágyazása a webalkalmazásába vagy webhelyére a válaszok gyűjtéséhez.",
|
||||
"assign": "= hozzárendelése",
|
||||
"audience": "Közönség",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "{choiceIndex}. lehetőség",
|
||||
"option_used_in_logic_error": "Ez a lehetőség használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
|
||||
"optional": "Választható",
|
||||
"options": "Beállítások",
|
||||
"options": "Beállítások*",
|
||||
"options_used_in_logic_bulk_error": "A következő lehetőségek használatban vannak a logikában: {questionIndexes}. Először távolítsa el azokat a logikából.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "A téma felülírása egyéni stílusokkal ennél a kérdőívnél.",
|
||||
"overwrite_global_waiting_time": "Egyéni várakozási időszak beállítása",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Kérdés törölve.",
|
||||
"question_duplicated": "Kérdés megkettőzve.",
|
||||
"question_id_updated": "Kérdésazonosító frissítve",
|
||||
"question_number": "{number}. kérdés",
|
||||
"question_used_in_logic_warning_text": "Ezen blokkból származó elemek egy logikai szabályban vannak használva, biztosan törölni szeretné?",
|
||||
"question_used_in_logic_warning_title": "Logikai következetlenség",
|
||||
"question_used_in_quota": "Ez a kérdés használatban van a(z) „{quotaName}” kvótában",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Várakozási időszak (kérdőívek között)",
|
||||
"waiting_time_across_surveys_description": "A kérdőívekbe való belefáradás megakadályozásához válassza ki, hogy ez a kérdőív hogyan lép kölcsönhatásba a munkaterület-szintű várakozási időszakkal.",
|
||||
"welcome_message": "Üdvözlő üzenet",
|
||||
"when": "Amikor",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Szűrő nélkül az összes felhasználója megkérdezhető.",
|
||||
"you_have_not_created_a_segment_yet": "Még nem hozott létre szakaszt",
|
||||
"your_description_here_recall_information_with": "Ide jön a leírás. Információk visszahívása a @ karakterrel.",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Hogyan tudná javítani a vállalat a jövőképe és stratégiája összehangolását?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Írja be ide a válaszát…",
|
||||
"back": "Vissza",
|
||||
"block_1": "1. blokk",
|
||||
"block_10": "10. blokk",
|
||||
"block_2": "2. blokk",
|
||||
"block_3": "3. blokk",
|
||||
"block_4": "4. blokk",
|
||||
"block_5": "5. blokk",
|
||||
"block_6": "6. blokk",
|
||||
"block_7": "7. blokk",
|
||||
"block_8": "8. blokk",
|
||||
"block_9": "9. blokk",
|
||||
"book_interview": "Interjú foglalása",
|
||||
"build_product_roadmap_description": "A felhasználók által leginkább igényelt EGY dolog azonosítása és összeállítása.",
|
||||
"build_product_roadmap_name": "Termékútiterv összeállítása",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Jaj, bocsánat! Tehetünk valamit, amivel javíthatnánk az élményén?",
|
||||
"csat_survey_question_3_placeholder": "Írja be ide a válaszát…",
|
||||
"cta_description": "Információk megjelenítése és a felhasználók felkérése egy bizonyos művelet elvégzésére",
|
||||
"custom_survey_block_1_name": "1. blokk",
|
||||
"custom_survey_description": "Kérdőív létrehozása sablon nélkül.",
|
||||
"custom_survey_name": "Kezdés a semmiből",
|
||||
"custom_survey_question_1_headline": "Mit szeretne tudni?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Magabiztosnak éreztem magam a rendszer használata során.",
|
||||
"usability_rating_description": "Az érzékelt használhatóság mérése arra kérve a felhasználókat, hogy értékeljék a termékkel kapcsolatos tapasztalataikat egy szabványosított, 10 kérdésből álló kérdőív használatával.",
|
||||
"usability_score_name": "Rendszer-használhatósági pontszám (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Köszönjük, hogy megosztotta velünk a munkafolyamat ötletét! Jelenleg ezen a funkción dolgozunk, és a visszajelzése segít nekünk pontosan azt megépíteni, amire szüksége van.",
|
||||
"coming_soon_title": "Majdnem kész vagyunk!",
|
||||
"follow_up_label": "Van még valami, amit hozzá szeretne tenni?",
|
||||
"follow_up_placeholder": "Milyen konkrét feladatokat szeretne automatizálni? Vannak olyan eszközök vagy integrációk, amelyeket szeretne belevenni?",
|
||||
"generate_button": "Munkafolyamat generálása",
|
||||
"heading": "Milyen munkafolyamatot szeretne létrehozni?",
|
||||
"placeholder": "Írja le a munkafolyamatot, amelyet generálni szeretne...",
|
||||
"subheading": "Generálja le a munkafolyamatát másodpercek alatt.",
|
||||
"submit_button": "Részletek hozzáadása",
|
||||
"thank_you_description": "A visszajelzése segít nekünk megépíteni azt a munkafolyamatok funkciót, amelyre tényleg szüksége van. Folyamatosan tájékoztatjuk az előrehaladásról.",
|
||||
"thank_you_title": "Köszönjük a visszajelzését!"
|
||||
}
|
||||
}
|
||||
|
||||
+17
-18
@@ -176,8 +176,9 @@
|
||||
"copy_code": "コードをコピー",
|
||||
"copy_link": "リンクをコピー",
|
||||
"count_attributes": "{count, plural, other {{count}個の属性}}",
|
||||
"count_contacts": "{count, plural, other {# 件の連絡先}}",
|
||||
"count_contacts": "{count, plural, other {{count}件の連絡先}}",
|
||||
"count_members": "{count, plural, other {{count}人のメンバー}}",
|
||||
"count_questions": "{count, plural, other {# 件の質問}}",
|
||||
"count_responses": "{count, plural, other {# 件の回答}}",
|
||||
"count_selections": "{count, plural, other {{count}件選択中}}",
|
||||
"create_new_organization": "新しい組織を作成",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "回答数を表示",
|
||||
"shown": "表示済み",
|
||||
"size": "サイズ",
|
||||
"skip": "スキップ",
|
||||
"skipped": "スキップ済み",
|
||||
"skips": "スキップ数",
|
||||
"some_files_failed_to_upload": "一部のファイルのアップロードに失敗しました",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "ウェブサイトフォーム",
|
||||
"weeks": "週間",
|
||||
"welcome_card": "ウェルカムカード",
|
||||
"workflows": "ワークフロー",
|
||||
"workspace_configuration": "ワークスペース設定",
|
||||
"workspace_created_successfully": "ワークスペースが正常に作成されました",
|
||||
"workspace_creation_description": "アクセス制御を改善するために、フォームをワークスペースで整理します。",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "「フォームはクローズしました」メッセージを調整",
|
||||
"adjust_survey_closed_message_description": "フォームがクローズしたときに訪問者が見るメッセージを変更します。",
|
||||
"adjust_the_theme_in_the": "テーマを",
|
||||
"all_are_true": "すべてが真である",
|
||||
"all_other_answers_will_continue_to": "他のすべての回答は引き続き",
|
||||
"allow_multi_select": "複数選択を許可",
|
||||
"allow_multiple_files": "複数のファイルを許可",
|
||||
"allow_users_to_select_more_than_one_image": "ユーザーが複数の画像を選択できるようにする",
|
||||
"and_launch_surveys_in_your_website_or_app": "ウェブサイトやアプリでフォームを公開できます。",
|
||||
"animation": "アニメーション",
|
||||
"any_is_true": "いずれかが真",
|
||||
"app_survey_description": "回答を収集するために、ウェブアプリまたはウェブサイトにフォームを埋め込みます。",
|
||||
"assign": "割り当て =",
|
||||
"audience": "オーディエンス",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "オプション {choiceIndex}",
|
||||
"option_used_in_logic_error": "このオプションは質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
"optional": "オプション",
|
||||
"options": "オプション",
|
||||
"options": "オプション*",
|
||||
"options_used_in_logic_bulk_error": "以下のオプションはロジックで使用されています:{questionIndexes}。まず、ロジックから削除してください。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "このフォームの個別のスタイルでテーマを上書きします。",
|
||||
"overwrite_global_waiting_time": "カスタムクールダウン期間を設定",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "質問を削除しました。",
|
||||
"question_duplicated": "質問を複製しました。",
|
||||
"question_id_updated": "質問IDを更新しました",
|
||||
"question_number": "質問 {number}",
|
||||
"question_used_in_logic_warning_text": "このブロックの要素はロジックルールで使用されていますが、本当に削除しますか?",
|
||||
"question_used_in_logic_warning_title": "ロジックの不整合",
|
||||
"question_used_in_quota": "この質問は“{quotaName}”クォータで使用されています",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "クールダウン期間(アンケート全体)",
|
||||
"waiting_time_across_surveys_description": "アンケート疲れを防ぐため、このアンケートがワークスペース全体のクールダウン期間とどのように連動するかを選択してください。",
|
||||
"welcome_message": "ウェルカムメッセージ",
|
||||
"when": "条件",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "フィルターがなければ、すべてのユーザーがフォームに回答できます。",
|
||||
"you_have_not_created_a_segment_yet": "まだセグメントを作成していません",
|
||||
"your_description_here_recall_information_with": "ここにあなたの説明。@ で情報を呼び出す",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "会社はビジョンと戦略の整合性をどのように改善できますか?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "ここに回答を入力してください...",
|
||||
"back": "戻る",
|
||||
"block_1": "ブロック 1",
|
||||
"block_10": "ブロック 10",
|
||||
"block_2": "ブロック 2",
|
||||
"block_3": "ブロック 3",
|
||||
"block_4": "ブロック 4",
|
||||
"block_5": "ブロック 5",
|
||||
"block_6": "ブロック 6",
|
||||
"block_7": "ブロック 7",
|
||||
"block_8": "ブロック 8",
|
||||
"block_9": "ブロック 9",
|
||||
"book_interview": "面談を予約する",
|
||||
"build_product_roadmap_description": "ユーザーが最も望んでいる「たった一つ」のものを特定し、構築する。",
|
||||
"build_product_roadmap_name": "製品ロードマップの構築",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "申し訳ありません!体験を改善するために何かできることはありますか?",
|
||||
"csat_survey_question_3_placeholder": "ここに回答を入力してください...",
|
||||
"cta_description": "情報を表示し、特定の行動を促す",
|
||||
"custom_survey_block_1_name": "ブロック1",
|
||||
"custom_survey_description": "テンプレートを使わずにアンケートを作成する。",
|
||||
"custom_survey_name": "最初から始める",
|
||||
"custom_survey_question_1_headline": "何を知りたいですか?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "システムを使っている間、自信がありました。",
|
||||
"usability_rating_description": "標準化された10の質問アンケートを使用して、製品に対するユーザーの体験を評価し、知覚された使いやすさを測定する。",
|
||||
"usability_score_name": "システムユーザビリティスコア(SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "ワークフローのアイデアを共有していただきありがとうございます!現在この機能を設計中で、あなたのフィードバックは私たちが必要とされる機能を構築するのに役立ちます。",
|
||||
"coming_soon_title": "もうすぐ完成です!",
|
||||
"follow_up_label": "他に追加したいことはありますか?",
|
||||
"follow_up_placeholder": "どのような作業を自動化したいですか?含めたいツールや連携機能はありますか?",
|
||||
"generate_button": "ワークフローを生成",
|
||||
"heading": "どのようなワークフローを作成しますか?",
|
||||
"placeholder": "生成したいワークフローを説明してください...",
|
||||
"subheading": "数秒でワークフローを生成します。",
|
||||
"submit_button": "詳細を追加",
|
||||
"thank_you_description": "あなたの意見は、実際に必要とされるワークフロー機能の構築に役立ちます。進捗状況をお知らせします。",
|
||||
"thank_you_title": "フィードバックありがとうございます!"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-17
@@ -178,6 +178,7 @@
|
||||
"count_attributes": "{count, plural, one {{count} attribuut} other {{count} attributen}}",
|
||||
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacten}}",
|
||||
"count_members": "{count, plural, one {{count} lid} other {{count} leden}}",
|
||||
"count_questions": "{count, plural, one {{count} vraag} other {{count} vragen}}",
|
||||
"count_responses": "{count, plural, one {{count} reactie} other {{count} reacties}}",
|
||||
"count_selections": "{count, plural, one {{count} selectie} other {{count} selecties}}",
|
||||
"create_new_organization": "Creëer een nieuwe organisatie",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Toon het aantal reacties",
|
||||
"shown": "Getoond",
|
||||
"size": "Maat",
|
||||
"skip": "Overslaan",
|
||||
"skipped": "Overgeslagen",
|
||||
"skips": "Overslaan",
|
||||
"some_files_failed_to_upload": "Sommige bestanden konden niet worden geüpload",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Website-enquête",
|
||||
"weeks": "weken",
|
||||
"welcome_card": "Welkomstkaart",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Werkruimte-configuratie",
|
||||
"workspace_created_successfully": "Project succesvol aangemaakt",
|
||||
"workspace_creation_description": "Organiseer enquêtes in werkruimtes voor beter toegangsbeheer.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Pas het bericht 'Enquête gesloten' aan",
|
||||
"adjust_survey_closed_message_description": "Wijzig het bericht dat bezoekers zien wanneer de enquête wordt gesloten.",
|
||||
"adjust_the_theme_in_the": "Pas het thema aan in de",
|
||||
"all_are_true": "alle zijn waar",
|
||||
"all_other_answers_will_continue_to": "Alle andere antwoorden blijven hetzelfde",
|
||||
"allow_multi_select": "Multi-select toestaan",
|
||||
"allow_multiple_files": "Meerdere bestanden toestaan",
|
||||
"allow_users_to_select_more_than_one_image": "Sta gebruikers toe meer dan één afbeelding te selecteren",
|
||||
"and_launch_surveys_in_your_website_or_app": "en start enquêtes op uw website of app.",
|
||||
"animation": "Animatie",
|
||||
"any_is_true": "een is waar",
|
||||
"app_survey_description": "Sluit een enquête in uw web-app of website in om reacties te verzamelen.",
|
||||
"assign": "Toewijzen =",
|
||||
"audience": "Publiek",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Optie {choiceIndex}",
|
||||
"option_used_in_logic_error": "Deze optie wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
||||
"optional": "Optioneel",
|
||||
"options": "Opties",
|
||||
"options": "Opties*",
|
||||
"options_used_in_logic_bulk_error": "De volgende opties worden gebruikt in logica: {questionIndexes}. Verwijder ze eerst uit de logica.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Overschrijf het thema met individuele stijlen voor deze enquête.",
|
||||
"overwrite_global_waiting_time": "Aangepaste afkoelperiode instellen",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Vraag verwijderd.",
|
||||
"question_duplicated": "Vraag dubbel gesteld.",
|
||||
"question_id_updated": "Vraag-ID bijgewerkt",
|
||||
"question_number": "Vraag {number}",
|
||||
"question_used_in_logic_warning_text": "Elementen uit dit blok worden gebruikt in een logische regel, weet je zeker dat je het wilt verwijderen?",
|
||||
"question_used_in_logic_warning_title": "Logica-inconsistentie",
|
||||
"question_used_in_quota": "Deze vraag wordt gebruikt in het quotum “{quotaName}”",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Afkoelperiode (voor alle enquêtes)",
|
||||
"waiting_time_across_surveys_description": "Om enquêtemoeheid te voorkomen, kies hoe deze enquête omgaat met de workspace-brede afkoelperiode.",
|
||||
"welcome_message": "Welkomstbericht",
|
||||
"when": "Wanneer",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Zonder filter kunnen al uw gebruikers worden bevraagd.",
|
||||
"you_have_not_created_a_segment_yet": "U heeft nog geen segment aangemaakt",
|
||||
"your_description_here_recall_information_with": "Uw beschrijving hier. Roep informatie op met @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Hoe kan het bedrijf de afstemming van zijn visie en strategie verbeteren?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Typ hier uw antwoord...",
|
||||
"back": "Rug",
|
||||
"block_1": "Blok 1",
|
||||
"block_10": "Blok 10",
|
||||
"block_2": "Blok 2",
|
||||
"block_3": "Blok 3",
|
||||
"block_4": "Blok 4",
|
||||
"block_5": "Blok 5",
|
||||
"block_6": "Blok 6",
|
||||
"block_7": "Blok 7",
|
||||
"block_8": "Blok 8",
|
||||
"block_9": "Blok 9",
|
||||
"book_interview": "Boek interview",
|
||||
"build_product_roadmap_description": "Identificeer het ENE wat uw gebruikers het liefst willen en bouw het.",
|
||||
"build_product_roadmap_name": "Productroadmap opstellen",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Euh, sorry! Kunnen we iets doen om uw ervaring te verbeteren?",
|
||||
"csat_survey_question_3_placeholder": "Typ hier uw antwoord...",
|
||||
"cta_description": "Geef informatie weer en vraag gebruikers om een specifieke actie te ondernemen",
|
||||
"custom_survey_block_1_name": "Blok 1",
|
||||
"custom_survey_description": "Maak een enquête zonder sjabloon.",
|
||||
"custom_survey_name": "Begin helemaal opnieuw",
|
||||
"custom_survey_question_1_headline": "Wat zou je willen weten?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Ik voelde me zelfverzekerd tijdens het gebruik van het systeem.",
|
||||
"usability_rating_description": "Meet de waargenomen bruikbaarheid door gebruikers te vragen hun ervaring met uw product te beoordelen met behulp van een gestandaardiseerde enquête met tien vragen.",
|
||||
"usability_score_name": "Systeembruikbaarheidsscore (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Bedankt voor het delen van je workflow-idee met ons! We zijn momenteel bezig met het ontwerpen van deze functie en jouw feedback helpt ons om precies te bouwen wat je nodig hebt.",
|
||||
"coming_soon_title": "We zijn er bijna!",
|
||||
"follow_up_label": "Is er nog iets dat je wilt toevoegen?",
|
||||
"follow_up_placeholder": "Welke specifieke taken wil je automatiseren? Zijn er tools of integraties die je graag zou willen zien?",
|
||||
"generate_button": "Genereer workflow",
|
||||
"heading": "Welke workflow wil je maken?",
|
||||
"placeholder": "Beschrijf de workflow die je wilt genereren...",
|
||||
"subheading": "Genereer je workflow in enkele seconden.",
|
||||
"submit_button": "Voeg details toe",
|
||||
"thank_you_description": "Jouw input helpt ons om de Workflows-functie te bouwen die je echt nodig hebt. We houden je op de hoogte van onze voortgang.",
|
||||
"thank_you_title": "Bedankt voor je feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
+17
-18
@@ -176,8 +176,9 @@
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar Link",
|
||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||
"count_contacts": "{count, plural, one {# contato} other {# contatos} }",
|
||||
"count_contacts": "{count, plural, one {{count} contato} other {{count} contatos}}",
|
||||
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
|
||||
"count_questions": "{count, plural, one {{count} pergunta} other {{count} perguntas}}",
|
||||
"count_responses": "{count, plural, other {# respostas}}",
|
||||
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Mostrar contagem de respostas",
|
||||
"shown": "mostrado",
|
||||
"size": "Tamanho",
|
||||
"skip": "Pular",
|
||||
"skipped": "Pulou",
|
||||
"skips": "Pula",
|
||||
"some_files_failed_to_upload": "Alguns arquivos falharam ao enviar",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Pesquisa de Site",
|
||||
"weeks": "semanas",
|
||||
"welcome_card": "Cartão de boas-vindas",
|
||||
"workflows": "Fluxos de trabalho",
|
||||
"workspace_configuration": "Configuração do projeto",
|
||||
"workspace_created_successfully": "Projeto criado com sucesso",
|
||||
"workspace_creation_description": "Organize pesquisas em projetos para melhor controle de acesso.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensagem 'Pesquisa Encerrada''",
|
||||
"adjust_survey_closed_message_description": "Mude a mensagem que os visitantes veem quando a pesquisa está fechada.",
|
||||
"adjust_the_theme_in_the": "Ajuste o tema no",
|
||||
"all_are_true": "todas são verdadeiras",
|
||||
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
|
||||
"allow_multi_select": "Permitir seleção múltipla",
|
||||
"allow_multiple_files": "Permitir vários arquivos",
|
||||
"allow_users_to_select_more_than_one_image": "Permitir que os usuários selecionem mais de uma imagem",
|
||||
"and_launch_surveys_in_your_website_or_app": "e lançar pesquisas no seu site ou app.",
|
||||
"animation": "animação",
|
||||
"any_is_true": "qualquer uma é verdadeira",
|
||||
"app_survey_description": "Embuta uma pesquisa no seu app ou site para coletar respostas.",
|
||||
"assign": "atribuir =",
|
||||
"audience": "Público",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Opção {choiceIndex}",
|
||||
"option_used_in_logic_error": "Esta opção é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opções",
|
||||
"options": "Opções*",
|
||||
"options_used_in_logic_bulk_error": "As seguintes opções são usadas na lógica: {questionIndexes}. Por favor, remova-as da lógica primeiro.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Substitua o tema com estilos individuais para essa pesquisa.",
|
||||
"overwrite_global_waiting_time": "Definir período de espera personalizado",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Pergunta deletada.",
|
||||
"question_duplicated": "Pergunta duplicada.",
|
||||
"question_id_updated": "ID da pergunta atualizado",
|
||||
"question_number": "Pergunta {number}",
|
||||
"question_used_in_logic_warning_text": "Elementos deste bloco são usados em uma regra de lógica, tem certeza de que deseja excluí-lo?",
|
||||
"question_used_in_logic_warning_title": "Inconsistência de lógica",
|
||||
"question_used_in_quota": "Esta pergunta está sendo usada na cota \"{quotaName}\"",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Período de espera (entre pesquisas)",
|
||||
"waiting_time_across_surveys_description": "Para evitar fadiga de pesquisas, escolha como esta pesquisa interage com o período de espera geral do workspace.",
|
||||
"welcome_message": "Mensagem de boas-vindas",
|
||||
"when": "Quando",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus usuários podem ser pesquisados.",
|
||||
"you_have_not_created_a_segment_yet": "Você ainda não criou um segmento.",
|
||||
"your_description_here_recall_information_with": "Sua descrição aqui. Lembre-se de informações com @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Como a empresa pode melhorar sua visão e direcionamento estratégico?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Digite sua resposta aqui...",
|
||||
"back": "voltar",
|
||||
"block_1": "Bloco 1",
|
||||
"block_10": "Bloco 10",
|
||||
"block_2": "Bloco 2",
|
||||
"block_3": "Bloco 3",
|
||||
"block_4": "Bloco 4",
|
||||
"block_5": "Bloco 5",
|
||||
"block_6": "Bloco 6",
|
||||
"block_7": "Bloco 7",
|
||||
"block_8": "Bloco 8",
|
||||
"block_9": "Bloco 9",
|
||||
"book_interview": "Marcar entrevista",
|
||||
"build_product_roadmap_description": "Identifique a ÚNICA coisa que seus usuários mais querem e construa isso.",
|
||||
"build_product_roadmap_name": "Construir Roteiro do Produto",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ah, foi mal! Tem algo que a gente possa fazer pra melhorar sua experiência?",
|
||||
"csat_survey_question_3_placeholder": "Digite sua resposta aqui...",
|
||||
"cta_description": "Mostrar informações e pedir para os usuários tomarem uma ação específica",
|
||||
"custom_survey_block_1_name": "Bloco 1",
|
||||
"custom_survey_description": "Crie uma pesquisa sem modelo.",
|
||||
"custom_survey_name": "Começar do zero",
|
||||
"custom_survey_question_1_headline": "O que você gostaria de saber?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Me senti confiante ao usar o sistema.",
|
||||
"usability_rating_description": "Meça a usabilidade percebida perguntando aos usuários para avaliar sua experiência com seu produto usando uma pesquisa padronizada de 10 perguntas.",
|
||||
"usability_score_name": "Pontuação de Usabilidade do Sistema (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Obrigado por compartilhar sua ideia de fluxo de trabalho conosco! Estamos atualmente projetando este recurso e seu feedback nos ajudará a construir exatamente o que você precisa.",
|
||||
"coming_soon_title": "Estamos quase lá!",
|
||||
"follow_up_label": "Há algo mais que você gostaria de adicionar?",
|
||||
"follow_up_placeholder": "Quais tarefas específicas você gostaria de automatizar? Alguma ferramenta ou integração que você gostaria de incluir?",
|
||||
"generate_button": "Gerar fluxo de trabalho",
|
||||
"heading": "Qual fluxo de trabalho você quer criar?",
|
||||
"placeholder": "Descreva o fluxo de trabalho que você quer gerar...",
|
||||
"subheading": "Gere seu fluxo de trabalho em segundos.",
|
||||
"submit_button": "Adicionar detalhes",
|
||||
"thank_you_description": "Sua contribuição nos ajuda a construir o recurso de Fluxos de trabalho que você realmente precisa. Manteremos você informado sobre nosso progresso.",
|
||||
"thank_you_title": "Obrigado pelo seu feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
+17
-18
@@ -176,8 +176,9 @@
|
||||
"copy_code": "Copiar código",
|
||||
"copy_link": "Copiar Link",
|
||||
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
|
||||
"count_contacts": "{count, plural, one {# contacto} other {# contactos} }",
|
||||
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
|
||||
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
|
||||
"count_questions": "{count, plural, one {{count} pergunta} other {{count} perguntas}}",
|
||||
"count_responses": "{count, plural, other {# respostas}}",
|
||||
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
|
||||
"create_new_organization": "Criar nova organização",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Mostrar contagem de respostas",
|
||||
"shown": "Mostrado",
|
||||
"size": "Tamanho",
|
||||
"skip": "Saltar",
|
||||
"skipped": "Ignorado",
|
||||
"skips": "Saltos",
|
||||
"some_files_failed_to_upload": "Alguns ficheiros falharam ao carregar",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Inquérito do Website",
|
||||
"weeks": "semanas",
|
||||
"welcome_card": "Cartão de boas-vindas",
|
||||
"workflows": "Fluxos de trabalho",
|
||||
"workspace_configuration": "Configuração do projeto",
|
||||
"workspace_created_successfully": "Projeto criado com sucesso",
|
||||
"workspace_creation_description": "Organize inquéritos em projetos para melhor controlo de acesso.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajustar mensagem de 'Inquérito Fechado'",
|
||||
"adjust_survey_closed_message_description": "Alterar a mensagem que os visitantes veem quando o inquérito está fechado.",
|
||||
"adjust_the_theme_in_the": "Ajustar o tema no",
|
||||
"all_are_true": "todas são verdadeiras",
|
||||
"all_other_answers_will_continue_to": "Todas as outras respostas continuarão a",
|
||||
"allow_multi_select": "Permitir seleção múltipla",
|
||||
"allow_multiple_files": "Permitir vários ficheiros",
|
||||
"allow_users_to_select_more_than_one_image": "Permitir aos utilizadores selecionar mais do que uma imagem",
|
||||
"and_launch_surveys_in_your_website_or_app": "e lance inquéritos no seu site ou aplicação.",
|
||||
"animation": "Animação",
|
||||
"any_is_true": "qualquer uma é verdadeira",
|
||||
"app_survey_description": "Incorpore um inquérito na sua aplicação web ou site para recolher respostas.",
|
||||
"assign": "Atribuir =",
|
||||
"audience": "Público",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Opção {choiceIndex}",
|
||||
"option_used_in_logic_error": "Esta opção é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"optional": "Opcional",
|
||||
"options": "Opções",
|
||||
"options": "Opções*",
|
||||
"options_used_in_logic_bulk_error": "As seguintes opções são usadas na lógica: {questionIndexes}. Por favor, remova-as da lógica primeiro.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Substituir o tema com estilos individuais para este inquérito.",
|
||||
"overwrite_global_waiting_time": "Definir período de espera personalizado",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Pergunta eliminada.",
|
||||
"question_duplicated": "Pergunta duplicada.",
|
||||
"question_id_updated": "ID da pergunta atualizado",
|
||||
"question_number": "Pergunta {number}",
|
||||
"question_used_in_logic_warning_text": "Os elementos deste bloco são utilizados numa regra de lógica, tem a certeza de que pretende eliminá-lo?",
|
||||
"question_used_in_logic_warning_title": "Inconsistência de lógica",
|
||||
"question_used_in_quota": "Esta pergunta está a ser usada na quota \"{quotaName}\"",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Período de espera (entre inquéritos)",
|
||||
"waiting_time_across_surveys_description": "Para prevenir fadiga de inquéritos, escolha como este inquérito interage com o período de espera geral do espaço de trabalho.",
|
||||
"welcome_message": "Mensagem de boas-vindas",
|
||||
"when": "Quando",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus utilizadores podem ser pesquisados.",
|
||||
"you_have_not_created_a_segment_yet": "Ainda não criou um segmento",
|
||||
"your_description_here_recall_information_with": "A sua descrição aqui. Recorde a informação com @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Como pode a empresa melhorar o alinhamento da sua visão e estratégia?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Escreva a sua resposta aqui...",
|
||||
"back": "Voltar",
|
||||
"block_1": "Bloco 1",
|
||||
"block_10": "Bloco 10",
|
||||
"block_2": "Bloco 2",
|
||||
"block_3": "Bloco 3",
|
||||
"block_4": "Bloco 4",
|
||||
"block_5": "Bloco 5",
|
||||
"block_6": "Bloco 6",
|
||||
"block_7": "Bloco 7",
|
||||
"block_8": "Bloco 8",
|
||||
"block_9": "Bloco 9",
|
||||
"book_interview": "Agendar entrevista",
|
||||
"build_product_roadmap_description": "Identifique a ÚNICA coisa que os seus utilizadores mais querem e construa-a.",
|
||||
"build_product_roadmap_name": "Construir Roteiro do Produto",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Oh, desculpe! Há algo que possamos fazer para melhorar a sua experiência?",
|
||||
"csat_survey_question_3_placeholder": "Escreva a sua resposta aqui...",
|
||||
"cta_description": "Exibir informações e solicitar aos utilizadores que tomem uma ação específica",
|
||||
"custom_survey_block_1_name": "Bloco 1",
|
||||
"custom_survey_description": "Crie um inquérito sem modelo.",
|
||||
"custom_survey_name": "Começar do zero",
|
||||
"custom_survey_question_1_headline": "O que gostaria de saber?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Eu senti-me confiante ao usar o sistema.",
|
||||
"usability_rating_description": "Meça a usabilidade percebida ao solicitar que os utilizadores avaliem a sua experiência com o seu produto usando um questionário padronizado de 10 perguntas.",
|
||||
"usability_score_name": "System Usability Score (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Obrigado por partilhar a sua ideia de fluxo de trabalho connosco! Estamos atualmente a desenhar esta funcionalidade e o seu feedback vai ajudar-nos a construir exatamente o que precisa.",
|
||||
"coming_soon_title": "Estamos quase lá!",
|
||||
"follow_up_label": "Há mais alguma coisa que gostaria de acrescentar?",
|
||||
"follow_up_placeholder": "Que tarefas específicas gostaria de automatizar? Alguma ferramenta ou integração que gostaria de incluir?",
|
||||
"generate_button": "Gerar fluxo de trabalho",
|
||||
"heading": "Que fluxo de trabalho quer criar?",
|
||||
"placeholder": "Descreva o fluxo de trabalho que quer gerar...",
|
||||
"subheading": "Gere o seu fluxo de trabalho em segundos.",
|
||||
"submit_button": "Adicionar detalhes",
|
||||
"thank_you_description": "A sua contribuição ajuda-nos a construir a funcionalidade de Fluxos de trabalho de que realmente precisa. Vamos mantê-lo informado sobre o nosso progresso.",
|
||||
"thank_you_title": "Obrigado pelo seu feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
+17
-18
@@ -176,8 +176,9 @@
|
||||
"copy_code": "Copiază codul",
|
||||
"copy_link": "Copiază legătura",
|
||||
"count_attributes": "{count, plural, one {{count} atribut} few {{count} atribute} other {{count} de atribute}}",
|
||||
"count_contacts": "{count, plural, one {# contact} other {# contacte} }",
|
||||
"count_contacts": "{count, plural, one {{count} contact} few {{count} contacte} other {{count} de contacte}}",
|
||||
"count_members": "{count, plural, one {{count} membru} few {{count} membri} other {{count} de membri}}",
|
||||
"count_questions": "{count, plural, one {# întrebare} few {# întrebări} other {# de întrebări}}",
|
||||
"count_responses": "{count, plural, one {# răspuns} other {# răspunsuri} }",
|
||||
"count_selections": "{count, plural, one {{count} selecție} few {{count} selecții} other {{count} de selecții}}",
|
||||
"create_new_organization": "Creează organizație nouă",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Afișează numărul de răspunsuri",
|
||||
"shown": "Afișat",
|
||||
"size": "Mărime",
|
||||
"skip": "Omite",
|
||||
"skipped": "Sărit",
|
||||
"skips": "Salturi",
|
||||
"some_files_failed_to_upload": "Unele fișiere nu au reușit să se încarce",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Chestionar despre site",
|
||||
"weeks": "săptămâni",
|
||||
"welcome_card": "Card de bun venit",
|
||||
"workflows": "Workflows",
|
||||
"workspace_configuration": "Configurare workspace",
|
||||
"workspace_created_successfully": "Spațiul de lucru a fost creat cu succes",
|
||||
"workspace_creation_description": "Organizează sondajele în workspaces pentru un control mai bun al accesului.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Ajustați mesajul 'Sondaj Închis'",
|
||||
"adjust_survey_closed_message_description": "Schimbați mesajul pe care îl văd vizitatorii atunci când sondajul este închis.",
|
||||
"adjust_the_theme_in_the": "Ajustați tema în",
|
||||
"all_are_true": "toate sunt adevărate",
|
||||
"all_other_answers_will_continue_to": "Toate celelalte răspunsuri vor continua să",
|
||||
"allow_multi_select": "Permite selectare multiplă",
|
||||
"allow_multiple_files": "Permite fișiere multiple",
|
||||
"allow_users_to_select_more_than_one_image": "Permite utilizatorilor să selecteze mai mult de o imagine",
|
||||
"and_launch_surveys_in_your_website_or_app": "și lansați chestionare pe site-ul sau în aplicația dvs.",
|
||||
"animation": "Animație",
|
||||
"any_is_true": "oricare este adevărată",
|
||||
"app_survey_description": "Incorporați un chestionar în aplicația web sau pe site-ul dvs. pentru a colecta răspunsuri.",
|
||||
"assign": "Atribuire =",
|
||||
"audience": "Public",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Opțiunea {choiceIndex}",
|
||||
"option_used_in_logic_error": "Această opțiune este folosită în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
||||
"optional": "Opțional",
|
||||
"options": "Opțiuni",
|
||||
"options": "Opțiuni*",
|
||||
"options_used_in_logic_bulk_error": "Următoarele opțiuni sunt folosite în logică: {questionIndexes}. Vă rugăm să le eliminați din logică mai întâi.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Suprascrie tema cu stiluri individuale pentru acest sondaj.",
|
||||
"overwrite_global_waiting_time": "Setează perioadă de răcire personalizată",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Întrebare ștearsă.",
|
||||
"question_duplicated": "Întrebare duplicată.",
|
||||
"question_id_updated": "ID întrebare actualizat",
|
||||
"question_number": "Întrebarea {number}",
|
||||
"question_used_in_logic_warning_text": "Elemente din acest bloc sunt folosite într-o regulă de logică. Sigur doriți să îl ștergeți?",
|
||||
"question_used_in_logic_warning_title": "Inconsistență logică",
|
||||
"question_used_in_quota": "Întrebarea aceasta este folosită în cota „{quotaName}”",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Perioadă de răcire (între sondaje)",
|
||||
"waiting_time_across_surveys_description": "Pentru a preveni oboseala cauzată de sondaje, alege cum interacționează acest sondaj cu perioada de răcire la nivel de workspace.",
|
||||
"welcome_message": "Mesaj de bun venit",
|
||||
"when": "Când",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Fără un filtru, toți utilizatorii pot fi chestionați.",
|
||||
"you_have_not_created_a_segment_yet": "Nu ai creat încă un segment",
|
||||
"your_description_here_recall_information_with": "Descrierea ta aici. Reamintiți informațiile cu @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Cum poate îmbunătăți compania alinierea viziunii și strategiei sale?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Tastează răspunsul aici...",
|
||||
"back": "Înapoi",
|
||||
"block_1": "Blocul 1",
|
||||
"block_10": "Blocul 10",
|
||||
"block_2": "Blocul 2",
|
||||
"block_3": "Blocul 3",
|
||||
"block_4": "Blocul 4",
|
||||
"block_5": "Blocul 5",
|
||||
"block_6": "Blocul 6",
|
||||
"block_7": "Blocul 7",
|
||||
"block_8": "Blocul 8",
|
||||
"block_9": "Blocul 9",
|
||||
"book_interview": "Rezervă interviu",
|
||||
"build_product_roadmap_description": "Identificați acel UN lucru pe care îl doresc cel mai mult utilizatorii și construiți-l.",
|
||||
"build_product_roadmap_name": "Crearea foii de parcurs a produsului",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Of, îmi pare rău! Există ceva ce putem face pentru a-ți îmbunătăți experiența?",
|
||||
"csat_survey_question_3_placeholder": "Tastează răspunsul aici...",
|
||||
"cta_description": "Afișează informații și solicită utilizatorilor să ia o acțiune specifică",
|
||||
"custom_survey_block_1_name": "Bloc 1",
|
||||
"custom_survey_description": "Creează un sondaj fără șablon.",
|
||||
"custom_survey_name": "Începe de la zero",
|
||||
"custom_survey_question_1_headline": "Ce ați dori să știți?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "M-am simțit încrezător în timp ce utilizam sistemul.",
|
||||
"usability_rating_description": "Măsurați uzabilitatea percepută cerând utilizatorilor să își evalueze experiența cu produsul dumneavoastră folosind un chestionar standardizat din 10 întrebări.",
|
||||
"usability_score_name": "Scor de Uzabilitate al Sistemului (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Îți mulțumim că ai împărtășit cu noi ideea ta de workflow! În prezent, lucrăm la această funcționalitate, iar feedback-ul tău ne ajută să construim exact ce ai nevoie.",
|
||||
"coming_soon_title": "Suntem aproape gata!",
|
||||
"follow_up_label": "Mai este ceva ce ai vrea să adaugi?",
|
||||
"follow_up_placeholder": "Ce sarcini specifice ai vrea să automatizezi? Există instrumente sau integrări pe care le-ai dori incluse?",
|
||||
"generate_button": "Generează workflow",
|
||||
"heading": "Ce workflow vrei să creezi?",
|
||||
"placeholder": "Descrie workflow-ul pe care vrei să-l generezi...",
|
||||
"subheading": "Generează-ți workflow-ul în câteva secunde.",
|
||||
"submit_button": "Adaugă detalii",
|
||||
"thank_you_description": "Contribuția ta ne ajută să construim funcția Workflows de care chiar ai nevoie. Te vom ține la curent cu progresul nostru.",
|
||||
"thank_you_title": "Îți mulțumim pentru feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
+17
-18
@@ -176,8 +176,9 @@
|
||||
"copy_code": "Скопировать код",
|
||||
"copy_link": "Скопировать ссылку",
|
||||
"count_attributes": "{count, plural, one {{count} атрибут} few {{count} атрибута} many {{count} атрибутов} other {{count} атрибута}}",
|
||||
"count_contacts": "{count, plural, one {{count} контакт} few {{count} контакта} many {{count} контактов} other {{count} контактов}}",
|
||||
"count_contacts": "{count, plural, one {{count} контакт} few {{count} контакта} many {{count} контактов} other {{count} контакта}}",
|
||||
"count_members": "{count, plural, one {{count} участник} few {{count} участника} many {{count} участников} other {{count} участника}}",
|
||||
"count_questions": "{count, plural, one {{count} вопрос} few {{count} вопроса} many {{count} вопросов} other {{count} вопросов}}",
|
||||
"count_responses": "{count, plural, one {{count} ответ} few {{count} ответа} many {{count} ответов} other {{count} ответов}}",
|
||||
"count_selections": "{count, plural, one {{count} выбран} few {{count} выбрано} many {{count} выбрано} other {{count} выбрано}}",
|
||||
"create_new_organization": "Создать новую организацию",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Показать количество ответов",
|
||||
"shown": "Показано",
|
||||
"size": "Размер",
|
||||
"skip": "Пропустить",
|
||||
"skipped": "Пропущено",
|
||||
"skips": "Пропуски",
|
||||
"some_files_failed_to_upload": "Не удалось загрузить некоторые файлы",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Опрос сайта",
|
||||
"weeks": "недели",
|
||||
"welcome_card": "Приветственная карточка",
|
||||
"workflows": "Воркфлоу",
|
||||
"workspace_configuration": "Настройка рабочего пространства",
|
||||
"workspace_created_successfully": "Рабочий проект успешно создан",
|
||||
"workspace_creation_description": "Организуйте опросы в рабочих пространствах для лучшего контроля доступа.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Изменить сообщение «Опрос закрыт»",
|
||||
"adjust_survey_closed_message_description": "Измените сообщение, которое видят посетители, когда опрос закрыт.",
|
||||
"adjust_the_theme_in_the": "Настройте тему в",
|
||||
"all_are_true": "все условия выполняются",
|
||||
"all_other_answers_will_continue_to": "Все остальные ответы будут продолжать",
|
||||
"allow_multi_select": "Разрешить множественный выбор",
|
||||
"allow_multiple_files": "Разрешить несколько файлов",
|
||||
"allow_users_to_select_more_than_one_image": "Разрешить пользователям выбирать более одного изображения",
|
||||
"and_launch_surveys_in_your_website_or_app": "и запускать опросы на вашем сайте или в приложении.",
|
||||
"animation": "Анимация",
|
||||
"any_is_true": "выполняется хотя бы одно условие",
|
||||
"app_survey_description": "Встраивайте опрос в ваше веб-приложение или сайт для сбора ответов.",
|
||||
"assign": "Назначить =",
|
||||
"audience": "Аудитория",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Вариант {choiceIndex}",
|
||||
"option_used_in_logic_error": "Этот вариант используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
|
||||
"optional": "Необязательно",
|
||||
"options": "Варианты",
|
||||
"options": "Варианты*",
|
||||
"options_used_in_logic_bulk_error": "Следующие варианты используются в логике: {questionIndexes}. Пожалуйста, сначала удалите их из логики.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Переопределить тему индивидуальными стилями для этого опроса.",
|
||||
"overwrite_global_waiting_time": "Установить свой период ожидания",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Вопрос удалён.",
|
||||
"question_duplicated": "Вопрос дублирован.",
|
||||
"question_id_updated": "ID вопроса обновлён",
|
||||
"question_number": "Вопрос {number}",
|
||||
"question_used_in_logic_warning_text": "Элементы из этого блока используются в правиле логики. Вы уверены, что хотите удалить его?",
|
||||
"question_used_in_logic_warning_title": "Несогласованность логики",
|
||||
"question_used_in_quota": "Этот вопрос используется в квоте «{quotaName}»",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Период ожидания (между опросами)",
|
||||
"waiting_time_across_surveys_description": "Чтобы избежать усталости от опросов, выберите, как этот опрос взаимодействует с общим периодом ожидания в рабочем пространстве.",
|
||||
"welcome_message": "Приветственное сообщение",
|
||||
"when": "Когда",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Без фильтра все ваши пользователи могут быть опрошены.",
|
||||
"you_have_not_created_a_segment_yet": "Вы ещё не создали сегмент",
|
||||
"your_description_here_recall_information_with": "Ваша инструкция здесь. Вспомните информацию с помощью @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Как компания может улучшить согласованность видения и стратегии?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Введите ваш ответ здесь...",
|
||||
"back": "Назад",
|
||||
"block_1": "Блок 1",
|
||||
"block_10": "Блок 10",
|
||||
"block_2": "Блок 2",
|
||||
"block_3": "Блок 3",
|
||||
"block_4": "Блок 4",
|
||||
"block_5": "Блок 5",
|
||||
"block_6": "Блок 6",
|
||||
"block_7": "Блок 7",
|
||||
"block_8": "Блок 8",
|
||||
"block_9": "Блок 9",
|
||||
"book_interview": "Записаться на интервью",
|
||||
"build_product_roadmap_description": "Определите ОДНУ вещь, которую ваши пользователи хотят больше всего, и реализуйте её.",
|
||||
"build_product_roadmap_name": "Построение продуктовой дорожной карты",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Ой, извините! Что мы можем сделать, чтобы улучшить ваш опыт?",
|
||||
"csat_survey_question_3_placeholder": "Введите ваш ответ здесь...",
|
||||
"cta_description": "Показывайте информацию и побуждайте пользователей к определённому действию",
|
||||
"custom_survey_block_1_name": "Блок 1",
|
||||
"custom_survey_description": "Создайте опрос без шаблона.",
|
||||
"custom_survey_name": "Начать с нуля",
|
||||
"custom_survey_question_1_headline": "Что вы хотели бы узнать?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Я чувствовал себя уверенно, используя систему.",
|
||||
"usability_rating_description": "Оцените воспринимаемую удобство, попросив пользователей оценить свой опыт работы с вашим продуктом с помощью стандартизированного опроса из 10 вопросов.",
|
||||
"usability_score_name": "Индекс удобства системы (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Спасибо, что поделился своей идеей воркфлоу с нами! Сейчас мы разрабатываем эту функцию, и твой отзыв поможет нам сделать именно то, что тебе нужно.",
|
||||
"coming_soon_title": "Мы почти готовы!",
|
||||
"follow_up_label": "Хочешь что-то ещё добавить?",
|
||||
"follow_up_placeholder": "Какие задачи ты хочешь автоматизировать? Какие инструменты или интеграции тебе нужны?",
|
||||
"generate_button": "Сгенерировать воркфлоу",
|
||||
"heading": "Какой воркфлоу ты хочешь создать?",
|
||||
"placeholder": "Опиши воркфлоу, который хочешь сгенерировать...",
|
||||
"subheading": "Сгенерируй свой воркфлоу за секунды.",
|
||||
"submit_button": "Добавить детали",
|
||||
"thank_you_description": "Твои идеи помогают нам создавать воркфлоу, которые действительно нужны. Мы будем держать тебя в курсе нашего прогресса.",
|
||||
"thank_you_title": "Спасибо за твой отзыв!"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-17
@@ -178,6 +178,7 @@
|
||||
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attribut}}",
|
||||
"count_contacts": "{count, plural, one {{count} kontakt} other {{count} kontakter}}",
|
||||
"count_members": "{count, plural, one {{count} medlem} other {{count} medlemmar}}",
|
||||
"count_questions": "{count, plural, one {{count} fråga} other {{count} frågor}}",
|
||||
"count_responses": "{count, plural, one {{count} svar} other {{count} svar}}",
|
||||
"count_selections": "{count, plural, one {{count} val} other {{count} val}}",
|
||||
"create_new_organization": "Skapa ny organisation",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "Visa antal svar",
|
||||
"shown": "Visad",
|
||||
"size": "Storlek",
|
||||
"skip": "Hoppa över",
|
||||
"skipped": "Överhoppad",
|
||||
"skips": "Överhoppningar",
|
||||
"some_files_failed_to_upload": "Några filer misslyckades att laddas upp",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "Webbplatsenkät",
|
||||
"weeks": "veckor",
|
||||
"welcome_card": "Välkomstkort",
|
||||
"workflows": "Arbetsflöden",
|
||||
"workspace_configuration": "Arbetsytans konfiguration",
|
||||
"workspace_created_successfully": "Arbetsytan har skapats",
|
||||
"workspace_creation_description": "Organisera enkäter i arbetsytor för bättre åtkomstkontroll.",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "Justera meddelande för 'Enkät stängd'",
|
||||
"adjust_survey_closed_message_description": "Ändra meddelandet besökare ser när enkäten är stängd.",
|
||||
"adjust_the_theme_in_the": "Justera temat i",
|
||||
"all_are_true": "alla är sanna",
|
||||
"all_other_answers_will_continue_to": "Alla andra svar fortsätter till",
|
||||
"allow_multi_select": "Tillåt flerval",
|
||||
"allow_multiple_files": "Tillåt flera filer",
|
||||
"allow_users_to_select_more_than_one_image": "Tillåt användare att välja mer än en bild",
|
||||
"and_launch_surveys_in_your_website_or_app": "och starta enkäter på din webbplats eller i din app.",
|
||||
"animation": "Animering",
|
||||
"any_is_true": "någon är sann",
|
||||
"app_survey_description": "Bädda in en enkät i din webbapp eller webbplats för att samla in svar.",
|
||||
"assign": "Tilldela =",
|
||||
"audience": "Målgrupp",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "Alternativ {choiceIndex}",
|
||||
"option_used_in_logic_error": "Detta alternativ används i logiken för fråga {questionIndex}. Vänligen ta bort det från logiken först.",
|
||||
"optional": "Valfritt",
|
||||
"options": "Alternativ",
|
||||
"options": "Alternativ*",
|
||||
"options_used_in_logic_bulk_error": "Följande alternativ används i logiken: {questionIndexes}. Vänligen ta bort dem från logiken först.",
|
||||
"override_theme_with_individual_styles_for_this_survey": "Åsidosätt temat med individuella stilar för denna enkät.",
|
||||
"overwrite_global_waiting_time": "Ange anpassad väntetid",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "Fråga borttagen.",
|
||||
"question_duplicated": "Fråga duplicerad.",
|
||||
"question_id_updated": "Fråge-ID uppdaterat",
|
||||
"question_number": "Fråga {number}",
|
||||
"question_used_in_logic_warning_text": "Element från det här blocket används i en logikregel. Är du säker på att du vill ta bort det?",
|
||||
"question_used_in_logic_warning_title": "Logikkonflikt",
|
||||
"question_used_in_quota": "Denna fråga används i kvoten “{quotaName}”",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "Väntetid (mellan enkäter)",
|
||||
"waiting_time_across_surveys_description": "För att undvika enkättrötthet, välj hur denna enkät ska förhålla sig till arbetsytans gemensamma väntetid.",
|
||||
"welcome_message": "Välkomstmeddelande",
|
||||
"when": "När",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "Utan ett filter kan alla dina användare enkäteras.",
|
||||
"you_have_not_created_a_segment_yet": "Du har inte skapat ett segment ännu",
|
||||
"your_description_here_recall_information_with": "Din beskrivning här. Återkalla information med @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "Hur kan företaget förbättra sin vision och strategiöverensstämmelse?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "Skriv ditt svar här...",
|
||||
"back": "Tillbaka",
|
||||
"block_1": "Block 1",
|
||||
"block_10": "Block 10",
|
||||
"block_2": "Block 2",
|
||||
"block_3": "Block 3",
|
||||
"block_4": "Block 4",
|
||||
"block_5": "Block 5",
|
||||
"block_6": "Block 6",
|
||||
"block_7": "Block 7",
|
||||
"block_8": "Block 8",
|
||||
"block_9": "Block 9",
|
||||
"book_interview": "Boka intervju",
|
||||
"build_product_roadmap_description": "Identifiera det EN sak dina användare vill ha mest och bygg den.",
|
||||
"build_product_roadmap_name": "Bygg produktroadmap",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "Aj, förlåt! Finns det något vi kan göra för att förbättra din upplevelse?",
|
||||
"csat_survey_question_3_placeholder": "Skriv ditt svar här...",
|
||||
"cta_description": "Visa information och uppmana användare att vidta en specifik åtgärd",
|
||||
"custom_survey_block_1_name": "Block 1",
|
||||
"custom_survey_description": "Skapa en enkät utan mall.",
|
||||
"custom_survey_name": "Börja från början",
|
||||
"custom_survey_question_1_headline": "Vad vill du veta?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "Jag kände mig trygg när jag använde systemet.",
|
||||
"usability_rating_description": "Mät upplevd användbarhet genom att be användare betygsätta sin upplevelse med din produkt med en standardiserad 10-frågors enkät.",
|
||||
"usability_score_name": "System Usability Score (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "Tack för att du delade din arbetsflödesidé med oss! Vi håller just nu på att designa den här funktionen och din feedback hjälper oss att bygga precis det du behöver.",
|
||||
"coming_soon_title": "Vi är nästan där!",
|
||||
"follow_up_label": "Är det något mer du vill lägga till?",
|
||||
"follow_up_placeholder": "Vilka specifika uppgifter vill du automatisera? Finns det några verktyg eller integrationer du vill ha med?",
|
||||
"generate_button": "Skapa arbetsflöde",
|
||||
"heading": "Vilket arbetsflöde vill du skapa?",
|
||||
"placeholder": "Beskriv arbetsflödet du vill skapa...",
|
||||
"subheading": "Skapa ditt arbetsflöde på några sekunder.",
|
||||
"submit_button": "Lägg till detaljer",
|
||||
"thank_you_description": "Din input hjälper oss att bygga arbetsflödesfunktionen du faktiskt behöver. Vi håller dig uppdaterad om hur det går.",
|
||||
"thank_you_title": "Tack för din feedback!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +175,10 @@
|
||||
"copy": "复制",
|
||||
"copy_code": "复制 代码",
|
||||
"copy_link": "复制 链接",
|
||||
"count_attributes": "{count, plural, one {{count} 个属性} other {{count} 个属性}}",
|
||||
"count_contacts": "{count, plural, other {{count} 联系人} }",
|
||||
"count_attributes": "{count, plural, other {{count} 个属性}}",
|
||||
"count_contacts": "{count, plural, other {{count} 个联系人}}",
|
||||
"count_members": "{count, plural, one {{count} 位成员} other {{count} 位成员}}",
|
||||
"count_questions": "{count, plural, other {{count} 个问题} }",
|
||||
"count_responses": "{count, plural, other {{count} 回复} }",
|
||||
"count_selections": "{count, plural, other {已选择{count}项}}",
|
||||
"create_new_organization": "创建 新的 组织",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "显示 响应 计数",
|
||||
"shown": "显示",
|
||||
"size": "尺寸",
|
||||
"skip": "跳过",
|
||||
"skipped": "跳过",
|
||||
"skips": "跳过",
|
||||
"some_files_failed_to_upload": "某些文件上传失败",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "网站 调查",
|
||||
"weeks": "周",
|
||||
"welcome_card": "欢迎 卡片",
|
||||
"workflows": "工作流",
|
||||
"workspace_configuration": "工作区配置",
|
||||
"workspace_created_successfully": "工作区创建成功",
|
||||
"workspace_creation_description": "在工作区中组织调查,以便更好地进行访问控制。",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "调整 \"调查 关闭\" 消息",
|
||||
"adjust_survey_closed_message_description": "更改 访客 看到 调查 关闭 时 的 消息。",
|
||||
"adjust_the_theme_in_the": "调整主题在",
|
||||
"all_are_true": "全部为真",
|
||||
"all_other_answers_will_continue_to": "所有其他答案将继续",
|
||||
"allow_multi_select": "允许 多选",
|
||||
"allow_multiple_files": "允许 多 个 文件",
|
||||
"allow_users_to_select_more_than_one_image": "允许 用户 选择 多于 一个 图片",
|
||||
"and_launch_surveys_in_your_website_or_app": "并 在 你 的 网站 或 应用 中 启动 问卷 。",
|
||||
"animation": "动画",
|
||||
"any_is_true": "任一为真",
|
||||
"app_survey_description": "在 你的 网络 应用 或 网站 中 嵌入 问卷 收集 反馈 。",
|
||||
"assign": "指派 =",
|
||||
"audience": "受众",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "选项 {choiceIndex}",
|
||||
"option_used_in_logic_error": "\"这个 选项 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
"optional": "可选",
|
||||
"options": "选项",
|
||||
"options": "选项*",
|
||||
"options_used_in_logic_bulk_error": "以下选项在逻辑中被使用:{questionIndexes}。请先从逻辑中删除它们。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "使用 个性化 样式 替代 这份 问卷 的 主题。",
|
||||
"overwrite_global_waiting_time": "自定义冷却期",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "问题 已删除",
|
||||
"question_duplicated": "问题重复。",
|
||||
"question_id_updated": "问题 ID 更新",
|
||||
"question_number": "第 {number} 题",
|
||||
"question_used_in_logic_warning_text": "此区块中的元素已被用于逻辑规则,您确定要删除吗?",
|
||||
"question_used_in_logic_warning_title": "逻辑不一致",
|
||||
"question_used_in_quota": "此问题正在被“{quotaName}”配额使用",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "冷却期(跨问卷)",
|
||||
"waiting_time_across_surveys_description": "为防止问卷疲劳,请选择此问卷与工作区冷却期的交互方式。",
|
||||
"welcome_message": "欢迎 信息",
|
||||
"when": "当",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "没有 过滤器 时 ,所有 用户 都可以 被 调查 。",
|
||||
"you_have_not_created_a_segment_yet": "您 还没有 创建 段落",
|
||||
"your_description_here_recall_information_with": "在此输入描述。 调用信息与 @",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "公司 如何 改进 其 愿景 与 战略 的 协同?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "输入您的答案...",
|
||||
"back": "返回",
|
||||
"block_1": "第 1 块",
|
||||
"block_10": "第 10 块",
|
||||
"block_2": "第 2 块",
|
||||
"block_3": "第 3 块",
|
||||
"block_4": "第 4 块",
|
||||
"block_5": "第 5 块",
|
||||
"block_6": "第 6 块",
|
||||
"block_7": "第 7 块",
|
||||
"block_8": "第 8 块",
|
||||
"block_9": "第 9 块",
|
||||
"book_interview": "预约 面试",
|
||||
"build_product_roadmap_description": "识别 用户 最 想要 的 一个 东西 并 构建 它 。",
|
||||
"build_product_roadmap_name": "构建 产品 路线图",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "糟糕, 对不起!我们可以做些什么来改善您的体验?",
|
||||
"csat_survey_question_3_placeholder": "在此输入您的答案...",
|
||||
"cta_description": "显示 信息 并 提示用户采取 特定行动",
|
||||
"custom_survey_block_1_name": "模块 1",
|
||||
"custom_survey_description": "创建 一个 没有 模板 的 调查。",
|
||||
"custom_survey_name": "从零开始",
|
||||
"custom_survey_question_1_headline": "你 想 知道 什么?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "使用 系统 时 我 感到 自信。",
|
||||
"usability_rating_description": "通过要求用户使用标准化的 10 问 调查 来 评价 他们对您产品的体验,以 测量 感知 的 可用性。",
|
||||
"usability_score_name": "系统 可用性 得分 ( SUS )"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "感谢你与我们分享你的工作流想法!我们目前正在设计这个功能,你的反馈将帮助我们打造真正适合你的工具。",
|
||||
"coming_soon_title": "我们快完成啦!",
|
||||
"follow_up_label": "你还有其他想补充的吗?",
|
||||
"follow_up_placeholder": "你希望自动化哪些具体任务?有没有想要集成的工具或服务?",
|
||||
"generate_button": "生成工作流",
|
||||
"heading": "你想创建什么样的工作流?",
|
||||
"placeholder": "描述一下你想生成的工作流……",
|
||||
"subheading": "几秒钟生成你的工作流。",
|
||||
"submit_button": "补充细节",
|
||||
"thank_you_description": "你的意见帮助我们打造真正适合你的工作流功能。我们会及时向你汇报进展。",
|
||||
"thank_you_title": "感谢你的反馈!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +175,10 @@
|
||||
"copy": "複製",
|
||||
"copy_code": "複製程式碼",
|
||||
"copy_link": "複製連結",
|
||||
"count_attributes": "{count, plural, one {{count} 個屬性} other {{count} 個屬性}}",
|
||||
"count_contacts": "{count, plural, other {{count} 聯絡人} }",
|
||||
"count_attributes": "{count, plural, other {{count} 個屬性}}",
|
||||
"count_contacts": "{count, plural, other {{count} 位聯絡人}}",
|
||||
"count_members": "{count, plural, one {{count} 位成員} other {{count} 位成員}}",
|
||||
"count_questions": "{count, plural, other {{count} 個問題}}",
|
||||
"count_responses": "{count, plural, other {{count} 回應} }",
|
||||
"count_selections": "{count, plural, one {{count} 個選項} other {{count} 個選項}}",
|
||||
"create_new_organization": "建立新組織",
|
||||
@@ -393,7 +394,6 @@
|
||||
"show_response_count": "顯示回應數",
|
||||
"shown": "已顯示",
|
||||
"size": "大小",
|
||||
"skip": "略過",
|
||||
"skipped": "已跳過",
|
||||
"skips": "跳過次數",
|
||||
"some_files_failed_to_upload": "部分檔案上傳失敗",
|
||||
@@ -463,7 +463,6 @@
|
||||
"website_survey": "網站問卷",
|
||||
"weeks": "週",
|
||||
"welcome_card": "歡迎卡片",
|
||||
"workflows": "工作流程",
|
||||
"workspace_configuration": "工作區設定",
|
||||
"workspace_created_successfully": "工作區已成功建立",
|
||||
"workspace_creation_description": "將問卷組織在工作區中,以便更好地控管存取權限。",
|
||||
@@ -1268,12 +1267,14 @@
|
||||
"adjust_survey_closed_message": "調整「問卷已關閉」訊息",
|
||||
"adjust_survey_closed_message_description": "變更訪客在問卷關閉時看到的訊息。",
|
||||
"adjust_the_theme_in_the": "在",
|
||||
"all_are_true": "全部為真",
|
||||
"all_other_answers_will_continue_to": "所有其他答案將繼續",
|
||||
"allow_multi_select": "允許多重選取",
|
||||
"allow_multiple_files": "允許上傳多個檔案",
|
||||
"allow_users_to_select_more_than_one_image": "允許使用者選取多張圖片",
|
||||
"and_launch_surveys_in_your_website_or_app": "並在您的網站或應用程式中啟動問卷。",
|
||||
"animation": "動畫",
|
||||
"any_is_true": "任一為真",
|
||||
"app_survey_description": "將問卷嵌入您的 Web 應用程式或網站中以收集回應。",
|
||||
"assign": "等於 =",
|
||||
"audience": "受眾",
|
||||
@@ -1539,7 +1540,7 @@
|
||||
"option_idx": "選項 '{'choiceIndex'}'",
|
||||
"option_used_in_logic_error": "此選項用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"optional": "選填",
|
||||
"options": "選項",
|
||||
"options": "選項*",
|
||||
"options_used_in_logic_bulk_error": "以下選項已用於邏輯中:{questionIndexes}。請先從邏輯中移除它們。",
|
||||
"override_theme_with_individual_styles_for_this_survey": "使用此問卷的個別樣式覆寫主題。",
|
||||
"overwrite_global_waiting_time": "自訂冷卻期",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"question_deleted": "問題已刪除。",
|
||||
"question_duplicated": "問題已複製。",
|
||||
"question_id_updated": "問題 ID 已更新",
|
||||
"question_number": "第 {number} 題",
|
||||
"question_used_in_logic_warning_text": "此區塊中的元素已用於邏輯規則,確定要刪除嗎?",
|
||||
"question_used_in_logic_warning_title": "邏輯不一致",
|
||||
"question_used_in_quota": "此問題正被使用於「{quotaName}」配額中",
|
||||
@@ -1757,6 +1759,7 @@
|
||||
"waiting_time_across_surveys": "冷卻期(跨問卷)",
|
||||
"waiting_time_across_surveys_description": "為避免問卷疲勞,請選擇此問卷如何與工作區的冷卻期互動。",
|
||||
"welcome_message": "歡迎訊息",
|
||||
"when": "當",
|
||||
"without_a_filter_all_of_your_users_can_be_surveyed": "如果沒有篩選器,則可以調查您的所有使用者。",
|
||||
"you_have_not_created_a_segment_yet": "您尚未建立區隔",
|
||||
"your_description_here_recall_information_with": "您的描述在這裡。使用 @ 回憶資訊",
|
||||
@@ -2400,6 +2403,16 @@
|
||||
"alignment_and_engagement_survey_question_4_headline": "公司如何改善其願景和策略一致性?",
|
||||
"alignment_and_engagement_survey_question_4_placeholder": "在此輸入您的答案...",
|
||||
"back": "返回",
|
||||
"block_1": "區塊 1",
|
||||
"block_10": "區塊 10",
|
||||
"block_2": "區塊 2",
|
||||
"block_3": "區塊 3",
|
||||
"block_4": "區塊 4",
|
||||
"block_5": "區塊 5",
|
||||
"block_6": "區塊 6",
|
||||
"block_7": "區塊 7",
|
||||
"block_8": "區塊 8",
|
||||
"block_9": "區塊 9",
|
||||
"book_interview": "預訂面試",
|
||||
"build_product_roadmap_description": "找出您的使用者最想要的一件事,然後建立它。",
|
||||
"build_product_roadmap_name": "建立產品路線圖",
|
||||
@@ -2607,7 +2620,6 @@
|
||||
"csat_survey_question_3_headline": "唉,抱歉!我們是否有任何可以改善您體驗的地方?",
|
||||
"csat_survey_question_3_placeholder": "在此輸入您的答案...",
|
||||
"cta_description": "顯示資訊並提示使用者採取特定操作",
|
||||
"custom_survey_block_1_name": "區塊 1",
|
||||
"custom_survey_description": "建立沒有範本的問卷。",
|
||||
"custom_survey_name": "從頭開始",
|
||||
"custom_survey_question_1_headline": "您想瞭解什麼?",
|
||||
@@ -3261,18 +3273,5 @@
|
||||
"usability_question_9_headline": "使用 系統 時,我 感到 有 信心。",
|
||||
"usability_rating_description": "透過使用標準化的 十個問題 問卷,要求使用者評估他們對 您 產品的使用體驗,來衡量感知的 可用性。",
|
||||
"usability_score_name": "系統 可用性 分數 (SUS)"
|
||||
},
|
||||
"workflows": {
|
||||
"coming_soon_description": "感謝你和我們分享你的工作流程想法!我們目前正在設計這個功能,你的回饋將幫助我們打造真正符合你需求的工具。",
|
||||
"coming_soon_title": "快完成囉!",
|
||||
"follow_up_label": "還有什麼想補充的嗎?",
|
||||
"follow_up_placeholder": "你想自動化哪些具體任務?有沒有想要整合的工具或服務?",
|
||||
"generate_button": "產生工作流程",
|
||||
"heading": "你想建立什麼樣的工作流程?",
|
||||
"placeholder": "請描述你想產生的工作流程⋯⋯",
|
||||
"subheading": "幾秒鐘就能產生你的工作流程。",
|
||||
"submit_button": "補充細節",
|
||||
"thank_you_description": "你的意見幫助我們打造真正符合你需求的工作流程功能。我們會隨時更新進度給你。",
|
||||
"thank_you_title": "感謝你的回饋!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,11 @@ const ZCreateTagAction = z.object({
|
||||
tagName: z.string(),
|
||||
});
|
||||
|
||||
export const createTagAction = authenticatedActionClient.inputSchema(ZCreateTagAction).action(
|
||||
export const createTagAction = authenticatedActionClient.schema(ZCreateTagAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"tag",
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZCreateTagAction>;
|
||||
}) => {
|
||||
async ({ parsedInput, ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
@@ -71,125 +65,103 @@ const ZCreateTagToResponseAction = z.object({
|
||||
tagId: ZId,
|
||||
});
|
||||
|
||||
export const createTagToResponseAction = authenticatedActionClient
|
||||
.inputSchema(ZCreateTagToResponseAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"addedToResponse",
|
||||
"tag",
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZCreateTagToResponseAction>;
|
||||
}) => {
|
||||
const responseEnvironmentId = await getEnvironmentIdFromResponseId(parsedInput.responseId);
|
||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||
export const createTagToResponseAction = authenticatedActionClient.schema(ZCreateTagToResponseAction).action(
|
||||
withAuditLogging(
|
||||
"addedToResponse",
|
||||
"tag",
|
||||
async ({ parsedInput, ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const responseEnvironmentId = await getEnvironmentIdFromResponseId(parsedInput.responseId);
|
||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||
|
||||
if (!responseEnvironmentId || !tagEnvironment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||
throw new Error("Response and tag are not in the same environment");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(responseEnvironmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.tagId = parsedInput.tagId;
|
||||
const result = await addTagToRespone(parsedInput.responseId, parsedInput.tagId);
|
||||
ctx.auditLoggingCtx.newObject = result;
|
||||
return result;
|
||||
if (!responseEnvironmentId || !tagEnvironment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||
throw new Error("Response and tag are not in the same environment");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(responseEnvironmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.tagId = parsedInput.tagId;
|
||||
const result = await addTagToRespone(parsedInput.responseId, parsedInput.tagId);
|
||||
ctx.auditLoggingCtx.newObject = result;
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZDeleteTagOnResponseAction = z.object({
|
||||
responseId: ZId,
|
||||
tagId: ZId,
|
||||
});
|
||||
|
||||
export const deleteTagOnResponseAction = authenticatedActionClient
|
||||
.inputSchema(ZDeleteTagOnResponseAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"removedFromResponse",
|
||||
"tag",
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZDeleteTagOnResponseAction>;
|
||||
}) => {
|
||||
const responseEnvironmentId = await getEnvironmentIdFromResponseId(parsedInput.responseId);
|
||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
|
||||
if (!responseEnvironmentId || !tagEnvironment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||
throw new Error("Response and tag are not in the same environment");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.tagId = parsedInput.tagId;
|
||||
const result = await deleteTagOnResponse(parsedInput.responseId, parsedInput.tagId);
|
||||
ctx.auditLoggingCtx.oldObject = result;
|
||||
return result;
|
||||
export const deleteTagOnResponseAction = authenticatedActionClient.schema(ZDeleteTagOnResponseAction).action(
|
||||
withAuditLogging(
|
||||
"removedFromResponse",
|
||||
"tag",
|
||||
async ({ parsedInput, ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const responseEnvironmentId = await getEnvironmentIdFromResponseId(parsedInput.responseId);
|
||||
const tagEnvironment = await getTag(parsedInput.tagId);
|
||||
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
|
||||
if (!responseEnvironmentId || !tagEnvironment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (responseEnvironmentId !== tagEnvironment.environmentId) {
|
||||
throw new Error("Response and tag are not in the same environment");
|
||||
}
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(responseEnvironmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.tagId = parsedInput.tagId;
|
||||
const result = await deleteTagOnResponse(parsedInput.responseId, parsedInput.tagId);
|
||||
ctx.auditLoggingCtx.oldObject = result;
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZDeleteResponseAction = z.object({
|
||||
responseId: ZId,
|
||||
decrementQuotas: z.boolean().prefault(false),
|
||||
decrementQuotas: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const deleteResponseAction = authenticatedActionClient.inputSchema(ZDeleteResponseAction).action(
|
||||
export const deleteResponseAction = authenticatedActionClient.schema(ZDeleteResponseAction).action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"response",
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZDeleteResponseAction>;
|
||||
}) => {
|
||||
async ({ parsedInput, ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const organizationId = await getOrganizationIdFromResponseId(parsedInput.responseId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -220,7 +192,7 @@ const ZGetResponseAction = z.object({
|
||||
});
|
||||
|
||||
export const getResponseAction = authenticatedActionClient
|
||||
.inputSchema(ZGetResponseAction)
|
||||
.schema(ZGetResponseAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZOverallHealthStatus = z
|
||||
.object({
|
||||
main_database: z
|
||||
.boolean()
|
||||
.meta({
|
||||
example: true,
|
||||
})
|
||||
.describe("Main database connection status - true if database is reachable and running"),
|
||||
cache_database: z
|
||||
.boolean()
|
||||
.meta({
|
||||
example: true,
|
||||
})
|
||||
.describe("Cache database connection status - true if cache database is reachable and running"),
|
||||
main_database: z.boolean().openapi({
|
||||
description: "Main database connection status - true if database is reachable and running",
|
||||
example: true,
|
||||
}),
|
||||
cache_database: z.boolean().openapi({
|
||||
description: "Cache database connection status - true if cache database is reachable and running",
|
||||
example: true,
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
.openapi({
|
||||
title: "Health Check Response",
|
||||
})
|
||||
.describe("Health check status for critical application dependencies");
|
||||
description: "Health check status for critical application dependencies",
|
||||
});
|
||||
|
||||
export type OverallHealthStatus = z.infer<typeof ZOverallHealthStatus>;
|
||||
|
||||
+10
-6
@@ -1,22 +1,26 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZContactAttributeKeyIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
id: "contactAttributeKeyId",
|
||||
.openapi({
|
||||
ref: "contactAttributeKeyId",
|
||||
description: "The ID of the contact attribute key",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
})
|
||||
.describe("The ID of the contact attribute key");
|
||||
});
|
||||
|
||||
export const ZContactAttributeKeyUpdateSchema = ZContactAttributeKey.pick({
|
||||
name: true,
|
||||
description: true,
|
||||
}).meta({
|
||||
id: "contactAttributeKeyUpdate",
|
||||
}).openapi({
|
||||
ref: "contactAttributeKeyUpdate",
|
||||
description: "A contact attribute key to update. Key cannot be changed.",
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const getContactAttributeKeysEndpoint: ZodOpenApiOperationObject = {
|
||||
description: "Gets contact attribute keys from the database.",
|
||||
tags: ["Management API - Contact Attribute Keys"],
|
||||
requestParams: {
|
||||
query: ZGetContactAttributeKeysFilter,
|
||||
query: ZGetContactAttributeKeysFilter.sourceType(),
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
|
||||
@@ -17,7 +17,7 @@ export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetContactAttributeKeysFilter,
|
||||
query: ZGetContactAttributeKeysFilter.sourceType(),
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
const { query } = parsedInput;
|
||||
@@ -49,7 +49,7 @@ export const POST = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
body: ZContactAttributeKeyInput,
|
||||
body: ZContactAttributeKeyInput.sourceType(),
|
||||
},
|
||||
handler: async ({ authentication, parsedInput, auditLog }) => {
|
||||
const { body } = parsedInput;
|
||||
|
||||
+7
-4
@@ -1,10 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys";
|
||||
import { isSafeIdentifier } from "@/lib/utils/safe-identifier";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZGetContactAttributeKeysFilter = ZGetFilter.extend({
|
||||
environmentId: z.cuid2().optional().describe("The environment ID to filter by"),
|
||||
environmentId: z.string().cuid2().optional().describe("The environment ID to filter by"),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -34,15 +37,15 @@ export const ZContactAttributeKeyInput = ZContactAttributeKey.pick({
|
||||
// Enforce safe identifier format for key
|
||||
if (!isSafeIdentifier(data.key)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
"Key must be a safe identifier: only lowercase letters, numbers, and underscores, and must start with a letter",
|
||||
path: ["key"],
|
||||
});
|
||||
}
|
||||
})
|
||||
.meta({
|
||||
id: "contactAttributeKeyInput",
|
||||
.openapi({
|
||||
ref: "contactAttributeKeyInput",
|
||||
description: "Input data for creating or updating a contact attribute",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZResponse } from "@formbricks/database/zod/responses";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZResponseIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
id: "responseId",
|
||||
.openapi({
|
||||
ref: "responseId",
|
||||
description: "The ID of the response",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
})
|
||||
.describe("The ID of the response");
|
||||
});
|
||||
|
||||
export const ZResponseUpdateSchema = ZResponse.omit({
|
||||
id: true,
|
||||
surveyId: true,
|
||||
}).meta({
|
||||
id: "responseUpdate",
|
||||
}).openapi({
|
||||
ref: "responseUpdate",
|
||||
description: "A response to update.",
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export const getResponsesEndpoint: ZodOpenApiOperationObject = {
|
||||
summary: "Get responses",
|
||||
description: "Gets responses from the database.",
|
||||
requestParams: {
|
||||
query: ZGetResponsesFilter,
|
||||
query: ZGetResponsesFilter.sourceType(),
|
||||
},
|
||||
tags: ["Management API - Responses"],
|
||||
responses: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetResponsesFilter,
|
||||
query: ZGetResponsesFilter.sourceType(),
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
const { query } = parsedInput;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ZResponse } from "@formbricks/database/zod/responses";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZGetResponsesFilter = ZGetFilter.extend({
|
||||
surveyId: z.cuid2().optional(),
|
||||
surveyId: z.string().cuid2().optional(),
|
||||
contactId: z.string().optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export const getPersonalizedSurveyLink: ZodOpenApiOperationObject = {
|
||||
schema: makePartialSchema(
|
||||
z.object({
|
||||
data: z.object({
|
||||
surveyUrl: z.url(),
|
||||
surveyUrl: z.string().url(),
|
||||
expiresAt: z
|
||||
.string()
|
||||
.nullable()
|
||||
|
||||
+11
-6
@@ -1,18 +1,23 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZContactLinkParams = z.object({
|
||||
surveyId: z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
.openapi({
|
||||
description: "The ID of the survey",
|
||||
param: { name: "surveyId", in: "path" },
|
||||
})
|
||||
.describe("The ID of the survey"),
|
||||
}),
|
||||
contactId: z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
.openapi({
|
||||
description: "The ID of the contact",
|
||||
param: { name: "contactId", in: "path" },
|
||||
})
|
||||
.describe("The ID of the contact"),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZContactLinkQuery = z.object({
|
||||
|
||||
+13
-8
@@ -1,19 +1,24 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZContactLinksBySegmentParams = z.object({
|
||||
surveyId: z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
.openapi({
|
||||
description: "The ID of the survey",
|
||||
param: { name: "surveyId", in: "path" },
|
||||
})
|
||||
.describe("The ID of the survey"),
|
||||
}),
|
||||
segmentId: z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
.openapi({
|
||||
description: "The ID of the segment",
|
||||
param: { name: "segmentId", in: "path" },
|
||||
})
|
||||
.describe("The ID of the segment"),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZContactLinksBySegmentQuery = ZGetFilter.pick({
|
||||
@@ -25,7 +30,7 @@ export const ZContactLinksBySegmentQuery = ZGetFilter.pick({
|
||||
.min(1)
|
||||
.max(365)
|
||||
.nullish()
|
||||
.prefault(null)
|
||||
.default(null)
|
||||
.describe("Number of days until the generated JWT expires. If not provided, there is no expiration."),
|
||||
attributeKeys: z
|
||||
.string()
|
||||
@@ -47,7 +52,7 @@ export type TContactWithAttributes = {
|
||||
|
||||
export const ZContactLinkResponse = z.object({
|
||||
contactId: z.string().describe("The ID of the contact"),
|
||||
surveyUrl: z.url().describe("Personalized survey link"),
|
||||
surveyUrl: z.string().url().describe("Personalized survey link"),
|
||||
expiresAt: z.string().nullable().describe("The date and time the link expires, null if no expiration"),
|
||||
attributes: z.record(z.string(), z.string()).describe("The attributes of the contact"),
|
||||
});
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const surveyIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
id: "surveyId",
|
||||
.openapi({
|
||||
ref: "surveyId",
|
||||
description: "The ID of the survey",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
})
|
||||
.describe("The ID of the survey");
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZSurveyWithoutQuestionType } from "@formbricks/database/zod/surveys";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZGetSurveysFilter = z
|
||||
.object({
|
||||
limit: z.coerce.number().positive().min(1).max(100).optional().prefault(10),
|
||||
skip: z.coerce.number().nonnegative().optional().prefault(0),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional().prefault("createdAt"),
|
||||
order: z.enum(["asc", "desc"]).optional().prefault("desc"),
|
||||
limit: z.coerce.number().positive().min(1).max(100).optional().default(10),
|
||||
skip: z.coerce.number().nonnegative().optional().default(0),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional().default("createdAt"),
|
||||
order: z.enum(["asc", "desc"]).optional().default("desc"),
|
||||
startDate: z.coerce.date().optional(),
|
||||
endDate: z.coerce.date().optional(),
|
||||
surveyType: z.enum(["link", "app"]).optional(),
|
||||
@@ -20,7 +23,7 @@ export const ZGetSurveysFilter = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
error: "startDate must be before endDate",
|
||||
message: "startDate must be before endDate",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -66,8 +69,8 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
|
||||
inlineTriggers: true,
|
||||
displayPercentage: true,
|
||||
})
|
||||
.meta({
|
||||
id: "surveyInput",
|
||||
.openapi({
|
||||
ref: "surveyInput",
|
||||
description: "A survey input object for creating or updating surveys",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZWebhookIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
id: "webhookId",
|
||||
.openapi({
|
||||
ref: "webhookId",
|
||||
description: "The ID of the webhook",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
})
|
||||
.describe("The ID of the webhook");
|
||||
});
|
||||
|
||||
export const ZWebhookUpdateSchema = ZWebhook.omit({
|
||||
id: true,
|
||||
@@ -18,7 +22,7 @@ export const ZWebhookUpdateSchema = ZWebhook.omit({
|
||||
updatedAt: true,
|
||||
environmentId: true,
|
||||
secret: true,
|
||||
}).meta({
|
||||
id: "webhookUpdate",
|
||||
}).openapi({
|
||||
ref: "webhookUpdate",
|
||||
description: "A webhook to update.",
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export const getWebhooksEndpoint: ZodOpenApiOperationObject = {
|
||||
summary: "Get webhooks",
|
||||
description: "Gets webhooks from the database.",
|
||||
requestParams: {
|
||||
query: ZGetWebhooksFilter,
|
||||
query: ZGetWebhooksFilter.sourceType(),
|
||||
},
|
||||
tags: ["Management API - Webhooks"],
|
||||
responses: {
|
||||
|
||||
@@ -11,7 +11,7 @@ export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetWebhooksFilter,
|
||||
query: ZGetWebhooksFilter.sourceType(),
|
||||
},
|
||||
handler: async ({ authentication, parsedInput }) => {
|
||||
const { query } = parsedInput;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZGetWebhooksFilter = ZGetFilter.extend({
|
||||
surveyIds: z.array(z.cuid2()).optional(),
|
||||
surveyIds: z.array(z.string().cuid2()).optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.startDate && data.endDate && data.startDate > data.endDate) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as yaml from "yaml";
|
||||
import { createDocument } from "zod-openapi";
|
||||
import { z } from "zod";
|
||||
import { createDocument, extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZApiKeyData } from "@formbricks/database/zod/api-keys";
|
||||
import { ZContact } from "@formbricks/database/zod/contact";
|
||||
import { ZContactAttributeKey } from "@formbricks/database/zod/contact-attribute-keys";
|
||||
@@ -26,6 +27,8 @@ import { rolePaths } from "@/modules/api/v2/roles/lib/openapi";
|
||||
import { bulkContactPaths } from "@/modules/ee/contacts/api/v2/management/contacts/bulk/lib/openapi";
|
||||
import { contactPaths } from "@/modules/ee/contacts/api/v2/management/contacts/lib/openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
const document = createDocument({
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const getProjectTeamsEndpoint: ZodOpenApiOperationObject = {
|
||||
summary: "Get project teams",
|
||||
description: "Gets projectTeams from the database.",
|
||||
requestParams: {
|
||||
query: ZGetProjectTeamsFilter,
|
||||
query: ZGetProjectTeamsFilter.sourceType(),
|
||||
path: z.object({
|
||||
organizationId: ZOrganizationIdSchema,
|
||||
}),
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function GET(request: Request, props: { params: Promise<{ organizat
|
||||
return authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetProjectTeamsFilter,
|
||||
query: ZGetProjectTeamsFilter.sourceType(),
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
|
||||
+4
-4
@@ -3,8 +3,8 @@ import { ZProjectTeam } from "@formbricks/database/zod/project-teams";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZGetProjectTeamsFilter = ZGetFilter.extend({
|
||||
teamId: z.cuid2().optional(),
|
||||
projectId: z.cuid2().optional(),
|
||||
teamId: z.string().cuid2().optional(),
|
||||
projectId: z.string().cuid2().optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.startDate && data.endDate && data.startDate > data.endDate) {
|
||||
@@ -28,8 +28,8 @@ export const ZProjectTeamInput = ZProjectTeam.pick({
|
||||
export type TProjectTeamInput = z.infer<typeof ZProjectTeamInput>;
|
||||
|
||||
export const ZGetProjectTeamUpdateFilter = z.object({
|
||||
teamId: z.cuid2(),
|
||||
projectId: z.cuid2(),
|
||||
teamId: z.string().cuid2(),
|
||||
projectId: z.string().cuid2(),
|
||||
});
|
||||
|
||||
export const ZProjectZTeamUpdateSchema = ZProjectTeam.pick({
|
||||
|
||||
+8
-4
@@ -1,16 +1,20 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
import { ZTeam } from "@formbricks/database/zod/teams";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZTeamIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
id: "teamId",
|
||||
.openapi({
|
||||
ref: "teamId",
|
||||
description: "The ID of the team",
|
||||
param: {
|
||||
name: "id",
|
||||
in: "path",
|
||||
},
|
||||
})
|
||||
.describe("The ID of the team");
|
||||
});
|
||||
|
||||
export const ZTeamUpdateSchema = ZTeam.omit({
|
||||
id: true,
|
||||
|
||||
@@ -21,7 +21,7 @@ export const getTeamsEndpoint: ZodOpenApiOperationObject = {
|
||||
path: z.object({
|
||||
organizationId: ZOrganizationIdSchema,
|
||||
}),
|
||||
query: ZGetTeamsFilter,
|
||||
query: ZGetTeamsFilter.sourceType(),
|
||||
},
|
||||
tags: ["Organizations API - Teams"],
|
||||
responses: {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const GET = async (request: NextRequest, props: { params: Promise<{ organ
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetTeamsFilter,
|
||||
query: ZGetTeamsFilter.sourceType(),
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
|
||||
extendZodWithOpenApi(z);
|
||||
|
||||
export const ZOrganizationIdSchema = z
|
||||
.string()
|
||||
.cuid2()
|
||||
.meta({
|
||||
id: "organizationId",
|
||||
.openapi({
|
||||
ref: "organizationId",
|
||||
description: "The ID of the organization",
|
||||
param: {
|
||||
name: "organizationId",
|
||||
in: "path",
|
||||
},
|
||||
})
|
||||
.describe("The ID of the organization");
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ export const getUsersEndpoint: ZodOpenApiOperationObject = {
|
||||
path: z.object({
|
||||
organizationId: ZOrganizationIdSchema,
|
||||
}),
|
||||
query: ZGetUsersFilter,
|
||||
query: ZGetUsersFilter.sourceType(),
|
||||
},
|
||||
tags: ["Organizations API - Users"],
|
||||
responses: {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const GET = async (request: NextRequest, props: { params: Promise<{ organ
|
||||
authenticatedApiClient({
|
||||
request,
|
||||
schemas: {
|
||||
query: ZGetUsersFilter,
|
||||
query: ZGetUsersFilter.sourceType(),
|
||||
params: z.object({ organizationId: ZOrganizationIdSchema }),
|
||||
},
|
||||
externalParams: props.params,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZGetFilter = z.object({
|
||||
limit: z.coerce.number().min(1).max(250).optional().prefault(50).describe("Number of items to return"),
|
||||
skip: z.coerce.number().min(0).optional().prefault(0).describe("Number of items to skip"),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional().prefault("createdAt").describe("Sort by field"),
|
||||
order: z.enum(["asc", "desc"]).optional().prefault("desc").describe("Sort order"),
|
||||
limit: z.coerce.number().min(1).max(250).optional().default(50).describe("Number of items to return"),
|
||||
skip: z.coerce.number().min(0).optional().default(0).describe("Number of items to skip"),
|
||||
sortBy: z.enum(["createdAt", "updatedAt"]).optional().default("createdAt").describe("Sort by field"),
|
||||
order: z.enum(["asc", "desc"]).optional().default("desc").describe("Sort order"),
|
||||
startDate: z.coerce.date().optional().describe("Start date"),
|
||||
endDate: z.coerce.date().optional().describe("End date"),
|
||||
filterDateField: z.enum(["createdAt", "updatedAt"]).optional().describe("Date field to filter by"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export function responseWithMetaSchema<T extends z.ZodType>(contentSchema: T) {
|
||||
export function responseWithMetaSchema<T extends z.ZodTypeAny>(contentSchema: T) {
|
||||
return z.object({
|
||||
data: z.array(contentSchema).optional(),
|
||||
meta: z
|
||||
|
||||
@@ -7,16 +7,11 @@ import { getUserByEmail } from "@/lib/user/service";
|
||||
import { actionClient } from "@/lib/utils/action-client";
|
||||
|
||||
const ZCreateEmailTokenAction = z.object({
|
||||
email: z
|
||||
.email({
|
||||
error: "Invalid email",
|
||||
})
|
||||
.min(5)
|
||||
.max(255),
|
||||
email: z.string().min(5).max(255).email({ message: "Invalid email" }),
|
||||
});
|
||||
|
||||
export const createEmailTokenAction = actionClient
|
||||
.inputSchema(ZCreateEmailTokenAction)
|
||||
.schema(ZCreateEmailTokenAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const user = await getUserByEmail(parsedInput.email);
|
||||
if (!user) {
|
||||
|
||||
@@ -33,7 +33,7 @@ vi.mock("@/modules/email", () => ({
|
||||
|
||||
vi.mock("@/lib/utils/action-client", () => ({
|
||||
actionClient: {
|
||||
inputSchema: vi.fn().mockReturnThis(),
|
||||
schema: vi.fn().mockReturnThis(),
|
||||
action: vi.fn((fn) => fn),
|
||||
},
|
||||
}));
|
||||
@@ -50,7 +50,7 @@ describe("forgotPasswordAction", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ const ZForgotPasswordAction = z.object({
|
||||
});
|
||||
|
||||
export const forgotPasswordAction = actionClient
|
||||
.inputSchema(ZForgotPasswordAction)
|
||||
.schema(ZForgotPasswordAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
await applyIPRateLimit(rateLimitConfigs.auth.forgotPassword);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
|
||||
const ZForgotPasswordForm = z.object({
|
||||
email: z.email(),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
type TForgotPasswordForm = z.infer<typeof ZForgotPasswordForm>;
|
||||
|
||||
@@ -16,7 +16,7 @@ const ZResetPasswordAction = z.object({
|
||||
password: ZUserPassword,
|
||||
});
|
||||
|
||||
export const resetPasswordAction = actionClient.inputSchema(ZResetPasswordAction).action(
|
||||
export const resetPasswordAction = actionClient.schema(ZResetPasswordAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"user",
|
||||
|
||||
@@ -2,50 +2,87 @@ import { Authenticator } from "@otplib/core";
|
||||
import type { AuthenticatorOptions } from "@otplib/core/authenticator";
|
||||
import { createDigest, createRandomBytes } from "@otplib/plugin-crypto";
|
||||
import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { totpAuthenticatorCheck } from "./totp";
|
||||
|
||||
const createAuthenticator = (opts: Partial<AuthenticatorOptions> = {}) =>
|
||||
new Authenticator({
|
||||
createDigest,
|
||||
createRandomBytes,
|
||||
keyDecoder,
|
||||
keyEncoder,
|
||||
...opts,
|
||||
});
|
||||
vi.mock("@otplib/core");
|
||||
vi.mock("@otplib/plugin-crypto");
|
||||
vi.mock("@otplib/plugin-thirty-two");
|
||||
|
||||
describe("totpAuthenticatorCheck", () => {
|
||||
const token = "123456";
|
||||
const secret = "JBSWY3DPEHPK3PXP";
|
||||
const fixedEpoch = 1_700_000_000_000;
|
||||
const opts: Partial<AuthenticatorOptions> = { window: [1, 0] };
|
||||
|
||||
test("should check a TOTP token with a base32-encoded secret", () => {
|
||||
const token = createAuthenticator({ epoch: fixedEpoch }).generate(secret);
|
||||
const result = totpAuthenticatorCheck(token, secret, { epoch: fixedEpoch, window: [1, 0] });
|
||||
const checkMock = vi.fn().mockReturnValue(true);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
}));
|
||||
|
||||
const result = totpAuthenticatorCheck(token, secret, opts);
|
||||
|
||||
expect(Authenticator).toHaveBeenCalledWith({
|
||||
createDigest,
|
||||
createRandomBytes,
|
||||
keyDecoder,
|
||||
keyEncoder,
|
||||
window: [1, 0],
|
||||
});
|
||||
expect(checkMock).toHaveBeenCalledWith(token, secret);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should use default window if none is provided", () => {
|
||||
// Generate a token for one time-step in the past and verify it at current epoch.
|
||||
// Default window is [1, 0], so previous-step tokens are accepted.
|
||||
const token = createAuthenticator({ epoch: fixedEpoch }).generate(secret);
|
||||
const result = totpAuthenticatorCheck(token, secret, { epoch: fixedEpoch + 30_000 });
|
||||
const checkMock = vi.fn().mockReturnValue(true);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
}));
|
||||
|
||||
const result = totpAuthenticatorCheck(token, secret);
|
||||
|
||||
expect(Authenticator).toHaveBeenCalledWith({
|
||||
createDigest,
|
||||
createRandomBytes,
|
||||
keyDecoder,
|
||||
keyEncoder,
|
||||
window: [1, 0],
|
||||
});
|
||||
expect(checkMock).toHaveBeenCalledWith(token, secret);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for invalid token format", () => {
|
||||
const result = totpAuthenticatorCheck("invalidToken", secret);
|
||||
expect(result).toBe(false);
|
||||
test("should throw an error for invalid token format", () => {
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: () => {
|
||||
throw new Error("Invalid token format");
|
||||
},
|
||||
}));
|
||||
|
||||
expect(() => {
|
||||
totpAuthenticatorCheck("invalidToken", secret);
|
||||
}).toThrow("Invalid token format");
|
||||
});
|
||||
|
||||
test("should return false for invalid secret format", () => {
|
||||
const token = createAuthenticator({ epoch: fixedEpoch }).generate(secret);
|
||||
const result = totpAuthenticatorCheck(token, "invalidSecret", { epoch: fixedEpoch });
|
||||
expect(result).toBe(false);
|
||||
test("should throw an error for invalid secret format", () => {
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: () => {
|
||||
throw new Error("Invalid secret format");
|
||||
},
|
||||
}));
|
||||
|
||||
expect(() => {
|
||||
totpAuthenticatorCheck(token, "invalidSecret");
|
||||
}).toThrow("Invalid secret format");
|
||||
});
|
||||
|
||||
test("should return false if token verification fails", () => {
|
||||
const token = createAuthenticator({ epoch: fixedEpoch }).generate(secret);
|
||||
const result = totpAuthenticatorCheck(token, secret, { epoch: fixedEpoch + 60_000 });
|
||||
const checkMock = vi.fn().mockReturnValue(false);
|
||||
(Authenticator as unknown as vi.Mock).mockImplementation(() => ({
|
||||
check: checkMock,
|
||||
}));
|
||||
|
||||
const result = totpAuthenticatorCheck(token, secret);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,15 +21,11 @@ import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/compon
|
||||
import { PasswordInput } from "@/modules/ui/components/password-input";
|
||||
|
||||
const ZLoginForm = z.object({
|
||||
email: z.email(),
|
||||
email: z.string().email(),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, {
|
||||
error: "Password must be at least 8 characters long",
|
||||
})
|
||||
.max(128, {
|
||||
error: "Password must be 128 characters or less",
|
||||
}),
|
||||
.min(8, { message: "Password must be at least 8 characters long" })
|
||||
.max(128, { message: "Password must be 128 characters or less" }),
|
||||
totpCode: z.string().optional(),
|
||||
backupCode: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -177,7 +177,7 @@ async function handlePostUserCreation(
|
||||
}
|
||||
}
|
||||
|
||||
export const createUserAction = actionClient.inputSchema(ZCreateUserAction).action(
|
||||
export const createUserAction = actionClient.schema(ZCreateUserAction).action(
|
||||
withAuditLogging(
|
||||
"created",
|
||||
"user",
|
||||
|
||||
@@ -24,7 +24,7 @@ import { PasswordChecks } from "./password-checks";
|
||||
|
||||
const ZSignupInput = z.object({
|
||||
name: ZUserName,
|
||||
email: z.email(),
|
||||
email: z.string().email(),
|
||||
password: ZUserPassword,
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
|
||||
|
||||
vi.mock("@/lib/utils/action-client", () => ({
|
||||
actionClient: {
|
||||
inputSchema: vi.fn().mockReturnThis(),
|
||||
schema: vi.fn().mockReturnThis(),
|
||||
action: vi.fn((fn) => fn),
|
||||
},
|
||||
}));
|
||||
@@ -66,7 +66,7 @@ describe("resendVerificationEmailAction", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -15,7 +15,7 @@ const ZResendVerificationEmailAction = z.object({
|
||||
email: ZUserEmail,
|
||||
});
|
||||
|
||||
export const resendVerificationEmailAction = actionClient.inputSchema(ZResendVerificationEmailAction).action(
|
||||
export const resendVerificationEmailAction = actionClient.schema(ZResendVerificationEmailAction).action(
|
||||
withAuditLogging(
|
||||
"verificationEmailSent",
|
||||
"user",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { updateBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||
import { getUser, updateUser } from "@/modules/auth/lib/user";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
|
||||
export const verifyEmailChangeAction = actionClient.inputSchema(z.object({ token: z.string() })).action(
|
||||
export const verifyEmailChangeAction = actionClient.schema(z.object({ token: z.string() })).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"user",
|
||||
|
||||
@@ -2,9 +2,9 @@ import { z } from "zod";
|
||||
|
||||
export const ZRateLimitConfig = z.object({
|
||||
/** Rate limit window in seconds */
|
||||
interval: z.int().positive().describe("Rate limit window in seconds"),
|
||||
interval: z.number().int().positive().describe("Rate limit window in seconds"),
|
||||
/** Maximum allowed requests per interval */
|
||||
allowedPerInterval: z.int().positive().describe("Maximum allowed requests per interval"),
|
||||
allowedPerInterval: z.number().int().positive().describe("Maximum allowed requests per interval"),
|
||||
/** Namespace for grouping rate limit per feature */
|
||||
namespace: z.string().min(1).describe("Namespace for grouping rate limit per feature"),
|
||||
});
|
||||
|
||||
@@ -73,12 +73,12 @@ export const ZAuditLogEventSchema = z.object({
|
||||
type: ZAuditTarget,
|
||||
}),
|
||||
status: ZAuditStatus,
|
||||
timestamp: z.iso.datetime(),
|
||||
timestamp: z.string().datetime(),
|
||||
organizationId: z.string(),
|
||||
ipAddress: z.string().optional(), // Not using the .ip() here because if we don't enabled it we want to put UNKNOWN_DATA string, to keep the same pattern as the other fields
|
||||
changes: z.record(z.string(), z.any()).optional(),
|
||||
changes: z.record(z.any()).optional(),
|
||||
eventId: z.string().optional(),
|
||||
apiUrl: z.url().optional(),
|
||||
apiUrl: z.string().url().optional(),
|
||||
});
|
||||
|
||||
export type TAuditLogEvent = z.infer<typeof ZAuditLogEventSchema>;
|
||||
|
||||
@@ -16,20 +16,14 @@ import { isSubscriptionCancelled } from "@/modules/ee/billing/api/lib/is-subscri
|
||||
|
||||
const ZUpgradePlanAction = z.object({
|
||||
environmentId: ZId,
|
||||
priceLookupKey: z.enum(STRIPE_PRICE_LOOKUP_KEYS),
|
||||
priceLookupKey: z.nativeEnum(STRIPE_PRICE_LOOKUP_KEYS),
|
||||
});
|
||||
|
||||
export const upgradePlanAction = authenticatedActionClient.inputSchema(ZUpgradePlanAction).action(
|
||||
export const upgradePlanAction = authenticatedActionClient.schema(ZUpgradePlanAction).action(
|
||||
withAuditLogging(
|
||||
"subscriptionUpdated",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZUpgradePlanAction>;
|
||||
}) => {
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
@@ -59,57 +53,49 @@ const ZManageSubscriptionAction = z.object({
|
||||
environmentId: ZId,
|
||||
});
|
||||
|
||||
export const manageSubscriptionAction = authenticatedActionClient
|
||||
.inputSchema(ZManageSubscriptionAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"subscriptionAccessed",
|
||||
"organization",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager", "billing"],
|
||||
},
|
||||
],
|
||||
});
|
||||
export const manageSubscriptionAction = authenticatedActionClient.schema(ZManageSubscriptionAction).action(
|
||||
withAuditLogging(
|
||||
"subscriptionAccessed",
|
||||
"organization",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager", "billing"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("organization", organizationId);
|
||||
}
|
||||
|
||||
if (!organization.billing.stripeCustomerId) {
|
||||
throw new AuthorizationError("You do not have an associated Stripe CustomerId");
|
||||
}
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
const result = await createCustomerPortalSession(
|
||||
organization.billing.stripeCustomerId,
|
||||
`${WEBAPP_URL}/environments/${parsedInput.environmentId}/settings/billing`
|
||||
);
|
||||
ctx.auditLoggingCtx.newObject = { portalSession: result };
|
||||
return result;
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("organization", organizationId);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (!organization.billing.stripeCustomerId) {
|
||||
throw new AuthorizationError("You do not have an associated Stripe CustomerId");
|
||||
}
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
const result = await createCustomerPortalSession(
|
||||
organization.billing.stripeCustomerId,
|
||||
`${WEBAPP_URL}/environments/${parsedInput.environmentId}/settings/billing`
|
||||
);
|
||||
ctx.auditLoggingCtx.newObject = { portalSession: result };
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZIsSubscriptionCancelledAction = z.object({
|
||||
organizationId: ZId,
|
||||
});
|
||||
|
||||
export const isSubscriptionCancelledAction = authenticatedActionClient
|
||||
.inputSchema(ZIsSubscriptionCancelledAction)
|
||||
.schema(ZIsSubscriptionCancelledAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import Stripe from "stripe";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@/lib/constants";
|
||||
import { BILLING_LIMITS, PROJECT_FEATURE_KEYS, STRIPE_API_VERSION } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { getOrganization, updateOrganization } from "@/lib/organization/service";
|
||||
import { getStripeClient } from "./stripe-client";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
|
||||
export const handleCheckoutSessionCompleted = async (event: Stripe.Event) => {
|
||||
const stripe = getStripeClient();
|
||||
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||
if (!checkoutSession.metadata?.organizationId)
|
||||
throw new ResourceNotFoundError("No organizationId found in checkout session", checkoutSession.id);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import Stripe from "stripe";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { STRIPE_PRICE_LOOKUP_KEYS, WEBAPP_URL } from "@/lib/constants";
|
||||
import { STRIPE_API_VERSION, STRIPE_PRICE_LOOKUP_KEYS, WEBAPP_URL } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getStripeClient } from "./stripe-client";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
|
||||
export const createSubscription = async (
|
||||
organizationId: string,
|
||||
@@ -9,7 +14,6 @@ export const createSubscription = async (
|
||||
priceLookupKey: STRIPE_PRICE_LOOKUP_KEYS
|
||||
) => {
|
||||
try {
|
||||
const stripe = getStripeClient();
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) throw new Error("Organization not found.");
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ import { getOrganization, updateOrganization } from "@/lib/organization/service"
|
||||
export const handleInvoiceFinalized = async (event: Stripe.Event) => {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
|
||||
const subscription = invoice.parent?.subscription_details?.subscription;
|
||||
const subscriptionId = typeof subscription === "string" ? subscription : subscription?.id;
|
||||
const subscriptionId = invoice.subscription as string;
|
||||
if (!subscriptionId) {
|
||||
logger.warn({ invoiceId: invoice.id }, "Invoice finalized without subscription ID");
|
||||
return { status: 400, message: "No subscription ID found in invoice" };
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import Stripe from "stripe";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { STRIPE_API_VERSION } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getStripeClient } from "./stripe-client";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
|
||||
export const isSubscriptionCancelled = async (
|
||||
organizationId: string
|
||||
@@ -9,7 +15,6 @@ export const isSubscriptionCancelled = async (
|
||||
date: Date | null;
|
||||
}> => {
|
||||
try {
|
||||
const stripe = getStripeClient();
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) throw new Error("Team not found.");
|
||||
let isNewTeam =
|
||||
@@ -29,10 +34,9 @@ export const isSubscriptionCancelled = async (
|
||||
|
||||
for (const subscription of subscriptions.data) {
|
||||
if (subscription.cancel_at_period_end) {
|
||||
const periodEndTimestamp = subscription.cancel_at ?? subscription.items.data[0]?.current_period_end;
|
||||
return {
|
||||
cancelled: true,
|
||||
date: periodEndTimestamp ? new Date(periodEndTimestamp * 1000) : null,
|
||||
date: new Date(subscription.current_period_end * 1000),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import Stripe from "stripe";
|
||||
import { STRIPE_API_VERSION } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
export const getStripeClient = () => {
|
||||
if (!env.STRIPE_SECRET_KEY) {
|
||||
throw new Error("Stripe is not enabled; STRIPE_SECRET_KEY is not set.");
|
||||
}
|
||||
|
||||
return new Stripe(env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
};
|
||||
|
||||
export const getStripeWebhookSecret = () => {
|
||||
if (!env.STRIPE_WEBHOOK_SECRET) {
|
||||
throw new Error("Stripe webhook is not enabled; STRIPE_WEBHOOK_SECRET is not set.");
|
||||
}
|
||||
|
||||
return env.STRIPE_WEBHOOK_SECRET;
|
||||
};
|
||||
@@ -1,24 +1,20 @@
|
||||
import Stripe from "stripe";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { STRIPE_API_VERSION } from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { handleCheckoutSessionCompleted } from "@/modules/ee/billing/api/lib/checkout-session-completed";
|
||||
import { handleInvoiceFinalized } from "@/modules/ee/billing/api/lib/invoice-finalized";
|
||||
import { handleSubscriptionDeleted } from "@/modules/ee/billing/api/lib/subscription-deleted";
|
||||
import { getStripeClient, getStripeWebhookSecret } from "./stripe-client";
|
||||
|
||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: STRIPE_API_VERSION,
|
||||
});
|
||||
|
||||
const webhookSecret: string = env.STRIPE_WEBHOOK_SECRET!;
|
||||
|
||||
export const webhookHandler = async (requestBody: string, stripeSignature: string) => {
|
||||
let stripe: Stripe;
|
||||
let webhookSecret: string;
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
stripe = getStripeClient();
|
||||
webhookSecret = getStripeWebhookSecret();
|
||||
} catch (err: unknown) {
|
||||
logger.error(err, "Error getting Stripe client or webhook secret");
|
||||
logger.warn("Stripe webhook skipped: Stripe is not configured");
|
||||
return { status: 503, message: "Stripe webhook is not configured" };
|
||||
}
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(requestBody, stripeSignature, webhookSecret);
|
||||
} catch (err) {
|
||||
|
||||
@@ -15,7 +15,7 @@ const ZGeneratePersonalSurveyLinkAction = z.object({
|
||||
});
|
||||
|
||||
export const generatePersonalSurveyLinkAction = authenticatedActionClient
|
||||
.inputSchema(ZGeneratePersonalSurveyLinkAction)
|
||||
.schema(ZGeneratePersonalSurveyLinkAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromContactId(parsedInput.contactId);
|
||||
const projectId = await getProjectIdFromContactId(parsedInput.contactId);
|
||||
|
||||
@@ -23,12 +23,12 @@ import {
|
||||
|
||||
const ZGetContactsAction = z.object({
|
||||
environmentId: ZId,
|
||||
offset: z.int().nonnegative(),
|
||||
offset: z.number().int().nonnegative(),
|
||||
searchValue: z.string().optional(),
|
||||
});
|
||||
|
||||
export const getContactsAction = authenticatedActionClient
|
||||
.inputSchema(ZGetContactsAction)
|
||||
.schema(ZGetContactsAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
@@ -53,7 +53,7 @@ const ZContactDeleteAction = z.object({
|
||||
contactId: ZId,
|
||||
});
|
||||
|
||||
export const deleteContactAction = authenticatedActionClient.inputSchema(ZContactDeleteAction).action(
|
||||
export const deleteContactAction = authenticatedActionClient.schema(ZContactDeleteAction).action(
|
||||
withAuditLogging(
|
||||
"deleted",
|
||||
"contact",
|
||||
@@ -95,54 +95,46 @@ const ZCreateContactsFromCSV = z.object({
|
||||
attributeMap: ZContactCSVAttributeMap,
|
||||
});
|
||||
|
||||
export const createContactsFromCSVAction = authenticatedActionClient
|
||||
.inputSchema(ZCreateContactsFromCSV)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"createdFromCSV",
|
||||
"contact",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: Record<string, any>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
export const createContactsFromCSVAction = authenticatedActionClient.schema(ZCreateContactsFromCSV).action(
|
||||
withAuditLogging(
|
||||
"createdFromCSV",
|
||||
"contact",
|
||||
async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record<string, any> }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
const result = await createContactsFromCSV(
|
||||
parsedInput.csvData,
|
||||
parsedInput.environmentId,
|
||||
parsedInput.duplicateContactsAction,
|
||||
parsedInput.attributeMap
|
||||
);
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
const result = await createContactsFromCSV(
|
||||
parsedInput.csvData,
|
||||
parsedInput.environmentId,
|
||||
parsedInput.duplicateContactsAction,
|
||||
parsedInput.attributeMap
|
||||
);
|
||||
|
||||
if ("contacts" in result) {
|
||||
ctx.auditLoggingCtx.newObject = {
|
||||
contacts: result.contacts,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
if ("contacts" in result) {
|
||||
ctx.auditLoggingCtx.newObject = {
|
||||
contacts: result.contacts,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const ZUpdateContactAttributesAction = z.object({
|
||||
contactId: ZId,
|
||||
@@ -151,7 +143,7 @@ const ZUpdateContactAttributesAction = z.object({
|
||||
|
||||
export type TUpdateContactAttributesAction = z.infer<typeof ZUpdateContactAttributesAction>;
|
||||
export const updateContactAttributesAction = authenticatedActionClient
|
||||
.inputSchema(ZUpdateContactAttributesAction)
|
||||
.schema(ZUpdateContactAttributesAction)
|
||||
.action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user