Compare commits

..

5 Commits

Author SHA1 Message Date
Dhruwang a6b777db6f fix: translation 2026-03-10 11:27:57 +05:30
Balázs Úr 34c587342c update translatable keys 2026-03-06 12:19:45 +01:00
Balázs Úr 5a0b421153 merged main 2026-03-06 11:47:59 +01:00
Balázs Úr f6fab9a996 fix connector rendering 2026-02-26 09:39:15 +01:00
Balázs Úr fe33527da8 fix: mark strings as translatable in survey editor 2026-02-26 08:29:05 +01:00
252 changed files with 5661 additions and 6470 deletions
+6 -6
View File
@@ -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 = [
@@ -12,7 +12,7 @@ const ZUpdateNotificationSettingsAction = z.object({
});
export const updateNotificationSettingsAction = authenticatedActionClient
.inputSchema(ZUpdateNotificationSettingsAction)
.schema(ZUpdateNotificationSettingsAction)
.action(
withAuditLogging(
"updated",
@@ -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",
@@ -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);
}
)
);
@@ -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,
@@ -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,
@@ -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",
@@ -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",
@@ -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,
@@ -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);
+5 -1
View File
@@ -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,
};
+3 -3
View File
@@ -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;
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
View File
@@ -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
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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
View File
@@ -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(),
+1 -1
View File
@@ -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;
+44 -39
View File
@@ -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);
+1 -1
View File
@@ -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";
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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!"
}
}
+18 -19
View File
@@ -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": "感谢你的反馈!"
}
}
+18 -19
View File
@@ -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>;
@@ -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;
@@ -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) => {
@@ -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()
@@ -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({
@@ -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) {
+4 -1
View File
@@ -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,
@@ -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({
@@ -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,
+4 -4
View File
@@ -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
+2 -7
View File
@@ -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",
+62 -25
View File
@@ -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(),
});
+1 -1
View File
@@ -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>;
+38 -52
View File
@@ -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);
+41 -49
View File
@@ -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