mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-14 11:30:11 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 587b4a8924 |
@@ -32,20 +32,21 @@ jobs:
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Node.js 22.x
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
|
||||
with:
|
||||
version: 9.15.9
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Validate translation keys
|
||||
run: |
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
**/.next/
|
||||
**/out/
|
||||
**/build
|
||||
**/next-env.d.ts
|
||||
|
||||
# node
|
||||
**/dist/
|
||||
@@ -64,5 +63,3 @@ packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdat
|
||||
.cursorrules
|
||||
i18n.cache
|
||||
stats.html
|
||||
# next-agents-md
|
||||
.next-docs/
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
module.exports = {
|
||||
extends: ["@formbricks/eslint-config/legacy-next.js"],
|
||||
ignorePatterns: ["**/package.json", "**/tsconfig.json"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["locales/*.json"],
|
||||
plugins: ["i18n-json"],
|
||||
rules: {
|
||||
"i18n-json/identical-keys": [
|
||||
"error",
|
||||
{
|
||||
filePath: require("path").join(__dirname, "locales", "en-US.json"),
|
||||
checkExtraKeys: false,
|
||||
checkMissingKeys: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+18
-10
@@ -1,4 +1,4 @@
|
||||
FROM node:24-alpine3.23 AS base
|
||||
FROM node:22-alpine3.22 AS base
|
||||
|
||||
#
|
||||
## step 1: Prune monorepo
|
||||
@@ -20,7 +20,7 @@ FROM base AS installer
|
||||
# Enable corepack and prepare pnpm
|
||||
RUN npm install --ignore-scripts -g corepack@latest
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@10.28.2 --activate
|
||||
RUN corepack prepare pnpm@9.15.9 --activate
|
||||
|
||||
# Install necessary build tools and compilers
|
||||
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
||||
@@ -69,14 +69,20 @@ RUN --mount=type=secret,id=database_url \
|
||||
--mount=type=secret,id=sentry_auth_token \
|
||||
/tmp/read-secrets.sh pnpm build --filter=@formbricks/web...
|
||||
|
||||
# Extract Prisma version
|
||||
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
|
||||
|
||||
#
|
||||
## step 3: setup production runner
|
||||
#
|
||||
FROM base AS runner
|
||||
|
||||
# Update npm to latest, then create user
|
||||
# Note: npm's bundled tar has a known vulnerability but npm is only used during build, not at runtime
|
||||
RUN npm install --ignore-scripts -g npm@latest \
|
||||
RUN npm install --ignore-scripts -g corepack@latest && \
|
||||
corepack enable
|
||||
|
||||
RUN apk add --no-cache curl \
|
||||
&& apk add --no-cache supercronic \
|
||||
# && addgroup --system --gid 1001 nodejs \
|
||||
&& addgroup -S nextjs \
|
||||
&& adduser -S -u 1001 -G nextjs nextjs
|
||||
|
||||
@@ -107,13 +113,15 @@ RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./package
|
||||
COPY --from=installer /app/packages/database/dist ./packages/database/dist
|
||||
RUN chown -R nextjs:nextjs ./packages/database/dist && chmod -R 755 ./packages/database/dist
|
||||
|
||||
# Copy prisma client packages
|
||||
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
|
||||
|
||||
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
|
||||
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
|
||||
|
||||
COPY --from=installer /prisma_version.txt .
|
||||
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
|
||||
|
||||
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
|
||||
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2
|
||||
|
||||
@@ -126,9 +134,7 @@ RUN chmod -R 755 ./node_modules/@noble/hashes
|
||||
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
||||
RUN chmod -R 755 ./node_modules/zod
|
||||
|
||||
# Install prisma CLI globally for database migrations and fix permissions for nextjs user
|
||||
RUN npm install --ignore-scripts -g prisma@6 \
|
||||
&& chown -R nextjs:nextjs /usr/local/lib/node_modules/prisma
|
||||
RUN npm install -g prisma@6
|
||||
|
||||
# Create a startup script to handle the conditional logic
|
||||
COPY --from=installer /app/apps/web/scripts/docker/next-start.sh /home/nextjs/start.sh
|
||||
@@ -138,8 +144,10 @@ EXPOSE 3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
USER nextjs
|
||||
|
||||
# Prepare pnpm as the nextjs user to ensure it's available at runtime
|
||||
# Prepare volumes for uploads and SAML connections
|
||||
RUN mkdir -p /home/nextjs/apps/web/uploads/ && \
|
||||
RUN corepack prepare pnpm@9.15.9 --activate && \
|
||||
mkdir -p /home/nextjs/apps/web/uploads/ && \
|
||||
mkdir -p /home/nextjs/apps/web/saml-connection
|
||||
|
||||
VOLUME /home/nextjs/apps/web/uploads/
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ const mockProject: TProject = {
|
||||
},
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
languages: [],
|
||||
logo: null,
|
||||
|
||||
+3
-12
@@ -3,7 +3,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { previewSurvey } from "@/app/lib/templates";
|
||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
||||
import { buildStylingFromBrandColor } from "@/lib/styling/constants";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
||||
@@ -65,17 +64,10 @@ export const ProjectSettings = ({
|
||||
const { t } = useTranslation();
|
||||
const addProject = async (data: TProjectUpdateInput) => {
|
||||
try {
|
||||
// Build the full styling from the chosen brand color so all derived
|
||||
// colours (question, button, input, option, progress, etc.) are persisted.
|
||||
// Without this, only brandColor is saved and the look-and-feel page falls
|
||||
// back to STYLE_DEFAULTS computed from the default brand (#64748b).
|
||||
const fullStyling = buildStylingFromBrandColor(data.styling?.brandColor?.light);
|
||||
|
||||
const createProjectResponse = await createProjectAction({
|
||||
organizationId,
|
||||
data: {
|
||||
...data,
|
||||
styling: fullStyling,
|
||||
config: { channel, industry },
|
||||
teamIds: data.teamIds,
|
||||
},
|
||||
@@ -120,7 +112,6 @@ export const ProjectSettings = ({
|
||||
const projectName = form.watch("name");
|
||||
const logoUrl = form.watch("logo.url");
|
||||
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
|
||||
const previewStyling = useMemo(() => buildStylingFromBrandColor(brandColor), [brandColor]);
|
||||
const { isSubmitting } = form.formState;
|
||||
|
||||
const organizationTeamsOptions = organizationTeams.map((team) => ({
|
||||
@@ -235,7 +226,7 @@ export const ProjectSettings = ({
|
||||
alt="Logo"
|
||||
width={256}
|
||||
height={56}
|
||||
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
||||
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
||||
/>
|
||||
)}
|
||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||
@@ -244,7 +235,7 @@ export const ProjectSettings = ({
|
||||
appUrl={publicDomain}
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(projectName || "my Product", t)}
|
||||
styling={previewStyling}
|
||||
styling={{ brandColor: { light: brandColor } }}
|
||||
isBrandingEnabled={false}
|
||||
languageCode="default"
|
||||
onFileUpload={async (file) => file.name}
|
||||
|
||||
+1
-7
@@ -7,7 +7,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
@@ -33,12 +32,7 @@ export const DeleteOrganization = ({
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const result = await deleteOrganizationAction({ organizationId: organization.id });
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
setIsDeleting(false);
|
||||
return;
|
||||
}
|
||||
await deleteOrganizationAction({ organizationId: organization.id });
|
||||
toast.success(t("environments.settings.general.organization_deleted_successfully"));
|
||||
if (typeof localStorage !== "undefined") {
|
||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
|
||||
-9
@@ -316,14 +316,6 @@ export const generateResponseTableColumns = (
|
||||
},
|
||||
};
|
||||
|
||||
const responseIdColumn: ColumnDef<TResponseTableData> = {
|
||||
accessorKey: "responseId",
|
||||
header: () => <div className="gap-x-1.5">{t("common.response_id")}</div>,
|
||||
cell: ({ row }) => {
|
||||
return <IdBadge id={row.original.responseId} />;
|
||||
},
|
||||
};
|
||||
|
||||
const quotasColumn: ColumnDef<TResponseTableData> = {
|
||||
accessorKey: "quota",
|
||||
header: t("common.quota"),
|
||||
@@ -422,7 +414,6 @@ export const generateResponseTableColumns = (
|
||||
const baseColumns = [
|
||||
personColumn,
|
||||
singleUseIdColumn,
|
||||
responseIdColumn,
|
||||
dateColumn,
|
||||
...(showQuotasColumn ? [quotasColumn] : []),
|
||||
statusColumn,
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { TSurvey, TSurveyElementSummaryFileUpload } from "@formbricks/types/surv
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { getOriginalFileNameFromUrl } from "@/modules/storage/url-helpers";
|
||||
import { getOriginalFileNameFromUrl } from "@/modules/storage/utils";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
|
||||
+2
-14
@@ -21,7 +21,6 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir
|
||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/components/BaseSelectDropdown";
|
||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/workspace/integrations/airtable/lib/airtable";
|
||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||
@@ -269,14 +268,7 @@ export const AddIntegrationModal = ({
|
||||
airtableIntegrationData.config?.data.push(integrationData);
|
||||
}
|
||||
|
||||
const result = await createOrUpdateIntegrationAction({
|
||||
environmentId,
|
||||
integrationData: airtableIntegrationData,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: airtableIntegrationData });
|
||||
if (isEditMode) {
|
||||
toast.success(t("environments.integrations.integration_updated_successfully"));
|
||||
} else {
|
||||
@@ -312,11 +304,7 @@ export const AddIntegrationModal = ({
|
||||
const integrationData = structuredClone(airtableIntegrationData);
|
||||
integrationData.config.data.splice(index, 1);
|
||||
|
||||
const result = await createOrUpdateIntegrationAction({ environmentId, integrationData });
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData });
|
||||
handleClose();
|
||||
router.refresh();
|
||||
|
||||
|
||||
+3
-17
@@ -165,14 +165,7 @@ export const AddIntegrationModal = ({
|
||||
// create action
|
||||
googleSheetIntegrationData.config.data.push(integrationData);
|
||||
}
|
||||
const result = await createOrUpdateIntegrationAction({
|
||||
environmentId,
|
||||
integrationData: googleSheetIntegrationData,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
if (selectedIntegration) {
|
||||
toast.success(t("environments.integrations.integration_updated_successfully"));
|
||||
} else {
|
||||
@@ -212,14 +205,7 @@ export const AddIntegrationModal = ({
|
||||
googleSheetIntegrationData.config.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
const result = await createOrUpdateIntegrationAction({
|
||||
environmentId,
|
||||
integrationData: googleSheetIntegrationData,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: googleSheetIntegrationData });
|
||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
@@ -280,7 +266,7 @@ export const AddIntegrationModal = ({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
|
||||
<div className="mt-1 max-h-[15vh] overflow-x-hidden overflow-y-auto rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{surveyElements.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
|
||||
+2
-17
@@ -22,7 +22,6 @@ import {
|
||||
createEmptyMapping,
|
||||
} from "@/app/(app)/environments/[environmentId]/workspace/integrations/notion/components/MappingRow";
|
||||
import NotionLogo from "@/images/notion.png";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -218,14 +217,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegrationData.config.data.push(integrationData);
|
||||
}
|
||||
|
||||
const result = await createOrUpdateIntegrationAction({
|
||||
environmentId,
|
||||
integrationData: notionIntegrationData,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
if (selectedIntegration) {
|
||||
toast.success(t("environments.integrations.integration_updated_successfully"));
|
||||
} else {
|
||||
@@ -244,14 +236,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegrationData.config.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
const result = await createOrUpdateIntegrationAction({
|
||||
environmentId,
|
||||
integrationData: notionIntegrationData,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: notionIntegrationData });
|
||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
+2
-17
@@ -17,7 +17,6 @@ import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/workspace/integrations/actions";
|
||||
import SlackLogo from "@/images/slacklogo.png";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||
@@ -145,14 +144,7 @@ export const AddChannelMappingModal = ({
|
||||
// create action
|
||||
slackIntegrationData.config.data.push(integrationData);
|
||||
}
|
||||
const result = await createOrUpdateIntegrationAction({
|
||||
environmentId,
|
||||
integrationData: slackIntegrationData,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
if (selectedIntegration) {
|
||||
toast.success(t("environments.integrations.integration_updated_successfully"));
|
||||
} else {
|
||||
@@ -189,14 +181,7 @@ export const AddChannelMappingModal = ({
|
||||
slackIntegrationData.config.data.splice(selectedIntegration!.index, 1);
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
const result = await createOrUpdateIntegrationAction({
|
||||
environmentId,
|
||||
integrationData: slackIntegrationData,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await createOrUpdateIntegrationAction({ environmentId, integrationData: slackIntegrationData });
|
||||
toast.success(t("environments.integrations.integration_removed_successfully"));
|
||||
setOpen(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -64,7 +64,7 @@ const mockProject = {
|
||||
linkSurveyBranding: true,
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
languages: [],
|
||||
} as unknown as TProject;
|
||||
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getEnvironmentStateData } from "./data";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
environment: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/survey/lib/utils", () => ({
|
||||
transformPrismaSurvey: vi.fn((survey) => survey),
|
||||
}));
|
||||
|
||||
const environmentId = "cjld2cjxh0000qzrmn831i7rn";
|
||||
|
||||
const mockEnvironmentData = {
|
||||
id: environmentId,
|
||||
type: "production",
|
||||
appSetupCompleted: true,
|
||||
project: {
|
||||
id: "project-123",
|
||||
recontactDays: 30,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
placement: "bottomRight",
|
||||
inAppSurveyBranding: true,
|
||||
styling: { allowStyleOverwrite: false },
|
||||
organization: {
|
||||
id: "org-123",
|
||||
billing: {
|
||||
plan: "free",
|
||||
limits: { monthly: { responses: 100 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
actionClasses: [
|
||||
{
|
||||
id: "action-1",
|
||||
type: "code",
|
||||
name: "Test Action",
|
||||
key: "test-action",
|
||||
noCodeConfig: null,
|
||||
},
|
||||
],
|
||||
surveys: [
|
||||
{
|
||||
id: "survey-1",
|
||||
name: "Test Survey",
|
||||
type: "app",
|
||||
status: "inProgress",
|
||||
welcomeCard: { enabled: false },
|
||||
questions: [],
|
||||
blocks: null,
|
||||
variables: [],
|
||||
showLanguageSwitch: false,
|
||||
languages: [],
|
||||
endings: [],
|
||||
autoClose: null,
|
||||
styling: null,
|
||||
recaptcha: { enabled: false },
|
||||
segment: null,
|
||||
recontactDays: null,
|
||||
displayLimit: null,
|
||||
displayOption: "displayOnce",
|
||||
hiddenFields: { enabled: false },
|
||||
isBackButtonHidden: false,
|
||||
triggers: [],
|
||||
displayPercentage: null,
|
||||
delay: 0,
|
||||
projectOverwrites: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("getEnvironmentStateData", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("should return environment state data when environment exists", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(mockEnvironmentData as never);
|
||||
|
||||
const result = await getEnvironmentStateData(environmentId);
|
||||
|
||||
expect(result).toEqual({
|
||||
environment: {
|
||||
id: environmentId,
|
||||
type: "production",
|
||||
appSetupCompleted: true,
|
||||
project: {
|
||||
id: "project-123",
|
||||
recontactDays: 30,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
placement: "bottomRight",
|
||||
inAppSurveyBranding: true,
|
||||
styling: { allowStyleOverwrite: false },
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
id: "org-123",
|
||||
billing: {
|
||||
plan: "free",
|
||||
limits: { monthly: { responses: 100 } },
|
||||
},
|
||||
},
|
||||
surveys: mockEnvironmentData.surveys,
|
||||
actionClasses: mockEnvironmentData.actionClasses,
|
||||
});
|
||||
|
||||
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: environmentId },
|
||||
select: expect.objectContaining({
|
||||
id: true,
|
||||
type: true,
|
||||
appSetupCompleted: true,
|
||||
project: expect.any(Object),
|
||||
actionClasses: expect.any(Object),
|
||||
surveys: expect.any(Object),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError when environment is not found", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
|
||||
|
||||
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(ResourceNotFoundError);
|
||||
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow("environment");
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError when project is not found", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
project: null,
|
||||
} as never);
|
||||
|
||||
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError when organization is not found", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
project: {
|
||||
...mockEnvironmentData.project,
|
||||
organization: null,
|
||||
},
|
||||
} as never);
|
||||
|
||||
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("should throw DatabaseError on Prisma database errors", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Connection failed", {
|
||||
code: "P2024",
|
||||
clientVersion: "5.0.0",
|
||||
});
|
||||
vi.mocked(prisma.environment.findUnique).mockRejectedValue(prismaError);
|
||||
|
||||
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow(DatabaseError);
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should rethrow unexpected errors", async () => {
|
||||
const unexpectedError = new Error("Unexpected error");
|
||||
vi.mocked(prisma.environment.findUnique).mockRejectedValue(unexpectedError);
|
||||
|
||||
await expect(getEnvironmentStateData(environmentId)).rejects.toThrow("Unexpected error");
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle empty surveys array", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
surveys: [],
|
||||
} as never);
|
||||
|
||||
const result = await getEnvironmentStateData(environmentId);
|
||||
|
||||
expect(result.surveys).toEqual([]);
|
||||
});
|
||||
|
||||
test("should handle empty actionClasses array", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
actionClasses: [],
|
||||
} as never);
|
||||
|
||||
const result = await getEnvironmentStateData(environmentId);
|
||||
|
||||
expect(result.actionClasses).toEqual([]);
|
||||
});
|
||||
|
||||
test("should transform surveys using transformPrismaSurvey", async () => {
|
||||
const multipleSurveys = [
|
||||
...mockEnvironmentData.surveys,
|
||||
{
|
||||
...mockEnvironmentData.surveys[0],
|
||||
id: "survey-2",
|
||||
name: "Second Survey",
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
surveys: multipleSurveys,
|
||||
} as never);
|
||||
|
||||
const result = await getEnvironmentStateData(environmentId);
|
||||
|
||||
expect(result.surveys).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("should correctly map project properties to environment.project", async () => {
|
||||
const customProject = {
|
||||
...mockEnvironmentData.project,
|
||||
recontactDays: 14,
|
||||
clickOutsideClose: false,
|
||||
overlay: "dark",
|
||||
placement: "center",
|
||||
inAppSurveyBranding: false,
|
||||
styling: { allowStyleOverwrite: true, brandColor: "#ff0000" },
|
||||
};
|
||||
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
project: customProject,
|
||||
} as never);
|
||||
|
||||
const result = await getEnvironmentStateData(environmentId);
|
||||
|
||||
expect(result.environment.project).toEqual({
|
||||
id: "project-123",
|
||||
recontactDays: 14,
|
||||
clickOutsideClose: false,
|
||||
overlay: "dark",
|
||||
placement: "center",
|
||||
inAppSurveyBranding: false,
|
||||
styling: { allowStyleOverwrite: true, brandColor: "#ff0000" },
|
||||
});
|
||||
});
|
||||
|
||||
test("should validate environmentId input", async () => {
|
||||
// Invalid CUID should throw validation error
|
||||
await expect(getEnvironmentStateData("invalid-id")).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("should handle different environment types", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
type: "development",
|
||||
} as never);
|
||||
|
||||
const result = await getEnvironmentStateData(environmentId);
|
||||
|
||||
expect(result.environment.type).toBe("development");
|
||||
});
|
||||
|
||||
test("should handle appSetupCompleted false", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
appSetupCompleted: false,
|
||||
} as never);
|
||||
|
||||
const result = await getEnvironmentStateData(environmentId);
|
||||
|
||||
expect(result.environment.appSetupCompleted).toBe(false);
|
||||
});
|
||||
|
||||
test("should correctly extract organization billing data", async () => {
|
||||
const customBilling = {
|
||||
plan: "enterprise",
|
||||
stripeCustomerId: "cus_123",
|
||||
limits: {
|
||||
monthly: { responses: 10000, miu: 50000 },
|
||||
projects: 100,
|
||||
},
|
||||
};
|
||||
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
...mockEnvironmentData,
|
||||
project: {
|
||||
...mockEnvironmentData.project,
|
||||
organization: {
|
||||
id: "org-enterprise",
|
||||
billing: customBilling,
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
const result = await getEnvironmentStateData(environmentId);
|
||||
|
||||
expect(result.organization).toEqual({
|
||||
id: "org-enterprise",
|
||||
billing: customBilling,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -54,7 +54,7 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
|
||||
id: true,
|
||||
recontactDays: true,
|
||||
clickOutsideClose: true,
|
||||
overlay: true,
|
||||
darkOverlay: true,
|
||||
placement: true,
|
||||
inAppSurveyBranding: true,
|
||||
styling: true,
|
||||
@@ -174,7 +174,7 @@ export const getEnvironmentStateData = async (environmentId: string): Promise<En
|
||||
id: environmentData.project.id,
|
||||
recontactDays: environmentData.project.recontactDays,
|
||||
clickOutsideClose: environmentData.project.clickOutsideClose,
|
||||
overlay: environmentData.project.overlay,
|
||||
darkOverlay: environmentData.project.darkOverlay,
|
||||
placement: environmentData.project.placement,
|
||||
inAppSurveyBranding: environmentData.project.inAppSurveyBranding,
|
||||
styling: environmentData.project.styling,
|
||||
|
||||
@@ -58,7 +58,7 @@ const mockProject: TJsEnvironmentStateProject = {
|
||||
inAppSurveyBranding: true,
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
styling: {
|
||||
allowStyleOverwrite: false,
|
||||
},
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseUpdateInput, ZResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { ZResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getResponse } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
@@ -33,38 +31,6 @@ const handleDatabaseError = (error: Error, url: string, endpoint: string, respon
|
||||
return responses.internalServerErrorResponse("Unknown error occurred", true);
|
||||
};
|
||||
|
||||
const validateResponse = (
|
||||
response: TResponse,
|
||||
survey: TSurvey,
|
||||
responseUpdateInput: TResponseUpdateInput
|
||||
) => {
|
||||
// Validate response data against validation rules
|
||||
const mergedData = {
|
||||
...response.data,
|
||||
...responseUpdateInput.data,
|
||||
};
|
||||
|
||||
const isFinished = responseUpdateInput.finished ?? false;
|
||||
|
||||
const validationErrors = validateResponseData(
|
||||
survey.blocks,
|
||||
mergedData,
|
||||
responseUpdateInput.language ?? response.language ?? "en",
|
||||
isFinished,
|
||||
survey.questions
|
||||
);
|
||||
|
||||
if (validationErrors) {
|
||||
return {
|
||||
response: responses.badRequestResponse(
|
||||
"Validation failed",
|
||||
formatValidationErrorsForV1Api(validationErrors),
|
||||
true
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const PUT = withV1ApiWrapper({
|
||||
handler: async ({
|
||||
req,
|
||||
@@ -105,6 +71,12 @@ export const PUT = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Response", responseId, true),
|
||||
};
|
||||
}
|
||||
|
||||
if (response.finished) {
|
||||
return {
|
||||
response: responses.badRequestResponse("Response is already finished", undefined, true),
|
||||
@@ -147,11 +119,6 @@ export const PUT = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const validationResult = validateResponse(response, survey, inputValidation.data);
|
||||
if (validationResult) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
// update response with quota evaluation
|
||||
let updatedResponse;
|
||||
try {
|
||||
|
||||
@@ -6,14 +6,12 @@ import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
@@ -35,27 +33,6 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
);
|
||||
};
|
||||
|
||||
const validateResponse = (responseInputData: TResponseInput, survey: TSurvey) => {
|
||||
// Validate response data against validation rules
|
||||
const validationErrors = validateResponseData(
|
||||
survey.blocks,
|
||||
responseInputData.data,
|
||||
responseInputData.language ?? "en",
|
||||
responseInputData.finished,
|
||||
survey.questions
|
||||
);
|
||||
|
||||
if (validationErrors) {
|
||||
return {
|
||||
response: responses.badRequestResponse(
|
||||
"Validation failed",
|
||||
formatValidationErrorsForV1Api(validationErrors),
|
||||
true
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const POST = withV1ApiWrapper({
|
||||
handler: async ({ req, props }: { req: NextRequest; props: Context }) => {
|
||||
const params = await props.params;
|
||||
@@ -146,11 +123,6 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const validationResult = validateResponse(responseInputData, survey);
|
||||
if (validationResult) {
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
let response: TResponseWithQuotaFull;
|
||||
try {
|
||||
const meta: TResponseInput["meta"] = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { ImageResponse } from "@vercel/og";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const GET = async (req: NextRequest) => {
|
||||
|
||||
@@ -8,7 +8,10 @@ import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { deleteResponse, getResponse } from "@/lib/response/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import {
|
||||
formatValidationErrorsForV1Api,
|
||||
validateResponseData,
|
||||
} from "@/modules/api/v2/management/responses/lib/validation";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
import { updateResponseWithQuotaEvaluation } from "./lib/response";
|
||||
@@ -146,7 +149,6 @@ export const PUT = withV1ApiWrapper({
|
||||
result.survey.blocks,
|
||||
responseUpdate.data,
|
||||
responseUpdate.language ?? "en",
|
||||
responseUpdate.finished,
|
||||
result.survey.questions
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { TApiAuditLog, TApiKeyAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import {
|
||||
formatValidationErrorsForV1Api,
|
||||
validateResponseData,
|
||||
} from "@/modules/api/v2/management/responses/lib/validation";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
import {
|
||||
@@ -155,7 +158,6 @@ export const POST = withV1ApiWrapper({
|
||||
surveyResult.survey.blocks,
|
||||
responseInput.data,
|
||||
responseInput.language ?? "en",
|
||||
responseInput.finished,
|
||||
surveyResult.survey.questions
|
||||
);
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||
@@ -107,23 +106,6 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
);
|
||||
}
|
||||
|
||||
// Validate response data against validation rules
|
||||
const validationErrors = validateResponseData(
|
||||
survey.blocks,
|
||||
responseInputData.data,
|
||||
responseInputData.language ?? "en",
|
||||
responseInputData.finished,
|
||||
survey.questions
|
||||
);
|
||||
|
||||
if (validationErrors) {
|
||||
return responses.badRequestResponse(
|
||||
"Validation failed",
|
||||
formatValidationErrorsForV1Api(validationErrors),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
let response: TResponseWithQuotaFull;
|
||||
try {
|
||||
const meta: TResponseInputV2["meta"] = {
|
||||
|
||||
@@ -4848,26 +4848,12 @@ export const previewSurvey = (projectName: string, t: TFunction): TSurvey => {
|
||||
t("templates.preview_survey_question_2_choice_2_label"),
|
||||
],
|
||||
headline: t("templates.preview_survey_question_2_headline"),
|
||||
subheader: t("templates.preview_survey_question_2_subheader"),
|
||||
required: true,
|
||||
shuffleOption: "none",
|
||||
}),
|
||||
isDraft: true,
|
||||
},
|
||||
{
|
||||
...buildOpenTextElement({
|
||||
id: "preview_input_field_element",
|
||||
headline: t("templates.preview_survey_input_headline"),
|
||||
subheader: "",
|
||||
placeholder: t("templates.preview_survey_input_placeholder"),
|
||||
required: false,
|
||||
inputType: "text",
|
||||
longAnswer: false,
|
||||
}),
|
||||
isDraft: true,
|
||||
},
|
||||
],
|
||||
buttonLabel: createI18nString(t("templates.next"), []),
|
||||
backButtonLabel: createI18nString(t("templates.preview_survey_question_2_back_button_label"), []),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ import { authorizePrivateDownload } from "@/app/storage/[environmentId]/[accessT
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { applyRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { deleteFile, getFileStreamForDownload } from "@/modules/storage/service";
|
||||
import { deleteFile, getSignedUrlForDownload } from "@/modules/storage/service";
|
||||
import { getErrorResponseFromStorageError } from "@/modules/storage/utils";
|
||||
import { logFileDeletion } from "./lib/audit-logs";
|
||||
|
||||
@@ -39,25 +39,21 @@ export const GET = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Stream the file directly
|
||||
const streamResult = await getFileStreamForDownload(fileName, environmentId, accessType);
|
||||
const signedUrlResult = await getSignedUrlForDownload(fileName, environmentId, accessType);
|
||||
|
||||
if (!streamResult.ok) {
|
||||
const errorResponse = getErrorResponseFromStorageError(streamResult.error, { fileName });
|
||||
if (!signedUrlResult.ok) {
|
||||
const errorResponse = getErrorResponseFromStorageError(signedUrlResult.error, { fileName });
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
const { body, contentType, contentLength } = streamResult.data;
|
||||
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
...(contentLength > 0 && { "Content-Length": String(contentLength) }),
|
||||
Location: signedUrlResult.data,
|
||||
"Cache-Control":
|
||||
accessType === "private"
|
||||
? "no-store, no-cache, must-revalidate"
|
||||
: "public, max-age=31536000, immutable",
|
||||
: "public, max-age=300, s-maxage=300, stale-while-revalidate=300",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
+14
-80
@@ -258,7 +258,6 @@ checksums:
|
||||
common/no_background_image_found: 4108a781a9022c65671a826d4e299d5b
|
||||
common/no_code: f602144ab7d28a5b19a446bf74b4dcc4
|
||||
common/no_files_uploaded: c97be829e195a41b2f6b6717b87a232b
|
||||
common/no_overlay: 03cde9e91f08e4dd539d788e1e01407f
|
||||
common/no_quotas_found: 19dea6bcc39b579351073b3974990cb6
|
||||
common/no_result_found: fedddbc0149972ea072a9e063198a16d
|
||||
common/no_results: 0e9b73265c6542240f5a3bf6b43e9280
|
||||
@@ -285,7 +284,6 @@ checksums:
|
||||
common/organization_teams_not_found: ce29fcb7a4e8b4582f92b65dea9b7d4e
|
||||
common/other: 79acaa6cd481262bea4e743a422529d2
|
||||
common/others: 39160224ce0e35eb4eb252c997edf4d8
|
||||
common/overlay_color: 4b72073285d13fff93d094aabffe05ac
|
||||
common/overview: 30c54e4dc4ce599b87d94be34a8617f5
|
||||
common/password: 223a61cf906ab9c40d22612c588dff48
|
||||
common/paused: edb1f7b7219e1c9b7aa67159090d6991
|
||||
@@ -325,7 +323,6 @@ checksums:
|
||||
common/request_trial_license: 560df1240ef621f7c60d3f7d65422ccd
|
||||
common/reset_to_default: 68ee98b46677392f44b505b268053b26
|
||||
common/response: c7a9d88269d8ff117abcbc0d97f88b2c
|
||||
common/response_id: 73375099cc976dc7203b8e27f5f709e0
|
||||
common/responses: 14bb6c69f906d7bbd1359f7ef1bb3c28
|
||||
common/restart: bab6232e89f24e3129f8e48268739d5b
|
||||
common/role: 53743bbb6ca938f5b893552e839d067f
|
||||
@@ -931,7 +928,7 @@ checksums:
|
||||
environments/settings/general/from_your_organization: 4b7970431edb3d0f13c394dbd755a055
|
||||
environments/settings/general/invitation_sent_once_more: e6e5ea066810f9dcb65788aa4f05d6e2
|
||||
environments/settings/general/invite_deleted_successfully: 1c7dca6d0f6870d945288e38cfd2f943
|
||||
environments/settings/general/invite_expires_on: 6fd2356ad91a5f189070c43855904bb4
|
||||
environments/settings/general/invited_on: 83476ce4bcdfc3ccf524d1cd91b758a8
|
||||
environments/settings/general/invites_failed: 180ffb8db417050227cc2b2ea74b7aae
|
||||
environments/settings/general/leave_organization: e74132cb4a0dc98c41e61ea3b2dd268b
|
||||
environments/settings/general/leave_organization_description: 2d0cd65e4e78a9b2835cf88c4de407fb
|
||||
@@ -1085,6 +1082,7 @@ checksums:
|
||||
environments/surveys/edit/add_fallback_placeholder: 0e77ea487ddd7bc7fc2f1574b018dc08
|
||||
environments/surveys/edit/add_hidden_field_id: a8f55b51b790cf5f4d898af7770ad1ed
|
||||
environments/surveys/edit/add_highlight_border: 66f52b21fbb9aa6561c98a090abaaf8f
|
||||
environments/surveys/edit/add_highlight_border_description: 1c04654a393c0fa31d2b58abb6f85b4b
|
||||
environments/surveys/edit/add_logic: f234c9f1393a9ed4792dfbd15838c951
|
||||
environments/surveys/edit/add_none_of_the_above: dbe1ada4512d6c3f80c54c8fac107ec6
|
||||
environments/surveys/edit/add_option: 143c54f0b201067fe5159284d6daeca2
|
||||
@@ -1123,7 +1121,6 @@ checksums:
|
||||
environments/surveys/edit/block_duplicated: dc9e9fab2b1cd91f6c265324b34c6376
|
||||
environments/surveys/edit/bold: 4d7306bc355ed2befd6a9237c5452ee6
|
||||
environments/surveys/edit/brand_color: 84ddb5736deb9f5c081ffe4962a6c63e
|
||||
environments/surveys/edit/brand_color_description: 1cd10092621d375a37e297cc6353bce7
|
||||
environments/surveys/edit/brightness: 45425b6db1872225bfff71cf619d0e64
|
||||
environments/surveys/edit/bulk_edit: 59bd1a55587c8cbad716afbf2509e5bb
|
||||
environments/surveys/edit/bulk_edit_description: 9b5b2c6183c6c51689e16d7ba02ec9bb
|
||||
@@ -1141,9 +1138,7 @@ checksums:
|
||||
environments/surveys/edit/capture_new_action: 0aa2a3c399b62b1a52307deedf4922e8
|
||||
environments/surveys/edit/card_arrangement_for_survey_type_derived: c06b9aaebcc11bc16e57a445b62361fc
|
||||
environments/surveys/edit/card_background_color: acd5d023e1d1a4471b053dce504c7a83
|
||||
environments/surveys/edit/card_background_color_description: c96baa7fab5f2dfc41ff2e6a4e0242b0
|
||||
environments/surveys/edit/card_border_color: 8d7c7f4cbd99f154ce892dfa258eb504
|
||||
environments/surveys/edit/card_border_color_description: 57828ef76f8d055c530c1e0b0c0ddc09
|
||||
environments/surveys/edit/card_styling: 47137a7e809b060ca94418202a8fd3c5
|
||||
environments/surveys/edit/casual: 6534fe68718fade470a9031f7390409e
|
||||
environments/surveys/edit/caution_edit_duplicate: ee93bccb34fcd707e1ef4735f1c2fc31
|
||||
@@ -1154,12 +1149,20 @@ checksums:
|
||||
environments/surveys/edit/caution_explanation_responses_are_safe: 090ff00b7922a49c273e67c5f364730d
|
||||
environments/surveys/edit/caution_recommendation: b15090fe878ff17f2ee7cc2082dd9018
|
||||
environments/surveys/edit/caution_text: 3291e962c0e4c4656832837ddc512918
|
||||
environments/surveys/edit/centered_modal_overlay_color: 1124ba61ee2ecb18a7175ff780dc3b60
|
||||
environments/surveys/edit/change_anyway: 6377497d40373f6d0f082670194981ab
|
||||
environments/surveys/edit/change_background: fa71a993869f7d3ac553c547c12c3e9b
|
||||
environments/surveys/edit/change_question_type: 2d555ae48df8dbedfc6a4e1ad492f4aa
|
||||
environments/surveys/edit/change_survey_type: c26322043a476da6d94adb8b4efe1e93
|
||||
environments/surveys/edit/change_the_background_color_of_the_card: 41d805ef753a7d1e272b48519967bbd4
|
||||
environments/surveys/edit/change_the_background_color_of_the_input_fields: 4edbc9a9f5d145ed096cf5b4f8bdaac0
|
||||
environments/surveys/edit/change_the_background_to_a_color_image_or_animation: f1b9c9eb61497dd91b2550dd50c77836
|
||||
environments/surveys/edit/change_the_border_color_of_the_card: 64d76b247ab192343bb327f92a5f220c
|
||||
environments/surveys/edit/change_the_border_color_of_the_input_fields: bb687f41af15a1dd9494c14f97b10425
|
||||
environments/surveys/edit/change_the_border_radius_of_the_card_and_the_inputs: 9eccf688a7a67dfeeeed3de5209058b0
|
||||
environments/surveys/edit/change_the_brand_color_of_the_survey: ecc420c641fb58daaf4d2d0086357b7f
|
||||
environments/surveys/edit/change_the_placement_of_this_survey: 64359611bfb23bacc614ffe0b08fbe5d
|
||||
environments/surveys/edit/change_the_question_color_of_the_survey: ab6942138a8c5fc6c8c3b9f8dd95e980
|
||||
environments/surveys/edit/changes_saved: 90aab363c9e96eaa1295a997c48f97f6
|
||||
environments/surveys/edit/changing_survey_type_will_remove_existing_distribution_channels: 9ce817be04f13f2f0db981145ec48df4
|
||||
environments/surveys/edit/checkbox_label: 12a07d6bdf38e283a2e95892ec49b7f8
|
||||
@@ -1299,6 +1302,7 @@ checksums:
|
||||
environments/surveys/edit/hide_progress_bar: 7eefe7db6a051105bded521d94204933
|
||||
environments/surveys/edit/hide_question_settings: 99127cd016db2f7fc80333b36473c0ef
|
||||
environments/surveys/edit/hostname: 9bdaa7692869999df51bb60d58d9ef62
|
||||
environments/surveys/edit/how_funky_do_you_want_your_cards_in_survey_type_derived_surveys: 3cb16b37510c01af20a80f51b598346e
|
||||
environments/surveys/edit/if_you_need_more_please: a7d208c283caf6b93800b809fca80768
|
||||
environments/surveys/edit/if_you_really_want_that_answer_ask_until_you_get_it: 31c18a8c7c578db2ba49eed663d1739f
|
||||
environments/surveys/edit/ignore_global_waiting_time: e08db543ace4935625e0961cc6e60489
|
||||
@@ -1309,9 +1313,7 @@ checksums:
|
||||
environments/surveys/edit/initial_value: 809ee46fd787f4dc0146b3a80af5c2de
|
||||
environments/surveys/edit/inner_text: d1c7c98cfdb2fae3be91b7ee44288847
|
||||
environments/surveys/edit/input_border_color: d8a68d6b573189c291db6d83496210f6
|
||||
environments/surveys/edit/input_border_color_description: d338a4a6556db30ae7d5f8c7027bdcd4
|
||||
environments/surveys/edit/input_color: 55a0a092d16a1a6899c07b1b00d08604
|
||||
environments/surveys/edit/input_color_description: fa9f72ea65480c6b6e9e14b89109af03
|
||||
environments/surveys/edit/insert_link: c42ce4cb6ed35d5bd1389523585cc57e
|
||||
environments/surveys/edit/invalid_targeting: db9d1143c82a085c5dddf09492ea753c
|
||||
environments/surveys/edit/invalid_video_url_warning: 2e6a8eb121b46d7c3cc79d541b6a3cd5
|
||||
@@ -1395,6 +1397,7 @@ checksums:
|
||||
environments/surveys/edit/protect_survey_with_pin_description: 0e55d19b6f3578b1024e03606172a5d2
|
||||
environments/surveys/edit/publish: 4aa95ba4793bb293e771bd73b4f87c0f
|
||||
environments/surveys/edit/question: 0576462ce60d4263d7c482463fcc9547
|
||||
environments/surveys/edit/question_color: 6e69cb5699368bc68b2e1e1501f555c9
|
||||
environments/surveys/edit/question_deleted: ecdeb22b81ae2d732656a7742c1eec7b
|
||||
environments/surveys/edit/question_duplicated: 3f02439fd0a8b818bc84c1b1b473898c
|
||||
environments/surveys/edit/question_id_updated: e8d94dbefcbad00c7464b3d1fb0ee81a
|
||||
@@ -1454,7 +1457,6 @@ checksums:
|
||||
environments/surveys/edit/response_limits_redirections_and_more: e4f1cf94e56ad0e1b08701158d688802
|
||||
environments/surveys/edit/response_options: 2988136d5248d7726583108992dcbaee
|
||||
environments/surveys/edit/roundness: 5a161c8f5f258defb57ed1d551737cc4
|
||||
environments/surveys/edit/roundness_description: bde131aa5674836416dcdf2ff517d899
|
||||
environments/surveys/edit/row_used_in_logic_error: f89453ff1b6db77ad84af840fedd9813
|
||||
environments/surveys/edit/rows: 8f41f34e6ca28221cf1ebd948af4c151
|
||||
environments/surveys/edit/save_and_close: 6ede705b3f82f30269ff3054a5049e34
|
||||
@@ -1496,6 +1498,7 @@ checksums:
|
||||
environments/surveys/edit/styling_set_to_theme_styles: f2c108bf422372b00cf7c87f1b042f69
|
||||
environments/surveys/edit/subheading: c0f6f57155692fd8006381518ce4fef0
|
||||
environments/surveys/edit/subtract: 2d83b8b9ef35110f2583ddc155b6c486
|
||||
environments/surveys/edit/suggest_colors: ddc4543b416ab774007b10a3434343cd
|
||||
environments/surveys/edit/survey_completed_heading: dae5ac4a02a886dc9d9fc40927091919
|
||||
environments/surveys/edit/survey_completed_subheading: db537c356c3ab6564d24de0d11a0fee2
|
||||
environments/surveys/edit/survey_display_settings: 8ed19e6a8e1376f7a1ba037d82c4ae11
|
||||
@@ -1935,71 +1938,9 @@ checksums:
|
||||
environments/workspace/languages/translate: 59f9803b27e2030ba7323ed239116cf7
|
||||
environments/workspace/look/add_background_color: 9be512ee1246e32d3958c56097d202d9
|
||||
environments/workspace/look/add_background_color_description: adb6fcb392862b3d0e9420d9b5405ddb
|
||||
environments/workspace/look/advanced_styling_field_border_radius: 63b8f3541a9792d705e67d5aca7b6451
|
||||
environments/workspace/look/advanced_styling_field_button_bg: fc103ab926721e6213d39cc1f913c018
|
||||
environments/workspace/look/advanced_styling_field_button_bg_description: 9f14ec79ed40c0d3eb168cc46a9e0a14
|
||||
environments/workspace/look/advanced_styling_field_button_border_radius_description: 5677ee84511896ab9c369c0aced4c352
|
||||
environments/workspace/look/advanced_styling_field_button_font_size_description: 59508854b0101a89fab8250f79c0f3ba
|
||||
environments/workspace/look/advanced_styling_field_button_font_weight_description: d3dab571b0f1bc09d645be66c6686a06
|
||||
environments/workspace/look/advanced_styling_field_button_height_description: 1ab13a11281d2c303146e0483f12d948
|
||||
environments/workspace/look/advanced_styling_field_button_padding_x_description: 10e14296468321c13fda77fd1ba58dfd
|
||||
environments/workspace/look/advanced_styling_field_button_padding_y_description: 98b4aeff2940516d05ea61bdc1211d0d
|
||||
environments/workspace/look/advanced_styling_field_button_text: 3304e88bcc3869f3a306634b541e1e07
|
||||
environments/workspace/look/advanced_styling_field_button_text_description: 088f12906c8d2c06d3506f51d8ef8a89
|
||||
environments/workspace/look/advanced_styling_field_description_color: e2f4cbc96d3f0b75837a9edc95a5eeda
|
||||
environments/workspace/look/advanced_styling_field_description_color_description: f69d10a21c9233e0803f024f2e555485
|
||||
environments/workspace/look/advanced_styling_field_description_size: a0d51c3ab7dc56320ecedc2b27917842
|
||||
environments/workspace/look/advanced_styling_field_description_size_description: ff880ea1beddd1b1ec7416d0b8a69cf3
|
||||
environments/workspace/look/advanced_styling_field_description_weight: 514680cc7202ad29835c1cbcde3def1c
|
||||
environments/workspace/look/advanced_styling_field_description_weight_description: 441ac8db1a32557813eb68fbfd759061
|
||||
environments/workspace/look/advanced_styling_field_font_size: ca44d14429b2175a1b194793b4ab8f6b
|
||||
environments/workspace/look/advanced_styling_field_font_weight: bfef83778146cf40550df9650d8a07da
|
||||
environments/workspace/look/advanced_styling_field_headline_color: 4ccf3935ad90c88ad4add24f498673ce
|
||||
environments/workspace/look/advanced_styling_field_headline_color_description: b3fa9c2fc5da9ee11c1f279e4f949600
|
||||
environments/workspace/look/advanced_styling_field_headline_size: ddc49fa27fc97ed286d5c4309edd9a3c
|
||||
environments/workspace/look/advanced_styling_field_headline_size_description: 13debc3855e4edae992c7a1ebff599c3
|
||||
environments/workspace/look/advanced_styling_field_headline_weight: 0c8b8262945c61f8e2978502362e0a42
|
||||
environments/workspace/look/advanced_styling_field_headline_weight_description: 1a9c40bd76ff5098b1e48b1d3893171b
|
||||
environments/workspace/look/advanced_styling_field_height: f4da6d7ecd26e3fa75cfea03abb60c00
|
||||
environments/workspace/look/advanced_styling_field_indicator_bg: 00febda2901af0f1b0c17e44f9917c38
|
||||
environments/workspace/look/advanced_styling_field_indicator_bg_description: 7eb3b54a8b331354ec95c0dc1545c620
|
||||
environments/workspace/look/advanced_styling_field_input_border_radius_description: 0007f1bb572b35d9a3720daeb7a55617
|
||||
environments/workspace/look/advanced_styling_field_input_font_size_description: 5311f95dcbd083623e35c98ea5374c3b
|
||||
environments/workspace/look/advanced_styling_field_input_height_description: b704fc67e805223992c811d6f86a9c00
|
||||
environments/workspace/look/advanced_styling_field_input_padding_x_description: 10e14296468321c13fda77fd1ba58dfd
|
||||
environments/workspace/look/advanced_styling_field_input_padding_y_description: 98b4aeff2940516d05ea61bdc1211d0d
|
||||
environments/workspace/look/advanced_styling_field_input_placeholder_opacity_description: f55a6700884d24014404e58876121ddf
|
||||
environments/workspace/look/advanced_styling_field_input_shadow_description: b59ea4007756cecda47f216987ba05f6
|
||||
environments/workspace/look/advanced_styling_field_input_text: 4999bfded16b7d0bbcc858b399745eaa
|
||||
environments/workspace/look/advanced_styling_field_input_text_description: 460450df24ea0cc902710118a5000feb
|
||||
environments/workspace/look/advanced_styling_field_option_bg: 0ceaed10d99ed4ad83cb0934ab970174
|
||||
environments/workspace/look/advanced_styling_field_option_bg_description: 6cd6ccecbbb9f2f19439d7c682eb67c1
|
||||
environments/workspace/look/advanced_styling_field_option_border_radius_description: 23f81c25b2681a7c9e2c4f2e7d2e0656
|
||||
environments/workspace/look/advanced_styling_field_option_font_size_description: 5430fd9b08819972f0a613bf3fa659da
|
||||
environments/workspace/look/advanced_styling_field_option_label: 2767a5db32742073a01aac16488e93dc
|
||||
environments/workspace/look/advanced_styling_field_option_label_description: f42c9fc7b19cc2cb9b366a4cd31ae695
|
||||
environments/workspace/look/advanced_styling_field_option_padding_x_description: 10e14296468321c13fda77fd1ba58dfd
|
||||
environments/workspace/look/advanced_styling_field_option_padding_y_description: 98b4aeff2940516d05ea61bdc1211d0d
|
||||
environments/workspace/look/advanced_styling_field_padding_x: 74b440237b4ba662c9898d92e2e06217
|
||||
environments/workspace/look/advanced_styling_field_padding_y: 441d777bdc1cd1e792bf9815cc937c6a
|
||||
environments/workspace/look/advanced_styling_field_placeholder_opacity: fddcbc6e4fc5757aab807a6282d26627
|
||||
environments/workspace/look/advanced_styling_field_shadow: 7b4af1b447ece2b19b5d7717b2e15c4e
|
||||
environments/workspace/look/advanced_styling_field_track_bg: e569155b24616ba6d0a89a07bc85955c
|
||||
environments/workspace/look/advanced_styling_field_track_bg_description: 8a56258273dfe49e83fe752ea9e8daed
|
||||
environments/workspace/look/advanced_styling_field_track_height: 9ce57cb4583039c224a37e013efb6b8f
|
||||
environments/workspace/look/advanced_styling_field_track_height_description: 90243a4374e15d9118ad0fd93d5f3614
|
||||
environments/workspace/look/advanced_styling_field_upper_label_color: 65d75c60dfdba88e5fed38bcb24a0a5d
|
||||
environments/workspace/look/advanced_styling_field_upper_label_color_description: ae2276506807c7ceeb7a8b87723a8dd4
|
||||
environments/workspace/look/advanced_styling_field_upper_label_size: ea0ca9a3ffa1650f97a31df453b0afc7
|
||||
environments/workspace/look/advanced_styling_field_upper_label_size_description: 061668625be0f7a68ebc2e2ebe49e5a9
|
||||
environments/workspace/look/advanced_styling_field_upper_label_weight: 946c5836d2cfaaee21e494424d550887
|
||||
environments/workspace/look/advanced_styling_field_upper_label_weight_description: 916b03c719a8dead351679336aabcf53
|
||||
environments/workspace/look/advanced_styling_section_buttons: 3b44d6e2800e7bf3f133f1bce435f4c2
|
||||
environments/workspace/look/advanced_styling_section_headlines: 6def704c0ac2ecb5951400c806856a41
|
||||
environments/workspace/look/advanced_styling_section_inputs: 76bbeb561122a72fd3ec8c49eff7c563
|
||||
environments/workspace/look/advanced_styling_section_options: a92819a15bc8c3eb44bdd82a5075c9e2
|
||||
environments/workspace/look/app_survey_placement: f09cddac6bbb77d4694df223c6edf6b6
|
||||
environments/workspace/look/app_survey_placement_settings_description: d81bcff7a866a2f83ff76936dbad4770
|
||||
environments/workspace/look/centered_modal_overlay_color: 1124ba61ee2ecb18a7175ff780dc3b60
|
||||
environments/workspace/look/email_customization: ae399f381183a4fe0ffd41ab496b5d8f
|
||||
environments/workspace/look/email_customization_description: 5ccaf1769b2c39d7e87f3a08d056a374
|
||||
environments/workspace/look/enable_custom_styling: 4774d8fb009c27044aa0191ebcccdcc2
|
||||
@@ -2010,9 +1951,6 @@ checksums:
|
||||
environments/workspace/look/formbricks_branding_hidden: fda9ba81f8d7fdaacf8dc1642034e145
|
||||
environments/workspace/look/formbricks_branding_settings_description: 5bb39206c6412c703895593f465a01f9
|
||||
environments/workspace/look/formbricks_branding_shown: 6c9861cf8f95e8a68c5c64b2630d96cd
|
||||
environments/workspace/look/generate_theme_btn: 0345bf322c191e70d01fd6607ec5c2f8
|
||||
environments/workspace/look/generate_theme_confirmation: f119dbb85fb2bda1c0bcdc581724ef3b
|
||||
environments/workspace/look/generate_theme_header: 4df5f30a20cf78e248465915f222fd1b
|
||||
environments/workspace/look/logo_removed_successfully: f3a7f9d226affa91121e90ff360553aa
|
||||
environments/workspace/look/logo_settings_description: da155953f55cb44d0e563d9e740241aa
|
||||
environments/workspace/look/logo_updated_successfully: 170250f18062b79be6ac0481ec9d4368
|
||||
@@ -2027,7 +1965,6 @@ checksums:
|
||||
environments/workspace/look/show_formbricks_branding_in: 80fabfec9b34a13c0445d02b923216ed
|
||||
environments/workspace/look/show_powered_by_formbricks: a0e96edadec8ef326423feccc9d06be7
|
||||
environments/workspace/look/styling_updated_successfully: b8b74b50dde95abcd498633e9d0c891f
|
||||
environments/workspace/look/suggest_colors: ddc4543b416ab774007b10a3434343cd
|
||||
environments/workspace/look/theme: 21fe00b7a518089576fb83c08631107a
|
||||
environments/workspace/look/theme_settings_description: 9fc45322818c3774ab4a44ea14d7836e
|
||||
environments/workspace/tags/add: 87c4a663507f2bcbbf79934af8164e13
|
||||
@@ -2744,8 +2681,6 @@ checksums:
|
||||
templates/picture_selection_description: 44115e3668be00ae4854c299e5976398
|
||||
templates/preview_survey_ending_card_description: 6a98bdb1b083e437bb71d8b6c27b0e3a
|
||||
templates/preview_survey_ending_card_headline: 3512326ea9ad9a8a6c420a1e69d4d93f
|
||||
templates/preview_survey_input_headline: 277d044caa828f35758b165516da917d
|
||||
templates/preview_survey_input_placeholder: 743c34ea842287e929a5e7e6b7a4e89b
|
||||
templates/preview_survey_name: 82503c2dba9f071c93860e5ce9584e14
|
||||
templates/preview_survey_question_1_headline: 91622f99f8018adefb3ed0921bc1ad7a
|
||||
templates/preview_survey_question_1_lower_label: ff4681be0a94185111459994fe58478c
|
||||
@@ -2755,7 +2690,6 @@ checksums:
|
||||
templates/preview_survey_question_2_choice_1_label: 7885d14d0e01962fd290395ccd96ecfc
|
||||
templates/preview_survey_question_2_choice_2_label: 1af148222f327f28cf0db6513de5989e
|
||||
templates/preview_survey_question_2_headline: 5cfb173d156555227fbc2c97ad921e72
|
||||
templates/preview_survey_question_2_subheader: 2e652d8acd68d072e5a0ae686c4011c0
|
||||
templates/preview_survey_welcome_card_headline: 8778dc41547a2778d0f9482da989fc00
|
||||
templates/prioritize_features_description: 1eae41fad0e3947f803d8539081e59ec
|
||||
templates/prioritize_features_name: 4ca59ff1f9c319aaa68c3106d820fd6a
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -106,7 +106,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -171,7 +171,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -196,7 +196,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -250,7 +250,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -324,7 +324,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -378,7 +378,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -403,7 +403,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
@@ -448,7 +448,7 @@ describe("Project Service", () => {
|
||||
},
|
||||
placement: WidgetPlacement.bottomRight,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
styling: {
|
||||
allowStyleOverwrite: true,
|
||||
|
||||
@@ -22,7 +22,7 @@ const selectProject = {
|
||||
config: true,
|
||||
placement: true,
|
||||
clickOutsideClose: true,
|
||||
overlay: true,
|
||||
darkOverlay: true,
|
||||
environments: true,
|
||||
styling: true,
|
||||
logo: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// https://github.com/airbnb/javascript/#naming--uppercase
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { isLight, mixColor } from "@/lib/utils/colors";
|
||||
|
||||
export const COLOR_DEFAULTS = {
|
||||
brandColor: "#64748b",
|
||||
@@ -12,174 +11,32 @@ export const COLOR_DEFAULTS = {
|
||||
highlightBorderColor: "#64748b",
|
||||
} as const;
|
||||
|
||||
const DEFAULT_BRAND_COLOR = "#64748b";
|
||||
|
||||
/**
|
||||
* Derives a complete set of suggested color values from a single brand color.
|
||||
*
|
||||
* Used by the project-level "Suggest Colors" button **and** to build
|
||||
* `STYLE_DEFAULTS` so that a fresh install always has colours that are
|
||||
* visually cohesive with the default brand.
|
||||
*
|
||||
* The returned object is a flat map of form-field paths to values so it
|
||||
* can be spread directly into form defaults or applied via `form.setValue`.
|
||||
*/
|
||||
export const getSuggestedColors = (brandColor: string = DEFAULT_BRAND_COLOR) => {
|
||||
// Question / dark text: brand darkened with black (visible brand tint)
|
||||
const questionColor = mixColor(brandColor, "#000000", 0.35);
|
||||
// Input / option background: white with noticeable brand tint
|
||||
const inputBg = mixColor(brandColor, "#ffffff", 0.92);
|
||||
// Input border: visible brand-tinted border
|
||||
const inputBorder = mixColor(brandColor, "#ffffff", 0.6);
|
||||
// Card tones
|
||||
const cardBg = mixColor(brandColor, "#ffffff", 0.97);
|
||||
const cardBorder = mixColor(brandColor, "#ffffff", 0.8);
|
||||
// Page background
|
||||
const pageBg = mixColor(brandColor, "#ffffff", 0.855);
|
||||
|
||||
return {
|
||||
// General
|
||||
"brandColor.light": brandColor,
|
||||
"questionColor.light": questionColor,
|
||||
|
||||
// Headlines & Descriptions — use questionColor to match the legacy behaviour
|
||||
// where all text elements derived their color from questionColor.
|
||||
"elementHeadlineColor.light": questionColor,
|
||||
"elementDescriptionColor.light": questionColor,
|
||||
"elementUpperLabelColor.light": questionColor,
|
||||
|
||||
// Buttons — use the brand color so the button matches the user's intent.
|
||||
"buttonBgColor.light": brandColor,
|
||||
"buttonTextColor.light": isLight(brandColor) ? "#0f172a" : "#ffffff",
|
||||
|
||||
// Inputs
|
||||
"inputColor.light": inputBg,
|
||||
"inputBorderColor.light": inputBorder,
|
||||
"inputTextColor.light": questionColor,
|
||||
|
||||
// Options (Radio / Checkbox)
|
||||
"optionBgColor.light": inputBg,
|
||||
"optionLabelColor.light": questionColor,
|
||||
|
||||
// Card
|
||||
"cardBackgroundColor.light": cardBg,
|
||||
"cardBorderColor.light": cardBorder,
|
||||
|
||||
// Highlight / Focus
|
||||
"highlightBorderColor.light": mixColor(brandColor, "#ffffff", 0.25),
|
||||
|
||||
// Progress Bar — indicator uses the brand color; track is a lighter tint.
|
||||
"progressIndicatorBgColor.light": brandColor,
|
||||
"progressTrackBgColor.light": mixColor(brandColor, "#ffffff", 0.8),
|
||||
|
||||
// Background
|
||||
background: { bg: pageBg, bgType: "color" as const, brightness: 100 },
|
||||
};
|
||||
};
|
||||
|
||||
// Pre-compute colors derived from the default brand color.
|
||||
const _colors = getSuggestedColors(DEFAULT_BRAND_COLOR);
|
||||
|
||||
/**
|
||||
* Single source of truth for every styling default.
|
||||
*
|
||||
* Color values are derived from the default brand color (#64748b) via
|
||||
* `getSuggestedColors()`. Non-color values (dimensions, weights, sizes)
|
||||
* are hardcoded here and must be kept in sync with globals.css.
|
||||
*
|
||||
* Used everywhere: form defaults, preview rendering, email templates,
|
||||
* and as the reset target for "Restore defaults".
|
||||
*/
|
||||
export const STYLE_DEFAULTS: TProjectStyling = {
|
||||
export const defaultStyling: TProjectStyling = {
|
||||
allowStyleOverwrite: true,
|
||||
brandColor: { light: _colors["brandColor.light"] },
|
||||
questionColor: { light: _colors["questionColor.light"] },
|
||||
inputColor: { light: _colors["inputColor.light"] },
|
||||
inputBorderColor: { light: _colors["inputBorderColor.light"] },
|
||||
cardBackgroundColor: { light: _colors["cardBackgroundColor.light"] },
|
||||
cardBorderColor: { light: _colors["cardBorderColor.light"] },
|
||||
brandColor: {
|
||||
light: COLOR_DEFAULTS.brandColor,
|
||||
},
|
||||
questionColor: {
|
||||
light: COLOR_DEFAULTS.questionColor,
|
||||
},
|
||||
inputColor: {
|
||||
light: COLOR_DEFAULTS.inputColor,
|
||||
},
|
||||
inputBorderColor: {
|
||||
light: COLOR_DEFAULTS.inputBorderColor,
|
||||
},
|
||||
cardBackgroundColor: {
|
||||
light: COLOR_DEFAULTS.cardBackgroundColor,
|
||||
},
|
||||
cardBorderColor: {
|
||||
light: COLOR_DEFAULTS.cardBorderColor,
|
||||
},
|
||||
isLogoHidden: false,
|
||||
highlightBorderColor: { light: _colors["highlightBorderColor.light"] },
|
||||
highlightBorderColor: undefined,
|
||||
isDarkModeEnabled: false,
|
||||
roundness: 8,
|
||||
cardArrangement: { linkSurveys: "simple", appSurveys: "simple" },
|
||||
|
||||
// Headlines & Descriptions
|
||||
elementHeadlineColor: { light: _colors["elementHeadlineColor.light"] },
|
||||
elementHeadlineFontSize: 16,
|
||||
elementHeadlineFontWeight: 600,
|
||||
elementDescriptionColor: { light: _colors["elementDescriptionColor.light"] },
|
||||
elementDescriptionFontSize: 14,
|
||||
elementDescriptionFontWeight: 400,
|
||||
elementUpperLabelColor: { light: _colors["elementUpperLabelColor.light"] },
|
||||
elementUpperLabelFontSize: 12,
|
||||
elementUpperLabelFontWeight: 400,
|
||||
|
||||
// Inputs
|
||||
inputTextColor: { light: _colors["inputTextColor.light"] },
|
||||
inputBorderRadius: 8,
|
||||
inputHeight: 40,
|
||||
inputFontSize: 14,
|
||||
inputPaddingX: 16,
|
||||
inputPaddingY: 16,
|
||||
inputPlaceholderOpacity: 0.5,
|
||||
inputShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||
|
||||
// Buttons
|
||||
buttonBgColor: { light: _colors["buttonBgColor.light"] },
|
||||
buttonTextColor: { light: _colors["buttonTextColor.light"] },
|
||||
buttonBorderRadius: 8,
|
||||
buttonHeight: "auto",
|
||||
buttonFontSize: 16,
|
||||
buttonFontWeight: 500,
|
||||
buttonPaddingX: 12,
|
||||
buttonPaddingY: 12,
|
||||
|
||||
// Options
|
||||
optionBgColor: { light: _colors["optionBgColor.light"] },
|
||||
optionLabelColor: { light: _colors["optionLabelColor.light"] },
|
||||
optionBorderRadius: 8,
|
||||
optionPaddingX: 16,
|
||||
optionPaddingY: 16,
|
||||
optionFontSize: 14,
|
||||
|
||||
// Progress Bar
|
||||
progressTrackHeight: 8,
|
||||
progressTrackBgColor: { light: _colors["progressTrackBgColor.light"] },
|
||||
progressIndicatorBgColor: { light: _colors["progressIndicatorBgColor.light"] },
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a complete TProjectStyling object from a single brand color.
|
||||
*
|
||||
* Uses STYLE_DEFAULTS for all non-color properties (dimensions, weights, etc.)
|
||||
* and derives every color from the given brand color via getSuggestedColors().
|
||||
*
|
||||
* Useful when only a brand color is known (e.g. onboarding) and a fully
|
||||
* coherent styling object is needed for both preview rendering and persistence.
|
||||
*/
|
||||
export const buildStylingFromBrandColor = (brandColor: string = DEFAULT_BRAND_COLOR): TProjectStyling => {
|
||||
const colors = getSuggestedColors(brandColor);
|
||||
|
||||
return {
|
||||
...STYLE_DEFAULTS,
|
||||
brandColor: { light: colors["brandColor.light"] },
|
||||
questionColor: { light: colors["questionColor.light"] },
|
||||
elementHeadlineColor: { light: colors["elementHeadlineColor.light"] },
|
||||
elementDescriptionColor: { light: colors["elementDescriptionColor.light"] },
|
||||
elementUpperLabelColor: { light: colors["elementUpperLabelColor.light"] },
|
||||
buttonBgColor: { light: colors["buttonBgColor.light"] },
|
||||
buttonTextColor: { light: colors["buttonTextColor.light"] },
|
||||
inputColor: { light: colors["inputColor.light"] },
|
||||
inputBorderColor: { light: colors["inputBorderColor.light"] },
|
||||
inputTextColor: { light: colors["inputTextColor.light"] },
|
||||
optionBgColor: { light: colors["optionBgColor.light"] },
|
||||
optionLabelColor: { light: colors["optionLabelColor.light"] },
|
||||
cardBackgroundColor: { light: colors["cardBackgroundColor.light"] },
|
||||
cardBorderColor: { light: colors["cardBorderColor.light"] },
|
||||
highlightBorderColor: { light: colors["highlightBorderColor.light"] },
|
||||
progressIndicatorBgColor: { light: colors["progressIndicatorBgColor.light"] },
|
||||
progressTrackBgColor: { light: colors["progressTrackBgColor.light"] },
|
||||
background: colors.background,
|
||||
};
|
||||
cardArrangement: {
|
||||
linkSurveys: "straight",
|
||||
appSurveys: "straight",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ export const mockProject: TProject = {
|
||||
inAppSurveyBranding: false,
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: false,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [],
|
||||
languages: [],
|
||||
config: {
|
||||
|
||||
@@ -141,52 +141,5 @@ describe("Time Utilities", () => {
|
||||
expect(convertDatesInObject("string")).toBe("string");
|
||||
expect(convertDatesInObject(123)).toBe(123);
|
||||
});
|
||||
|
||||
test("should not convert dates in contactAttributes", () => {
|
||||
const input = {
|
||||
createdAt: "2024-03-20T15:30:00",
|
||||
contactAttributes: {
|
||||
createdAt: "2024-03-20T16:30:00",
|
||||
email: "test@example.com",
|
||||
},
|
||||
};
|
||||
|
||||
const result = convertDatesInObject(input);
|
||||
expect(result.createdAt).toBeInstanceOf(Date);
|
||||
expect(result.contactAttributes.createdAt).toBe("2024-03-20T16:30:00");
|
||||
expect(result.contactAttributes.email).toBe("test@example.com");
|
||||
});
|
||||
|
||||
test("should not convert dates in variables", () => {
|
||||
const input = {
|
||||
updatedAt: "2024-03-20T15:30:00",
|
||||
variables: {
|
||||
createdAt: "2024-03-20T16:30:00",
|
||||
userId: "123",
|
||||
},
|
||||
};
|
||||
|
||||
const result = convertDatesInObject(input);
|
||||
expect(result.updatedAt).toBeInstanceOf(Date);
|
||||
expect(result.variables.createdAt).toBe("2024-03-20T16:30:00");
|
||||
expect(result.variables.userId).toBe("123");
|
||||
});
|
||||
|
||||
test("should not convert dates in data or meta", () => {
|
||||
const input = {
|
||||
createdAt: "2024-03-20T15:30:00",
|
||||
data: {
|
||||
createdAt: "2024-03-20T16:30:00",
|
||||
},
|
||||
meta: {
|
||||
updatedAt: "2024-03-20T17:30:00",
|
||||
},
|
||||
};
|
||||
|
||||
const result = convertDatesInObject(input);
|
||||
expect(result.createdAt).toBeInstanceOf(Date);
|
||||
expect(result.data.createdAt).toBe("2024-03-20T16:30:00");
|
||||
expect(result.meta.updatedAt).toBe("2024-03-20T17:30:00");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,12 +160,7 @@ export const convertDatesInObject = <T>(obj: T): T => {
|
||||
return obj.map((item) => convertDatesInObject(item)) as unknown as T;
|
||||
}
|
||||
const newObj: any = {};
|
||||
const keysToIgnore = new Set(["contactAttributes", "variables", "data", "meta"]);
|
||||
for (const key in obj) {
|
||||
if (keysToIgnore.has(key)) {
|
||||
newObj[key] = obj[key];
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(key === "createdAt" || key === "updatedAt") &&
|
||||
typeof obj[key] === "string" &&
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Kein Hintergrundbild gefunden.",
|
||||
"no_code": "No Code",
|
||||
"no_files_uploaded": "Keine Dateien hochgeladen",
|
||||
"no_overlay": "Kein Overlay",
|
||||
"no_quotas_found": "Keine Kontingente gefunden",
|
||||
"no_result_found": "Kein Ergebnis gefunden",
|
||||
"no_results": "Keine Ergebnisse",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Organisations-Teams nicht gefunden",
|
||||
"other": "Andere",
|
||||
"others": "Andere",
|
||||
"overlay_color": "Overlay-Farbe",
|
||||
"overview": "Überblick",
|
||||
"password": "Passwort",
|
||||
"paused": "Pausiert",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Testlizenz anfordern",
|
||||
"reset_to_default": "Auf Standard zurücksetzen",
|
||||
"response": "Antwort",
|
||||
"response_id": "Antwort-ID",
|
||||
"responses": "Antworten",
|
||||
"restart": "Neustart",
|
||||
"role": "Rolle",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "von deiner Organisation",
|
||||
"invitation_sent_once_more": "Einladung nochmal gesendet.",
|
||||
"invite_deleted_successfully": "Einladung erfolgreich gelöscht",
|
||||
"invite_expires_on": "Einladung läuft ab am {date}",
|
||||
"invited_on": "Eingeladen am {date}",
|
||||
"invites_failed": "Einladungen fehlgeschlagen",
|
||||
"leave_organization": "Organisation verlassen",
|
||||
"leave_organization_description": "Du wirst diese Organisation verlassen und den Zugriff auf alle Umfragen und Antworten verlieren. Du kannst nur wieder beitreten, wenn Du erneut eingeladen wirst.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Platzhalter hinzufügen, falls kein Wert zur Verfügung steht.",
|
||||
"add_hidden_field_id": "Verstecktes Feld ID hinzufügen",
|
||||
"add_highlight_border": "Rahmen hinzufügen",
|
||||
"add_highlight_border_description": "Füge deiner Umfragekarte einen äußeren Rahmen hinzu.",
|
||||
"add_logic": "Logik hinzufügen",
|
||||
"add_none_of_the_above": "Füge \"Keine der oben genannten Optionen\" hinzu",
|
||||
"add_option": "Option hinzufügen",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Block dupliziert.",
|
||||
"bold": "Fett",
|
||||
"brand_color": "Markenfarbe",
|
||||
"brand_color_description": "Wird auf Buttons, Links und Hervorhebungen angewendet.",
|
||||
"brightness": "Helligkeit",
|
||||
"bulk_edit": "Massenbearbeitung",
|
||||
"bulk_edit_description": "Bearbeiten Sie alle Optionen unten, eine pro Zeile. Leere Zeilen werden übersprungen und Duplikate entfernt.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Neue Aktion erfassen",
|
||||
"card_arrangement_for_survey_type_derived": "Kartenanordnung für {surveyTypeDerived} Umfragen",
|
||||
"card_background_color": "Hintergrundfarbe der Karte",
|
||||
"card_background_color_description": "Füllt den Bereich der Umfragekarte.",
|
||||
"card_border_color": "Farbe des Kartenrandes",
|
||||
"card_border_color_description": "Umrandet die Umfragekarte.",
|
||||
"card_styling": "Kartengestaltung",
|
||||
"casual": "Lässig",
|
||||
"caution_edit_duplicate": "Duplizieren & bearbeiten",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Ältere und neuere Antworten vermischen sich, was zu irreführenden Datensummen führen kann.",
|
||||
"caution_recommendation": "Dies kann im Umfrageübersicht zu Dateninkonsistenzen führen. Wir empfehlen stattdessen, die Umfrage zu duplizieren.",
|
||||
"caution_text": "Änderungen werden zu Inkonsistenzen führen",
|
||||
"centered_modal_overlay_color": "Zentrierte modale Überlagerungsfarbe",
|
||||
"change_anyway": "Trotzdem ändern",
|
||||
"change_background": "Hintergrund ändern",
|
||||
"change_question_type": "Fragetyp ändern",
|
||||
"change_survey_type": "Die Änderung des Umfragetypen kann vorhandenen Zugriff beeinträchtigen",
|
||||
"change_the_background_color_of_the_card": "Hintergrundfarbe der Karte ändern.",
|
||||
"change_the_background_color_of_the_input_fields": "Hintergrundfarbe der Eingabefelder ändern.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Hintergrund zu einer Farbe, einem Bild oder einer Animation ändern.",
|
||||
"change_the_border_color_of_the_card": "Randfarbe der Karte ändern.",
|
||||
"change_the_border_color_of_the_input_fields": "Randfarbe der Eingabefelder ändern.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Radius der Ränder der Karte und der Eingabefelder ändern.",
|
||||
"change_the_brand_color_of_the_survey": "Markenfarbe der Umfrage ändern.",
|
||||
"change_the_placement_of_this_survey": "Platzierung dieser Umfrage ändern.",
|
||||
"change_the_question_color_of_the_survey": "Fragefarbe der Umfrage ändern.",
|
||||
"changes_saved": "Änderungen gespeichert.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "\"Das Ändern des Umfragetypen beeinflusst, wie er geteilt werden kann. Wenn Teilnehmer bereits Zugriffslinks für den aktuellen Typ haben, könnten sie das Zugriffsrecht nach dem Wechsel verlieren.\"",
|
||||
"checkbox_label": "Checkbox-Beschriftung",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Fortschrittsbalken ausblenden",
|
||||
"hide_question_settings": "Frageeinstellungen ausblenden",
|
||||
"hostname": "Hostname",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Wie funky sollen deine Karten in {surveyTypeDerived} Umfragen sein",
|
||||
"if_you_need_more_please": "Wenn Sie mehr benötigen, bitte",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Weiterhin anzeigen, wenn ausgelöst, bis eine Antwort abgegeben wird.",
|
||||
"ignore_global_waiting_time": "Abkühlphase ignorieren",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Anfangswert",
|
||||
"inner_text": "Innerer Text",
|
||||
"input_border_color": "Randfarbe des Eingabefelds",
|
||||
"input_border_color_description": "Umrandet Texteingaben und Textbereiche.",
|
||||
"input_color": "Farbe des Eingabefelds",
|
||||
"input_color_description": "Füllt das Innere von Texteingaben.",
|
||||
"insert_link": "Link einfügen",
|
||||
"invalid_targeting": "Ungültiges Targeting: Bitte überprüfe deine Zielgruppenfilter",
|
||||
"invalid_video_url_warning": "Bitte gib eine gültige YouTube-, Vimeo- oder Loom-URL ein. Andere Video-Plattformen werden derzeit nicht unterstützt.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Nur Benutzer, die die PIN haben, können auf die Umfrage zugreifen.",
|
||||
"publish": "Veröffentlichen",
|
||||
"question": "Frage",
|
||||
"question_color": "Fragefarbe",
|
||||
"question_deleted": "Frage gelöscht.",
|
||||
"question_duplicated": "Frage dupliziert.",
|
||||
"question_id_updated": "Frage-ID aktualisiert",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Antwort Limits, Weiterleitungen und mehr.",
|
||||
"response_options": "Antwortoptionen",
|
||||
"roundness": "Rundheit",
|
||||
"roundness_description": "Steuert, wie abgerundet die Kartenecken sind.",
|
||||
"row_used_in_logic_error": "Diese Zeile wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne sie zuerst aus der Logik.",
|
||||
"rows": "Zeilen",
|
||||
"save_and_close": "Speichern & Schließen",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Styling auf Themenstile eingestellt",
|
||||
"subheading": "Zwischenüberschrift",
|
||||
"subtract": "Subtrahieren -",
|
||||
"suggest_colors": "Farben vorschlagen",
|
||||
"survey_completed_heading": "Umfrage abgeschlossen",
|
||||
"survey_completed_subheading": "Diese kostenlose und quelloffene Umfrage wurde geschlossen",
|
||||
"survey_display_settings": "Einstellungen zur Anzeige der Umfrage",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Hintergrundfarbe hinzufügen",
|
||||
"add_background_color_description": "Füge dem Logo-Container eine Hintergrundfarbe hinzu.",
|
||||
"advanced_styling_field_border_radius": "Rahmenradius",
|
||||
"advanced_styling_field_button_bg": "Button-Hintergrund",
|
||||
"advanced_styling_field_button_bg_description": "Füllt den Weiter-/Absenden-Button.",
|
||||
"advanced_styling_field_button_border_radius_description": "Rundet die Button-Ecken ab.",
|
||||
"advanced_styling_field_button_font_size_description": "Skaliert den Text der Button-Beschriftung.",
|
||||
"advanced_styling_field_button_font_weight_description": "Macht den Button-Text heller oder fetter.",
|
||||
"advanced_styling_field_button_height_description": "Steuert die Button-Höhe.",
|
||||
"advanced_styling_field_button_padding_x_description": "Fügt links und rechts Abstand hinzu.",
|
||||
"advanced_styling_field_button_padding_y_description": "Fügt oben und unten Abstand hinzu.",
|
||||
"advanced_styling_field_button_text": "Button-Text",
|
||||
"advanced_styling_field_button_text_description": "Färbt die Beschriftung innerhalb von Buttons.",
|
||||
"advanced_styling_field_description_color": "Beschreibungsfarbe",
|
||||
"advanced_styling_field_description_color_description": "Färbt den Text unterhalb jeder Überschrift.",
|
||||
"advanced_styling_field_description_size": "Schriftgröße der Beschreibung",
|
||||
"advanced_styling_field_description_size_description": "Skaliert den Beschreibungstext.",
|
||||
"advanced_styling_field_description_weight": "Schriftstärke der Beschreibung",
|
||||
"advanced_styling_field_description_weight_description": "Macht den Beschreibungstext heller oder fetter.",
|
||||
"advanced_styling_field_font_size": "Schriftgröße",
|
||||
"advanced_styling_field_font_weight": "Schriftstärke",
|
||||
"advanced_styling_field_headline_color": "Überschriftsfarbe",
|
||||
"advanced_styling_field_headline_color_description": "Färbt den Hauptfragetext.",
|
||||
"advanced_styling_field_headline_size": "Schriftgröße der Überschrift",
|
||||
"advanced_styling_field_headline_size_description": "Skaliert den Überschriftentext.",
|
||||
"advanced_styling_field_headline_weight": "Schriftstärke der Überschrift",
|
||||
"advanced_styling_field_headline_weight_description": "Macht den Überschriftentext heller oder fetter.",
|
||||
"advanced_styling_field_height": "Höhe",
|
||||
"advanced_styling_field_indicator_bg": "Indikator-Hintergrund",
|
||||
"advanced_styling_field_indicator_bg_description": "Färbt den gefüllten Teil des Balkens.",
|
||||
"advanced_styling_field_input_border_radius_description": "Rundet die Eingabeecken ab.",
|
||||
"advanced_styling_field_input_font_size_description": "Skaliert den eingegebenen Text in Eingabefeldern.",
|
||||
"advanced_styling_field_input_height_description": "Steuert die Höhe des Eingabefelds.",
|
||||
"advanced_styling_field_input_padding_x_description": "Fügt links und rechts Abstand hinzu.",
|
||||
"advanced_styling_field_input_padding_y_description": "Fügt oben und unten Abstand hinzu.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Blendet den Platzhaltertext aus.",
|
||||
"advanced_styling_field_input_shadow_description": "Fügt einen Schlagschatten um Eingabefelder hinzu.",
|
||||
"advanced_styling_field_input_text": "Eingabetext",
|
||||
"advanced_styling_field_input_text_description": "Färbt den eingegebenen Text in Eingabefeldern.",
|
||||
"advanced_styling_field_option_bg": "Hintergrund",
|
||||
"advanced_styling_field_option_bg_description": "Füllt die Optionselemente.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rundet die Ecken der Optionen ab.",
|
||||
"advanced_styling_field_option_font_size_description": "Skaliert den Text der Optionsbeschriftung.",
|
||||
"advanced_styling_field_option_label": "Label-Farbe",
|
||||
"advanced_styling_field_option_label_description": "Färbt den Text der Optionsbeschriftung.",
|
||||
"advanced_styling_field_option_padding_x_description": "Fügt links und rechts Abstand hinzu.",
|
||||
"advanced_styling_field_option_padding_y_description": "Fügt oben und unten Abstand hinzu.",
|
||||
"advanced_styling_field_padding_x": "Innenabstand X",
|
||||
"advanced_styling_field_padding_y": "Innenabstand Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Platzhalter-Deckkraft",
|
||||
"advanced_styling_field_shadow": "Schatten",
|
||||
"advanced_styling_field_track_bg": "Track-Hintergrund",
|
||||
"advanced_styling_field_track_bg_description": "Färbt den nicht ausgefüllten Teil des Balkens.",
|
||||
"advanced_styling_field_track_height": "Track-Höhe",
|
||||
"advanced_styling_field_track_height_description": "Steuert die Dicke des Fortschrittsbalkens.",
|
||||
"advanced_styling_field_upper_label_color": "Farbe des oberen Labels",
|
||||
"advanced_styling_field_upper_label_color_description": "Färbt die kleine Beschriftung über Eingabefeldern.",
|
||||
"advanced_styling_field_upper_label_size": "Schriftgröße des oberen Labels",
|
||||
"advanced_styling_field_upper_label_size_description": "Skaliert die kleine Beschriftung über Eingabefeldern.",
|
||||
"advanced_styling_field_upper_label_weight": "Schriftstärke des oberen Labels",
|
||||
"advanced_styling_field_upper_label_weight_description": "Macht die Beschriftung leichter oder fetter.",
|
||||
"advanced_styling_section_buttons": "Buttons",
|
||||
"advanced_styling_section_headlines": "Überschriften & Beschreibungen",
|
||||
"advanced_styling_section_inputs": "Eingabefelder",
|
||||
"advanced_styling_section_options": "Optionen (Radio/Checkbox)",
|
||||
"app_survey_placement": "Platzierung der App-Umfrage",
|
||||
"app_survey_placement_settings_description": "Ändere, wo Umfragen in deiner Web-App oder Website angezeigt werden.",
|
||||
"centered_modal_overlay_color": "Zentrierte modale Überlagerungsfarbe",
|
||||
"email_customization": "E-Mail-Anpassung",
|
||||
"email_customization_description": "Ändere das Aussehen und die Gestaltung von E-Mails, die Formbricks in deinem Namen versendet.",
|
||||
"enable_custom_styling": "Benutzerdefiniertes Styling aktivieren",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Formbricks-Branding ist ausgeblendet.",
|
||||
"formbricks_branding_settings_description": "Wir freuen uns über deine Unterstützung, haben aber Verständnis, wenn du es ausschaltest.",
|
||||
"formbricks_branding_shown": "Formbricks-Branding wird angezeigt.",
|
||||
"generate_theme_btn": "Generieren",
|
||||
"generate_theme_confirmation": "Möchtest du ein passendes Farbschema basierend auf deiner Markenfarbe generieren? Dies überschreibt deine aktuellen Farbeinstellungen.",
|
||||
"generate_theme_header": "Farbschema generieren?",
|
||||
"logo_removed_successfully": "Logo erfolgreich entfernt",
|
||||
"logo_settings_description": "Lade dein Firmenlogo hoch, um Umfragen und Link-Vorschauen zu branden.",
|
||||
"logo_updated_successfully": "Logo erfolgreich aktualisiert",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Formbricks-Branding in {type}-Umfragen anzeigen",
|
||||
"show_powered_by_formbricks": "\"Powered by Formbricks\"-Signatur anzeigen",
|
||||
"styling_updated_successfully": "Styling erfolgreich aktualisiert",
|
||||
"suggest_colors": "Farben vorschlagen",
|
||||
"theme": "Theme",
|
||||
"theme_settings_description": "Erstelle ein Style-Theme für alle Umfragen. Du kannst für jede Umfrage individuelles Styling aktivieren."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Bitte die Befragten, ein oder mehrere Bilder auszuwählen",
|
||||
"preview_survey_ending_card_description": "Mach bitte mit deinem Onboarding weiter.",
|
||||
"preview_survey_ending_card_headline": "Geschafft!",
|
||||
"preview_survey_input_headline": "Möchtest Du noch etwas mitteilen?",
|
||||
"preview_survey_input_placeholder": "Hinterlasse eine Notiz...",
|
||||
"preview_survey_name": "Vorschau",
|
||||
"preview_survey_question_1_headline": "Wie würdest Du {projectName} bewerten?",
|
||||
"preview_survey_question_1_lower_label": "Nicht gut",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Ja, halte mich auf dem Laufenden.",
|
||||
"preview_survey_question_2_choice_2_label": "Nein, danke!",
|
||||
"preview_survey_question_2_headline": "Möchtest Du auf dem Laufenden bleiben?",
|
||||
"preview_survey_question_2_subheader": "Dies ist eine Beispielbeschreibung.",
|
||||
"preview_survey_welcome_card_headline": "Willkommen!",
|
||||
"prioritize_features_description": "Identifiziere die Funktionen, die deine Nutzer am meisten und am wenigsten brauchen.",
|
||||
"prioritize_features_name": "Funktionen priorisieren",
|
||||
|
||||
+18
-97
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "No background image found.",
|
||||
"no_code": "No code",
|
||||
"no_files_uploaded": "No files were uploaded",
|
||||
"no_overlay": "No overlay",
|
||||
"no_quotas_found": "No quotas found",
|
||||
"no_result_found": "No result found",
|
||||
"no_results": "No results",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Organization teams not found",
|
||||
"other": "Other",
|
||||
"others": "Others",
|
||||
"overlay_color": "Overlay color",
|
||||
"overview": "Overview",
|
||||
"password": "Password",
|
||||
"paused": "Paused",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Request trial license",
|
||||
"reset_to_default": "Reset to default",
|
||||
"response": "Response",
|
||||
"response_id": "Response ID",
|
||||
"responses": "Responses",
|
||||
"restart": "Restart",
|
||||
"role": "Role",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "from your organization",
|
||||
"invitation_sent_once_more": "Invitation sent once more.",
|
||||
"invite_deleted_successfully": "Invite deleted successfully",
|
||||
"invite_expires_on": "Invite expires on {date}",
|
||||
"invited_on": "Invited on {date}",
|
||||
"invites_failed": "Invites failed",
|
||||
"leave_organization": "Leave organization",
|
||||
"leave_organization_description": "You wil leave this organization and loose access to all surveys and responses. You can only rejoin if you are invited again.",
|
||||
@@ -1156,7 +1153,7 @@
|
||||
"add_fallback_placeholder": "Add a placeholder to show if there is no value to recall.",
|
||||
"add_hidden_field_id": "Add hidden field ID",
|
||||
"add_highlight_border": "Add highlight border",
|
||||
|
||||
"add_highlight_border_description": "Add an outer border to your survey card.",
|
||||
"add_logic": "Add logic",
|
||||
"add_none_of_the_above": "Add “None of the Above”",
|
||||
"add_option": "Add option",
|
||||
@@ -1195,7 +1192,6 @@
|
||||
"block_duplicated": "Block duplicated.",
|
||||
"bold": "Bold",
|
||||
"brand_color": "Brand color",
|
||||
"brand_color_description": "Applied to buttons, links and highlights.",
|
||||
"brightness": "Brightness",
|
||||
"bulk_edit": "Bulk edit",
|
||||
"bulk_edit_description": "Edit all options below, one per line. Empty lines will be skipped and duplicates removed.",
|
||||
@@ -1213,9 +1209,7 @@
|
||||
"capture_new_action": "Capture new action",
|
||||
"card_arrangement_for_survey_type_derived": "Card Arrangement for {surveyTypeDerived} Surveys",
|
||||
"card_background_color": "Card background color",
|
||||
"card_background_color_description": "Fills the survey card area.",
|
||||
"card_border_color": "Card border color",
|
||||
"card_border_color_description": "Outlines the survey card.",
|
||||
"card_styling": "Card styling",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicate & edit",
|
||||
@@ -1226,15 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Older and newer responses get mixed which can lead to misleading data summaries.",
|
||||
"caution_recommendation": "This may cause data inconsistencies in the survey summary. We recommend duplicating the survey instead.",
|
||||
"caution_text": "Changes will lead to inconsistencies",
|
||||
"centered_modal_overlay_color": "Centered modal overlay color",
|
||||
"change_anyway": "Change anyway",
|
||||
"change_background": "Change background",
|
||||
"change_question_type": "Change question type",
|
||||
"change_survey_type": "Switching survey type affects existing access",
|
||||
|
||||
"change_the_background_color_of_the_card": "Change the background color of the card.",
|
||||
"change_the_background_color_of_the_input_fields": "Change the background color of the input fields.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Change the background to a color, image or animation.",
|
||||
|
||||
"change_the_border_color_of_the_card": "Change the border color of the card.",
|
||||
"change_the_border_color_of_the_input_fields": "Change the border color of the input fields.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Change the border radius of the card and the inputs.",
|
||||
"change_the_brand_color_of_the_survey": "Change the brand color of the survey.",
|
||||
"change_the_placement_of_this_survey": "Change the placement of this survey.",
|
||||
|
||||
"change_the_question_color_of_the_survey": "Change the question color of the survey.",
|
||||
"changes_saved": "Changes saved.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Changing the survey type will affect how it can be shared. If respondents already have access links for the current type, they may lose access after the switch.",
|
||||
"checkbox_label": "Checkbox Label",
|
||||
@@ -1268,13 +1267,14 @@
|
||||
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
||||
"date_format": "Date format",
|
||||
"days_before_showing_this_survey_again": "or more days to pass between the last shown survey and showing this survey.",
|
||||
"display_type": "Display type",
|
||||
"dropdown": "Dropdown",
|
||||
"delete_anyways": "Delete anyways",
|
||||
"delete_block": "Delete block",
|
||||
"delete_choice": "Delete choice",
|
||||
"disable_the_visibility_of_survey_progress": "Disable the visibility of survey progress.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Display an estimate of completion time for survey",
|
||||
"display_number_of_responses_for_survey": "Display number of responses for survey",
|
||||
"display_type": "Display type",
|
||||
"divide": "Divide /",
|
||||
"does_not_contain": "Does not contain",
|
||||
"does_not_end_with": "Does not end with",
|
||||
@@ -1282,7 +1282,6 @@
|
||||
"does_not_include_all_of": "Does not include all of",
|
||||
"does_not_include_one_of": "Does not include one of",
|
||||
"does_not_start_with": "Does not start with",
|
||||
"dropdown": "Dropdown",
|
||||
"duplicate_block": "Duplicate block",
|
||||
"duplicate_question": "Duplicate question",
|
||||
"edit_link": "Edit link",
|
||||
@@ -1374,7 +1373,7 @@
|
||||
"hide_progress_bar": "Hide progress bar",
|
||||
"hide_question_settings": "Hide Question settings",
|
||||
"hostname": "Hostname",
|
||||
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "How funky do you want your cards in {surveyTypeDerived} Surveys",
|
||||
"if_you_need_more_please": "If you need more, please",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Keep showing whenever triggered until a response is submitted.",
|
||||
"ignore_global_waiting_time": "Ignore Cooldown Period",
|
||||
@@ -1385,9 +1384,7 @@
|
||||
"initial_value": "Initial value",
|
||||
"inner_text": "Inner Text",
|
||||
"input_border_color": "Input border color",
|
||||
"input_border_color_description": "Outlines text inputs and textareas.",
|
||||
"input_color": "Input color",
|
||||
"input_color_description": "Fills the inside of text inputs.",
|
||||
"insert_link": "Insert link",
|
||||
"invalid_targeting": "Invalid targeting: Please check your audience filters",
|
||||
"invalid_video_url_warning": "Please enter a valid YouTube, Vimeo, or Loom URL. We currently do not support other video hosting providers.",
|
||||
@@ -1417,11 +1414,11 @@
|
||||
"limit_the_maximum_file_size": "Limit the maximum file size for uploads.",
|
||||
"limit_upload_file_size_to": "Limit upload file size to",
|
||||
"link_survey_description": "Share a link to a survey page or embed it in a web page or email.",
|
||||
"list": "List",
|
||||
"load_segment": "Load segment",
|
||||
"logic_error_warning": "Changing will cause logic errors",
|
||||
"logic_error_warning_text": "Changing the question type will remove the logic conditions from this question",
|
||||
"logo_settings": "Logo settings",
|
||||
"list": "List",
|
||||
"long_answer": "Long answer",
|
||||
"long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.",
|
||||
"lower_label": "Lower Label",
|
||||
@@ -1471,7 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Only users who have the PIN can access the survey.",
|
||||
"publish": "Publish",
|
||||
"question": "Question",
|
||||
|
||||
"question_color": "Question color",
|
||||
"question_deleted": "Question deleted.",
|
||||
"question_duplicated": "Question duplicated.",
|
||||
"question_id_updated": "Question ID updated",
|
||||
@@ -1533,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Response limits, redirections and more.",
|
||||
"response_options": "Response Options",
|
||||
"roundness": "Roundness",
|
||||
"roundness_description": "Controls how rounded the card corners are.",
|
||||
"row_used_in_logic_error": "This row is used in logic of question {questionIndex}. Please remove it from logic first.",
|
||||
"rows": "Rows",
|
||||
"save_and_close": "Save & Close",
|
||||
@@ -1575,7 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Styling set to theme styles",
|
||||
"subheading": "Subheading",
|
||||
"subtract": "Subtract -",
|
||||
|
||||
"suggest_colors": "Suggest colors",
|
||||
"survey_completed_heading": "Survey Completed",
|
||||
"survey_completed_subheading": "This free & open-source survey has been closed",
|
||||
"survey_display_settings": "Survey Display Settings",
|
||||
@@ -2059,77 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Add background color",
|
||||
"add_background_color_description": "Add a background color to the logo container.",
|
||||
|
||||
"advanced_styling_field_border_radius": "Border Radius",
|
||||
|
||||
"advanced_styling_field_button_bg": "Button Background",
|
||||
"advanced_styling_field_button_bg_description": "Fills the Next / Submit button.",
|
||||
"advanced_styling_field_button_border_radius_description": "Rounds the button corners.",
|
||||
"advanced_styling_field_button_font_size_description": "Scales the button label text.",
|
||||
"advanced_styling_field_button_font_weight_description": "Makes button text lighter or bolder.",
|
||||
"advanced_styling_field_button_height_description": "Controls the button height.",
|
||||
"advanced_styling_field_button_padding_x_description": "Adds space on the left and right.",
|
||||
"advanced_styling_field_button_padding_y_description": "Adds space on the top and bottom.",
|
||||
"advanced_styling_field_button_text": "Button Text",
|
||||
"advanced_styling_field_button_text_description": "Colors the label inside buttons.",
|
||||
"advanced_styling_field_description_color": "Description Color",
|
||||
"advanced_styling_field_description_color_description": "Colors the text below each headline.",
|
||||
"advanced_styling_field_description_size": "Description Font Size",
|
||||
"advanced_styling_field_description_size_description": "Scales the description text.",
|
||||
"advanced_styling_field_description_weight": "Description Font Weight",
|
||||
"advanced_styling_field_description_weight_description": "Makes description text lighter or bolder.",
|
||||
|
||||
"advanced_styling_field_font_size": "Font Size",
|
||||
"advanced_styling_field_font_weight": "Font Weight",
|
||||
"advanced_styling_field_headline_color": "Headline Color",
|
||||
"advanced_styling_field_headline_color_description": "Colors the main question text.",
|
||||
"advanced_styling_field_headline_size": "Headline Font Size",
|
||||
"advanced_styling_field_headline_size_description": "Scales the headline text.",
|
||||
"advanced_styling_field_headline_weight": "Headline Font Weight",
|
||||
"advanced_styling_field_headline_weight_description": "Makes headline text lighter or bolder.",
|
||||
"advanced_styling_field_height": "Height",
|
||||
"advanced_styling_field_indicator_bg": "Indicator Background",
|
||||
"advanced_styling_field_indicator_bg_description": "Colors the filled portion of the bar.",
|
||||
|
||||
"advanced_styling_field_input_border_radius_description": "Rounds the input corners.",
|
||||
"advanced_styling_field_input_font_size_description": "Scales the typed text in inputs.",
|
||||
"advanced_styling_field_input_height_description": "Controls the input field height.",
|
||||
"advanced_styling_field_input_padding_x_description": "Adds space on the left and right.",
|
||||
"advanced_styling_field_input_padding_y_description": "Adds space on the top and bottom.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Fades the placeholder hint text.",
|
||||
"advanced_styling_field_input_shadow_description": "Adds a drop shadow around inputs.",
|
||||
"advanced_styling_field_input_text": "Input Text",
|
||||
"advanced_styling_field_input_text_description": "Colors the typed text in inputs.",
|
||||
"advanced_styling_field_option_bg": "Background",
|
||||
"advanced_styling_field_option_bg_description": "Fills the option items.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rounds the option corners.",
|
||||
"advanced_styling_field_option_font_size_description": "Scales the option label text.",
|
||||
"advanced_styling_field_option_label": "Label Color",
|
||||
"advanced_styling_field_option_label_description": "Colors the option label text.",
|
||||
"advanced_styling_field_option_padding_x_description": "Adds space on the left and right.",
|
||||
"advanced_styling_field_option_padding_y_description": "Adds space on the top and bottom.",
|
||||
"advanced_styling_field_padding_x": "Padding X",
|
||||
"advanced_styling_field_padding_y": "Padding Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Placeholder Opacity",
|
||||
"advanced_styling_field_shadow": "Shadow",
|
||||
"advanced_styling_field_track_bg": "Track Background",
|
||||
"advanced_styling_field_track_bg_description": "Colors the unfilled portion of the bar.",
|
||||
"advanced_styling_field_track_height": "Track Height",
|
||||
"advanced_styling_field_track_height_description": "Controls the progress bar thickness.",
|
||||
|
||||
"advanced_styling_field_upper_label_color": "Headline Label Color",
|
||||
"advanced_styling_field_upper_label_color_description": "Colors the small label above inputs.",
|
||||
"advanced_styling_field_upper_label_size": "Headline Label Font Size",
|
||||
"advanced_styling_field_upper_label_size_description": "Scales the small label above inputs.",
|
||||
"advanced_styling_field_upper_label_weight": "Headline Label Font Weight",
|
||||
"advanced_styling_field_upper_label_weight_description": "Makes the label lighter or bolder.",
|
||||
"advanced_styling_section_buttons": "Buttons",
|
||||
"advanced_styling_section_headlines": "Headlines & Descriptions",
|
||||
"advanced_styling_section_inputs": "Inputs",
|
||||
"advanced_styling_section_options": "Options (Radio/Checkbox)",
|
||||
|
||||
"app_survey_placement": "App Survey Placement",
|
||||
"app_survey_placement_settings_description": "Change where surveys will be shown in your web app or website.",
|
||||
"centered_modal_overlay_color": "Centered modal overlay color",
|
||||
"email_customization": "Email Customization",
|
||||
"email_customization_description": "Change the look and feel of emails Formbricks sends out on your behalf.",
|
||||
"enable_custom_styling": "Enable custom styling",
|
||||
@@ -2140,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Formbricks branding is hidden.",
|
||||
"formbricks_branding_settings_description": "We love your support but understand if you toggle it off.",
|
||||
"formbricks_branding_shown": "Formbricks branding is shown.",
|
||||
"generate_theme_btn": "Generate",
|
||||
"generate_theme_confirmation": "Would you like to generate a matching color theme based on your brand color? This will overwrite your current color settings.",
|
||||
"generate_theme_header": "Generate Color Theme?",
|
||||
"logo_removed_successfully": "Logo removed successfully",
|
||||
"logo_settings_description": "Upload your company logo to brand surveys and link previews.",
|
||||
"logo_updated_successfully": "Logo updated successfully",
|
||||
@@ -2157,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Show Formbricks Branding in {type} surveys",
|
||||
"show_powered_by_formbricks": "Show “Powered by Formbricks” Signature",
|
||||
"styling_updated_successfully": "Styling updated successfully",
|
||||
"suggest_colors": "Suggest colors",
|
||||
"theme": "Theme",
|
||||
"theme_settings_description": "Create a style theme for all surveys. You can enable custom styling for each survey."
|
||||
},
|
||||
@@ -2912,8 +2836,6 @@
|
||||
"picture_selection_description": "Ask respondents to choose one or more images",
|
||||
"preview_survey_ending_card_description": "Please continue your onboarding.",
|
||||
"preview_survey_ending_card_headline": "You did it!",
|
||||
"preview_survey_input_headline": "Anything else to share?",
|
||||
"preview_survey_input_placeholder": "Leave a note...",
|
||||
"preview_survey_name": "New Survey",
|
||||
"preview_survey_question_1_headline": "How would you rate {projectName}?",
|
||||
"preview_survey_question_1_lower_label": "Not good",
|
||||
@@ -2923,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Yes, keep me informed.",
|
||||
"preview_survey_question_2_choice_2_label": "No, thank you!",
|
||||
"preview_survey_question_2_headline": "Want to stay in the loop?",
|
||||
"preview_survey_question_2_subheader": "This is an example description.",
|
||||
"preview_survey_welcome_card_headline": "Welcome!",
|
||||
"prioritize_features_description": "Identify features your users need most and least.",
|
||||
"prioritize_features_name": "Prioritize Features",
|
||||
@@ -3173,4 +3094,4 @@
|
||||
"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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "No se encontró imagen de fondo.",
|
||||
"no_code": "Sin código",
|
||||
"no_files_uploaded": "No se subieron archivos",
|
||||
"no_overlay": "Sin superposición",
|
||||
"no_quotas_found": "No se encontraron cuotas",
|
||||
"no_result_found": "No se encontró resultado",
|
||||
"no_results": "Sin resultados",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Equipos de la organización no encontrados",
|
||||
"other": "Otro",
|
||||
"others": "Otros",
|
||||
"overlay_color": "Color de superposición",
|
||||
"overview": "Resumen",
|
||||
"password": "Contraseña",
|
||||
"paused": "Pausado",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Solicitar licencia de prueba",
|
||||
"reset_to_default": "Restablecer a valores predeterminados",
|
||||
"response": "Respuesta",
|
||||
"response_id": "ID de respuesta",
|
||||
"responses": "Respuestas",
|
||||
"restart": "Reiniciar",
|
||||
"role": "Rol",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "de tu organización",
|
||||
"invitation_sent_once_more": "Invitación enviada una vez más.",
|
||||
"invite_deleted_successfully": "Invitación eliminada correctamente",
|
||||
"invite_expires_on": "La invitación expira el {date}",
|
||||
"invited_on": "Invitado el {date}",
|
||||
"invites_failed": "Las invitaciones fallaron",
|
||||
"leave_organization": "Abandonar organización",
|
||||
"leave_organization_description": "Abandonarás esta organización y perderás acceso a todas las encuestas y respuestas. Solo podrás volver a unirte si te invitan de nuevo.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Añadir un marcador de posición para mostrar si no hay valor que recuperar.",
|
||||
"add_hidden_field_id": "Añadir ID de campo oculto",
|
||||
"add_highlight_border": "Añadir borde destacado",
|
||||
"add_highlight_border_description": "Añadir un borde exterior a tu tarjeta de encuesta.",
|
||||
"add_logic": "Añadir lógica",
|
||||
"add_none_of_the_above": "Añadir \"Ninguna de las anteriores\"",
|
||||
"add_option": "Añadir opción",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Bloque duplicado.",
|
||||
"bold": "Negrita",
|
||||
"brand_color": "Color de marca",
|
||||
"brand_color_description": "Se aplica a botones, enlaces y resaltados.",
|
||||
"brightness": "Brillo",
|
||||
"bulk_edit": "Edición masiva",
|
||||
"bulk_edit_description": "Edita todas las opciones a continuación, una por línea. Las líneas vacías se omitirán y los duplicados se eliminarán.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Capturar nueva acción",
|
||||
"card_arrangement_for_survey_type_derived": "Disposición de tarjetas para encuestas de tipo {surveyTypeDerived}",
|
||||
"card_background_color": "Color de fondo de la tarjeta",
|
||||
"card_background_color_description": "Rellena el área de la tarjeta de encuesta.",
|
||||
"card_border_color": "Color del borde de la tarjeta",
|
||||
"card_border_color_description": "Delinea la tarjeta de encuesta.",
|
||||
"card_styling": "Estilo de la tarjeta",
|
||||
"casual": "Informal",
|
||||
"caution_edit_duplicate": "Duplicar y editar",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Las respuestas antiguas y nuevas se mezclan, lo que puede llevar a resúmenes de datos engañosos.",
|
||||
"caution_recommendation": "Esto puede causar inconsistencias de datos en el resumen de la encuesta. Recomendamos duplicar la encuesta en su lugar.",
|
||||
"caution_text": "Los cambios provocarán inconsistencias",
|
||||
"centered_modal_overlay_color": "Color de superposición del modal centrado",
|
||||
"change_anyway": "Cambiar de todos modos",
|
||||
"change_background": "Cambiar fondo",
|
||||
"change_question_type": "Cambiar tipo de pregunta",
|
||||
"change_survey_type": "Cambiar el tipo de encuesta afecta al acceso existente",
|
||||
"change_the_background_color_of_the_card": "Cambiar el color de fondo de la tarjeta.",
|
||||
"change_the_background_color_of_the_input_fields": "Cambiar el color de fondo de los campos de entrada.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Cambiar el fondo a un color, imagen o animación.",
|
||||
"change_the_border_color_of_the_card": "Cambiar el color del borde de la tarjeta.",
|
||||
"change_the_border_color_of_the_input_fields": "Cambiar el color del borde de los campos de entrada.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Cambiar el radio del borde de la tarjeta y las entradas.",
|
||||
"change_the_brand_color_of_the_survey": "Cambiar el color de marca de la encuesta.",
|
||||
"change_the_placement_of_this_survey": "Cambiar la ubicación de esta encuesta.",
|
||||
"change_the_question_color_of_the_survey": "Cambiar el color de las preguntas de la encuesta.",
|
||||
"changes_saved": "Cambios guardados.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Cambiar el tipo de encuesta afectará a cómo se puede compartir. Si los encuestados ya tienen enlaces de acceso para el tipo actual, podrían perder el acceso después del cambio.",
|
||||
"checkbox_label": "Etiqueta de casilla de verificación",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Ocultar barra de progreso",
|
||||
"hide_question_settings": "Ocultar ajustes de la pregunta",
|
||||
"hostname": "Nombre de host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "¿Cuánto estilo quieres darle a tus tarjetas en las encuestas de tipo {surveyTypeDerived}?",
|
||||
"if_you_need_more_please": "Si necesitas más, por favor",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Seguir mostrando cuando se active hasta que se envíe una respuesta.",
|
||||
"ignore_global_waiting_time": "Ignorar periodo de espera",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Valor inicial",
|
||||
"inner_text": "Texto interior",
|
||||
"input_border_color": "Color del borde de entrada",
|
||||
"input_border_color_description": "Delinea los campos de texto y áreas de texto.",
|
||||
"input_color": "Color de entrada",
|
||||
"input_color_description": "Rellena el interior de los campos de texto.",
|
||||
"insert_link": "Insertar enlace",
|
||||
"invalid_targeting": "Segmentación no válida: por favor, comprueba tus filtros de audiencia",
|
||||
"invalid_video_url_warning": "Por favor, introduce una URL válida de YouTube, Vimeo o Loom. Actualmente no admitimos otros proveedores de alojamiento de vídeos.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Solo los usuarios que tengan el PIN pueden acceder a la encuesta.",
|
||||
"publish": "Publicar",
|
||||
"question": "Pregunta",
|
||||
"question_color": "Color de la pregunta",
|
||||
"question_deleted": "Pregunta eliminada.",
|
||||
"question_duplicated": "Pregunta duplicada.",
|
||||
"question_id_updated": "ID de pregunta actualizado",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Límites de respuestas, redirecciones y más.",
|
||||
"response_options": "Opciones de respuesta",
|
||||
"roundness": "Redondez",
|
||||
"roundness_description": "Controla qué tan redondeadas están las esquinas de la tarjeta.",
|
||||
"row_used_in_logic_error": "Esta fila se utiliza en la lógica de la pregunta {questionIndex}. Por favor, elimínala de la lógica primero.",
|
||||
"rows": "Filas",
|
||||
"save_and_close": "Guardar y cerrar",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Estilo configurado según los estilos del tema",
|
||||
"subheading": "Subtítulo",
|
||||
"subtract": "Restar -",
|
||||
"suggest_colors": "Sugerir colores",
|
||||
"survey_completed_heading": "Encuesta completada",
|
||||
"survey_completed_subheading": "Esta encuesta gratuita y de código abierto ha sido cerrada",
|
||||
"survey_display_settings": "Ajustes de visualización de la encuesta",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Añadir color de fondo",
|
||||
"add_background_color_description": "Añade un color de fondo al contenedor del logotipo.",
|
||||
"advanced_styling_field_border_radius": "Radio del borde",
|
||||
"advanced_styling_field_button_bg": "Fondo del botón",
|
||||
"advanced_styling_field_button_bg_description": "Rellena el botón siguiente / enviar.",
|
||||
"advanced_styling_field_button_border_radius_description": "Redondea las esquinas del botón.",
|
||||
"advanced_styling_field_button_font_size_description": "Escala el texto de la etiqueta del botón.",
|
||||
"advanced_styling_field_button_font_weight_description": "Hace el texto del botón más ligero o más grueso.",
|
||||
"advanced_styling_field_button_height_description": "Controla la altura del botón.",
|
||||
"advanced_styling_field_button_padding_x_description": "Añade espacio a la izquierda y a la derecha.",
|
||||
"advanced_styling_field_button_padding_y_description": "Añade espacio arriba y abajo.",
|
||||
"advanced_styling_field_button_text": "Texto del botón",
|
||||
"advanced_styling_field_button_text_description": "Colorea la etiqueta dentro de los botones.",
|
||||
"advanced_styling_field_description_color": "Color de la descripción",
|
||||
"advanced_styling_field_description_color_description": "Colorea el texto debajo de cada titular.",
|
||||
"advanced_styling_field_description_size": "Tamaño de fuente de la descripción",
|
||||
"advanced_styling_field_description_size_description": "Escala el texto de la descripción.",
|
||||
"advanced_styling_field_description_weight": "Grosor de fuente de la descripción",
|
||||
"advanced_styling_field_description_weight_description": "Hace el texto de la descripción más ligero o más grueso.",
|
||||
"advanced_styling_field_font_size": "Tamaño de fuente",
|
||||
"advanced_styling_field_font_weight": "Grosor de fuente",
|
||||
"advanced_styling_field_headline_color": "Color del titular",
|
||||
"advanced_styling_field_headline_color_description": "Colorea el texto principal de la pregunta.",
|
||||
"advanced_styling_field_headline_size": "Tamaño de fuente del titular",
|
||||
"advanced_styling_field_headline_size_description": "Escala el texto del titular.",
|
||||
"advanced_styling_field_headline_weight": "Grosor de fuente del titular",
|
||||
"advanced_styling_field_headline_weight_description": "Hace el texto del titular más ligero o más grueso.",
|
||||
"advanced_styling_field_height": "Altura",
|
||||
"advanced_styling_field_indicator_bg": "Fondo del indicador",
|
||||
"advanced_styling_field_indicator_bg_description": "Colorea la porción rellena de la barra.",
|
||||
"advanced_styling_field_input_border_radius_description": "Redondea las esquinas del campo.",
|
||||
"advanced_styling_field_input_font_size_description": "Escala el texto escrito en los campos.",
|
||||
"advanced_styling_field_input_height_description": "Controla la altura del campo de entrada.",
|
||||
"advanced_styling_field_input_padding_x_description": "Añade espacio a la izquierda y a la derecha.",
|
||||
"advanced_styling_field_input_padding_y_description": "Añade espacio en la parte superior e inferior.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Atenúa el texto de sugerencia del marcador de posición.",
|
||||
"advanced_styling_field_input_shadow_description": "Añade una sombra alrededor de los campos de entrada.",
|
||||
"advanced_styling_field_input_text": "Texto de entrada",
|
||||
"advanced_styling_field_input_text_description": "Colorea el texto escrito en los campos de entrada.",
|
||||
"advanced_styling_field_option_bg": "Fondo",
|
||||
"advanced_styling_field_option_bg_description": "Rellena los elementos de opción.",
|
||||
"advanced_styling_field_option_border_radius_description": "Redondea las esquinas de las opciones.",
|
||||
"advanced_styling_field_option_font_size_description": "Escala el texto de la etiqueta de opción.",
|
||||
"advanced_styling_field_option_label": "Color de la etiqueta",
|
||||
"advanced_styling_field_option_label_description": "Colorea el texto de la etiqueta de opción.",
|
||||
"advanced_styling_field_option_padding_x_description": "Añade espacio a la izquierda y a la derecha.",
|
||||
"advanced_styling_field_option_padding_y_description": "Añade espacio en la parte superior e inferior.",
|
||||
"advanced_styling_field_padding_x": "Relleno X",
|
||||
"advanced_styling_field_padding_y": "Relleno Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Opacidad del marcador de posición",
|
||||
"advanced_styling_field_shadow": "Sombra",
|
||||
"advanced_styling_field_track_bg": "Fondo de la pista",
|
||||
"advanced_styling_field_track_bg_description": "Colorea la parte no rellenada de la barra.",
|
||||
"advanced_styling_field_track_height": "Altura de la pista",
|
||||
"advanced_styling_field_track_height_description": "Controla el grosor de la barra de progreso.",
|
||||
"advanced_styling_field_upper_label_color": "Color de la etiqueta del titular",
|
||||
"advanced_styling_field_upper_label_color_description": "Colorea la etiqueta pequeña sobre los campos de entrada.",
|
||||
"advanced_styling_field_upper_label_size": "Tamaño de fuente de la etiqueta del titular",
|
||||
"advanced_styling_field_upper_label_size_description": "Escala la etiqueta pequeña sobre los campos de entrada.",
|
||||
"advanced_styling_field_upper_label_weight": "Grosor de fuente de la etiqueta del titular",
|
||||
"advanced_styling_field_upper_label_weight_description": "Hace que la etiqueta sea más ligera o más gruesa.",
|
||||
"advanced_styling_section_buttons": "Botones",
|
||||
"advanced_styling_section_headlines": "Títulos y descripciones",
|
||||
"advanced_styling_section_inputs": "Campos de entrada",
|
||||
"advanced_styling_section_options": "Opciones (radio/casilla de verificación)",
|
||||
"app_survey_placement": "Ubicación de encuesta de aplicación",
|
||||
"app_survey_placement_settings_description": "Cambia dónde se mostrarán las encuestas en tu aplicación web o sitio web.",
|
||||
"centered_modal_overlay_color": "Color de superposición del modal centrado",
|
||||
"email_customization": "Personalización de correo electrónico",
|
||||
"email_customization_description": "Cambia el aspecto de los correos electrónicos que Formbricks envía en tu nombre.",
|
||||
"enable_custom_styling": "Habilitar estilo personalizado",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "La marca de Formbricks está oculta.",
|
||||
"formbricks_branding_settings_description": "Nos encanta tu apoyo, pero lo entendemos si lo desactivas.",
|
||||
"formbricks_branding_shown": "La marca de Formbricks se muestra.",
|
||||
"generate_theme_btn": "Generar",
|
||||
"generate_theme_confirmation": "¿Te gustaría generar un tema de colores que combine con el color de tu marca? Esto sobrescribirá tu configuración de colores actual.",
|
||||
"generate_theme_header": "¿Generar tema de colores?",
|
||||
"logo_removed_successfully": "Logotipo eliminado correctamente",
|
||||
"logo_settings_description": "Sube el logotipo de tu empresa para personalizar las encuestas y las vistas previas de enlaces.",
|
||||
"logo_updated_successfully": "Logotipo actualizado correctamente",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Mostrar marca de Formbricks en encuestas de {type}",
|
||||
"show_powered_by_formbricks": "Mostrar firma 'Powered by Formbricks'",
|
||||
"styling_updated_successfully": "Estilo actualizado correctamente",
|
||||
"suggest_colors": "Sugerir colores",
|
||||
"theme": "Tema",
|
||||
"theme_settings_description": "Crea un tema de estilo para todas las encuestas. Puedes activar el estilo personalizado para cada encuesta."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Pide a los encuestados que elijan una o más imágenes",
|
||||
"preview_survey_ending_card_description": "Por favor, continúa con tu incorporación.",
|
||||
"preview_survey_ending_card_headline": "¡Lo has conseguido!",
|
||||
"preview_survey_input_headline": "¿Algo más que compartir?",
|
||||
"preview_survey_input_placeholder": "Deja una nota...",
|
||||
"preview_survey_name": "Encuesta nueva",
|
||||
"preview_survey_question_1_headline": "¿Cómo valorarías {projectName}?",
|
||||
"preview_survey_question_1_lower_label": "No es bueno",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Sí, mantenme informado.",
|
||||
"preview_survey_question_2_choice_2_label": "¡No, gracias!",
|
||||
"preview_survey_question_2_headline": "¿Quieres estar al tanto?",
|
||||
"preview_survey_question_2_subheader": "Esta es una descripción de ejemplo.",
|
||||
"preview_survey_welcome_card_headline": "¡Bienvenido!",
|
||||
"prioritize_features_description": "Identifica las funciones que tus usuarios necesitan más y menos.",
|
||||
"prioritize_features_name": "Priorizar funciones",
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Aucune image de fond trouvée.",
|
||||
"no_code": "Sans code",
|
||||
"no_files_uploaded": "Aucun fichier n'a été téléchargé.",
|
||||
"no_overlay": "Aucune superposition",
|
||||
"no_quotas_found": "Aucun quota trouvé",
|
||||
"no_result_found": "Aucun résultat trouvé",
|
||||
"no_results": "Aucun résultat",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Équipes d'organisation non trouvées",
|
||||
"other": "Autre",
|
||||
"others": "Autres",
|
||||
"overlay_color": "Couleur de superposition",
|
||||
"overview": "Aperçu",
|
||||
"password": "Mot de passe",
|
||||
"paused": "En pause",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Demander une licence d'essai",
|
||||
"reset_to_default": "Réinitialiser par défaut",
|
||||
"response": "Réponse",
|
||||
"response_id": "ID de réponse",
|
||||
"responses": "Réponses",
|
||||
"restart": "Recommencer",
|
||||
"role": "Rôle",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "de votre organisation",
|
||||
"invitation_sent_once_more": "Invitation envoyée une fois de plus.",
|
||||
"invite_deleted_successfully": "Invitation supprimée avec succès",
|
||||
"invite_expires_on": "L'invitation expire le {date}",
|
||||
"invited_on": "Invité le {date}",
|
||||
"invites_failed": "Invitations échouées",
|
||||
"leave_organization": "Quitter l'organisation",
|
||||
"leave_organization_description": "Vous quitterez cette organisation et perdrez l'accès à toutes les enquêtes et réponses. Vous ne pourrez revenir que si vous êtes de nouveau invité.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Ajouter un espace réservé à afficher s'il n'y a pas de valeur à rappeler.",
|
||||
"add_hidden_field_id": "Ajouter un champ caché ID",
|
||||
"add_highlight_border": "Ajouter une bordure de surlignage",
|
||||
"add_highlight_border_description": "Ajoutez une bordure extérieure à votre carte d'enquête.",
|
||||
"add_logic": "Ajouter de la logique",
|
||||
"add_none_of_the_above": "Ajouter \"Aucun des éléments ci-dessus\"",
|
||||
"add_option": "Ajouter une option",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Bloc dupliqué.",
|
||||
"bold": "Gras",
|
||||
"brand_color": "Couleur de marque",
|
||||
"brand_color_description": "Appliqué aux boutons, liens et éléments mis en évidence.",
|
||||
"brightness": "Luminosité",
|
||||
"bulk_edit": "Modification en masse",
|
||||
"bulk_edit_description": "Modifiez toutes les options ci-dessous, une par ligne. Les lignes vides seront ignorées et les doublons supprimés.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Capturer une nouvelle action",
|
||||
"card_arrangement_for_survey_type_derived": "Disposition des cartes pour les enquêtes {surveyTypeDerived}",
|
||||
"card_background_color": "Couleur de fond de la carte",
|
||||
"card_background_color_description": "Remplit la zone de la carte d'enquête.",
|
||||
"card_border_color": "Couleur de la bordure de la carte",
|
||||
"card_border_color_description": "Délimite la carte d'enquête.",
|
||||
"card_styling": "Style de carte",
|
||||
"casual": "Décontracté",
|
||||
"caution_edit_duplicate": "Dupliquer et modifier",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Les réponses anciennes et nouvelles se mélangent, ce qui peut entraîner des résumés de données trompeurs.",
|
||||
"caution_recommendation": "Cela peut entraîner des incohérences de données dans le résumé du sondage. Nous recommandons de dupliquer le sondage à la place.",
|
||||
"caution_text": "Les changements entraîneront des incohérences.",
|
||||
"centered_modal_overlay_color": "Couleur de superposition modale centrée",
|
||||
"change_anyway": "Changer de toute façon",
|
||||
"change_background": "Changer l'arrière-plan",
|
||||
"change_question_type": "Changer le type de question",
|
||||
"change_survey_type": "Le changement de type de sondage affecte l'accès existant",
|
||||
"change_the_background_color_of_the_card": "Changez la couleur de fond de la carte.",
|
||||
"change_the_background_color_of_the_input_fields": "Vous pouvez modifier la couleur d'arrière-plan des champs de saisie.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Changez l'arrière-plan en une couleur, une image ou une animation.",
|
||||
"change_the_border_color_of_the_card": "Changez la couleur de la bordure de la carte.",
|
||||
"change_the_border_color_of_the_input_fields": "Vous pouvez modifier la couleur de la bordure des champs de saisie.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Vous pouvez arrondir la bordure des encadrés et des champs de saisie.",
|
||||
"change_the_brand_color_of_the_survey": "Vous pouvez modifier la couleur dominante d'une enquête.",
|
||||
"change_the_placement_of_this_survey": "Changez le placement de cette enquête.",
|
||||
"change_the_question_color_of_the_survey": "Vous pouvez modifier la couleur des questions d'une enquête.",
|
||||
"changes_saved": "Modifications enregistrées.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Le changement du type de sondage affectera la façon dont il peut être partagé. Si les répondants ont déjà des liens d'accès pour le type actuel, ils peuvent perdre l'accès après le changement.",
|
||||
"checkbox_label": "Étiquette de case à cocher",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Cacher la barre de progression",
|
||||
"hide_question_settings": "Masquer les paramètres de la question",
|
||||
"hostname": "Nom d'hôte",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "À quel point voulez-vous que vos cartes soient funky dans les enquêtes {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Si vous avez besoin de plus, veuillez",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuer à afficher à chaque déclenchement jusqu'à ce qu'une réponse soit soumise.",
|
||||
"ignore_global_waiting_time": "Ignorer la période de refroidissement",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Valeur initiale",
|
||||
"inner_text": "Texte interne",
|
||||
"input_border_color": "Couleur de la bordure des champs de saisie",
|
||||
"input_border_color_description": "Délimite les champs de texte et les zones de texte.",
|
||||
"input_color": "Couleur d'arrière-plan des champs de saisie",
|
||||
"input_color_description": "Remplit l'intérieur des champs de texte.",
|
||||
"insert_link": "Insérer un lien",
|
||||
"invalid_targeting": "Ciblage invalide : Veuillez vérifier vos filtres d'audience",
|
||||
"invalid_video_url_warning": "Merci d'entrer une URL YouTube, Vimeo ou Loom valide. Les autres plateformes vidéo ne sont pas encore supportées.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Seules les personnes ayant le code PIN peuvent accéder à l'enquête.",
|
||||
"publish": "Publier",
|
||||
"question": "Question",
|
||||
"question_color": "Couleur des questions",
|
||||
"question_deleted": "Question supprimée.",
|
||||
"question_duplicated": "Question dupliquée.",
|
||||
"question_id_updated": "ID de la question mis à jour",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Limites de réponse, redirections et plus.",
|
||||
"response_options": "Options de réponse",
|
||||
"roundness": "Rondeur",
|
||||
"roundness_description": "Contrôle l'arrondi des coins de la carte.",
|
||||
"row_used_in_logic_error": "Cette ligne est utilisée dans la logique de la question {questionIndex}. Veuillez d'abord la supprimer de la logique.",
|
||||
"rows": "Lignes",
|
||||
"save_and_close": "Enregistrer et fermer",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Style défini sur les styles du thème",
|
||||
"subheading": "Sous-titre",
|
||||
"subtract": "Soustraire -",
|
||||
"suggest_colors": "Suggérer des couleurs",
|
||||
"survey_completed_heading": "Enquête terminée",
|
||||
"survey_completed_subheading": "Cette enquête gratuite et open-source a été fermée",
|
||||
"survey_display_settings": "Paramètres d'affichage de l'enquête",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Ajouter une couleur d'arrière-plan",
|
||||
"add_background_color_description": "Ajoutez une couleur d'arrière-plan au conteneur du logo.",
|
||||
"advanced_styling_field_border_radius": "Rayon de bordure",
|
||||
"advanced_styling_field_button_bg": "Arrière-plan du bouton",
|
||||
"advanced_styling_field_button_bg_description": "Remplit le bouton Suivant / Envoyer.",
|
||||
"advanced_styling_field_button_border_radius_description": "Arrondit les coins du bouton.",
|
||||
"advanced_styling_field_button_font_size_description": "Ajuste la taille du texte du libellé du bouton.",
|
||||
"advanced_styling_field_button_font_weight_description": "Rend le texte du bouton plus léger ou plus gras.",
|
||||
"advanced_styling_field_button_height_description": "Contrôle la hauteur du bouton.",
|
||||
"advanced_styling_field_button_padding_x_description": "Ajoute de l'espace à gauche et à droite.",
|
||||
"advanced_styling_field_button_padding_y_description": "Ajoute de l'espace en haut et en bas.",
|
||||
"advanced_styling_field_button_text": "Texte du bouton",
|
||||
"advanced_styling_field_button_text_description": "Colore le libellé à l'intérieur des boutons.",
|
||||
"advanced_styling_field_description_color": "Couleur de la description",
|
||||
"advanced_styling_field_description_color_description": "Colore le texte sous chaque titre.",
|
||||
"advanced_styling_field_description_size": "Taille de police de la description",
|
||||
"advanced_styling_field_description_size_description": "Ajuste la taille du texte de description.",
|
||||
"advanced_styling_field_description_weight": "Graisse de police de la description",
|
||||
"advanced_styling_field_description_weight_description": "Rend le texte de description plus léger ou plus gras.",
|
||||
"advanced_styling_field_font_size": "Taille de police",
|
||||
"advanced_styling_field_font_weight": "Graisse de police",
|
||||
"advanced_styling_field_headline_color": "Couleur du titre",
|
||||
"advanced_styling_field_headline_color_description": "Colore le texte principal de la question.",
|
||||
"advanced_styling_field_headline_size": "Taille de police du titre",
|
||||
"advanced_styling_field_headline_size_description": "Ajuste la taille du texte du titre.",
|
||||
"advanced_styling_field_headline_weight": "Graisse de police du titre",
|
||||
"advanced_styling_field_headline_weight_description": "Rend le texte du titre plus léger ou plus gras.",
|
||||
"advanced_styling_field_height": "Hauteur",
|
||||
"advanced_styling_field_indicator_bg": "Arrière-plan de l'indicateur",
|
||||
"advanced_styling_field_indicator_bg_description": "Colore la partie remplie de la barre.",
|
||||
"advanced_styling_field_input_border_radius_description": "Arrondit les coins du champ de saisie.",
|
||||
"advanced_styling_field_input_font_size_description": "Ajuste la taille du texte saisi dans les champs.",
|
||||
"advanced_styling_field_input_height_description": "Contrôle la hauteur du champ de saisie.",
|
||||
"advanced_styling_field_input_padding_x_description": "Ajoute de l'espace à gauche et à droite.",
|
||||
"advanced_styling_field_input_padding_y_description": "Ajoute de l'espace en haut et en bas.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Atténue le texte d'indication du placeholder.",
|
||||
"advanced_styling_field_input_shadow_description": "Ajoute une ombre portée autour des champs de saisie.",
|
||||
"advanced_styling_field_input_text": "Texte de saisie",
|
||||
"advanced_styling_field_input_text_description": "Colore le texte saisi dans les champs.",
|
||||
"advanced_styling_field_option_bg": "Arrière-plan",
|
||||
"advanced_styling_field_option_bg_description": "Remplit les éléments d'option.",
|
||||
"advanced_styling_field_option_border_radius_description": "Arrondit les coins des options.",
|
||||
"advanced_styling_field_option_font_size_description": "Ajuste la taille du texte des libellés d'option.",
|
||||
"advanced_styling_field_option_label": "Couleur de l'étiquette",
|
||||
"advanced_styling_field_option_label_description": "Colore le texte des libellés d'option.",
|
||||
"advanced_styling_field_option_padding_x_description": "Ajoute de l'espace à gauche et à droite.",
|
||||
"advanced_styling_field_option_padding_y_description": "Ajoute de l'espace en haut et en bas.",
|
||||
"advanced_styling_field_padding_x": "Marge intérieure X",
|
||||
"advanced_styling_field_padding_y": "Marge intérieure Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Opacité du placeholder",
|
||||
"advanced_styling_field_shadow": "Ombre",
|
||||
"advanced_styling_field_track_bg": "Arrière-plan de la piste",
|
||||
"advanced_styling_field_track_bg_description": "Colore la partie non remplie de la barre.",
|
||||
"advanced_styling_field_track_height": "Hauteur de la piste",
|
||||
"advanced_styling_field_track_height_description": "Contrôle l'épaisseur de la barre de progression.",
|
||||
"advanced_styling_field_upper_label_color": "Couleur de l'étiquette du titre",
|
||||
"advanced_styling_field_upper_label_color_description": "Colore le petit libellé au-dessus des champs de saisie.",
|
||||
"advanced_styling_field_upper_label_size": "Taille de police de l'étiquette du titre",
|
||||
"advanced_styling_field_upper_label_size_description": "Ajuste la taille du petit libellé au-dessus des champs de saisie.",
|
||||
"advanced_styling_field_upper_label_weight": "Graisse de police de l'étiquette du titre",
|
||||
"advanced_styling_field_upper_label_weight_description": "Rend le libellé plus léger ou plus gras.",
|
||||
"advanced_styling_section_buttons": "Boutons",
|
||||
"advanced_styling_section_headlines": "Titres et descriptions",
|
||||
"advanced_styling_section_inputs": "Champs de saisie",
|
||||
"advanced_styling_section_options": "Options (boutons radio/cases à cocher)",
|
||||
"app_survey_placement": "Placement du sondage d'application",
|
||||
"app_survey_placement_settings_description": "Modifiez l'emplacement où les sondages seront affichés dans votre application web ou site web.",
|
||||
"centered_modal_overlay_color": "Couleur de superposition modale centrée",
|
||||
"email_customization": "Personnalisation des e-mails",
|
||||
"email_customization_description": "Modifiez l'apparence des e-mails que Formbricks envoie en votre nom.",
|
||||
"enable_custom_styling": "Activer le style personnalisé",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Le logo Formbricks est masqué.",
|
||||
"formbricks_branding_settings_description": "Nous apprécions votre soutien mais comprenons si vous choisissez de le désactiver.",
|
||||
"formbricks_branding_shown": "Le logo Formbricks est affiché.",
|
||||
"generate_theme_btn": "Générer",
|
||||
"generate_theme_confirmation": "Souhaitez-vous générer un thème de couleurs assorti basé sur votre couleur de marque ? Cela écrasera vos paramètres de couleur actuels.",
|
||||
"generate_theme_header": "Générer un thème de couleurs ?",
|
||||
"logo_removed_successfully": "Logo supprimé avec succès",
|
||||
"logo_settings_description": "Téléchargez le logo de votre entreprise pour personnaliser les enquêtes et les aperçus de liens.",
|
||||
"logo_updated_successfully": "Logo mis à jour avec succès",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Afficher le logo Formbricks dans les enquêtes {type}",
|
||||
"show_powered_by_formbricks": "Afficher la signature « Propulsé par Formbricks »",
|
||||
"styling_updated_successfully": "Style mis à jour avec succès",
|
||||
"suggest_colors": "Suggérer des couleurs",
|
||||
"theme": "Thème",
|
||||
"theme_settings_description": "Créez un thème de style pour toutes les enquêtes. Vous pouvez activer un style personnalisé pour chaque enquête."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Demandez aux répondants de choisir une ou plusieurs images",
|
||||
"preview_survey_ending_card_description": "Continue ton onboarding, s'il te plaît.",
|
||||
"preview_survey_ending_card_headline": "C'est fait !",
|
||||
"preview_survey_input_headline": "Autre chose à partager ?",
|
||||
"preview_survey_input_placeholder": "Laisse une note...",
|
||||
"preview_survey_name": "Nouveau Sondage",
|
||||
"preview_survey_question_1_headline": "Comment évalueriez-vous {projectName} ?",
|
||||
"preview_survey_question_1_lower_label": "Pas bon",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Oui, tenez-moi au courant.",
|
||||
"preview_survey_question_2_choice_2_label": "Non, merci !",
|
||||
"preview_survey_question_2_headline": "Souhaitez-vous être informé ?",
|
||||
"preview_survey_question_2_subheader": "Ceci est un exemple de description.",
|
||||
"preview_survey_welcome_card_headline": "Bienvenue !",
|
||||
"prioritize_features_description": "Identifiez les fonctionnalités dont vos utilisateurs ont le plus et le moins besoin.",
|
||||
"prioritize_features_name": "Prioriser les fonctionnalités",
|
||||
|
||||
+40
-106
@@ -254,7 +254,7 @@
|
||||
"label": "Címke",
|
||||
"language": "Nyelv",
|
||||
"learn_more": "Tudjon meg többet",
|
||||
"license_expired": "A licenc lejárt",
|
||||
"license_expired": "Licenc lejárt",
|
||||
"light_overlay": "Világos rávetítés",
|
||||
"limits_reached": "Korlátok elérve",
|
||||
"link": "Összekapcsolás",
|
||||
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Nem található háttérkép.",
|
||||
"no_code": "Kód nélkül",
|
||||
"no_files_uploaded": "Nem lettek fájlok feltöltve",
|
||||
"no_overlay": "Nincs átfedés",
|
||||
"no_quotas_found": "Nem találhatók kvóták",
|
||||
"no_result_found": "Nem található eredmény",
|
||||
"no_results": "Nincs találat",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "A szervezeti csapatok nem találhatók",
|
||||
"other": "Egyéb",
|
||||
"others": "Egyebek",
|
||||
"overlay_color": "Átfedés színe",
|
||||
"overview": "Áttekintés",
|
||||
"password": "Jelszó",
|
||||
"paused": "Szüneteltetve",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Próbalicenc kérése",
|
||||
"reset_to_default": "Visszaállítás az alapértelmezettre",
|
||||
"response": "Válasz",
|
||||
"response_id": "Válasz azonosító",
|
||||
"responses": "Válaszok",
|
||||
"restart": "Újraindítás",
|
||||
"role": "Szerep",
|
||||
@@ -465,7 +462,7 @@
|
||||
"you_have_reached_your_monthly_miu_limit_of": "Elérte a havi MIU-korlátját ennek:",
|
||||
"you_have_reached_your_monthly_response_limit_of": "Elérte a havi válaszkorlátját ennek:",
|
||||
"you_will_be_downgraded_to_the_community_edition_on_date": "Vissza lesz állítva a közösségi kiadásra ekkor: {date}.",
|
||||
"your_license_has_expired_please_renew": "A vállalati licence lejárt. Újítsa meg, hogy továbbra is használhassa a vállalati funkciókat."
|
||||
"your_license_has_expired_please_renew": "A vállalati licenced lejárt. Kérjük, újítsd meg, hogy továbbra is használhasd a vállalati funkciókat."
|
||||
},
|
||||
"emails": {
|
||||
"accept": "Elfogadás",
|
||||
@@ -814,7 +811,7 @@
|
||||
"webhook_deleted_successfully": "A webhorog sikeresen törölve",
|
||||
"webhook_name_placeholder": "Választható: címkézze meg a webhorgot az egyszerű azonosításért",
|
||||
"webhook_test_failed_due_to": "A webhorog tesztelése sikertelen a következő miatt:",
|
||||
"webhook_updated_successfully": "A webhorog sikeresen frissítve",
|
||||
"webhook_updated_successfully": "A webhorog sikeresen frissítve.",
|
||||
"webhook_url_placeholder": "Illessze be azt az URL-t, amelyen az eseményt aktiválni szeretné"
|
||||
},
|
||||
"website_or_app_integration_description": "A Formbricks integrálása a webhelyébe vagy alkalmazásába",
|
||||
@@ -823,7 +820,7 @@
|
||||
"segments": {
|
||||
"add_filter_below": "Szűrő hozzáadása lent",
|
||||
"add_your_first_filter_to_get_started": "Adja hozzá az első szűrőt a kezdéshez",
|
||||
"cannot_delete_segment_used_in_surveys": "Nem tudja törölni ezt a szakaszt, mert még mindig használatban van ezekben a kérdőívekben:",
|
||||
"cannot_delete_segment_used_in_surveys": "Nem tudja eltávolítani ezt a szakaszt, mert még mindig használatban van ezekben a kérdőívekben:",
|
||||
"clone_and_edit_segment": "Szakasz klónozása és szerkesztése",
|
||||
"create_group": "Csoport létrehozása",
|
||||
"create_your_first_segment": "Hozza létre az első szakaszt a kezdéshez",
|
||||
@@ -854,11 +851,11 @@
|
||||
"reset_all_filters": "Összes szűrő visszaállítása",
|
||||
"save_as_new_segment": "Mentés új szakaszként",
|
||||
"save_your_filters_as_a_segment_to_use_it_in_other_surveys": "A szűrők mentése szakaszként más kérdőívekben való használathoz",
|
||||
"segment_created_successfully": "A szakasz sikeresen létrehozva",
|
||||
"segment_deleted_successfully": "A szakasz sikeresen törölve",
|
||||
"segment_created_successfully": "A szakasz sikeresen létrehozva!",
|
||||
"segment_deleted_successfully": "A szakasz sikeresen törölve!",
|
||||
"segment_id": "Szakaszazonosító",
|
||||
"segment_saved_successfully": "A szakasz sikeresen elmentve",
|
||||
"segment_updated_successfully": "A szakasz sikeresen frissítve",
|
||||
"segment_updated_successfully": "A szakasz sikeresen frissítve!",
|
||||
"segments_help_you_target_users_with_same_characteristics_easily": "A szakaszok segítik a hasonló jellemzőkkel rendelkező felhasználók könnyű megcélzását",
|
||||
"target_audience": "Célközönség",
|
||||
"this_action_resets_all_filters_in_this_survey": "Ez a művelet visszaállítja az összes szűrőt ebben a kérdőívben.",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "a szervezetétől",
|
||||
"invitation_sent_once_more": "A meghívó még egyszer elküldve.",
|
||||
"invite_deleted_successfully": "A meghívó sikeresen törölve",
|
||||
"invite_expires_on": "A meghívó lejár: {date}",
|
||||
"invited_on": "Meghívva ekkor: {date}",
|
||||
"invites_failed": "A meghívás sikertelen",
|
||||
"leave_organization": "Szervezet elhagyása",
|
||||
"leave_organization_description": "Elhagyja ezt a szervezetet, és elveszíti az összes kérdőívhez és válaszhoz való hozzáférését. Csak akkor tud ismét csatlakozni, ha újra meghívják.",
|
||||
@@ -1007,8 +1004,8 @@
|
||||
"member_invited_successfully": "A tag sikeresen meghívva",
|
||||
"once_its_gone_its_gone": "Ha egyszer eltűnt, akkor eltűnt.",
|
||||
"only_org_owner_can_perform_action": "Csak a szervezet tulajdonosai férhetnek hozzá ehhez a beállításhoz.",
|
||||
"organization_created_successfully": "A szervezet sikeresen létrehozva",
|
||||
"organization_deleted_successfully": "A szervezet sikeresen törölve",
|
||||
"organization_created_successfully": "A szervezet sikeresen létrehozva!",
|
||||
"organization_deleted_successfully": "A szervezet sikeresen törölve.",
|
||||
"organization_invite_link_ready": "A szervezete meghívási hivatkozása készen áll!",
|
||||
"organization_name": "Szervezet neve",
|
||||
"organization_name_description": "Adjon a szervezetének egy leíró nevet.",
|
||||
@@ -1108,8 +1105,8 @@
|
||||
"select_member": "Tag kiválasztása",
|
||||
"select_workspace": "Munkaterület kiválasztása",
|
||||
"team_admin": "Csapatadminisztrátor",
|
||||
"team_created_successfully": "A csapat sikeresen létrehozva",
|
||||
"team_deleted_successfully": "A csapat sikeresen törölve",
|
||||
"team_created_successfully": "A csapat sikeresen létrehozva.",
|
||||
"team_deleted_successfully": "A csapat sikeresen törölve.",
|
||||
"team_deletion_not_allowed": "Önnek nem engedélyezett ennek a csapatnak a törlése.",
|
||||
"team_name": "Csapat neve",
|
||||
"team_name_settings_title": "{teamName} beállításai",
|
||||
@@ -1132,7 +1129,7 @@
|
||||
"copy_survey_error": "Nem sikerült másolni a kérdőívet",
|
||||
"copy_survey_link_to_clipboard": "Kérdőív hivatkozásának másolása a vágólapra",
|
||||
"copy_survey_partially_success": "{success} kérdőív sikeresen másolva, {error} sikertelen.",
|
||||
"copy_survey_success": "A kérdőív sikeresen másolva",
|
||||
"copy_survey_success": "A kérdőív sikeresen másolva!",
|
||||
"delete_survey_and_responses_warning": "Biztosan törölni szeretné ezt a kérdőívet és az összes válaszát?",
|
||||
"edit": {
|
||||
"1_choose_the_default_language_for_this_survey": "1. Válassza ki a kérdőív alapértelmezett nyelvét:",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Helykitöltő hozzáadása annak megjelenítéshez, hogy nincs visszahívandó érték.",
|
||||
"add_hidden_field_id": "Rejtett mezőazonosító hozzáadása",
|
||||
"add_highlight_border": "Kiemelési szegély hozzáadása",
|
||||
"add_highlight_border_description": "Külső szegély hozzáadása a kérdőív kártyájához.",
|
||||
"add_logic": "Logika hozzáadása",
|
||||
"add_none_of_the_above": "„A fentiek közül egyik sem” hozzáadása",
|
||||
"add_option": "Lehetőség hozzáadása",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "A blokk kettőzve.",
|
||||
"bold": "Félkövér",
|
||||
"brand_color": "Márkajel színe",
|
||||
"brand_color_description": "Gomboknál, hivatkozásoknál és kiemeléseknél alkalmazva.",
|
||||
"brightness": "Fényerő",
|
||||
"bulk_edit": "Tömeges szerkesztés",
|
||||
"bulk_edit_description": "Az összes lenti lehetőség szerkesztése, soronként egy. Az üres sorok kihagyásra kerülnek, az ismétlődések pedig el lesznek távolítva.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Új művelet rögzítése",
|
||||
"card_arrangement_for_survey_type_derived": "Kártyaelrendezés a(z) {surveyTypeDerived} kérdőíveknél",
|
||||
"card_background_color": "Kártya hátterének színe",
|
||||
"card_background_color_description": "Kitölti a kérdőívkártya területét.",
|
||||
"card_border_color": "Kártya szegélyének színe",
|
||||
"card_border_color_description": "Körvonalazza a kérdőívkártyát.",
|
||||
"card_styling": "Kártya stílusának beállítása",
|
||||
"casual": "Alkalmi",
|
||||
"caution_edit_duplicate": "Kettőzés és szerkesztés",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "A régebbi és az újabb válaszok összekeverednek, ami félrevezető adatösszegzésekhez vezethet.",
|
||||
"caution_recommendation": "Ez adatellentmondásokat okozhat a kérdőív összegzésében. Azt javasoljuk, hogy inkább kettőzze meg a kérdőívet.",
|
||||
"caution_text": "A változtatások következetlenségekhez vezetnek",
|
||||
"centered_modal_overlay_color": "Középre helyezett kizárólagos rátét színe",
|
||||
"change_anyway": "Változtatás mindenképp",
|
||||
"change_background": "Háttér megváltoztatása",
|
||||
"change_question_type": "Kérdés típusának megváltoztatása",
|
||||
"change_survey_type": "A kérdőív típusának megváltoztatása befolyásolja a meglévő hozzáférést",
|
||||
"change_the_background_color_of_the_card": "A kártya háttérszínének megváltoztatása.",
|
||||
"change_the_background_color_of_the_input_fields": "A beviteli mezők háttérszínének megváltoztatása.",
|
||||
"change_the_background_to_a_color_image_or_animation": "A háttér megváltoztatása színre, képre vagy animációra.",
|
||||
"change_the_border_color_of_the_card": "A kártya szegélyszínének megváltoztatása.",
|
||||
"change_the_border_color_of_the_input_fields": "A beviteli mezők szegélyszínének megváltoztatása.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "A kártya és a beviteli mezők szegélysugarának megváltoztatása.",
|
||||
"change_the_brand_color_of_the_survey": "A kérdőív márkajelszínének megváltoztatása.",
|
||||
"change_the_placement_of_this_survey": "A kérdőív elhelyezésének megváltoztatása.",
|
||||
"change_the_question_color_of_the_survey": "A kérdőív kérdésszínének megváltoztatása.",
|
||||
"changes_saved": "Változtatások elmentve.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "A kérdőív típusának megváltoztatása hatással lesz arra, hogy hogyan lehet megosztani azt. Ha a válaszadók már rendelkeznek a jelenlegi típushoz tartozó hozzáférési hivatkozásokkal, akkor elveszíthetik a hozzáférést a váltás után.",
|
||||
"checkbox_label": "Jelölőnégyzet címkéje",
|
||||
@@ -1270,7 +1273,7 @@
|
||||
"disable_the_visibility_of_survey_progress": "A kérdőív előrehaladási folyamata láthatóságának letiltása.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "A kérdőív becsült kitöltési idejének megjelenítése",
|
||||
"display_number_of_responses_for_survey": "A kérdőív válaszai számának megjelenítése",
|
||||
"display_type": "Megjelenített típus",
|
||||
"display_type": "Megjelenítési típus",
|
||||
"divide": "Osztás /",
|
||||
"does_not_contain": "Nem tartalmazza",
|
||||
"does_not_end_with": "Nem ezzel végződik",
|
||||
@@ -1278,7 +1281,7 @@
|
||||
"does_not_include_all_of": "Nem tartalmazza ezekből az összeset",
|
||||
"does_not_include_one_of": "Nem tartalmazza ezek egyikét",
|
||||
"does_not_start_with": "Nem ezzel kezdődik",
|
||||
"dropdown": "Legördülő",
|
||||
"dropdown": "Legördülő menü",
|
||||
"duplicate_block": "Blokk kettőzése",
|
||||
"duplicate_question": "Kérdés kettőzése",
|
||||
"edit_link": "Hivatkozás szerkesztése",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Folyamatjelző elrejtése",
|
||||
"hide_question_settings": "Kérdésbeállítások elrejtése",
|
||||
"hostname": "Gépnév",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Mennyire szeretné vagánnyá tenni a kártyáit a(z) {surveyTypeDerived} kérdőívekben",
|
||||
"if_you_need_more_please": "Ha többre van szüksége, akkor",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Maradjon megjelenítve bármikor is aktiválódott, amíg egy választ el nem küldenek.",
|
||||
"ignore_global_waiting_time": "Várakozási időszak figyelmen kívül hagyása",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Kezdeti érték",
|
||||
"inner_text": "Belső szöveg",
|
||||
"input_border_color": "Beviteli mező szegélyének színe",
|
||||
"input_border_color_description": "Körvonalazza a szövegbeviteli mezőket és szövegterületeket.",
|
||||
"input_color": "Beviteli mező színe",
|
||||
"input_color_description": "Kitölti a szövegbeviteli mezők belsejét.",
|
||||
"insert_link": "Hivatkozás beszúrása",
|
||||
"invalid_targeting": "Érvénytelen célzás: ellenőrizze a közönség szűrőit",
|
||||
"invalid_video_url_warning": "Adjon meg egy érvényes YouTube, Vimeo vagy Loom URL-t. Jelenleg nem támogatunk más videomegosztó szolgáltatókat.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Csak a PIN-kóddal rendelkező felhasználók férhetnek hozzá a kérdőívhez.",
|
||||
"publish": "Közzététel",
|
||||
"question": "Kérdés",
|
||||
"question_color": "Kérdés színe",
|
||||
"question_deleted": "Kérdés törölve.",
|
||||
"question_duplicated": "Kérdés megkettőzve.",
|
||||
"question_id_updated": "Kérdésazonosító frissítve",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Válaszkorlátok, átirányítások és egyebek.",
|
||||
"response_options": "Válasz beállításai",
|
||||
"roundness": "Kerekesség",
|
||||
"roundness_description": "Szabályozza a kártya sarkai lekerekítését.",
|
||||
"row_used_in_logic_error": "Ez a sor használatban van a(z) {questionIndex}. kérdés logikájában. Először távolítsa el a logikából.",
|
||||
"rows": "Sorok",
|
||||
"save_and_close": "Mentés és bezárás",
|
||||
@@ -1543,7 +1545,7 @@
|
||||
"send_survey_to_audience_who_match": "Kérdőív küldése az erre illeszkedő közönségnek…",
|
||||
"send_your_respondents_to_a_page_of_your_choice": "A válaszadók küldése a választási lehetőség oldalára.",
|
||||
"set_the_global_placement_in_the_look_feel_settings": "A globális elhelyezés beállítása a megjelenítési beállításokban.",
|
||||
"settings_saved_successfully": "A beállítások sikeresen elmentve",
|
||||
"settings_saved_successfully": "A beállítások sikeresen elmentve.",
|
||||
"seven_points": "7 pont",
|
||||
"show_block_settings": "Blokkbeállítások megjelenítése",
|
||||
"show_button": "Gomb megjelenítése",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "A stílus a téma stílusaira állítva",
|
||||
"subheading": "Alcím",
|
||||
"subtract": "Kivonás -",
|
||||
"suggest_colors": "Színek ajánlása",
|
||||
"survey_completed_heading": "A kérdőív kitöltve",
|
||||
"survey_completed_subheading": "Ez a szabad és nyílt forráskódú kérdőív le lett zárva",
|
||||
"survey_display_settings": "Kérdőív megjelenítésének beállításai",
|
||||
@@ -1709,7 +1712,7 @@
|
||||
"person_attributes": "A személy jellemzői a beküldés időpontjában",
|
||||
"phone": "Telefon",
|
||||
"respondent_skipped_questions": "A válaszadó kihagyta ezeket a kérdéseket.",
|
||||
"response_deleted_successfully": "A válasz sikeresen törölve",
|
||||
"response_deleted_successfully": "A válasz sikeresen törölve.",
|
||||
"single_use_id": "Egyszer használatos azonosító",
|
||||
"source": "Forrás",
|
||||
"state_region": "Állam vagy régió",
|
||||
@@ -1721,7 +1724,7 @@
|
||||
"search_by_survey_name": "Keresés kérőívnév alapján",
|
||||
"share": {
|
||||
"anonymous_links": {
|
||||
"custom_single_use_id_description": "Ha nem titkosítja az egyszer használatos azonosítókat, akkor a „suid=…” bármilyen értéke működik egy válasznál.",
|
||||
"custom_single_use_id_description": "Ha nem titkosítja az egyszer használatos azonosítót, akkor a „suid=…” bármilyen értéke működik egy válasznál.",
|
||||
"custom_single_use_id_title": "Bármilyen értéket beállíthat egyszer használatos azonosítóként az URL-ben.",
|
||||
"custom_start_point": "Egyéni kezdési pont",
|
||||
"data_prefilling": "Adatok előre kitöltése",
|
||||
@@ -1946,8 +1949,8 @@
|
||||
"your_survey_is_public": "A kérdőíve nyilvános",
|
||||
"youre_not_plugged_in_yet": "Még nincs csatlakoztatva!"
|
||||
},
|
||||
"survey_deleted_successfully": "A kérdőív sikeresen törölve",
|
||||
"survey_duplicated_successfully": "A kérdőív sikeresen megkettőzve",
|
||||
"survey_deleted_successfully": "A kérdőív sikeresen törölve!",
|
||||
"survey_duplicated_successfully": "A kérdőív sikeresen megkettőzve.",
|
||||
"survey_duplication_error": "Nem sikerült megkettőzni a kérdőívet.",
|
||||
"templates": {
|
||||
"all_channels": "Összes csatorna",
|
||||
@@ -2010,8 +2013,8 @@
|
||||
"custom_scripts_updated_successfully": "Az egyéni parancsfájlok sikeres frissítve",
|
||||
"custom_scripts_warning": "A parancsfájlok teljes böngésző-hozzáféréssel kerülnek végrehajtásra. Csak megbízható forrásokból származó parancsfájlokat adjon hozzá.",
|
||||
"delete_workspace": "Munkaterület törlése",
|
||||
"delete_workspace_confirmation": "Biztosan törölni szeretné a(z) {projectName} munkaterületet? Ezt a műveletet nem lehet visszavonni.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "A(z) {projectName} munkaterület törlése, beleértve az összes kérdőívet, választ, személyt, műveletet és attribútumot is.",
|
||||
"delete_workspace_confirmation": "Biztosan törölni szeretné a(z) {projectName} projektet? Ezt a műveletet nem lehet visszavonni.",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "A(z) {projectName} projekt törlése, beleértve az összes kérdőívet, választ, személyt, műveletet és attribútumot is.",
|
||||
"delete_workspace_settings_description": "A munkaterület törlése az összes kérdőívvel, válasszal, személlyel, művelettel és attribútummal együtt. Ezt nem lehet visszavonni.",
|
||||
"error_saving_workspace_information": "Hiba a munkaterület-információk mentésekor",
|
||||
"only_owners_or_managers_can_delete_workspaces": "Csak tulajdonosok vagy kezelők törölhetnek munkaterületeket",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Háttérszín hozzáadása",
|
||||
"add_background_color_description": "Hátérszín hozzáadása a logó tárolódobozához.",
|
||||
"advanced_styling_field_border_radius": "Szegély lekerekítése",
|
||||
"advanced_styling_field_button_bg": "Gomb háttere",
|
||||
"advanced_styling_field_button_bg_description": "Kitölti a következő / küldés gombot.",
|
||||
"advanced_styling_field_button_border_radius_description": "Lekerekíti a gomb sarkait.",
|
||||
"advanced_styling_field_button_font_size_description": "Méretezi a gomb feliratának szövegét.",
|
||||
"advanced_styling_field_button_font_weight_description": "Vékonyabbá vagy vastagabbá teszi a gomb szövegét.",
|
||||
"advanced_styling_field_button_height_description": "Szabályozza a gomb magasságát.",
|
||||
"advanced_styling_field_button_padding_x_description": "Térközt ad hozzá bal és jobb oldalon.",
|
||||
"advanced_styling_field_button_padding_y_description": "Térközt ad hozzá fent és lent.",
|
||||
"advanced_styling_field_button_text": "Gomb szövege",
|
||||
"advanced_styling_field_button_text_description": "Színezi a gombokon belüli feliratot.",
|
||||
"advanced_styling_field_description_color": "Leírás színe",
|
||||
"advanced_styling_field_description_color_description": "Színezi az egyes címsorok alatti szöveget.",
|
||||
"advanced_styling_field_description_size": "Leírás betűmérete",
|
||||
"advanced_styling_field_description_size_description": "Méretezi a leírás szövegét.",
|
||||
"advanced_styling_field_description_weight": "Leírás betűvastagsága",
|
||||
"advanced_styling_field_description_weight_description": "Vékonyabbá vagy vastagabbá teszi a leírás szövegét.",
|
||||
"advanced_styling_field_font_size": "Betűméret",
|
||||
"advanced_styling_field_font_weight": "Betűvastagság",
|
||||
"advanced_styling_field_headline_color": "Címsor színe",
|
||||
"advanced_styling_field_headline_color_description": "Színezi a fő kérdés szövegét.",
|
||||
"advanced_styling_field_headline_size": "Címsor betűmérete",
|
||||
"advanced_styling_field_headline_size_description": "Méretezi a címsor szövegét.",
|
||||
"advanced_styling_field_headline_weight": "Címsor betűvastagsága",
|
||||
"advanced_styling_field_headline_weight_description": "Vékonyabbá vagy vastagabbá teszi a címsor szövegét.",
|
||||
"advanced_styling_field_height": "Magasság",
|
||||
"advanced_styling_field_indicator_bg": "Jelző háttere",
|
||||
"advanced_styling_field_indicator_bg_description": "Színezi a sáv kitöltött részét.",
|
||||
"advanced_styling_field_input_border_radius_description": "Lekerekíti a beviteli mező sarkait.",
|
||||
"advanced_styling_field_input_font_size_description": "Méretezi a beviteli mezőkbe gépelt szöveget.",
|
||||
"advanced_styling_field_input_height_description": "Szabályozza a beviteli mező magasságát.",
|
||||
"advanced_styling_field_input_padding_x_description": "Térközt ad hozzá bal és jobb oldalon.",
|
||||
"advanced_styling_field_input_padding_y_description": "Térközt ad hozzá felül és alul.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Halványítja a helyőrző szöveget.",
|
||||
"advanced_styling_field_input_shadow_description": "Árnyékot ad a beviteli mezők köré.",
|
||||
"advanced_styling_field_input_text": "Beviteli mező szövege",
|
||||
"advanced_styling_field_input_text_description": "Színezi a beviteli mezőkbe írt szöveget.",
|
||||
"advanced_styling_field_option_bg": "Háttér",
|
||||
"advanced_styling_field_option_bg_description": "Kitölti az opciós elemeket.",
|
||||
"advanced_styling_field_option_border_radius_description": "Lekerekíti az opciók sarkait.",
|
||||
"advanced_styling_field_option_font_size_description": "Méretezi az opciók címkeszövegét.",
|
||||
"advanced_styling_field_option_label": "Címke színe",
|
||||
"advanced_styling_field_option_label_description": "Színezi az opciók címkeszövegét.",
|
||||
"advanced_styling_field_option_padding_x_description": "Térközt ad hozzá balra és jobbra.",
|
||||
"advanced_styling_field_option_padding_y_description": "Térközt ad hozzá felül és alul.",
|
||||
"advanced_styling_field_padding_x": "Belső margó X",
|
||||
"advanced_styling_field_padding_y": "Belső margó Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Helyőrző átlátszósága",
|
||||
"advanced_styling_field_shadow": "Árnyék",
|
||||
"advanced_styling_field_track_bg": "Sáv háttere",
|
||||
"advanced_styling_field_track_bg_description": "Színezi a sáv kitöltetlen részét.",
|
||||
"advanced_styling_field_track_height": "Sáv magassága",
|
||||
"advanced_styling_field_track_height_description": "Szabályozza a folyamatjelző sáv vastagságát.",
|
||||
"advanced_styling_field_upper_label_color": "Felső címke színe",
|
||||
"advanced_styling_field_upper_label_color_description": "Színezi a beviteli mezők feletti kis címkét.",
|
||||
"advanced_styling_field_upper_label_size": "Felső címke betűmérete",
|
||||
"advanced_styling_field_upper_label_size_description": "Méretezi a beviteli mezők feletti kis címkét.",
|
||||
"advanced_styling_field_upper_label_weight": "Felső címke betűvastagsága",
|
||||
"advanced_styling_field_upper_label_weight_description": "Vékonyabbá vagy vastagabbá teszi a címkét.",
|
||||
"advanced_styling_section_buttons": "Gombok",
|
||||
"advanced_styling_section_headlines": "Címsorok és leírások",
|
||||
"advanced_styling_section_inputs": "Beviteli mezők",
|
||||
"advanced_styling_section_options": "Opciók (rádiógomb/jelölőnégyzet)",
|
||||
"app_survey_placement": "Alkalmazás-kérdőív elhelyezése",
|
||||
"app_survey_placement_settings_description": "Annak megváltoztatása, hogy a kérdőívek hol jelennek meg a webalkalmazásban vagy a webhelyen.",
|
||||
"centered_modal_overlay_color": "Középre helyezett kizárólagos rátét színe",
|
||||
"email_customization": "E-mail személyre szabás",
|
||||
"email_customization_description": "Azon e-mailek megjelenésének megváltoztatása, amelyeket a Formbricks az Ön nevében küld ki.",
|
||||
"enable_custom_styling": "Egyéni stílus engedélyezése",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "A Formbricks márkajel rejtve van.",
|
||||
"formbricks_branding_settings_description": "Nagyra értékeljük a támogatását, de megértjük, ha kikapcsolja.",
|
||||
"formbricks_branding_shown": "A Formbricks márkajel megjelenik.",
|
||||
"generate_theme_btn": "Előállítás",
|
||||
"generate_theme_confirmation": "Szeretnél egy illeszkedő színtémát generálni a márka színed alapján? Ez felülírja a jelenlegi színbeállításaidat.",
|
||||
"generate_theme_header": "Színtéma előállítása?",
|
||||
"logo_removed_successfully": "A logó sikeresen eltávolítva",
|
||||
"logo_settings_description": "Vállalati logo feltöltése a kérdőívek és hivatkozások előnézeteinek márkaépítéséhez.",
|
||||
"logo_updated_successfully": "A logó sikeresen frissítve",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Formbricks márkajel megjelenítése a(z) {type} kérdőívekben",
|
||||
"show_powered_by_formbricks": "Az „A gépházban: Formbricks” aláírás megjelenítése",
|
||||
"styling_updated_successfully": "A stílus sikeresen frissítve",
|
||||
"suggest_colors": "Színek javaslása",
|
||||
"theme": "Téma",
|
||||
"theme_settings_description": "Stílustéma létrehozása az összes kérdőívhez. Egyéni stílust engedélyezhet minden egyes kérdőívhez."
|
||||
},
|
||||
@@ -2160,9 +2097,9 @@
|
||||
"search_tags": "Címkék keresése…",
|
||||
"tag": "Címke",
|
||||
"tag_already_exists": "A címke már létezik",
|
||||
"tag_deleted": "A címke sikeresen törölve",
|
||||
"tag_updated": "A címke sikeresen frissítve",
|
||||
"tags_merged": "A címkék sikeresen egyesítve"
|
||||
"tag_deleted": "Címke törölve",
|
||||
"tag_updated": "Címke frissítve",
|
||||
"tags_merged": "Címkék egyesítve"
|
||||
},
|
||||
"teams": {
|
||||
"manage_teams": "Csapatok kezelése",
|
||||
@@ -2357,7 +2294,7 @@
|
||||
"career_development_survey_question_5_choice_5": "Üzemeltetés",
|
||||
"career_development_survey_question_5_choice_6": "Egyéb",
|
||||
"career_development_survey_question_5_headline": "Milyen funkcióban dolgozik?",
|
||||
"career_development_survey_question_5_subheader": "Válassza ki a következő lehetőségek egyikét:",
|
||||
"career_development_survey_question_5_subheader": "Válassza a következők egyikét",
|
||||
"career_development_survey_question_6_choice_1": "Egyéni közreműködő",
|
||||
"career_development_survey_question_6_choice_2": "Igazgató",
|
||||
"career_development_survey_question_6_choice_3": "Vezető igazgató",
|
||||
@@ -2365,7 +2302,7 @@
|
||||
"career_development_survey_question_6_choice_5": "Igazgató",
|
||||
"career_development_survey_question_6_choice_6": "Egyéb",
|
||||
"career_development_survey_question_6_headline": "Az alábbiak közül melyik írja le legjobban a jelenlegi munkája szintjét?",
|
||||
"career_development_survey_question_6_subheader": "Válassza ki a következő lehetőségek egyikét:",
|
||||
"career_development_survey_question_6_subheader": "Válassza a következők egyikét",
|
||||
"cess_survey_name": "Ügyfél-erőfeszítési pontszám kérdőív",
|
||||
"cess_survey_question_1_headline": "A(z) $[projectName] megkönnyíti számomra a [CÉL HOZZÁADÁSA] tevékenységet",
|
||||
"cess_survey_question_1_lower_label": "Egyáltalán nem értek egyet",
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "A válaszadók megkérése, hogy egy vagy több képet válasszanak",
|
||||
"preview_survey_ending_card_description": "Folytassa a beléptetését.",
|
||||
"preview_survey_ending_card_headline": "Megcsinálta!",
|
||||
"preview_survey_input_headline": "Van még valami, amit meg szeretne osztani?",
|
||||
"preview_survey_input_placeholder": "Hagyj egy megjegyzést...",
|
||||
"preview_survey_name": "Új kérdőív",
|
||||
"preview_survey_question_1_headline": "Hogyan értékelné a(z) {projectName} projektet?",
|
||||
"preview_survey_question_1_lower_label": "Nem jó",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Igen, folyamatosan tájékoztassanak.",
|
||||
"preview_survey_question_2_choice_2_label": "Nem, köszönöm!",
|
||||
"preview_survey_question_2_headline": "Szeretne naprakész maradni?",
|
||||
"preview_survey_question_2_subheader": "Ez egy példa leírás.",
|
||||
"preview_survey_welcome_card_headline": "Üdvözöljük!",
|
||||
"prioritize_features_description": "A felhasználóknak leginkább és legkevésbé szükséges funkciók azonosítása.",
|
||||
"prioritize_features_name": "Funkciók rangsorolása",
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "背景画像が見つかりません。",
|
||||
"no_code": "ノーコード",
|
||||
"no_files_uploaded": "ファイルがアップロードされていません",
|
||||
"no_overlay": "オーバーレイなし",
|
||||
"no_quotas_found": "クォータが見つかりません",
|
||||
"no_result_found": "結果が見つかりません",
|
||||
"no_results": "結果なし",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "組織のチームが見つかりません",
|
||||
"other": "その他",
|
||||
"others": "その他",
|
||||
"overlay_color": "オーバーレイの色",
|
||||
"overview": "概要",
|
||||
"password": "パスワード",
|
||||
"paused": "一時停止",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "トライアルライセンスをリクエスト",
|
||||
"reset_to_default": "デフォルトにリセット",
|
||||
"response": "回答",
|
||||
"response_id": "回答ID",
|
||||
"responses": "回答",
|
||||
"restart": "再開",
|
||||
"role": "役割",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "あなたの組織から",
|
||||
"invitation_sent_once_more": "招待状を再度送信しました。",
|
||||
"invite_deleted_successfully": "招待を正常に削除しました",
|
||||
"invite_expires_on": "招待は{date}に期限切れ",
|
||||
"invited_on": "{date}に招待",
|
||||
"invites_failed": "招待に失敗しました",
|
||||
"leave_organization": "組織を離れる",
|
||||
"leave_organization_description": "この組織を離れ、すべてのフォームと回答へのアクセス権を失います。再度招待された場合にのみ再参加できます。",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "質問がスキップされた場合に表示するプレースホルダーを追加:",
|
||||
"add_hidden_field_id": "非表示フィールドIDを追加",
|
||||
"add_highlight_border": "ハイライトボーダーを追加",
|
||||
"add_highlight_border_description": "フォームカードに外側のボーダーを追加します。",
|
||||
"add_logic": "ロジックを追加",
|
||||
"add_none_of_the_above": "\"いずれも該当しません\" を追加",
|
||||
"add_option": "オプションを追加",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "ブロックが複製されました。",
|
||||
"bold": "太字",
|
||||
"brand_color": "ブランドカラー",
|
||||
"brand_color_description": "ボタン、リンク、ハイライトに適用されます。",
|
||||
"brightness": "明るさ",
|
||||
"bulk_edit": "一括編集",
|
||||
"bulk_edit_description": "以下のオプションを1行ずつ編集してください。空の行はスキップされ、重複は削除されます。",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "新しいアクションをキャプチャ",
|
||||
"card_arrangement_for_survey_type_derived": "{surveyTypeDerived} フォームのカード配置",
|
||||
"card_background_color": "カードの背景色",
|
||||
"card_background_color_description": "フォームカードエリアを塗りつぶします。",
|
||||
"card_border_color": "カードの枠線の色",
|
||||
"card_border_color_description": "フォームカードの輪郭を描きます。",
|
||||
"card_styling": "カードのスタイル設定",
|
||||
"casual": "カジュアル",
|
||||
"caution_edit_duplicate": "複製して編集",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "古い回答と新しい回答が混ざり、データの概要が誤解を招く可能性があります。",
|
||||
"caution_recommendation": "これにより、フォームの概要にデータの不整合が生じる可能性があります。代わりにフォームを複製することをお勧めします。",
|
||||
"caution_text": "変更は不整合を引き起こします",
|
||||
"centered_modal_overlay_color": "中央モーダルのオーバーレイ色",
|
||||
"change_anyway": "とにかく変更",
|
||||
"change_background": "背景を変更",
|
||||
"change_question_type": "質問の種類を変更",
|
||||
"change_survey_type": "フォームの種類を変更すると、既存のアクセスに影響します",
|
||||
"change_the_background_color_of_the_card": "カードの背景色を変更します。",
|
||||
"change_the_background_color_of_the_input_fields": "入力フィールドの背景色を変更します。",
|
||||
"change_the_background_to_a_color_image_or_animation": "背景を色、画像、またはアニメーションに変更します。",
|
||||
"change_the_border_color_of_the_card": "カードの枠線の色を変更します。",
|
||||
"change_the_border_color_of_the_input_fields": "入力フィールドの枠線の色を変更します。",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "カードと入力の角丸を変更します。",
|
||||
"change_the_brand_color_of_the_survey": "フォームのブランドカラーを変更します。",
|
||||
"change_the_placement_of_this_survey": "このフォームの配置を変更します。",
|
||||
"change_the_question_color_of_the_survey": "フォームの質問の色を変更します。",
|
||||
"changes_saved": "変更を保存しました。",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "フォームの種類を変更すると、共有方法に影響します。回答者が現在のタイプのアクセスリンクをすでに持っている場合、切り替え後にアクセスを失う可能性があります。",
|
||||
"checkbox_label": "チェックボックスのラベル",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "プログレスバーを非表示",
|
||||
"hide_question_settings": "質問設定を非表示",
|
||||
"hostname": "ホスト名",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "{surveyTypeDerived} フォームのカードをどれくらいユニークにしますか",
|
||||
"if_you_need_more_please": "さらに必要な場合は、",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "回答が提出されるまで、トリガーされるたびに表示し続けます。",
|
||||
"ignore_global_waiting_time": "クールダウン期間を無視",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "初期値",
|
||||
"inner_text": "内部テキスト",
|
||||
"input_border_color": "入力の枠線の色",
|
||||
"input_border_color_description": "テキスト入力とテキストエリアの輪郭を描きます。",
|
||||
"input_color": "入力の色",
|
||||
"input_color_description": "テキスト入力の内側を塗りつぶします。",
|
||||
"insert_link": "リンク を 挿入",
|
||||
"invalid_targeting": "無効なターゲティング: オーディエンスフィルターを確認してください",
|
||||
"invalid_video_url_warning": "有効なYouTube、Vimeo、またはLoomのURLを入力してください。現在、他の動画ホスティングプロバイダーはサポートしていません。",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "PINを持つユーザーのみがフォームにアクセスできます。",
|
||||
"publish": "公開",
|
||||
"question": "質問",
|
||||
"question_color": "質問の色",
|
||||
"question_deleted": "質問を削除しました。",
|
||||
"question_duplicated": "質問を複製しました。",
|
||||
"question_id_updated": "質問IDを更新しました",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "回答数の上限、リダイレクトなど。",
|
||||
"response_options": "回答オプション",
|
||||
"roundness": "丸み",
|
||||
"roundness_description": "カードの角の丸みを調整します。",
|
||||
"row_used_in_logic_error": "この行は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
"rows": "行",
|
||||
"save_and_close": "保存して閉じる",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "スタイルをテーマのスタイルに設定しました",
|
||||
"subheading": "サブ見出し",
|
||||
"subtract": "減算 -",
|
||||
"suggest_colors": "色を提案",
|
||||
"survey_completed_heading": "フォームが完了しました",
|
||||
"survey_completed_subheading": "この無料のオープンソースフォームは閉鎖されました",
|
||||
"survey_display_settings": "フォーム表示設定",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "背景色を追加",
|
||||
"add_background_color_description": "ロゴコンテナに背景色を追加します。",
|
||||
"advanced_styling_field_border_radius": "境界線の丸み",
|
||||
"advanced_styling_field_button_bg": "ボタンの背景",
|
||||
"advanced_styling_field_button_bg_description": "次へ/送信ボタンを塗りつぶします。",
|
||||
"advanced_styling_field_button_border_radius_description": "ボタンの角を丸めます。",
|
||||
"advanced_styling_field_button_font_size_description": "ボタンラベルのテキストサイズを調整します。",
|
||||
"advanced_styling_field_button_font_weight_description": "ボタンテキストを細くまたは太くします。",
|
||||
"advanced_styling_field_button_height_description": "ボタンの高さを調整します。",
|
||||
"advanced_styling_field_button_padding_x_description": "左右にスペースを追加します。",
|
||||
"advanced_styling_field_button_padding_y_description": "上下にスペースを追加します。",
|
||||
"advanced_styling_field_button_text": "ボタンのテキスト",
|
||||
"advanced_styling_field_button_text_description": "ボタン内のラベルに色を付けます。",
|
||||
"advanced_styling_field_description_color": "説明文の色",
|
||||
"advanced_styling_field_description_color_description": "各見出しの下のテキストに色を付けます。",
|
||||
"advanced_styling_field_description_size": "説明文のフォントサイズ",
|
||||
"advanced_styling_field_description_size_description": "説明テキストのサイズを調整します。",
|
||||
"advanced_styling_field_description_weight": "説明文のフォントの太さ",
|
||||
"advanced_styling_field_description_weight_description": "説明テキストを細くまたは太くします。",
|
||||
"advanced_styling_field_font_size": "フォントサイズ",
|
||||
"advanced_styling_field_font_weight": "フォントの太さ",
|
||||
"advanced_styling_field_headline_color": "見出しの色",
|
||||
"advanced_styling_field_headline_color_description": "メインの質問テキストに色を付けます。",
|
||||
"advanced_styling_field_headline_size": "見出しのフォントサイズ",
|
||||
"advanced_styling_field_headline_size_description": "見出しテキストのサイズを調整します。",
|
||||
"advanced_styling_field_headline_weight": "見出しのフォントの太さ",
|
||||
"advanced_styling_field_headline_weight_description": "見出しテキストを細くまたは太くします。",
|
||||
"advanced_styling_field_height": "高さ",
|
||||
"advanced_styling_field_indicator_bg": "インジケーターの背景",
|
||||
"advanced_styling_field_indicator_bg_description": "バーの塗りつぶし部分に色を付けます。",
|
||||
"advanced_styling_field_input_border_radius_description": "入力フィールドの角を丸めます。",
|
||||
"advanced_styling_field_input_font_size_description": "入力フィールド内の入力テキストのサイズを調整します。",
|
||||
"advanced_styling_field_input_height_description": "入力フィールドの高さを調整します。",
|
||||
"advanced_styling_field_input_padding_x_description": "左右にスペースを追加します。",
|
||||
"advanced_styling_field_input_padding_y_description": "上下にスペースを追加します。",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "プレースホルダーのヒントテキストを薄くします。",
|
||||
"advanced_styling_field_input_shadow_description": "入力フィールドの周囲にドロップシャドウを追加します。",
|
||||
"advanced_styling_field_input_text": "入力テキスト",
|
||||
"advanced_styling_field_input_text_description": "入力フィールドに入力されたテキストの色を設定します。",
|
||||
"advanced_styling_field_option_bg": "背景",
|
||||
"advanced_styling_field_option_bg_description": "オプション項目を塗りつぶします。",
|
||||
"advanced_styling_field_option_border_radius_description": "オプションの角を丸くします。",
|
||||
"advanced_styling_field_option_font_size_description": "オプションラベルのテキストサイズを調整します。",
|
||||
"advanced_styling_field_option_label": "ラベルの色",
|
||||
"advanced_styling_field_option_label_description": "オプションラベルのテキストの色を設定します。",
|
||||
"advanced_styling_field_option_padding_x_description": "左右にスペースを追加します。",
|
||||
"advanced_styling_field_option_padding_y_description": "上下にスペースを追加します。",
|
||||
"advanced_styling_field_padding_x": "パディングX",
|
||||
"advanced_styling_field_padding_y": "パディングY",
|
||||
"advanced_styling_field_placeholder_opacity": "プレースホルダーの不透明度",
|
||||
"advanced_styling_field_shadow": "影",
|
||||
"advanced_styling_field_track_bg": "トラックの背景",
|
||||
"advanced_styling_field_track_bg_description": "バーの未入力部分の色を設定します。",
|
||||
"advanced_styling_field_track_height": "トラックの高さ",
|
||||
"advanced_styling_field_track_height_description": "プログレスバーの太さを調整します。",
|
||||
"advanced_styling_field_upper_label_color": "見出しラベルの色",
|
||||
"advanced_styling_field_upper_label_color_description": "入力フィールド上部の小さなラベルの色を設定します。",
|
||||
"advanced_styling_field_upper_label_size": "見出しラベルのフォントサイズ",
|
||||
"advanced_styling_field_upper_label_size_description": "入力フィールド上部の小さなラベルのサイズを調整します。",
|
||||
"advanced_styling_field_upper_label_weight": "見出しラベルのフォントの太さ",
|
||||
"advanced_styling_field_upper_label_weight_description": "ラベルを細くまたは太くします。",
|
||||
"advanced_styling_section_buttons": "ボタン",
|
||||
"advanced_styling_section_headlines": "見出しと説明",
|
||||
"advanced_styling_section_inputs": "入力フィールド",
|
||||
"advanced_styling_section_options": "選択肢(ラジオボタン/チェックボックス)",
|
||||
"app_survey_placement": "アプリ内フォームの配置",
|
||||
"app_survey_placement_settings_description": "Webアプリまたはウェブサイトでフォームを表示する場所を変更します。",
|
||||
"centered_modal_overlay_color": "中央モーダルのオーバーレイ色",
|
||||
"email_customization": "メールのカスタマイズ",
|
||||
"email_customization_description": "Formbricksがあなたに代わって送信するメールの外観を変更します。",
|
||||
"enable_custom_styling": "カスタムスタイルを有効化",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Formbricksブランディングは非表示です。",
|
||||
"formbricks_branding_settings_description": "あなたのサポートに感謝していますが、オフにすることもご理解いただけます。",
|
||||
"formbricks_branding_shown": "Formbricksブランディングは表示されています。",
|
||||
"generate_theme_btn": "生成",
|
||||
"generate_theme_confirmation": "ブランドカラーに基づいて、マッチするカラーテーマを生成しますか?現在のカラー設定は上書きされます。",
|
||||
"generate_theme_header": "カラーテーマを生成しますか?",
|
||||
"logo_removed_successfully": "ロゴを正常に削除しました",
|
||||
"logo_settings_description": "会社のロゴをアップロードして、アンケートとリンクプレビューにブランディングを適用します。",
|
||||
"logo_updated_successfully": "ロゴを正常に更新しました",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "{type}アンケートにFormbricksブランディングを表示",
|
||||
"show_powered_by_formbricks": "「Powered by Formbricks」署名を表示",
|
||||
"styling_updated_successfully": "スタイルを正常に更新しました",
|
||||
"suggest_colors": "カラーを提案",
|
||||
"theme": "テーマ",
|
||||
"theme_settings_description": "すべてのアンケート用のスタイルテーマを作成します。各アンケートでカスタムスタイルを有効にできます。"
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "回答者に1つまたは複数の画像を選択させる",
|
||||
"preview_survey_ending_card_description": "オンボーディングを続けてください。",
|
||||
"preview_survey_ending_card_headline": "完了しました!",
|
||||
"preview_survey_input_headline": "他に共有したいことはありますか?",
|
||||
"preview_survey_input_placeholder": "メモを残す...",
|
||||
"preview_survey_name": "新しいアンケート",
|
||||
"preview_survey_question_1_headline": "{projectName}をどう評価しますか?",
|
||||
"preview_survey_question_1_lower_label": "良くない",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "はい、最新情報を知りたいです。",
|
||||
"preview_survey_question_2_choice_2_label": "いいえ、結構です!",
|
||||
"preview_survey_question_2_headline": "最新情報を知りたいですか?",
|
||||
"preview_survey_question_2_subheader": "これは説明の例です。",
|
||||
"preview_survey_welcome_card_headline": "ようこそ!",
|
||||
"prioritize_features_description": "ユーザーが最も必要とする機能と最も必要としない機能を特定する。",
|
||||
"prioritize_features_name": "機能の優先順位付け",
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Geen achtergrondafbeelding gevonden.",
|
||||
"no_code": "Geen code",
|
||||
"no_files_uploaded": "Er zijn geen bestanden geüpload",
|
||||
"no_overlay": "Geen overlay",
|
||||
"no_quotas_found": "Geen quota gevonden",
|
||||
"no_result_found": "Geen resultaat gevonden",
|
||||
"no_results": "Geen resultaten",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Organisatieteams niet gevonden",
|
||||
"other": "Ander",
|
||||
"others": "Anderen",
|
||||
"overlay_color": "Overlaykleur",
|
||||
"overview": "Overzicht",
|
||||
"password": "Wachtwoord",
|
||||
"paused": "Gepauzeerd",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Proeflicentie aanvragen",
|
||||
"reset_to_default": "Resetten naar standaard",
|
||||
"response": "Antwoord",
|
||||
"response_id": "Antwoord-ID",
|
||||
"responses": "Reacties",
|
||||
"restart": "Opnieuw opstarten",
|
||||
"role": "Rol",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "vanuit uw organisatie",
|
||||
"invitation_sent_once_more": "Uitnodiging nogmaals verzonden.",
|
||||
"invite_deleted_successfully": "Uitnodiging succesvol verwijderd",
|
||||
"invite_expires_on": "Uitnodiging verloopt op {date}",
|
||||
"invited_on": "Uitgenodigd op {date}",
|
||||
"invites_failed": "Uitnodigingen zijn mislukt",
|
||||
"leave_organization": "Verlaat de organisatie",
|
||||
"leave_organization_description": "U verlaat deze organisatie en verliest de toegang tot alle enquêtes en reacties. Je kunt alleen weer meedoen als je opnieuw wordt uitgenodigd.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Voeg een tijdelijke aanduiding toe om aan te geven of er geen waarde is om te onthouden.",
|
||||
"add_hidden_field_id": "Voeg een verborgen veld-ID toe",
|
||||
"add_highlight_border": "Markeerrand toevoegen",
|
||||
"add_highlight_border_description": "Voeg een buitenrand toe aan uw enquêtekaart.",
|
||||
"add_logic": "Voeg logica toe",
|
||||
"add_none_of_the_above": "Voeg 'Geen van bovenstaande' toe",
|
||||
"add_option": "Optie toevoegen",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Blok gedupliceerd.",
|
||||
"bold": "Vetgedrukt",
|
||||
"brand_color": "Merk kleur",
|
||||
"brand_color_description": "Toegepast op knoppen, links en highlights.",
|
||||
"brightness": "Helderheid",
|
||||
"bulk_edit": "Bulkbewerking",
|
||||
"bulk_edit_description": "Bewerk alle opties hieronder, één per regel. Lege regels worden overgeslagen en duplicaten verwijderd.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Leg nieuwe actie vast",
|
||||
"card_arrangement_for_survey_type_derived": "Kaartarrangement voor {surveyTypeDerived} enquêtes",
|
||||
"card_background_color": "Achtergrondkleur van de kaart",
|
||||
"card_background_color_description": "Vult het enquêtekaartgebied.",
|
||||
"card_border_color": "Randkleur kaart",
|
||||
"card_border_color_description": "Omlijnt de enquêtekaart.",
|
||||
"card_styling": "Kaartstijl",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Dupliceren en bewerken",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Oudere en nieuwere antwoorden lopen door elkaar heen, wat kan leiden tot misleidende gegevenssamenvattingen.",
|
||||
"caution_recommendation": "Dit kan inconsistenties in de gegevens in de onderzoekssamenvatting veroorzaken. Wij raden u aan de enquête te dupliceren.",
|
||||
"caution_text": "Veranderingen zullen tot inconsistenties leiden",
|
||||
"centered_modal_overlay_color": "Gecentreerde modale overlaykleur",
|
||||
"change_anyway": "Hoe dan ook veranderen",
|
||||
"change_background": "Achtergrond wijzigen",
|
||||
"change_question_type": "Vraagtype wijzigen",
|
||||
"change_survey_type": "Als u van enquêtetype verandert, heeft dit invloed op de bestaande toegang",
|
||||
"change_the_background_color_of_the_card": "Verander de achtergrondkleur van de kaart.",
|
||||
"change_the_background_color_of_the_input_fields": "Verander de achtergrondkleur van de invoervelden.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Verander de achtergrond in een kleur, afbeelding of animatie.",
|
||||
"change_the_border_color_of_the_card": "Verander de randkleur van de kaart.",
|
||||
"change_the_border_color_of_the_input_fields": "Wijzig de randkleur van de invoervelden.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Wijzig de randradius van de kaart en de ingangen.",
|
||||
"change_the_brand_color_of_the_survey": "Wijzig de merkkleur van de enquête.",
|
||||
"change_the_placement_of_this_survey": "Wijzig de plaatsing van deze enquête.",
|
||||
"change_the_question_color_of_the_survey": "Verander de vraagkleur van de enquête.",
|
||||
"changes_saved": "Wijzigingen opgeslagen.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Het wijzigen van het enquêtetype heeft invloed op de manier waarop deze kan worden gedeeld. Als respondenten al toegangslinks hebben voor het huidige type, verliezen ze mogelijk de toegang na de overstap.",
|
||||
"checkbox_label": "Selectievakje-label",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Voortgangsbalk verbergen",
|
||||
"hide_question_settings": "Vraaginstellingen verbergen",
|
||||
"hostname": "Hostnaam",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hoe funky wil je je kaarten hebben in {surveyTypeDerived} Enquêtes",
|
||||
"if_you_need_more_please": "Als je meer nodig hebt,",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Blijf tonen wanneer geactiveerd totdat een reactie is ingediend.",
|
||||
"ignore_global_waiting_time": "Afkoelperiode negeren",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Initiële waarde",
|
||||
"inner_text": "Innerlijke tekst",
|
||||
"input_border_color": "Randkleur invoeren",
|
||||
"input_border_color_description": "Omlijnt tekstvelden en tekstgebieden.",
|
||||
"input_color": "Kleur invoeren",
|
||||
"input_color_description": "Vult de binnenkant van tekstvelden.",
|
||||
"insert_link": "Link invoegen",
|
||||
"invalid_targeting": "Ongeldige targeting: controleer uw doelgroepfilters",
|
||||
"invalid_video_url_warning": "Voer een geldige YouTube-, Vimeo- of Loom-URL in. We ondersteunen momenteel geen andere videohostingproviders.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Alleen gebruikers die de pincode hebben, hebben toegang tot de enquête.",
|
||||
"publish": "Publiceren",
|
||||
"question": "Vraag",
|
||||
"question_color": "Vraag kleur",
|
||||
"question_deleted": "Vraag verwijderd.",
|
||||
"question_duplicated": "Vraag dubbel gesteld.",
|
||||
"question_id_updated": "Vraag-ID bijgewerkt",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Reactielimieten, omleidingen en meer.",
|
||||
"response_options": "Reactieopties",
|
||||
"roundness": "Rondheid",
|
||||
"roundness_description": "Bepaalt hoe afgerond de kaarthoeken zijn.",
|
||||
"row_used_in_logic_error": "Deze rij wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
|
||||
"rows": "Rijen",
|
||||
"save_and_close": "Opslaan en sluiten",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Styling ingesteld op themastijlen",
|
||||
"subheading": "Ondertitel",
|
||||
"subtract": "Aftrekken -",
|
||||
"suggest_colors": "Stel kleuren voor",
|
||||
"survey_completed_heading": "Enquête voltooid",
|
||||
"survey_completed_subheading": "Deze gratis en open source-enquête is gesloten",
|
||||
"survey_display_settings": "Enquêteweergave-instellingen",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Achtergrondkleur toevoegen",
|
||||
"add_background_color_description": "Voeg een achtergrondkleur toe aan de logocontainer.",
|
||||
"advanced_styling_field_border_radius": "Hoekradius",
|
||||
"advanced_styling_field_button_bg": "Knopachtergrond",
|
||||
"advanced_styling_field_button_bg_description": "Vult de volgende/verzend-knop.",
|
||||
"advanced_styling_field_button_border_radius_description": "Rondt de knophoeken af.",
|
||||
"advanced_styling_field_button_font_size_description": "Schaalt de tekst van het knoplabel.",
|
||||
"advanced_styling_field_button_font_weight_description": "Maakt knoptekst lichter of vetter.",
|
||||
"advanced_styling_field_button_height_description": "Bepaalt de knophoogte.",
|
||||
"advanced_styling_field_button_padding_x_description": "Voegt ruimte toe aan de linker- en rechterkant.",
|
||||
"advanced_styling_field_button_padding_y_description": "Voegt ruimte toe aan de boven- en onderkant.",
|
||||
"advanced_styling_field_button_text": "Knoptekst",
|
||||
"advanced_styling_field_button_text_description": "Kleurt het label binnen knoppen.",
|
||||
"advanced_styling_field_description_color": "Beschrijvingskleur",
|
||||
"advanced_styling_field_description_color_description": "Kleurt de tekst onder elke kop.",
|
||||
"advanced_styling_field_description_size": "Lettergrootte beschrijving",
|
||||
"advanced_styling_field_description_size_description": "Schaalt de beschrijvingstekst.",
|
||||
"advanced_styling_field_description_weight": "Letterdikte beschrijving",
|
||||
"advanced_styling_field_description_weight_description": "Maakt beschrijvingstekst lichter of vetter.",
|
||||
"advanced_styling_field_font_size": "Lettergrootte",
|
||||
"advanced_styling_field_font_weight": "Letterdikte",
|
||||
"advanced_styling_field_headline_color": "Kopkleur",
|
||||
"advanced_styling_field_headline_color_description": "Kleurt de hoofdvraagtekst.",
|
||||
"advanced_styling_field_headline_size": "Lettergrootte kop",
|
||||
"advanced_styling_field_headline_size_description": "Schaalt de koptekst.",
|
||||
"advanced_styling_field_headline_weight": "Letterdikte kop",
|
||||
"advanced_styling_field_headline_weight_description": "Maakt koptekst lichter of vetter.",
|
||||
"advanced_styling_field_height": "Hoogte",
|
||||
"advanced_styling_field_indicator_bg": "Indicatorachtergrond",
|
||||
"advanced_styling_field_indicator_bg_description": "Kleurt het gevulde deel van de balk.",
|
||||
"advanced_styling_field_input_border_radius_description": "Rondt de invoerhoeken af.",
|
||||
"advanced_styling_field_input_font_size_description": "Schaalt de getypte tekst in invoervelden.",
|
||||
"advanced_styling_field_input_height_description": "Bepaalt de hoogte van het invoerveld.",
|
||||
"advanced_styling_field_input_padding_x_description": "Voegt ruimte toe aan de linker- en rechterkant.",
|
||||
"advanced_styling_field_input_padding_y_description": "Voegt ruimte toe aan de boven- en onderkant.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Vervaagt de tijdelijke aanwijzingstekst.",
|
||||
"advanced_styling_field_input_shadow_description": "Voegt een slagschaduw toe rond invoervelden.",
|
||||
"advanced_styling_field_input_text": "Invoertekst",
|
||||
"advanced_styling_field_input_text_description": "Kleurt de getypte tekst in invoervelden.",
|
||||
"advanced_styling_field_option_bg": "Achtergrond",
|
||||
"advanced_styling_field_option_bg_description": "Vult de optie-items.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rondt de hoeken van opties af.",
|
||||
"advanced_styling_field_option_font_size_description": "Schaalt de tekst van optielabels.",
|
||||
"advanced_styling_field_option_label": "Labelkleur",
|
||||
"advanced_styling_field_option_label_description": "Kleurt de tekst van optielabels.",
|
||||
"advanced_styling_field_option_padding_x_description": "Voegt ruimte toe aan de linker- en rechterkant.",
|
||||
"advanced_styling_field_option_padding_y_description": "Voegt ruimte toe aan de boven- en onderkant.",
|
||||
"advanced_styling_field_padding_x": "Opvulling X",
|
||||
"advanced_styling_field_padding_y": "Opvulling Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Plaatshouderopaciteit",
|
||||
"advanced_styling_field_shadow": "Schaduw",
|
||||
"advanced_styling_field_track_bg": "Sporachtergrond",
|
||||
"advanced_styling_field_track_bg_description": "Kleurt het ongevulde gedeelte van de balk.",
|
||||
"advanced_styling_field_track_height": "Spoorhoogte",
|
||||
"advanced_styling_field_track_height_description": "Regelt de dikte van de voortgangsbalk.",
|
||||
"advanced_styling_field_upper_label_color": "Koplabelkleur",
|
||||
"advanced_styling_field_upper_label_color_description": "Kleurt het kleine label boven invoervelden.",
|
||||
"advanced_styling_field_upper_label_size": "Lettergrootte koplabel",
|
||||
"advanced_styling_field_upper_label_size_description": "Schaalt het kleine label boven invoervelden.",
|
||||
"advanced_styling_field_upper_label_weight": "Letterdikte koplabel",
|
||||
"advanced_styling_field_upper_label_weight_description": "Maakt het label lichter of vetter.",
|
||||
"advanced_styling_section_buttons": "Knoppen",
|
||||
"advanced_styling_section_headlines": "Koppen & beschrijvingen",
|
||||
"advanced_styling_section_inputs": "Invoervelden",
|
||||
"advanced_styling_section_options": "Opties (radio/checkbox)",
|
||||
"app_survey_placement": "App-enquête plaatsing",
|
||||
"app_survey_placement_settings_description": "Wijzig waar enquêtes worden weergegeven in uw web-app of website.",
|
||||
"centered_modal_overlay_color": "Gecentreerde modale overlaykleur",
|
||||
"email_customization": "E-mail aanpassing",
|
||||
"email_customization_description": "Wijzig het uiterlijk van e-mails die Formbricks namens u verstuurt.",
|
||||
"enable_custom_styling": "Aangepaste styling inschakelen",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Formbricks-branding is verborgen.",
|
||||
"formbricks_branding_settings_description": "We waarderen uw steun, maar begrijpen het als u dit uitschakelt.",
|
||||
"formbricks_branding_shown": "Formbricks-branding wordt weergegeven.",
|
||||
"generate_theme_btn": "Genereren",
|
||||
"generate_theme_confirmation": "Wil je een passend kleurthema genereren op basis van je merkkleur? Dit overschrijft je huidige kleurinstellingen.",
|
||||
"generate_theme_header": "Kleurthema genereren?",
|
||||
"logo_removed_successfully": "Logo succesvol verwijderd",
|
||||
"logo_settings_description": "Upload uw bedrijfslogo om enquêtes en linkvoorbeelden te voorzien van uw huisstijl.",
|
||||
"logo_updated_successfully": "Logo succesvol bijgewerkt",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Toon Formbricks-branding in {type} enquêtes",
|
||||
"show_powered_by_formbricks": "Toon 'Powered by Formbricks' handtekening",
|
||||
"styling_updated_successfully": "Styling succesvol bijgewerkt",
|
||||
"suggest_colors": "Kleuren voorstellen",
|
||||
"theme": "Thema",
|
||||
"theme_settings_description": "Maak een stijlthema voor alle enquêtes. Je kunt aangepaste styling inschakelen voor elke enquête."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Vraag respondenten om één of meerdere afbeeldingen te kiezen",
|
||||
"preview_survey_ending_card_description": "Ga alstublieft door met uw onboarding.",
|
||||
"preview_survey_ending_card_headline": "Je hebt het gedaan!",
|
||||
"preview_survey_input_headline": "Nog iets te delen?",
|
||||
"preview_survey_input_placeholder": "Laat een notitie achter...",
|
||||
"preview_survey_name": "Nieuwe enquête",
|
||||
"preview_survey_question_1_headline": "Hoe zou u {projectName} beoordelen?",
|
||||
"preview_survey_question_1_lower_label": "Niet goed",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Ja, houd mij op de hoogte.",
|
||||
"preview_survey_question_2_choice_2_label": "Nee, dank je!",
|
||||
"preview_survey_question_2_headline": "Wil je op de hoogte blijven?",
|
||||
"preview_survey_question_2_subheader": "Dit is een voorbeeldbeschrijving.",
|
||||
"preview_survey_welcome_card_headline": "Welkom!",
|
||||
"prioritize_features_description": "Identificeer functies die uw gebruikers het meest en het minst nodig hebben.",
|
||||
"prioritize_features_name": "Geef prioriteit aan functies",
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Imagem de fundo não encontrada.",
|
||||
"no_code": "Sem código",
|
||||
"no_files_uploaded": "Nenhum arquivo foi enviado",
|
||||
"no_overlay": "Sem sobreposição",
|
||||
"no_quotas_found": "Nenhuma cota encontrada",
|
||||
"no_result_found": "Nenhum resultado encontrado",
|
||||
"no_results": "Nenhum resultado",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Equipes da organização não encontradas",
|
||||
"other": "outro",
|
||||
"others": "Outros",
|
||||
"overlay_color": "Cor da sobreposição",
|
||||
"overview": "Visão Geral",
|
||||
"password": "Senha",
|
||||
"paused": "Pausado",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Pedir licença de teste",
|
||||
"reset_to_default": "Restaurar para o padrão",
|
||||
"response": "Resposta",
|
||||
"response_id": "ID da resposta",
|
||||
"responses": "Respostas",
|
||||
"restart": "Reiniciar",
|
||||
"role": "Rolê",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "da sua organização",
|
||||
"invitation_sent_once_more": "Convite enviado de novo.",
|
||||
"invite_deleted_successfully": "Convite deletado com sucesso",
|
||||
"invite_expires_on": "O convite expira em {date}",
|
||||
"invited_on": "Convidado em {date}",
|
||||
"invites_failed": "Convites falharam",
|
||||
"leave_organization": "Sair da organização",
|
||||
"leave_organization_description": "Você vai sair dessa organização e perder acesso a todas as pesquisas e respostas. Você só pode voltar se for convidado de novo.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Adicionar um texto padrão para mostrar se a pergunta for ignorada:",
|
||||
"add_hidden_field_id": "Adicionar campo oculto ID",
|
||||
"add_highlight_border": "Adicionar borda de destaque",
|
||||
"add_highlight_border_description": "Adicione uma borda externa ao seu cartão de pesquisa.",
|
||||
"add_logic": "Adicionar lógica",
|
||||
"add_none_of_the_above": "Adicionar \"Nenhuma das opções acima\"",
|
||||
"add_option": "Adicionar opção",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Bloco duplicado.",
|
||||
"bold": "Negrito",
|
||||
"brand_color": "Cor da marca",
|
||||
"brand_color_description": "Aplicado a botões, links e destaques.",
|
||||
"brightness": "brilho",
|
||||
"bulk_edit": "Edição em massa",
|
||||
"bulk_edit_description": "Edite todas as opções abaixo, uma por linha. Linhas vazias serão ignoradas e duplicatas removidas.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Capturar nova ação",
|
||||
"card_arrangement_for_survey_type_derived": "Arranjo de Cartões para Pesquisas {surveyTypeDerived}",
|
||||
"card_background_color": "Cor de fundo do cartão",
|
||||
"card_background_color_description": "Preenche a área do cartão da pesquisa.",
|
||||
"card_border_color": "Cor da borda do cartão",
|
||||
"card_border_color_description": "Contorna o cartão da pesquisa.",
|
||||
"card_styling": "Estilo do cartão",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicar e editar",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Respostas antigas e novas são misturadas, o que pode levar a resumos de dados enganosos.",
|
||||
"caution_recommendation": "Isso pode causar inconsistências de dados no resumo da pesquisa. Recomendamos duplicar a pesquisa em vez disso.",
|
||||
"caution_text": "Mudanças vão levar a inconsistências",
|
||||
"centered_modal_overlay_color": "cor de sobreposição modal centralizada",
|
||||
"change_anyway": "Mudar mesmo assim",
|
||||
"change_background": "Mudar fundo",
|
||||
"change_question_type": "Mudar tipo de pergunta",
|
||||
"change_survey_type": "Alterar o tipo de pesquisa afeta o acesso existente",
|
||||
"change_the_background_color_of_the_card": "Muda a cor de fundo do cartão.",
|
||||
"change_the_background_color_of_the_input_fields": "Mude a cor de fundo dos campos de entrada.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Mude o fundo para uma cor, imagem ou animação.",
|
||||
"change_the_border_color_of_the_card": "Muda a cor da borda do cartão.",
|
||||
"change_the_border_color_of_the_input_fields": "Mude a cor da borda dos campos de entrada.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Muda o raio da borda do card e dos inputs.",
|
||||
"change_the_brand_color_of_the_survey": "Muda a cor da marca da pesquisa.",
|
||||
"change_the_placement_of_this_survey": "Muda a posição dessa pesquisa.",
|
||||
"change_the_question_color_of_the_survey": "Muda a cor da pergunta da pesquisa.",
|
||||
"changes_saved": "Mudanças salvas.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Alterar o tipo de pesquisa afetará a forma como ela pode ser compartilhada. Se os respondentes já tiverem links de acesso para o tipo atual, podem perder o acesso após a mudança.",
|
||||
"checkbox_label": "Rótulo da Caixa de Seleção",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Esconder barra de progresso",
|
||||
"hide_question_settings": "Ocultar configurações da pergunta",
|
||||
"hostname": "nome do host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão descoladas você quer suas cartas em Pesquisas {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Se você precisar de mais, por favor",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuar mostrando sempre que acionada até que uma resposta seja enviada.",
|
||||
"ignore_global_waiting_time": "Ignorar período de espera",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Valor inicial",
|
||||
"inner_text": "Texto Interno",
|
||||
"input_border_color": "Cor da borda de entrada",
|
||||
"input_border_color_description": "Contorna campos de texto e áreas de texto.",
|
||||
"input_color": "Cor de entrada",
|
||||
"input_color_description": "Preenche o interior dos campos de texto.",
|
||||
"insert_link": "Inserir link",
|
||||
"invalid_targeting": "Segmentação inválida: Por favor, verifique os filtros do seu público",
|
||||
"invalid_video_url_warning": "Por favor, insira uma URL válida do YouTube, Vimeo ou Loom. No momento, não suportamos outros provedores de vídeo.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Somente usuários que têm o PIN podem acessar a pesquisa.",
|
||||
"publish": "Publicar",
|
||||
"question": "Pergunta",
|
||||
"question_color": "Cor da pergunta",
|
||||
"question_deleted": "Pergunta deletada.",
|
||||
"question_duplicated": "Pergunta duplicada.",
|
||||
"question_id_updated": "ID da pergunta atualizado",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
||||
"response_options": "Opções de Resposta",
|
||||
"roundness": "Circularidade",
|
||||
"roundness_description": "Controla o arredondamento dos cantos do cartão.",
|
||||
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"rows": "linhas",
|
||||
"save_and_close": "Salvar e Fechar",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Estilo definido para os estilos do tema",
|
||||
"subheading": "Subtítulo",
|
||||
"subtract": "Subtrair -",
|
||||
"suggest_colors": "Sugerir cores",
|
||||
"survey_completed_heading": "Pesquisa Concluída",
|
||||
"survey_completed_subheading": "Essa pesquisa gratuita e de código aberto foi encerrada",
|
||||
"survey_display_settings": "Configurações de Exibição da Pesquisa",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Adicionar cor de fundo",
|
||||
"add_background_color_description": "Adicione uma cor de fundo ao container do logo.",
|
||||
"advanced_styling_field_border_radius": "Raio da borda",
|
||||
"advanced_styling_field_button_bg": "Fundo do botão",
|
||||
"advanced_styling_field_button_bg_description": "Preenche o botão Próximo / Enviar.",
|
||||
"advanced_styling_field_button_border_radius_description": "Arredonda os cantos do botão.",
|
||||
"advanced_styling_field_button_font_size_description": "Ajusta o tamanho do texto do rótulo do botão.",
|
||||
"advanced_styling_field_button_font_weight_description": "Torna o texto do botão mais leve ou mais negrito.",
|
||||
"advanced_styling_field_button_height_description": "Controla a altura do botão.",
|
||||
"advanced_styling_field_button_padding_x_description": "Adiciona espaço à esquerda e à direita.",
|
||||
"advanced_styling_field_button_padding_y_description": "Adiciona espaço no topo e na base.",
|
||||
"advanced_styling_field_button_text": "Texto do botão",
|
||||
"advanced_styling_field_button_text_description": "Colore o rótulo dentro dos botões.",
|
||||
"advanced_styling_field_description_color": "Cor da descrição",
|
||||
"advanced_styling_field_description_color_description": "Colore o texto abaixo de cada título.",
|
||||
"advanced_styling_field_description_size": "Tamanho da fonte da descrição",
|
||||
"advanced_styling_field_description_size_description": "Ajusta o tamanho do texto da descrição.",
|
||||
"advanced_styling_field_description_weight": "Peso da fonte da descrição",
|
||||
"advanced_styling_field_description_weight_description": "Torna o texto da descrição mais leve ou mais negrito.",
|
||||
"advanced_styling_field_font_size": "Tamanho da fonte",
|
||||
"advanced_styling_field_font_weight": "Peso da fonte",
|
||||
"advanced_styling_field_headline_color": "Cor do título",
|
||||
"advanced_styling_field_headline_color_description": "Colore o texto principal da pergunta.",
|
||||
"advanced_styling_field_headline_size": "Tamanho da fonte do título",
|
||||
"advanced_styling_field_headline_size_description": "Ajusta o tamanho do texto do título.",
|
||||
"advanced_styling_field_headline_weight": "Peso da fonte do título",
|
||||
"advanced_styling_field_headline_weight_description": "Torna o texto do título mais leve ou mais negrito.",
|
||||
"advanced_styling_field_height": "Altura",
|
||||
"advanced_styling_field_indicator_bg": "Fundo do indicador",
|
||||
"advanced_styling_field_indicator_bg_description": "Colore a porção preenchida da barra.",
|
||||
"advanced_styling_field_input_border_radius_description": "Arredonda os cantos do campo.",
|
||||
"advanced_styling_field_input_font_size_description": "Ajusta o tamanho do texto digitado nos campos.",
|
||||
"advanced_styling_field_input_height_description": "Controla a altura do campo de entrada.",
|
||||
"advanced_styling_field_input_padding_x_description": "Adiciona espaço à esquerda e à direita.",
|
||||
"advanced_styling_field_input_padding_y_description": "Adiciona espaço na parte superior e inferior.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Esmaece o texto de dica do placeholder.",
|
||||
"advanced_styling_field_input_shadow_description": "Adiciona uma sombra ao redor dos campos de entrada.",
|
||||
"advanced_styling_field_input_text": "Texto de entrada",
|
||||
"advanced_styling_field_input_text_description": "Colore o texto digitado nos campos de entrada.",
|
||||
"advanced_styling_field_option_bg": "Fundo",
|
||||
"advanced_styling_field_option_bg_description": "Preenche os itens de opção.",
|
||||
"advanced_styling_field_option_border_radius_description": "Arredonda os cantos das opções.",
|
||||
"advanced_styling_field_option_font_size_description": "Ajusta o tamanho do texto do rótulo da opção.",
|
||||
"advanced_styling_field_option_label": "Cor do rótulo",
|
||||
"advanced_styling_field_option_label_description": "Colore o texto do rótulo da opção.",
|
||||
"advanced_styling_field_option_padding_x_description": "Adiciona espaço à esquerda e à direita.",
|
||||
"advanced_styling_field_option_padding_y_description": "Adiciona espaço na parte superior e inferior.",
|
||||
"advanced_styling_field_padding_x": "Espaçamento X",
|
||||
"advanced_styling_field_padding_y": "Espaçamento Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Opacidade do placeholder",
|
||||
"advanced_styling_field_shadow": "Sombra",
|
||||
"advanced_styling_field_track_bg": "Fundo da trilha",
|
||||
"advanced_styling_field_track_bg_description": "Colore a porção não preenchida da barra.",
|
||||
"advanced_styling_field_track_height": "Altura da trilha",
|
||||
"advanced_styling_field_track_height_description": "Controla a espessura da barra de progresso.",
|
||||
"advanced_styling_field_upper_label_color": "Cor do rótulo do título",
|
||||
"advanced_styling_field_upper_label_color_description": "Colore o pequeno rótulo acima dos campos de entrada.",
|
||||
"advanced_styling_field_upper_label_size": "Tamanho da fonte do rótulo do título",
|
||||
"advanced_styling_field_upper_label_size_description": "Ajusta o tamanho do pequeno rótulo acima dos campos de entrada.",
|
||||
"advanced_styling_field_upper_label_weight": "Peso da fonte do rótulo do título",
|
||||
"advanced_styling_field_upper_label_weight_description": "Torna o rótulo mais leve ou mais negrito.",
|
||||
"advanced_styling_section_buttons": "Botões",
|
||||
"advanced_styling_section_headlines": "Títulos e descrições",
|
||||
"advanced_styling_section_inputs": "Campos de entrada",
|
||||
"advanced_styling_section_options": "Opções (rádio/caixa de seleção)",
|
||||
"app_survey_placement": "Posicionamento da pesquisa de app",
|
||||
"app_survey_placement_settings_description": "Altere onde as pesquisas serão exibidas em seu aplicativo web ou site.",
|
||||
"centered_modal_overlay_color": "Cor de sobreposição modal centralizada",
|
||||
"email_customization": "Personalização de e-mail",
|
||||
"email_customization_description": "Altere a aparência dos e-mails que o Formbricks envia em seu nome.",
|
||||
"enable_custom_styling": "Habilitar estilização personalizada",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "A marca Formbricks está oculta.",
|
||||
"formbricks_branding_settings_description": "Adoramos seu apoio, mas entendemos se você desativar.",
|
||||
"formbricks_branding_shown": "A marca Formbricks está visível.",
|
||||
"generate_theme_btn": "Gerar",
|
||||
"generate_theme_confirmation": "Gostaria de gerar um tema de cores correspondente baseado na cor da sua marca? Isso substituirá suas configurações de cores atuais.",
|
||||
"generate_theme_header": "Gerar tema de cores?",
|
||||
"logo_removed_successfully": "Logo removido com sucesso",
|
||||
"logo_settings_description": "Faça upload do logo da sua empresa para personalizar pesquisas e pré-visualizações de links.",
|
||||
"logo_updated_successfully": "Logo atualizado com sucesso",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Mostrar marca Formbricks em pesquisas {type}",
|
||||
"show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'",
|
||||
"styling_updated_successfully": "Estilo atualizado com sucesso",
|
||||
"suggest_colors": "Sugerir cores",
|
||||
"theme": "Tema",
|
||||
"theme_settings_description": "Crie um tema de estilo para todas as pesquisas. Você pode ativar estilo personalizado para cada pesquisa."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Peça aos respondentes para escolherem uma ou mais imagens",
|
||||
"preview_survey_ending_card_description": "Por favor, continue seu onboarding.",
|
||||
"preview_survey_ending_card_headline": "Você conseguiu!",
|
||||
"preview_survey_input_headline": "Tem mais alguma coisa para compartilhar?",
|
||||
"preview_survey_input_placeholder": "Deixe uma nota...",
|
||||
"preview_survey_name": "Nova pesquisa",
|
||||
"preview_survey_question_1_headline": "Como você avaliaria {projectName}?",
|
||||
"preview_survey_question_1_lower_label": "Não tá bom",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Sim, me mantenha informado.",
|
||||
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
||||
"preview_survey_question_2_headline": "Quer ficar por dentro?",
|
||||
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
|
||||
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
||||
"prioritize_features_description": "Identifique os recursos que seus usuários mais e menos precisam.",
|
||||
"prioritize_features_name": "Priorizar Funcionalidades",
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Nenhuma imagem de fundo encontrada.",
|
||||
"no_code": "Sem código",
|
||||
"no_files_uploaded": "Nenhum ficheiro foi carregado",
|
||||
"no_overlay": "Sem sobreposição",
|
||||
"no_quotas_found": "Nenhum quota encontrado",
|
||||
"no_result_found": "Nenhum resultado encontrado",
|
||||
"no_results": "Nenhum resultado",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Equipas da organização não encontradas",
|
||||
"other": "Outro",
|
||||
"others": "Outros",
|
||||
"overlay_color": "Cor da sobreposição",
|
||||
"overview": "Visão geral",
|
||||
"password": "Palavra-passe",
|
||||
"paused": "Em pausa",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Solicitar licença de teste",
|
||||
"reset_to_default": "Repor para o padrão",
|
||||
"response": "Resposta",
|
||||
"response_id": "ID de resposta",
|
||||
"responses": "Respostas",
|
||||
"restart": "Reiniciar",
|
||||
"role": "Função",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "da sua organização",
|
||||
"invitation_sent_once_more": "Convite enviado mais uma vez.",
|
||||
"invite_deleted_successfully": "Convite eliminado com sucesso",
|
||||
"invite_expires_on": "O convite expira em {date}",
|
||||
"invited_on": "Convidado em {date}",
|
||||
"invites_failed": "Convites falharam",
|
||||
"leave_organization": "Sair da organização",
|
||||
"leave_organization_description": "Vai sair desta organização e perder o acesso a todos os inquéritos e respostas. Só pode voltar a juntar-se se for convidado novamente.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Adicionar um espaço reservado para mostrar se não houver valor para recordar.",
|
||||
"add_hidden_field_id": "Adicionar ID do campo oculto",
|
||||
"add_highlight_border": "Adicionar borda de destaque",
|
||||
"add_highlight_border_description": "Adicione uma borda externa ao seu cartão de inquérito.",
|
||||
"add_logic": "Adicionar lógica",
|
||||
"add_none_of_the_above": "Adicionar \"Nenhuma das Opções Acima\"",
|
||||
"add_option": "Adicionar opção",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Bloco duplicado.",
|
||||
"bold": "Negrito",
|
||||
"brand_color": "Cor da marca",
|
||||
"brand_color_description": "Aplicado a botões, links e destaques.",
|
||||
"brightness": "Brilho",
|
||||
"bulk_edit": "Edição em massa",
|
||||
"bulk_edit_description": "Edite todas as opções abaixo, uma por linha. Linhas vazias serão ignoradas e duplicados removidos.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Capturar nova ação",
|
||||
"card_arrangement_for_survey_type_derived": "Arranjo de Cartões para Inquéritos {surveyTypeDerived}",
|
||||
"card_background_color": "Cor de fundo do cartão",
|
||||
"card_background_color_description": "Preenche a área do cartão do inquérito.",
|
||||
"card_border_color": "Cor da borda do cartão",
|
||||
"card_border_color_description": "Contorna o cartão do inquérito.",
|
||||
"card_styling": "Estilo de cartão",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplicar e editar",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "As respostas mais antigas e mais recentes se misturam, o que pode levar a resumos de dados enganosos.",
|
||||
"caution_recommendation": "Isso pode causar inconsistências de dados no resumo do inquérito. Recomendamos duplicar o inquérito em vez disso.",
|
||||
"caution_text": "As alterações levarão a inconsistências",
|
||||
"centered_modal_overlay_color": "Cor da sobreposição modal centralizada",
|
||||
"change_anyway": "Alterar mesmo assim",
|
||||
"change_background": "Alterar fundo",
|
||||
"change_question_type": "Alterar tipo de pergunta",
|
||||
"change_survey_type": "Alterar o tipo de inquérito afeta o acesso existente",
|
||||
"change_the_background_color_of_the_card": "Alterar a cor de fundo do cartão",
|
||||
"change_the_background_color_of_the_input_fields": "Alterar a cor de fundo dos campos de entrada",
|
||||
"change_the_background_to_a_color_image_or_animation": "Altere o fundo para uma cor, imagem ou animação",
|
||||
"change_the_border_color_of_the_card": "Alterar a cor da borda do cartão.",
|
||||
"change_the_border_color_of_the_input_fields": "Alterar a cor da borda dos campos de entrada",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Alterar o raio da borda do cartão e dos campos de entrada",
|
||||
"change_the_brand_color_of_the_survey": "Alterar a cor da marca do inquérito",
|
||||
"change_the_placement_of_this_survey": "Alterar a colocação deste inquérito.",
|
||||
"change_the_question_color_of_the_survey": "Alterar a cor da pergunta do inquérito",
|
||||
"changes_saved": "Alterações guardadas.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Alterar o tipo de inquérito afetará como ele pode ser partilhado. Se os respondentes já tiverem links de acesso para o tipo atual, podem perder o acesso após a mudança.",
|
||||
"checkbox_label": "Rótulo da Caixa de Seleção",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Ocultar barra de progresso",
|
||||
"hide_question_settings": "Ocultar definições da pergunta",
|
||||
"hostname": "Nome do host",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão extravagantes quer os seus cartões em Inquéritos {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Se precisar de mais, por favor",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuar a mostrar sempre que acionado até que uma resposta seja submetida.",
|
||||
"ignore_global_waiting_time": "Ignorar período de espera",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Valor inicial",
|
||||
"inner_text": "Texto Interno",
|
||||
"input_border_color": "Cor da borda do campo de entrada",
|
||||
"input_border_color_description": "Contorna campos de texto e áreas de texto.",
|
||||
"input_color": "Cor do campo de entrada",
|
||||
"input_color_description": "Preenche o interior dos campos de texto.",
|
||||
"insert_link": "Inserir ligação",
|
||||
"invalid_targeting": "Segmentação inválida: Por favor, verifique os seus filtros de audiência",
|
||||
"invalid_video_url_warning": "Por favor, insira um URL válido do YouTube, Vimeo ou Loom. Atualmente, não suportamos outros fornecedores de hospedagem de vídeo.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Apenas utilizadores com o PIN podem aceder ao inquérito.",
|
||||
"publish": "Publicar",
|
||||
"question": "Pergunta",
|
||||
"question_color": "Cor da pergunta",
|
||||
"question_deleted": "Pergunta eliminada.",
|
||||
"question_duplicated": "Pergunta duplicada.",
|
||||
"question_id_updated": "ID da pergunta atualizado",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Limites de resposta, redirecionamentos e mais.",
|
||||
"response_options": "Opções de Resposta",
|
||||
"roundness": "Arredondamento",
|
||||
"roundness_description": "Controla o arredondamento dos cantos do cartão.",
|
||||
"row_used_in_logic_error": "Esta linha é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
|
||||
"rows": "Linhas",
|
||||
"save_and_close": "Guardar e Fechar",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Estilo definido para estilos do tema",
|
||||
"subheading": "Subtítulo",
|
||||
"subtract": "Subtrair -",
|
||||
"suggest_colors": "Sugerir cores",
|
||||
"survey_completed_heading": "Inquérito Concluído",
|
||||
"survey_completed_subheading": "Este inquérito gratuito e de código aberto foi encerrado",
|
||||
"survey_display_settings": "Configurações de Exibição do Inquérito",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Adicionar cor de fundo",
|
||||
"add_background_color_description": "Adicione uma cor de fundo ao contentor do logótipo.",
|
||||
"advanced_styling_field_border_radius": "Raio da borda",
|
||||
"advanced_styling_field_button_bg": "Fundo do botão",
|
||||
"advanced_styling_field_button_bg_description": "Preenche o botão Seguinte / Submeter.",
|
||||
"advanced_styling_field_button_border_radius_description": "Arredonda os cantos do botão.",
|
||||
"advanced_styling_field_button_font_size_description": "Ajusta o tamanho do texto da etiqueta do botão.",
|
||||
"advanced_styling_field_button_font_weight_description": "Torna o texto do botão mais leve ou mais negrito.",
|
||||
"advanced_styling_field_button_height_description": "Controla a altura do botão.",
|
||||
"advanced_styling_field_button_padding_x_description": "Adiciona espaço à esquerda e à direita.",
|
||||
"advanced_styling_field_button_padding_y_description": "Adiciona espaço no topo e na base.",
|
||||
"advanced_styling_field_button_text": "Texto do botão",
|
||||
"advanced_styling_field_button_text_description": "Colore a etiqueta dentro dos botões.",
|
||||
"advanced_styling_field_description_color": "Cor da descrição",
|
||||
"advanced_styling_field_description_color_description": "Colore o texto abaixo de cada título.",
|
||||
"advanced_styling_field_description_size": "Tamanho da fonte da descrição",
|
||||
"advanced_styling_field_description_size_description": "Ajusta o tamanho do texto da descrição.",
|
||||
"advanced_styling_field_description_weight": "Peso da fonte da descrição",
|
||||
"advanced_styling_field_description_weight_description": "Torna o texto da descrição mais leve ou mais negrito.",
|
||||
"advanced_styling_field_font_size": "Tamanho da fonte",
|
||||
"advanced_styling_field_font_weight": "Peso da fonte",
|
||||
"advanced_styling_field_headline_color": "Cor do título",
|
||||
"advanced_styling_field_headline_color_description": "Colore o texto principal da pergunta.",
|
||||
"advanced_styling_field_headline_size": "Tamanho da fonte do título",
|
||||
"advanced_styling_field_headline_size_description": "Ajusta o tamanho do texto do título.",
|
||||
"advanced_styling_field_headline_weight": "Peso da fonte do título",
|
||||
"advanced_styling_field_headline_weight_description": "Torna o texto do título mais leve ou mais negrito.",
|
||||
"advanced_styling_field_height": "Altura",
|
||||
"advanced_styling_field_indicator_bg": "Fundo do indicador",
|
||||
"advanced_styling_field_indicator_bg_description": "Colore a porção preenchida da barra.",
|
||||
"advanced_styling_field_input_border_radius_description": "Arredonda os cantos do campo.",
|
||||
"advanced_styling_field_input_font_size_description": "Ajusta o tamanho do texto digitado nos campos.",
|
||||
"advanced_styling_field_input_height_description": "Controla a altura do campo de entrada.",
|
||||
"advanced_styling_field_input_padding_x_description": "Adiciona espaço à esquerda e à direita.",
|
||||
"advanced_styling_field_input_padding_y_description": "Adiciona espaço no topo e na base.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Atenua o texto de sugestão do placeholder.",
|
||||
"advanced_styling_field_input_shadow_description": "Adiciona uma sombra ao redor dos campos de entrada.",
|
||||
"advanced_styling_field_input_text": "Texto de entrada",
|
||||
"advanced_styling_field_input_text_description": "Colore o texto digitado nos campos de entrada.",
|
||||
"advanced_styling_field_option_bg": "Fundo",
|
||||
"advanced_styling_field_option_bg_description": "Preenche os itens de opção.",
|
||||
"advanced_styling_field_option_border_radius_description": "Arredonda os cantos das opções.",
|
||||
"advanced_styling_field_option_font_size_description": "Ajusta o tamanho do texto da etiqueta da opção.",
|
||||
"advanced_styling_field_option_label": "Cor da etiqueta",
|
||||
"advanced_styling_field_option_label_description": "Colore o texto da etiqueta da opção.",
|
||||
"advanced_styling_field_option_padding_x_description": "Adiciona espaço à esquerda e à direita.",
|
||||
"advanced_styling_field_option_padding_y_description": "Adiciona espaço no topo e na base.",
|
||||
"advanced_styling_field_padding_x": "Espaçamento X",
|
||||
"advanced_styling_field_padding_y": "Espaçamento Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Opacidade do marcador de posição",
|
||||
"advanced_styling_field_shadow": "Sombra",
|
||||
"advanced_styling_field_track_bg": "Fundo da faixa",
|
||||
"advanced_styling_field_track_bg_description": "Colore a porção não preenchida da barra.",
|
||||
"advanced_styling_field_track_height": "Altura da faixa",
|
||||
"advanced_styling_field_track_height_description": "Controla a espessura da barra de progresso.",
|
||||
"advanced_styling_field_upper_label_color": "Cor da etiqueta do título",
|
||||
"advanced_styling_field_upper_label_color_description": "Colore a pequena etiqueta acima dos campos de entrada.",
|
||||
"advanced_styling_field_upper_label_size": "Tamanho da fonte da etiqueta do título",
|
||||
"advanced_styling_field_upper_label_size_description": "Ajusta o tamanho da pequena etiqueta acima dos campos de entrada.",
|
||||
"advanced_styling_field_upper_label_weight": "Peso da fonte da etiqueta do título",
|
||||
"advanced_styling_field_upper_label_weight_description": "Torna a etiqueta mais leve ou mais negrito.",
|
||||
"advanced_styling_section_buttons": "Botões",
|
||||
"advanced_styling_section_headlines": "Títulos e descrições",
|
||||
"advanced_styling_section_inputs": "Campos de entrada",
|
||||
"advanced_styling_section_options": "Opções (rádio/caixa de seleção)",
|
||||
"app_survey_placement": "Colocação do inquérito (app)",
|
||||
"app_survey_placement_settings_description": "Altere onde os inquéritos serão apresentados na sua aplicação web ou website.",
|
||||
"centered_modal_overlay_color": "Cor da sobreposição modal centralizada",
|
||||
"email_customization": "Personalização de e-mail",
|
||||
"email_customization_description": "Altere a aparência dos e-mails que a Formbricks envia em seu nome.",
|
||||
"enable_custom_styling": "Ativar estilização personalizada",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "A marca Formbricks está oculta.",
|
||||
"formbricks_branding_settings_description": "Adoramos o seu apoio, mas compreendemos se preferir desativar.",
|
||||
"formbricks_branding_shown": "A marca Formbricks está visível.",
|
||||
"generate_theme_btn": "Gerar",
|
||||
"generate_theme_confirmation": "Gostarias de gerar um tema de cores correspondente com base na cor da tua marca? Isto irá substituir as tuas definições de cor atuais.",
|
||||
"generate_theme_header": "Gerar tema de cores?",
|
||||
"logo_removed_successfully": "Logótipo removido com sucesso",
|
||||
"logo_settings_description": "Carregue o logótipo da sua empresa para personalizar inquéritos e pré-visualizações de links.",
|
||||
"logo_updated_successfully": "Logótipo atualizado com sucesso",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Mostrar marca Formbricks em inquéritos {type}",
|
||||
"show_powered_by_formbricks": "Mostrar assinatura 'Powered by Formbricks'",
|
||||
"styling_updated_successfully": "Estilo atualizado com sucesso",
|
||||
"suggest_colors": "Sugerir cores",
|
||||
"theme": "Tema",
|
||||
"theme_settings_description": "Crie um tema de estilo para todos os inquéritos. Pode ativar estilos personalizados para cada inquérito."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Peça aos respondentes para escolherem uma ou mais imagens",
|
||||
"preview_survey_ending_card_description": "Por favor, continue o seu onboarding.",
|
||||
"preview_survey_ending_card_headline": "Conseguiste!",
|
||||
"preview_survey_input_headline": "Mais alguma coisa a partilhar?",
|
||||
"preview_survey_input_placeholder": "Deixa uma nota...",
|
||||
"preview_survey_name": "Novo inquérito",
|
||||
"preview_survey_question_1_headline": "Como classificaria {projectName}?",
|
||||
"preview_survey_question_1_lower_label": "Não é bom",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Sim, mantenha-me informado.",
|
||||
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
|
||||
"preview_survey_question_2_headline": "Quer manter-se atualizado?",
|
||||
"preview_survey_question_2_subheader": "Este é um exemplo de descrição.",
|
||||
"preview_survey_welcome_card_headline": "Bem-vindo!",
|
||||
"prioritize_features_description": "Identifique as funcionalidades que os seus utilizadores precisam mais e menos.",
|
||||
"prioritize_features_name": "Priorizar Funcionalidades",
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Nu a fost găsită nicio imagine de fundal.",
|
||||
"no_code": "Fără Cod",
|
||||
"no_files_uploaded": "Nu au fost încărcate fișiere",
|
||||
"no_overlay": "Fără overlay",
|
||||
"no_quotas_found": "Nicio cotă găsită",
|
||||
"no_result_found": "Niciun rezultat găsit",
|
||||
"no_results": "Nicio rezultat",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Echipele organizației nu au fost găsite",
|
||||
"other": "Altele",
|
||||
"others": "Altele",
|
||||
"overlay_color": "Culoare overlay",
|
||||
"overview": "Prezentare generală",
|
||||
"password": "Parolă",
|
||||
"paused": "Pauză",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Solicitați o licență de încercare",
|
||||
"reset_to_default": "Revino la implicit",
|
||||
"response": "Răspuns",
|
||||
"response_id": "ID răspuns",
|
||||
"responses": "Răspunsuri",
|
||||
"restart": "Repornește",
|
||||
"role": "Rolul",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "din organizația ta",
|
||||
"invitation_sent_once_more": "Invitație trimisă din nou.",
|
||||
"invite_deleted_successfully": "Invitație ștearsă cu succes",
|
||||
"invite_expires_on": "Invitația expiră pe {date}",
|
||||
"invited_on": "Invitat pe {date}",
|
||||
"invites_failed": "Invitații eșuate",
|
||||
"leave_organization": "Părăsește organizația",
|
||||
"leave_organization_description": "Vei părăsi această organizație și vei pierde accesul la toate sondajele și răspunsurile. Poți să te alături din nou doar dacă ești invitat.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Adaugă un placeholder pentru a afișa dacă nu există valoare de reamintit",
|
||||
"add_hidden_field_id": "Adăugați ID câmp ascuns",
|
||||
"add_highlight_border": "Adaugă bordură evidențiată",
|
||||
"add_highlight_border_description": "Adaugă o margine exterioară cardului tău de sondaj.",
|
||||
"add_logic": "Adaugă logică",
|
||||
"add_none_of_the_above": "Adăugați \"Niciuna dintre cele de mai sus\"",
|
||||
"add_option": "Adăugați opțiune",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Bloc duplicat.",
|
||||
"bold": "Îngroșat",
|
||||
"brand_color": "Culoarea brandului",
|
||||
"brand_color_description": "Se aplică pe butoane, linkuri și evidențieri.",
|
||||
"brightness": "Luminozitate",
|
||||
"bulk_edit": "Editare în bloc",
|
||||
"bulk_edit_description": "Editați toate opțiunile de mai jos, câte una pe linie. Liniile goale vor fi omise, iar duplicatele vor fi eliminate.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Capturați acțiune nouă",
|
||||
"card_arrangement_for_survey_type_derived": "Aranjament de carduri pentru sondaje de tip {surveyTypeDerived}",
|
||||
"card_background_color": "Culoarea de fundal a cardului",
|
||||
"card_background_color_description": "Umple zona cardului de sondaj.",
|
||||
"card_border_color": "Culoarea bordurii cardului",
|
||||
"card_border_color_description": "Conturează cardul sondajului.",
|
||||
"card_styling": "Stilizare card",
|
||||
"casual": "Casual",
|
||||
"caution_edit_duplicate": "Duplică & editează",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Răspunsurile mai vechi și mai noi se amestecă, ceea ce poate duce la rezumate de date înșelătoare.",
|
||||
"caution_recommendation": "Aceasta poate cauza inconsistențe de date în rezultatul sondajului. Vă recomandăm să duplicați sondajul în schimb.",
|
||||
"caution_text": "Schimbările vor duce la inconsecvențe",
|
||||
"centered_modal_overlay_color": "Culoare suprapunere modală centralizată",
|
||||
"change_anyway": "Schimbă oricum",
|
||||
"change_background": "Schimbați fundalul",
|
||||
"change_question_type": "Schimbă tipul întrebării",
|
||||
"change_survey_type": "Schimbarea tipului chestionarului afectează accesul existent",
|
||||
"change_the_background_color_of_the_card": "Schimbați culoarea de fundal a cardului.",
|
||||
"change_the_background_color_of_the_input_fields": "Schimbați culoarea de fundal a câmpurilor de introducere.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Schimbați fundalul cu o culoare, imagine sau animație.",
|
||||
"change_the_border_color_of_the_card": "Schimbați culoarea bordurii cardului.",
|
||||
"change_the_border_color_of_the_input_fields": "Schimbați culoarea bordurii câmpurilor de introducere.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Schimbați raza de rotunjire a cardului și a câmpurilor de introducere.",
|
||||
"change_the_brand_color_of_the_survey": "Schimbați culoarea brandului chestionarului",
|
||||
"change_the_placement_of_this_survey": "Schimbă amplasarea acestui sondaj.",
|
||||
"change_the_question_color_of_the_survey": "Schimbați culoarea întrebării chestionarului.",
|
||||
"changes_saved": "Modificările au fost salvate",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Schimbarea tipului chestionarului va afecta modul în care acesta poate fi distribuit. Dacă respondenții au deja linkuri de acces pentru tipul curent, aceștia ar putea pierde accesul după schimbare.",
|
||||
"checkbox_label": "Etichetă casetă de selectare",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Ascunde bara de progres",
|
||||
"hide_question_settings": "Ascunde setările întrebării",
|
||||
"hostname": "Nume gazdă",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Cât de funky doriți să fie cardurile dumneavoastră în sondajele de tip {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Dacă aveți nevoie de mai mult, vă rugăm",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Continuă afișarea ori de câte ori este declanșat până când se trimite un răspuns.",
|
||||
"ignore_global_waiting_time": "Ignoră perioada de răcire",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Valoare inițială",
|
||||
"inner_text": "Text Interior",
|
||||
"input_border_color": "Culoarea graniței câmpului de introducere",
|
||||
"input_border_color_description": "Conturează câmpurile de text și zonele de text.",
|
||||
"input_color": "Culoarea câmpului de introducere",
|
||||
"input_color_description": "Umple interiorul câmpurilor de text.",
|
||||
"insert_link": "Inserează link",
|
||||
"invalid_targeting": "\"Targetare nevalidă: Vă rugăm să verificați filtrele pentru audiență\"",
|
||||
"invalid_video_url_warning": "Vă rugăm să introduceți un URL valid de YouTube, Vimeo sau Loom. În prezent nu susținem alți furnizori de găzduire video.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Doar utilizatorii care cunosc PIN-ul pot accesa sondajul.",
|
||||
"publish": "Publică",
|
||||
"question": "Întrebare",
|
||||
"question_color": "Culoarea întrebării",
|
||||
"question_deleted": "Întrebare ștearsă.",
|
||||
"question_duplicated": "Întrebare duplicată.",
|
||||
"question_id_updated": "ID întrebare actualizat",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Limite de răspunsuri, redirecționări și altele.",
|
||||
"response_options": "Opțiuni răspuns",
|
||||
"roundness": "Rotunjire",
|
||||
"roundness_description": "Controlează cât de rotunjite sunt colțurile cardului.",
|
||||
"row_used_in_logic_error": "Această linie este folosită în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
|
||||
"rows": "Rânduri",
|
||||
"save_and_close": "Salvează & Închide",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Stilizare setată la stilurile temei",
|
||||
"subheading": "Subtitlu",
|
||||
"subtract": "Scade -",
|
||||
"suggest_colors": "Sugerați culori",
|
||||
"survey_completed_heading": "Sondaj Completat",
|
||||
"survey_completed_subheading": "Acest sondaj gratuit și open-source a fost închis",
|
||||
"survey_display_settings": "Setări de afișare a sondajului",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Adăugați culoare de fundal",
|
||||
"add_background_color_description": "Adăugați o culoare de fundal la containerul siglei.",
|
||||
"advanced_styling_field_border_radius": "Raza colțurilor",
|
||||
"advanced_styling_field_button_bg": "Fundal buton",
|
||||
"advanced_styling_field_button_bg_description": "Umple butonul Următor / Trimite.",
|
||||
"advanced_styling_field_button_border_radius_description": "Rotunjește colțurile butonului.",
|
||||
"advanced_styling_field_button_font_size_description": "Scalează textul etichetei butonului.",
|
||||
"advanced_styling_field_button_font_weight_description": "Face textul butonului mai subțire sau mai îngroșat.",
|
||||
"advanced_styling_field_button_height_description": "Controlează înălțimea butonului.",
|
||||
"advanced_styling_field_button_padding_x_description": "Adaugă spațiu la stânga și la dreapta.",
|
||||
"advanced_styling_field_button_padding_y_description": "Adaugă spațiu sus și jos.",
|
||||
"advanced_styling_field_button_text": "Text buton",
|
||||
"advanced_styling_field_button_text_description": "Colorează eticheta din interiorul butoanelor.",
|
||||
"advanced_styling_field_description_color": "Culoare descriere",
|
||||
"advanced_styling_field_description_color_description": "Colorează textul de sub fiecare titlu.",
|
||||
"advanced_styling_field_description_size": "Mărime font descriere",
|
||||
"advanced_styling_field_description_size_description": "Scalează textul descrierii.",
|
||||
"advanced_styling_field_description_weight": "Grosime font descriere",
|
||||
"advanced_styling_field_description_weight_description": "Face textul descrierii mai subțire sau mai îngroșat.",
|
||||
"advanced_styling_field_font_size": "Mărime font",
|
||||
"advanced_styling_field_font_weight": "Grosime font",
|
||||
"advanced_styling_field_headline_color": "Culoare titlu",
|
||||
"advanced_styling_field_headline_color_description": "Colorează textul principal al întrebării.",
|
||||
"advanced_styling_field_headline_size": "Mărime font titlu",
|
||||
"advanced_styling_field_headline_size_description": "Scalează textul titlului.",
|
||||
"advanced_styling_field_headline_weight": "Grosime font titlu",
|
||||
"advanced_styling_field_headline_weight_description": "Face textul titlului mai subțire sau mai îngroșat.",
|
||||
"advanced_styling_field_height": "Înălțime",
|
||||
"advanced_styling_field_indicator_bg": "Fundal indicator",
|
||||
"advanced_styling_field_indicator_bg_description": "Colorează partea umplută a barei.",
|
||||
"advanced_styling_field_input_border_radius_description": "Rotunjește colțurile câmpurilor de introducere.",
|
||||
"advanced_styling_field_input_font_size_description": "Scalează textul introdus în câmpuri.",
|
||||
"advanced_styling_field_input_height_description": "Controlează înălțimea câmpului de introducere.",
|
||||
"advanced_styling_field_input_padding_x_description": "Adaugă spațiu la stânga și la dreapta.",
|
||||
"advanced_styling_field_input_padding_y_description": "Adaugă spațiu deasupra și dedesubt.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Estompează textul de sugestie din placeholder.",
|
||||
"advanced_styling_field_input_shadow_description": "Adaugă o umbră în jurul câmpurilor de introducere.",
|
||||
"advanced_styling_field_input_text": "Text câmp",
|
||||
"advanced_styling_field_input_text_description": "Colorează textul introdus în câmpuri.",
|
||||
"advanced_styling_field_option_bg": "Fundal",
|
||||
"advanced_styling_field_option_bg_description": "Umple elementele de opțiune.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rotunjește colțurile opțiunilor.",
|
||||
"advanced_styling_field_option_font_size_description": "Redimensionează textul etichetei opțiunii.",
|
||||
"advanced_styling_field_option_label": "Culoare etichetă",
|
||||
"advanced_styling_field_option_label_description": "Colorează textul etichetei opțiunii.",
|
||||
"advanced_styling_field_option_padding_x_description": "Adaugă spațiu în stânga și în dreapta.",
|
||||
"advanced_styling_field_option_padding_y_description": "Adaugă spațiu deasupra și dedesubt.",
|
||||
"advanced_styling_field_padding_x": "Spațiere X",
|
||||
"advanced_styling_field_padding_y": "Spațiere Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Opacitate placeholder",
|
||||
"advanced_styling_field_shadow": "Umbră",
|
||||
"advanced_styling_field_track_bg": "Fundal track",
|
||||
"advanced_styling_field_track_bg_description": "Colorează partea necompletată a barei.",
|
||||
"advanced_styling_field_track_height": "Înălțime track",
|
||||
"advanced_styling_field_track_height_description": "Controlează grosimea barei de progres.",
|
||||
"advanced_styling_field_upper_label_color": "Culoare etichetă titlu",
|
||||
"advanced_styling_field_upper_label_color_description": "Colorează eticheta mică de deasupra câmpurilor.",
|
||||
"advanced_styling_field_upper_label_size": "Mărime font etichetă titlu",
|
||||
"advanced_styling_field_upper_label_size_description": "Redimensionează eticheta mică de deasupra câmpurilor.",
|
||||
"advanced_styling_field_upper_label_weight": "Grosime font etichetă titlu",
|
||||
"advanced_styling_field_upper_label_weight_description": "Face eticheta mai subțire sau mai îngroșată.",
|
||||
"advanced_styling_section_buttons": "Butoane",
|
||||
"advanced_styling_section_headlines": "Titluri și descrieri",
|
||||
"advanced_styling_section_inputs": "Inputuri",
|
||||
"advanced_styling_section_options": "Opțiuni (Radio/Checkbox)",
|
||||
"app_survey_placement": "Amplasarea sondajului în aplicație",
|
||||
"app_survey_placement_settings_description": "Schimbați unde vor fi afișate sondajele în aplicația sau site-ul dvs. web.",
|
||||
"centered_modal_overlay_color": "Culoare suprapunere modală centralizată",
|
||||
"email_customization": "Personalizare email",
|
||||
"email_customization_description": "Schimbați aspectul și stilul emailurilor trimise de Formbricks în numele dvs.",
|
||||
"enable_custom_styling": "Activați stilizarea personalizată",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Brandingul Formbricks este ascuns.",
|
||||
"formbricks_branding_settings_description": "Ne bucurăm de susținerea ta, dar înțelegem dacă vrei să dezactivezi această opțiune.",
|
||||
"formbricks_branding_shown": "Brandingul Formbricks este afișat.",
|
||||
"generate_theme_btn": "Generează",
|
||||
"generate_theme_confirmation": "Vrei să generezi o temă de culori potrivită pe baza culorii brandului tău? Aceasta va suprascrie setările actuale de culoare.",
|
||||
"generate_theme_header": "Generezi temă de culori?",
|
||||
"logo_removed_successfully": "Sigla a fost eliminată cu succes",
|
||||
"logo_settings_description": "Încarcă sigla companiei pentru a personaliza sondajele și previzualizările de linkuri.",
|
||||
"logo_updated_successfully": "Sigla a fost actualizată cu succes",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Afișează brandingul Formbricks în sondajele de tip {type}",
|
||||
"show_powered_by_formbricks": "Afișează semnătura „Powered by Formbricks”",
|
||||
"styling_updated_successfully": "Stilizarea a fost actualizată cu succes",
|
||||
"suggest_colors": "Sugerează culori",
|
||||
"theme": "Temă",
|
||||
"theme_settings_description": "Creează o temă de stil pentru toate sondajele. Poți activa stilizare personalizată pentru fiecare sondaj."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Cereți respondenților să aleagă una sau mai multe imagini",
|
||||
"preview_survey_ending_card_description": "Vă rugăm să continuați onboarding-ul.",
|
||||
"preview_survey_ending_card_headline": "Ai reușit!",
|
||||
"preview_survey_input_headline": "Mai vrei să adaugi ceva?",
|
||||
"preview_survey_input_placeholder": "Lasă o notă...",
|
||||
"preview_survey_name": "Previzualizare chestionar",
|
||||
"preview_survey_question_1_headline": "Cum ai evalua {projectName}?",
|
||||
"preview_survey_question_1_lower_label": "Nu este bine",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Da, ține-mă informat.",
|
||||
"preview_survey_question_2_choice_2_label": "Nu, mulţumesc!",
|
||||
"preview_survey_question_2_headline": "Vrei să fii în temă?",
|
||||
"preview_survey_question_2_subheader": "Aceasta este o descriere exemplu.",
|
||||
"preview_survey_welcome_card_headline": "Bun venit!",
|
||||
"prioritize_features_description": "Identificați caracteristicile de care utilizatorii dumneavoastră au cel mai mult și cel mai puțin nevoie.",
|
||||
"prioritize_features_name": "Prioritizați caracteristicile",
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Фоновое изображение не найдено.",
|
||||
"no_code": "Нет кода",
|
||||
"no_files_uploaded": "Файлы не были загружены",
|
||||
"no_overlay": "Без наложения",
|
||||
"no_quotas_found": "Квоты не найдены",
|
||||
"no_result_found": "Результат не найден",
|
||||
"no_results": "Нет результатов",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Команды организации не найдены",
|
||||
"other": "Другое",
|
||||
"others": "Другие",
|
||||
"overlay_color": "Цвет наложения",
|
||||
"overview": "Обзор",
|
||||
"password": "Пароль",
|
||||
"paused": "Приостановлено",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Запросить пробную лицензию",
|
||||
"reset_to_default": "Сбросить по умолчанию",
|
||||
"response": "Ответ",
|
||||
"response_id": "ID ответа",
|
||||
"responses": "Ответы",
|
||||
"restart": "Перезапустить",
|
||||
"role": "Роль",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "из вашей организации",
|
||||
"invitation_sent_once_more": "Приглашение отправлено ещё раз.",
|
||||
"invite_deleted_successfully": "Приглашение успешно удалено",
|
||||
"invite_expires_on": "Приглашение истекает {date}",
|
||||
"invited_on": "Приглашён {date}",
|
||||
"invites_failed": "Не удалось отправить приглашения",
|
||||
"leave_organization": "Покинуть организацию",
|
||||
"leave_organization_description": "Вы покинете эту организацию и потеряете доступ ко всем опросам и ответам. Вы сможете вернуться только по новому приглашению.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Добавить плейсхолдер, который будет показан, если нет значения для отображения.",
|
||||
"add_hidden_field_id": "Добавить скрытый ID поля",
|
||||
"add_highlight_border": "Добавить выделяющую рамку",
|
||||
"add_highlight_border_description": "Добавьте внешнюю рамку к карточке опроса.",
|
||||
"add_logic": "Добавить логику",
|
||||
"add_none_of_the_above": "Добавить вариант «Ничего из вышеперечисленного»",
|
||||
"add_option": "Добавить вариант",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Блокировать дубликаты.",
|
||||
"bold": "Жирный",
|
||||
"brand_color": "Фирменный цвет",
|
||||
"brand_color_description": "Применяется к кнопкам, ссылкам и выделениям.",
|
||||
"brightness": "Яркость",
|
||||
"bulk_edit": "Массовое редактирование",
|
||||
"bulk_edit_description": "Отредактируйте все варианты ниже, по одному на строку. Пустые строки будут пропущены, а дубликаты удалены.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Захватить новое действие",
|
||||
"card_arrangement_for_survey_type_derived": "Расположение карточек для опросов типа {surveyTypeDerived}",
|
||||
"card_background_color": "Цвет фона карточки",
|
||||
"card_background_color_description": "Заполняет область карточки опроса.",
|
||||
"card_border_color": "Цвет рамки карточки",
|
||||
"card_border_color_description": "Обводит карточку опроса.",
|
||||
"card_styling": "Оформление карточки",
|
||||
"casual": "Неформальный",
|
||||
"caution_edit_duplicate": "Дублировать и редактировать",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Старые и новые ответы смешиваются, что может привести к искажённым итоговым данным.",
|
||||
"caution_recommendation": "Это может привести к несоответствиям в итогах опроса. Рекомендуем вместо этого дублировать опрос.",
|
||||
"caution_text": "Изменения приведут к несоответствиям",
|
||||
"centered_modal_overlay_color": "Цвет оверлея центрированного модального окна",
|
||||
"change_anyway": "Всё равно изменить",
|
||||
"change_background": "Изменить фон",
|
||||
"change_question_type": "Изменить тип вопроса",
|
||||
"change_survey_type": "Смена типа опроса влияет на существующий доступ",
|
||||
"change_the_background_color_of_the_card": "Изменить цвет фона карточки.",
|
||||
"change_the_background_color_of_the_input_fields": "Изменить цвет фона полей ввода.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Изменить фон на цвет, изображение или анимацию.",
|
||||
"change_the_border_color_of_the_card": "Изменить цвет рамки карточки.",
|
||||
"change_the_border_color_of_the_input_fields": "Изменить цвет рамки полей ввода.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Изменить скругление углов карточки и полей ввода.",
|
||||
"change_the_brand_color_of_the_survey": "Изменить фирменный цвет опроса.",
|
||||
"change_the_placement_of_this_survey": "Изменить размещение этого опроса.",
|
||||
"change_the_question_color_of_the_survey": "Изменить цвет вопросов в опросе.",
|
||||
"changes_saved": "Изменения сохранены.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Изменение типа опроса повлияет на способы его распространения. Если у респондентов уже есть ссылки для доступа к текущему типу, после смены они могут потерять доступ.",
|
||||
"checkbox_label": "Метка флажка",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Скрыть индикатор прогресса",
|
||||
"hide_question_settings": "Скрыть настройки вопроса",
|
||||
"hostname": "Имя хоста",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Насколько необычными вы хотите сделать карточки в опросах типа {surveyTypeDerived}",
|
||||
"if_you_need_more_please": "Если вам нужно больше, пожалуйста",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Показывать каждый раз при срабатывании, пока не будет получен ответ.",
|
||||
"ignore_global_waiting_time": "Игнорировать период ожидания",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Начальное значение",
|
||||
"inner_text": "Внутренний текст",
|
||||
"input_border_color": "Цвет рамки поля ввода",
|
||||
"input_border_color_description": "Обводит текстовые поля и текстовые области.",
|
||||
"input_color": "Цвет поля ввода",
|
||||
"input_color_description": "Заполняет внутреннюю часть текстовых полей.",
|
||||
"insert_link": "Вставить ссылку",
|
||||
"invalid_targeting": "Некорректный таргетинг: проверьте фильтры аудитории",
|
||||
"invalid_video_url_warning": "Пожалуйста, введите корректную ссылку на YouTube, Vimeo или Loom. В настоящее время другие видеохостинги не поддерживаются.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Только пользователи, у которых есть PIN-код, могут получить доступ к опросу.",
|
||||
"publish": "Опубликовать",
|
||||
"question": "Вопрос",
|
||||
"question_color": "Цвет вопроса",
|
||||
"question_deleted": "Вопрос удалён.",
|
||||
"question_duplicated": "Вопрос дублирован.",
|
||||
"question_id_updated": "ID вопроса обновлён",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Лимиты ответов, перенаправления и другое.",
|
||||
"response_options": "Параметры ответа",
|
||||
"roundness": "Скругление",
|
||||
"roundness_description": "Определяет степень скругления углов карточки.",
|
||||
"row_used_in_logic_error": "Эта строка используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите её из логики.",
|
||||
"rows": "Строки",
|
||||
"save_and_close": "Сохранить и закрыть",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Оформление установлено в соответствии с темой",
|
||||
"subheading": "Подзаголовок",
|
||||
"subtract": "Вычесть -",
|
||||
"suggest_colors": "Предложить цвета",
|
||||
"survey_completed_heading": "Опрос завершён",
|
||||
"survey_completed_subheading": "Этот бесплатный и открытый опрос был закрыт",
|
||||
"survey_display_settings": "Настройки отображения опроса",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Добавить цвет фона",
|
||||
"add_background_color_description": "Добавьте цвет фона для контейнера с логотипом.",
|
||||
"advanced_styling_field_border_radius": "Радиус скругления",
|
||||
"advanced_styling_field_button_bg": "Фон кнопки",
|
||||
"advanced_styling_field_button_bg_description": "Заполняет кнопку «Далее» / «Отправить».",
|
||||
"advanced_styling_field_button_border_radius_description": "Скругляет углы кнопки.",
|
||||
"advanced_styling_field_button_font_size_description": "Масштабирует текст на кнопке.",
|
||||
"advanced_styling_field_button_font_weight_description": "Делает текст на кнопке тоньше или жирнее.",
|
||||
"advanced_styling_field_button_height_description": "Определяет высоту кнопки.",
|
||||
"advanced_styling_field_button_padding_x_description": "Добавляет отступы слева и справа.",
|
||||
"advanced_styling_field_button_padding_y_description": "Добавляет отступы сверху и снизу.",
|
||||
"advanced_styling_field_button_text": "Текст кнопки",
|
||||
"advanced_styling_field_button_text_description": "Задаёт цвет текста на кнопках.",
|
||||
"advanced_styling_field_description_color": "Цвет описания",
|
||||
"advanced_styling_field_description_color_description": "Задаёт цвет текста под каждым заголовком.",
|
||||
"advanced_styling_field_description_size": "Размер шрифта описания",
|
||||
"advanced_styling_field_description_size_description": "Масштабирует текст описания.",
|
||||
"advanced_styling_field_description_weight": "Толщина шрифта описания",
|
||||
"advanced_styling_field_description_weight_description": "Делает текст описания тоньше или жирнее.",
|
||||
"advanced_styling_field_font_size": "Размер шрифта",
|
||||
"advanced_styling_field_font_weight": "Толщина шрифта",
|
||||
"advanced_styling_field_headline_color": "Цвет заголовка",
|
||||
"advanced_styling_field_headline_color_description": "Задаёт цвет основного текста вопроса.",
|
||||
"advanced_styling_field_headline_size": "Размер шрифта заголовка",
|
||||
"advanced_styling_field_headline_size_description": "Масштабирует текст заголовка.",
|
||||
"advanced_styling_field_headline_weight": "Толщина шрифта заголовка",
|
||||
"advanced_styling_field_headline_weight_description": "Делает текст заголовка тоньше или жирнее.",
|
||||
"advanced_styling_field_height": "Высота",
|
||||
"advanced_styling_field_indicator_bg": "Фон индикатора",
|
||||
"advanced_styling_field_indicator_bg_description": "Задаёт цвет заполненной части полосы.",
|
||||
"advanced_styling_field_input_border_radius_description": "Скругляет углы полей ввода.",
|
||||
"advanced_styling_field_input_font_size_description": "Масштабирует введённый текст в полях ввода.",
|
||||
"advanced_styling_field_input_height_description": "Определяет высоту поля ввода.",
|
||||
"advanced_styling_field_input_padding_x_description": "Добавляет отступы слева и справа.",
|
||||
"advanced_styling_field_input_padding_y_description": "Добавляет пространство сверху и снизу.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Делает текст подсказки менее заметным.",
|
||||
"advanced_styling_field_input_shadow_description": "Добавляет тень вокруг полей ввода.",
|
||||
"advanced_styling_field_input_text": "Текст ввода",
|
||||
"advanced_styling_field_input_text_description": "Задаёт цвет введённого текста в полях.",
|
||||
"advanced_styling_field_option_bg": "Фон",
|
||||
"advanced_styling_field_option_bg_description": "Заливает фон элементов опций.",
|
||||
"advanced_styling_field_option_border_radius_description": "Скругляет углы опций.",
|
||||
"advanced_styling_field_option_font_size_description": "Изменяет размер текста метки опции.",
|
||||
"advanced_styling_field_option_label": "Цвет метки",
|
||||
"advanced_styling_field_option_label_description": "Задаёт цвет текста метки опции.",
|
||||
"advanced_styling_field_option_padding_x_description": "Добавляет пространство слева и справа.",
|
||||
"advanced_styling_field_option_padding_y_description": "Добавляет пространство сверху и снизу.",
|
||||
"advanced_styling_field_padding_x": "Внутренний отступ по X",
|
||||
"advanced_styling_field_padding_y": "Внутренний отступ по Y",
|
||||
"advanced_styling_field_placeholder_opacity": "Прозрачность плейсхолдера",
|
||||
"advanced_styling_field_shadow": "Тень",
|
||||
"advanced_styling_field_track_bg": "Фон трека",
|
||||
"advanced_styling_field_track_bg_description": "Задаёт цвет незаполненной части полосы.",
|
||||
"advanced_styling_field_track_height": "Высота трека",
|
||||
"advanced_styling_field_track_height_description": "Управляет толщиной индикатора прогресса.",
|
||||
"advanced_styling_field_upper_label_color": "Цвет метки заголовка",
|
||||
"advanced_styling_field_upper_label_color_description": "Задаёт цвет маленькой метки над полями ввода.",
|
||||
"advanced_styling_field_upper_label_size": "Размер шрифта метки заголовка",
|
||||
"advanced_styling_field_upper_label_size_description": "Изменяет размер маленькой метки над полями ввода.",
|
||||
"advanced_styling_field_upper_label_weight": "Толщина шрифта метки заголовка",
|
||||
"advanced_styling_field_upper_label_weight_description": "Делает метку тоньше или жирнее.",
|
||||
"advanced_styling_section_buttons": "Кнопки",
|
||||
"advanced_styling_section_headlines": "Заголовки и описания",
|
||||
"advanced_styling_section_inputs": "Поля ввода",
|
||||
"advanced_styling_section_options": "Опции (радио/чекбокс)",
|
||||
"app_survey_placement": "Размещение опроса в приложении",
|
||||
"app_survey_placement_settings_description": "Измените, где будут отображаться опросы в вашем веб-приложении или на сайте.",
|
||||
"centered_modal_overlay_color": "Цвет оверлея центрированного модального окна",
|
||||
"email_customization": "Настройка email",
|
||||
"email_customization_description": "Измените внешний вид писем, которые Formbricks отправляет от вашего имени.",
|
||||
"enable_custom_styling": "Включить пользовательское оформление",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Брендинг Formbricks скрыт.",
|
||||
"formbricks_branding_settings_description": "Мы ценим вашу поддержку, но понимаем, если вы захотите отключить это.",
|
||||
"formbricks_branding_shown": "Брендинг Formbricks отображается.",
|
||||
"generate_theme_btn": "Сгенерировать",
|
||||
"generate_theme_confirmation": "Сгенерировать подходящую цветовую тему на основе цвета твоего бренда? Это перезапишет текущие цветовые настройки.",
|
||||
"generate_theme_header": "Сгенерировать цветовую тему?",
|
||||
"logo_removed_successfully": "Логотип успешно удалён",
|
||||
"logo_settings_description": "Загрузите логотип вашей компании для брендирования опросов и предпросмотра ссылок.",
|
||||
"logo_updated_successfully": "Логотип успешно обновлён",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Показывать брендинг Formbricks в опросах типа {type}",
|
||||
"show_powered_by_formbricks": "Показывать подпись «Работает на Formbricks»",
|
||||
"styling_updated_successfully": "Стили успешно обновлены",
|
||||
"suggest_colors": "Предложить цвета",
|
||||
"theme": "Тема",
|
||||
"theme_settings_description": "Создайте стиль для всех опросов. Вы можете включить индивидуальное оформление для каждого опроса."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Попросите респондентов выбрать одно или несколько изображений",
|
||||
"preview_survey_ending_card_description": "Пожалуйста, продолжайте регистрацию.",
|
||||
"preview_survey_ending_card_headline": "Вы справились!",
|
||||
"preview_survey_input_headline": "Есть ещё что-то, чем хочешь поделиться?",
|
||||
"preview_survey_input_placeholder": "Оставь заметку...",
|
||||
"preview_survey_name": "Новый опрос",
|
||||
"preview_survey_question_1_headline": "Как бы вы оценили {projectName}?",
|
||||
"preview_survey_question_1_lower_label": "Плохо",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Да, держите меня в курсе.",
|
||||
"preview_survey_question_2_choice_2_label": "Нет, спасибо!",
|
||||
"preview_survey_question_2_headline": "Хотите быть в курсе событий?",
|
||||
"preview_survey_question_2_subheader": "Это пример описания.",
|
||||
"preview_survey_welcome_card_headline": "Добро пожаловать!",
|
||||
"prioritize_features_description": "Определите, какие функции наиболее и наименее важны для ваших пользователей.",
|
||||
"prioritize_features_name": "Приоритизация функций",
|
||||
|
||||
+14
-80
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "Ingen bakgrundsbild hittades.",
|
||||
"no_code": "Ingen kod",
|
||||
"no_files_uploaded": "Inga filer laddades upp",
|
||||
"no_overlay": "Ingen overlay",
|
||||
"no_quotas_found": "Inga kvoter hittades",
|
||||
"no_result_found": "Inget resultat hittades",
|
||||
"no_results": "Inga resultat",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "Organisationsteam hittades inte",
|
||||
"other": "Annat",
|
||||
"others": "Andra",
|
||||
"overlay_color": "Overlay-färg",
|
||||
"overview": "Översikt",
|
||||
"password": "Lösenord",
|
||||
"paused": "Pausad",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "Begär provlicens",
|
||||
"reset_to_default": "Återställ till standard",
|
||||
"response": "Svar",
|
||||
"response_id": "Svar-ID",
|
||||
"responses": "Svar",
|
||||
"restart": "Starta om",
|
||||
"role": "Roll",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "från din organisation",
|
||||
"invitation_sent_once_more": "Inbjudan skickad igen.",
|
||||
"invite_deleted_successfully": "Inbjudan borttagen",
|
||||
"invite_expires_on": "Inbjudan går ut den {date}",
|
||||
"invited_on": "Inbjuden den {date}",
|
||||
"invites_failed": "Inbjudningar misslyckades",
|
||||
"leave_organization": "Lämna organisation",
|
||||
"leave_organization_description": "Du kommer att lämna denna organisation och förlora åtkomst till alla enkäter och svar. Du kan endast återansluta om du blir inbjuden igen.",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "Lägg till en platshållare att visa om det inte finns något värde att återkalla.",
|
||||
"add_hidden_field_id": "Lägg till dolt fält-ID",
|
||||
"add_highlight_border": "Lägg till markerad kant",
|
||||
"add_highlight_border_description": "Lägg till en yttre kant till ditt enkätkort.",
|
||||
"add_logic": "Lägg till logik",
|
||||
"add_none_of_the_above": "Lägg till \"Inget av ovanstående\"",
|
||||
"add_option": "Lägg till alternativ",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "Block duplicerat.",
|
||||
"bold": "Fet",
|
||||
"brand_color": "Varumärkesfärg",
|
||||
"brand_color_description": "Används för knappar, länkar och markeringar.",
|
||||
"brightness": "Ljusstyrka",
|
||||
"bulk_edit": "Massredigera",
|
||||
"bulk_edit_description": "Redigera alla alternativ nedan, ett per rad. Tomma rader kommer att hoppas över och dubbletter tas bort.",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "Fånga ny åtgärd",
|
||||
"card_arrangement_for_survey_type_derived": "Kortarrangemang för {surveyTypeDerived}-enkäter",
|
||||
"card_background_color": "Kortets bakgrundsfärg",
|
||||
"card_background_color_description": "Fyller enkätkortets yta.",
|
||||
"card_border_color": "Kortets kantfärg",
|
||||
"card_border_color_description": "Markerar enkätkortets kant.",
|
||||
"card_styling": "Kortstil",
|
||||
"casual": "Avslappnad",
|
||||
"caution_edit_duplicate": "Duplicera och redigera",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "Äldre och nyare svar blandas vilket kan leda till vilseledande datasammanfattningar.",
|
||||
"caution_recommendation": "Detta kan orsaka datainkonsekvenser i enkätsammanfattningen. Vi rekommenderar att duplicera enkäten istället.",
|
||||
"caution_text": "Ändringar kommer att leda till inkonsekvenser",
|
||||
"centered_modal_overlay_color": "Centrerad modal överläggsfärg",
|
||||
"change_anyway": "Ändra ändå",
|
||||
"change_background": "Ändra bakgrund",
|
||||
"change_question_type": "Ändra frågetyp",
|
||||
"change_survey_type": "Byte av enkättyp påverkar befintlig åtkomst",
|
||||
"change_the_background_color_of_the_card": "Ändra kortets bakgrundsfärg.",
|
||||
"change_the_background_color_of_the_input_fields": "Ändra inmatningsfältens bakgrundsfärg.",
|
||||
"change_the_background_to_a_color_image_or_animation": "Ändra bakgrunden till en färg, bild eller animering.",
|
||||
"change_the_border_color_of_the_card": "Ändra kortets kantfärg.",
|
||||
"change_the_border_color_of_the_input_fields": "Ändra inmatningsfältens kantfärg.",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "Ändra kantradie för kortet och inmatningsfälten.",
|
||||
"change_the_brand_color_of_the_survey": "Ändra enkätens varumärkesfärg.",
|
||||
"change_the_placement_of_this_survey": "Ändra placeringen av denna enkät.",
|
||||
"change_the_question_color_of_the_survey": "Ändra enkätens frågefärg.",
|
||||
"changes_saved": "Ändringar sparade.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Att ändra enkättypen påverkar hur den kan delas. Om respondenter redan har åtkomstlänkar för den nuvarande typen kan de förlora åtkomst efter bytet.",
|
||||
"checkbox_label": "Kryssruteetikett",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "Dölj framstegsindikator",
|
||||
"hide_question_settings": "Dölj frågeinställningar",
|
||||
"hostname": "Värdnamn",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Hur coola vill du att dina kort ska vara i {surveyTypeDerived}-enkäter",
|
||||
"if_you_need_more_please": "Om du behöver mer, vänligen",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Fortsätt visa när villkoren är uppfyllda tills ett svar skickas in.",
|
||||
"ignore_global_waiting_time": "Ignorera väntetid",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "Initialt värde",
|
||||
"inner_text": "Inre text",
|
||||
"input_border_color": "Inmatningsfältets kantfärg",
|
||||
"input_border_color_description": "Markerar kanten på textfält och textområden.",
|
||||
"input_color": "Inmatningsfärg",
|
||||
"input_color_description": "Fyller insidan av textfält.",
|
||||
"insert_link": "Infoga länk",
|
||||
"invalid_targeting": "Ogiltig målgruppsinriktning: Vänligen kontrollera dina målgruppsfilter",
|
||||
"invalid_video_url_warning": "Vänligen ange en giltig YouTube-, Vimeo- eller Loom-URL. Vi stöder för närvarande inte andra videohostingleverantörer.",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "Endast användare som har PIN-koden kan komma åt enkäten.",
|
||||
"publish": "Publicera",
|
||||
"question": "Fråga",
|
||||
"question_color": "Frågefärg",
|
||||
"question_deleted": "Fråga borttagen.",
|
||||
"question_duplicated": "Fråga duplicerad.",
|
||||
"question_id_updated": "Fråge-ID uppdaterat",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "Svarsgränser, omdirigeringar och mer.",
|
||||
"response_options": "Svarsalternativ",
|
||||
"roundness": "Rundhet",
|
||||
"roundness_description": "Styr hur rundade kortets hörn är.",
|
||||
"row_used_in_logic_error": "Denna rad används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
|
||||
"rows": "Rader",
|
||||
"save_and_close": "Spara och stäng",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "Styling inställd på temastil",
|
||||
"subheading": "Underrubrik",
|
||||
"subtract": "Subtrahera -",
|
||||
"suggest_colors": "Föreslå färger",
|
||||
"survey_completed_heading": "Enkät slutförd",
|
||||
"survey_completed_subheading": "Denna gratis och öppenkällkodsenkät har stängts",
|
||||
"survey_display_settings": "Visningsinställningar för enkät",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "Lägg till bakgrundsfärg",
|
||||
"add_background_color_description": "Lägg till en bakgrundsfärg i logobehållaren.",
|
||||
"advanced_styling_field_border_radius": "Hörnradie",
|
||||
"advanced_styling_field_button_bg": "Knappens bakgrund",
|
||||
"advanced_styling_field_button_bg_description": "Fyller Nästa / Skicka-knappen.",
|
||||
"advanced_styling_field_button_border_radius_description": "Rundar av knappens hörn.",
|
||||
"advanced_styling_field_button_font_size_description": "Ändrar storleken på knappens text.",
|
||||
"advanced_styling_field_button_font_weight_description": "Gör knapptexten tunnare eller fetare.",
|
||||
"advanced_styling_field_button_height_description": "Styr knappens höjd.",
|
||||
"advanced_styling_field_button_padding_x_description": "Lägger till utrymme till vänster och höger.",
|
||||
"advanced_styling_field_button_padding_y_description": "Lägger till utrymme upptill och nedtill.",
|
||||
"advanced_styling_field_button_text": "Knapptext",
|
||||
"advanced_styling_field_button_text_description": "Färglägger texten i knappar.",
|
||||
"advanced_styling_field_description_color": "Beskrivningsfärg",
|
||||
"advanced_styling_field_description_color_description": "Färglägger texten under varje rubrik.",
|
||||
"advanced_styling_field_description_size": "Beskrivningens teckenstorlek",
|
||||
"advanced_styling_field_description_size_description": "Ändrar storleken på beskrivningstexten.",
|
||||
"advanced_styling_field_description_weight": "Beskrivningens teckentjocklek",
|
||||
"advanced_styling_field_description_weight_description": "Gör beskrivningstexten tunnare eller fetare.",
|
||||
"advanced_styling_field_font_size": "Teckenstorlek",
|
||||
"advanced_styling_field_font_weight": "Teckentjocklek",
|
||||
"advanced_styling_field_headline_color": "Rubrikfärg",
|
||||
"advanced_styling_field_headline_color_description": "Färglägger huvudfrågan.",
|
||||
"advanced_styling_field_headline_size": "Rubrikens teckenstorlek",
|
||||
"advanced_styling_field_headline_size_description": "Ändrar storleken på rubriken.",
|
||||
"advanced_styling_field_headline_weight": "Rubrikens teckentjocklek",
|
||||
"advanced_styling_field_headline_weight_description": "Gör rubriktexten tunnare eller fetare.",
|
||||
"advanced_styling_field_height": "Höjd",
|
||||
"advanced_styling_field_indicator_bg": "Indikatorns bakgrund",
|
||||
"advanced_styling_field_indicator_bg_description": "Färglägger den fyllda delen av stapeln.",
|
||||
"advanced_styling_field_input_border_radius_description": "Rundar av hörnen på inmatningsfält.",
|
||||
"advanced_styling_field_input_font_size_description": "Ändrar storleken på texten i inmatningsfält.",
|
||||
"advanced_styling_field_input_height_description": "Styr höjden på inmatningsfältet.",
|
||||
"advanced_styling_field_input_padding_x_description": "Lägger till utrymme till vänster och höger.",
|
||||
"advanced_styling_field_input_padding_y_description": "Lägger till utrymme upptill och nedtill.",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "Tonar ut platshållartexten.",
|
||||
"advanced_styling_field_input_shadow_description": "Lägger till en skugga runt inmatningsfälten.",
|
||||
"advanced_styling_field_input_text": "Inmatningstext",
|
||||
"advanced_styling_field_input_text_description": "Färgar den inmatade texten i fälten.",
|
||||
"advanced_styling_field_option_bg": "Bakgrund",
|
||||
"advanced_styling_field_option_bg_description": "Fyller alternativraderna.",
|
||||
"advanced_styling_field_option_border_radius_description": "Rundar hörnen på alternativen.",
|
||||
"advanced_styling_field_option_font_size_description": "Skalar textstorleken på alternativetiketten.",
|
||||
"advanced_styling_field_option_label": "Etikettfärg",
|
||||
"advanced_styling_field_option_label_description": "Färgar texten på alternativetiketten.",
|
||||
"advanced_styling_field_option_padding_x_description": "Lägger till utrymme till vänster och höger.",
|
||||
"advanced_styling_field_option_padding_y_description": "Lägger till utrymme upptill och nedtill.",
|
||||
"advanced_styling_field_padding_x": "Horisontell padding",
|
||||
"advanced_styling_field_padding_y": "Vertikal padding",
|
||||
"advanced_styling_field_placeholder_opacity": "Platshållarens opacitet",
|
||||
"advanced_styling_field_shadow": "Skugga",
|
||||
"advanced_styling_field_track_bg": "Spårets bakgrund",
|
||||
"advanced_styling_field_track_bg_description": "Färgar den ofyllda delen av stapeln.",
|
||||
"advanced_styling_field_track_height": "Spårets höjd",
|
||||
"advanced_styling_field_track_height_description": "Styr tjockleken på förloppsstapeln.",
|
||||
"advanced_styling_field_upper_label_color": "Rubriketikettens färg",
|
||||
"advanced_styling_field_upper_label_color_description": "Färgar den lilla etiketten ovanför fälten.",
|
||||
"advanced_styling_field_upper_label_size": "Rubriketikettens teckenstorlek",
|
||||
"advanced_styling_field_upper_label_size_description": "Skalar storleken på den lilla etiketten ovanför fälten.",
|
||||
"advanced_styling_field_upper_label_weight": "Rubriketikettens teckentjocklek",
|
||||
"advanced_styling_field_upper_label_weight_description": "Gör etiketten tunnare eller fetare.",
|
||||
"advanced_styling_section_buttons": "Knappar",
|
||||
"advanced_styling_section_headlines": "Rubriker & beskrivningar",
|
||||
"advanced_styling_section_inputs": "Inmatningar",
|
||||
"advanced_styling_section_options": "Alternativ (Radio/Checkbox)",
|
||||
"app_survey_placement": "App-enkätplacering",
|
||||
"app_survey_placement_settings_description": "Ändra var enkäter visas i din webbapp eller på din webbplats.",
|
||||
"centered_modal_overlay_color": "Centrerad modal överläggsfärg",
|
||||
"email_customization": "E-postanpassning",
|
||||
"email_customization_description": "Ändra utseendet på de e-postmeddelanden som Formbricks skickar åt dig.",
|
||||
"enable_custom_styling": "Aktivera anpassad styling",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Formbricks-varumärket är dolt.",
|
||||
"formbricks_branding_settings_description": "Vi uppskattar ditt stöd men förstår om du vill stänga av det.",
|
||||
"formbricks_branding_shown": "Formbricks-varumärket visas.",
|
||||
"generate_theme_btn": "Generera",
|
||||
"generate_theme_confirmation": "Vill du generera ett matchande färgtema baserat på din varumärkesfärg? Detta kommer att skriva över dina nuvarande färginställningar.",
|
||||
"generate_theme_header": "Generera färgtema?",
|
||||
"logo_removed_successfully": "Logotyp borttagen",
|
||||
"logo_settings_description": "Ladda upp företagets logotyp för att profilera enkäter och länkförhandsvisningar.",
|
||||
"logo_updated_successfully": "Logotyp uppdaterad",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "Visa Formbricks-varumärket i {type}-enkäter",
|
||||
"show_powered_by_formbricks": "Visa 'Powered by Formbricks'-signatur",
|
||||
"styling_updated_successfully": "Stiluppdatering lyckades",
|
||||
"suggest_colors": "Föreslå färger",
|
||||
"theme": "Tema",
|
||||
"theme_settings_description": "Skapa ett stilmall för alla undersökningar. Du kan aktivera anpassad stil för varje undersökning."
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "Be respondenter välja en eller flera bilder",
|
||||
"preview_survey_ending_card_description": "Vänligen fortsätt din onboarding.",
|
||||
"preview_survey_ending_card_headline": "Du klarade det!",
|
||||
"preview_survey_input_headline": "Något mer du vill dela med dig av?",
|
||||
"preview_survey_input_placeholder": "Lämna en kommentar...",
|
||||
"preview_survey_name": "Ny enkät",
|
||||
"preview_survey_question_1_headline": "Hur skulle du betygsätta {projectName}?",
|
||||
"preview_survey_question_1_lower_label": "Inte bra",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "Ja, håll mig informerad.",
|
||||
"preview_survey_question_2_choice_2_label": "Nej, tack!",
|
||||
"preview_survey_question_2_headline": "Vill du hållas uppdaterad?",
|
||||
"preview_survey_question_2_subheader": "Det här är ett exempel på en beskrivning.",
|
||||
"preview_survey_welcome_card_headline": "Välkommen!",
|
||||
"prioritize_features_description": "Identifiera vilka funktioner dina användare behöver mest och minst.",
|
||||
"prioritize_features_name": "Prioritera funktioner",
|
||||
|
||||
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "未找到 背景 图片。",
|
||||
"no_code": "无代码",
|
||||
"no_files_uploaded": "没有 文件 被 上传",
|
||||
"no_overlay": "无覆盖层",
|
||||
"no_quotas_found": "未找到配额",
|
||||
"no_result_found": "没有 结果",
|
||||
"no_results": "没有 结果",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "未找到 组织 团队",
|
||||
"other": "其他",
|
||||
"others": "其他",
|
||||
"overlay_color": "覆盖层颜色",
|
||||
"overview": "概览",
|
||||
"password": "密码",
|
||||
"paused": "暂停",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "申请试用许可证",
|
||||
"reset_to_default": "重置为 默认",
|
||||
"response": "响应",
|
||||
"response_id": "响应 ID",
|
||||
"responses": "反馈",
|
||||
"restart": "重新启动",
|
||||
"role": "角色",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "来自你的组织",
|
||||
"invitation_sent_once_more": "再次发送邀请。",
|
||||
"invite_deleted_successfully": "邀请 删除 成功",
|
||||
"invite_expires_on": "邀请将于 {date} 过期",
|
||||
"invited_on": "受邀于 {date}",
|
||||
"invites_failed": "邀请失败",
|
||||
"leave_organization": "离开 组织",
|
||||
"leave_organization_description": "您将离开此组织,并失去对所有调查和响应的访问权限。只有再次被邀请后,您才能重新加入。",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "添加 占位符 显示 如果 没有 值以 回忆",
|
||||
"add_hidden_field_id": "添加 隐藏 字段 ID",
|
||||
"add_highlight_border": "添加 高亮 边框",
|
||||
"add_highlight_border_description": "在 你的 调查 卡片 添加 外 边框。",
|
||||
"add_logic": "添加逻辑",
|
||||
"add_none_of_the_above": "添加 “以上 都 不 是”",
|
||||
"add_option": "添加 选项",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "区块已复制。",
|
||||
"bold": "粗体",
|
||||
"brand_color": "品牌 颜色",
|
||||
"brand_color_description": "应用于按钮、链接和高亮部分。",
|
||||
"brightness": "亮度",
|
||||
"bulk_edit": "批量编辑",
|
||||
"bulk_edit_description": "编辑以下所有选项,每行一个。空行将被跳过,重复项将被移除。",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "捕获 新动作",
|
||||
"card_arrangement_for_survey_type_derived": "{surveyTypeDerived} 调查 的 卡片 布局",
|
||||
"card_background_color": "卡片 的 背景 颜色",
|
||||
"card_background_color_description": "填充调查卡区域。",
|
||||
"card_border_color": "卡片 的 边框 颜色",
|
||||
"card_border_color_description": "勾勒调查卡边框。",
|
||||
"card_styling": "卡片样式",
|
||||
"casual": "休闲",
|
||||
"caution_edit_duplicate": "复制 并 编辑",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "旧 与 新 的 回复 混合 , 这 可能 导致 数据 总结 有误 。",
|
||||
"caution_recommendation": "这 可能 会 导致 调查 统计 数据 的 不一致 。 我们 建议 复制 调查 。",
|
||||
"caution_text": "更改 会导致 不一致",
|
||||
"centered_modal_overlay_color": "居中 模态遮罩层颜色",
|
||||
"change_anyway": "还是更改",
|
||||
"change_background": "更改 背景",
|
||||
"change_question_type": "更改 问题类型",
|
||||
"change_survey_type": "更改 调查 类型 会影 响 现有 访问",
|
||||
"change_the_background_color_of_the_card": "更改 卡片 的 背景 颜色",
|
||||
"change_the_background_color_of_the_input_fields": "更改 输入字段 的 背景颜色",
|
||||
"change_the_background_to_a_color_image_or_animation": "将 背景 更改为 颜色 、 图像 或 动画。",
|
||||
"change_the_border_color_of_the_card": "更改 卡片 的 边框 颜色",
|
||||
"change_the_border_color_of_the_input_fields": "更改 输入字段 的边框颜色。",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "更改 卡片 和 输入 的 边框 半径",
|
||||
"change_the_brand_color_of_the_survey": "更改调查的品牌颜色",
|
||||
"change_the_placement_of_this_survey": "更改 此 调查 的 放置。",
|
||||
"change_the_question_color_of_the_survey": "更改调查的 问题颜色",
|
||||
"changes_saved": "更改 已 保存",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "更改 调查 类型 会影 响 分享 方式 。 如果 受访者 已经 拥有 当前 类型 的 访问 链接 , 在 更改 之后 ,他们 可能 会 失去 访问 权限 。",
|
||||
"checkbox_label": "复选框 标签",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "隐藏 进度 条",
|
||||
"hide_question_settings": "隐藏问题设置",
|
||||
"hostname": "主 机 名",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "在 {surveyTypeDerived} 调查 中,您 想要 卡片 多么 有趣",
|
||||
"if_you_need_more_please": "如果您需要更多,请",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "每次触发时都会显示,直到提交回应为止。",
|
||||
"ignore_global_waiting_time": "忽略冷却期",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "初始 值",
|
||||
"inner_text": "内文",
|
||||
"input_border_color": "输入 边框 颜色",
|
||||
"input_border_color_description": "勾勒文本输入框和多行文本框的边框。",
|
||||
"input_color": "输入颜色",
|
||||
"input_color_description": "填充文本输入框内部。",
|
||||
"insert_link": "插入 链接",
|
||||
"invalid_targeting": "无效的目标: 请检查 您 的受众过滤器",
|
||||
"invalid_video_url_warning": "请输入有效的 YouTube、Vimeo 或 Loom URL 。我们目前不支持其他 视频 托管服务提供商。",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "只有 拥有 PIN 的 用户 可以 访问 调查。",
|
||||
"publish": "发布",
|
||||
"question": "问题",
|
||||
"question_color": "问题颜色",
|
||||
"question_deleted": "问题 已删除",
|
||||
"question_duplicated": "问题重复。",
|
||||
"question_id_updated": "问题 ID 更新",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "响应 限制 、 重定向 和 更多 。",
|
||||
"response_options": "响应 选项",
|
||||
"roundness": "圆度",
|
||||
"roundness_description": "控制卡片角的圆润程度。",
|
||||
"row_used_in_logic_error": "\"这个 行 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
"rows": "行",
|
||||
"save_and_close": "保存 和 关闭",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "样式 设置 为 主题 风格",
|
||||
"subheading": "子标题",
|
||||
"subtract": "减 -",
|
||||
"suggest_colors": "建议颜色",
|
||||
"survey_completed_heading": "调查 完成",
|
||||
"survey_completed_subheading": "此 免费 & 开源 调查 已 关闭",
|
||||
"survey_display_settings": "调查显示设置",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "添加背景色",
|
||||
"add_background_color_description": "为 logo 容器添加背景色。",
|
||||
"advanced_styling_field_border_radius": "边框圆角",
|
||||
"advanced_styling_field_button_bg": "按钮背景",
|
||||
"advanced_styling_field_button_bg_description": "填充“下一步/提交”按钮。",
|
||||
"advanced_styling_field_button_border_radius_description": "设置按钮圆角。",
|
||||
"advanced_styling_field_button_font_size_description": "调整按钮标签文字大小。",
|
||||
"advanced_styling_field_button_font_weight_description": "设置按钮文字的粗细。",
|
||||
"advanced_styling_field_button_height_description": "控制按钮高度。",
|
||||
"advanced_styling_field_button_padding_x_description": "增加左右间距。",
|
||||
"advanced_styling_field_button_padding_y_description": "增加上下间距。",
|
||||
"advanced_styling_field_button_text": "按钮文字",
|
||||
"advanced_styling_field_button_text_description": "设置按钮内标签的颜色。",
|
||||
"advanced_styling_field_description_color": "描述颜色",
|
||||
"advanced_styling_field_description_color_description": "设置每个标题下方文字的颜色。",
|
||||
"advanced_styling_field_description_size": "描述字体大小",
|
||||
"advanced_styling_field_description_size_description": "调整描述文字大小。",
|
||||
"advanced_styling_field_description_weight": "描述字体粗细",
|
||||
"advanced_styling_field_description_weight_description": "设置描述文字的粗细。",
|
||||
"advanced_styling_field_font_size": "字体大小",
|
||||
"advanced_styling_field_font_weight": "字体粗细",
|
||||
"advanced_styling_field_headline_color": "标题颜色",
|
||||
"advanced_styling_field_headline_color_description": "设置主问题文字的颜色。",
|
||||
"advanced_styling_field_headline_size": "标题字体大小",
|
||||
"advanced_styling_field_headline_size_description": "调整主标题文字大小。",
|
||||
"advanced_styling_field_headline_weight": "标题字体粗细",
|
||||
"advanced_styling_field_headline_weight_description": "设置主标题文字的粗细。",
|
||||
"advanced_styling_field_height": "高度",
|
||||
"advanced_styling_field_indicator_bg": "指示器背景",
|
||||
"advanced_styling_field_indicator_bg_description": "设置进度条已填充部分的颜色。",
|
||||
"advanced_styling_field_input_border_radius_description": "设置输入框圆角。",
|
||||
"advanced_styling_field_input_font_size_description": "调整输入框内文字大小。",
|
||||
"advanced_styling_field_input_height_description": "控制输入框高度。",
|
||||
"advanced_styling_field_input_padding_x_description": "增加输入框左右间距。",
|
||||
"advanced_styling_field_input_padding_y_description": "为输入框上下添加间距。",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "调整占位提示文字的透明度。",
|
||||
"advanced_styling_field_input_shadow_description": "为输入框添加投影效果。",
|
||||
"advanced_styling_field_input_text": "输入文字",
|
||||
"advanced_styling_field_input_text_description": "设置输入框内已输入文字的颜色。",
|
||||
"advanced_styling_field_option_bg": "背景色",
|
||||
"advanced_styling_field_option_bg_description": "设置选项项的背景色。",
|
||||
"advanced_styling_field_option_border_radius_description": "设置选项的圆角。",
|
||||
"advanced_styling_field_option_font_size_description": "调整选项标签文字的大小。",
|
||||
"advanced_styling_field_option_label": "标签颜色",
|
||||
"advanced_styling_field_option_label_description": "设置选项标签文字的颜色。",
|
||||
"advanced_styling_field_option_padding_x_description": "为选项左右添加间距。",
|
||||
"advanced_styling_field_option_padding_y_description": "为选项上下添加间距。",
|
||||
"advanced_styling_field_padding_x": "横向内边距",
|
||||
"advanced_styling_field_padding_y": "纵向内边距",
|
||||
"advanced_styling_field_placeholder_opacity": "占位符透明度",
|
||||
"advanced_styling_field_shadow": "阴影",
|
||||
"advanced_styling_field_track_bg": "轨道背景",
|
||||
"advanced_styling_field_track_bg_description": "设置进度条未填充部分的颜色。",
|
||||
"advanced_styling_field_track_height": "轨道高度",
|
||||
"advanced_styling_field_track_height_description": "控制进度条的粗细。",
|
||||
"advanced_styling_field_upper_label_color": "标题标签颜色",
|
||||
"advanced_styling_field_upper_label_color_description": "设置输入框上方小标签的颜色。",
|
||||
"advanced_styling_field_upper_label_size": "标题标签字体大小",
|
||||
"advanced_styling_field_upper_label_size_description": "调整输入框上方小标签的大小。",
|
||||
"advanced_styling_field_upper_label_weight": "标题标签字体粗细",
|
||||
"advanced_styling_field_upper_label_weight_description": "设置标签文字的粗细。",
|
||||
"advanced_styling_section_buttons": "按钮",
|
||||
"advanced_styling_section_headlines": "标题和描述",
|
||||
"advanced_styling_section_inputs": "输入项",
|
||||
"advanced_styling_section_options": "选项(单选/多选)",
|
||||
"app_survey_placement": "应用调查放置位置",
|
||||
"app_survey_placement_settings_description": "更改调查在您的 Web 应用或网站中显示的位置。",
|
||||
"centered_modal_overlay_color": "居中模态遮罩层颜色",
|
||||
"email_customization": "邮件自定义",
|
||||
"email_customization_description": "更改 Formbricks 代表您发送邮件的外观和风格。",
|
||||
"enable_custom_styling": "启用自定义样式",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Formbricks 品牌标识已隐藏。",
|
||||
"formbricks_branding_settings_description": "我们很感谢您的支持,但如果您关闭它,我们也能理解。",
|
||||
"formbricks_branding_shown": "Formbricks 品牌标识已显示。",
|
||||
"generate_theme_btn": "生成",
|
||||
"generate_theme_confirmation": "要根据你的品牌色生成一个匹配的配色主题吗?这将覆盖你当前的颜色设置。",
|
||||
"generate_theme_header": "生成配色主题?",
|
||||
"logo_removed_successfully": "logo 移除成功",
|
||||
"logo_settings_description": "上传您的公司 logo,用于品牌调查和链接预览。",
|
||||
"logo_updated_successfully": "logo 更新成功",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "在 {type} 调查中显示 Formbricks 品牌标识",
|
||||
"show_powered_by_formbricks": "显示“Powered by Formbricks”标识",
|
||||
"styling_updated_successfully": "样式更新成功",
|
||||
"suggest_colors": "推荐颜色",
|
||||
"theme": "主题",
|
||||
"theme_settings_description": "为所有问卷创建一个样式主题。你可以为每个问卷启用自定义样式。"
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "要求 受访者 选择 一个 或 多个 图片",
|
||||
"preview_survey_ending_card_description": "请继续您的 入职培训。",
|
||||
"preview_survey_ending_card_headline": "你 做 到 了 !",
|
||||
"preview_survey_input_headline": "还有其他想分享的吗?",
|
||||
"preview_survey_input_placeholder": "留下备注……",
|
||||
"preview_survey_name": "预览 调查",
|
||||
"preview_survey_question_1_headline": "您 如何 评价 {projectName} ?",
|
||||
"preview_survey_question_1_lower_label": "不 好",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "是 , 保持我 更新 。",
|
||||
"preview_survey_question_2_choice_2_label": "不,谢谢!",
|
||||
"preview_survey_question_2_headline": "想 了解 最新信息吗?",
|
||||
"preview_survey_question_2_subheader": "这是一个示例描述。",
|
||||
"preview_survey_welcome_card_headline": "欢迎!",
|
||||
"prioritize_features_description": "确定 用户 最 需要 和 最 不 需要 的 功能。",
|
||||
"prioritize_features_name": "优先 功能",
|
||||
|
||||
@@ -285,7 +285,6 @@
|
||||
"no_background_image_found": "找不到背景圖片。",
|
||||
"no_code": "無程式碼",
|
||||
"no_files_uploaded": "沒有上傳任何檔案",
|
||||
"no_overlay": "無覆蓋層",
|
||||
"no_quotas_found": "找不到 配額",
|
||||
"no_result_found": "找不到結果",
|
||||
"no_results": "沒有結果",
|
||||
@@ -312,7 +311,6 @@
|
||||
"organization_teams_not_found": "找不到組織團隊",
|
||||
"other": "其他",
|
||||
"others": "其他",
|
||||
"overlay_color": "覆蓋層顏色",
|
||||
"overview": "概覽",
|
||||
"password": "密碼",
|
||||
"paused": "已暫停",
|
||||
@@ -352,7 +350,6 @@
|
||||
"request_trial_license": "請求試用授權",
|
||||
"reset_to_default": "重設為預設值",
|
||||
"response": "回應",
|
||||
"response_id": "回應 ID",
|
||||
"responses": "回應",
|
||||
"restart": "重新開始",
|
||||
"role": "角色",
|
||||
@@ -992,7 +989,7 @@
|
||||
"from_your_organization": "來自您的組織",
|
||||
"invitation_sent_once_more": "已再次發送邀請。",
|
||||
"invite_deleted_successfully": "邀請已成功刪除",
|
||||
"invite_expires_on": "邀請將於 '{'date'}' 過期",
|
||||
"invited_on": "邀請於 '{'date'}'",
|
||||
"invites_failed": "邀請失敗",
|
||||
"leave_organization": "離開組織",
|
||||
"leave_organization_description": "您將離開此組織並失去對所有問卷和回應的存取權限。只有再次收到邀請,您才能重新加入。",
|
||||
@@ -1156,6 +1153,7 @@
|
||||
"add_fallback_placeholder": "新增 預設 以顯示是否沒 有 值 可 回憶 。",
|
||||
"add_hidden_field_id": "新增隱藏欄位 ID",
|
||||
"add_highlight_border": "新增醒目提示邊框",
|
||||
"add_highlight_border_description": "在您的問卷卡片新增外邊框。",
|
||||
"add_logic": "新增邏輯",
|
||||
"add_none_of_the_above": "新增 \"以上皆非\"",
|
||||
"add_option": "新增選項",
|
||||
@@ -1194,7 +1192,6 @@
|
||||
"block_duplicated": "區塊已複製。",
|
||||
"bold": "粗體",
|
||||
"brand_color": "品牌顏色",
|
||||
"brand_color_description": "應用於按鈕、連結和重點標示。",
|
||||
"brightness": "亮度",
|
||||
"bulk_edit": "批次編輯",
|
||||
"bulk_edit_description": "在下方逐行編輯所有選項。空白行將被略過,重複項目將被移除。",
|
||||
@@ -1212,9 +1209,7 @@
|
||||
"capture_new_action": "擷取新操作",
|
||||
"card_arrangement_for_survey_type_derived": "'{'surveyTypeDerived'}' 問卷的卡片排列",
|
||||
"card_background_color": "卡片背景顏色",
|
||||
"card_background_color_description": "填滿問卷卡片區域。",
|
||||
"card_border_color": "卡片邊框顏色",
|
||||
"card_border_color_description": "描繪問卷卡片的邊框。",
|
||||
"card_styling": "卡片樣式",
|
||||
"casual": "隨意",
|
||||
"caution_edit_duplicate": "複製 & 編輯",
|
||||
@@ -1225,12 +1220,20 @@
|
||||
"caution_explanation_responses_are_safe": "較舊和較新的回應會混在一起,可能導致數據摘要失準。",
|
||||
"caution_recommendation": "這可能導致調查摘要中的數據不一致。我們建議複製這個調查。",
|
||||
"caution_text": "變更會導致不一致",
|
||||
"centered_modal_overlay_color": "置中彈窗覆蓋顏色",
|
||||
"change_anyway": "仍然變更",
|
||||
"change_background": "變更背景",
|
||||
"change_question_type": "變更問題類型",
|
||||
"change_survey_type": "切換問卷類型會影響現有訪問",
|
||||
"change_the_background_color_of_the_card": "變更卡片的背景顏色。",
|
||||
"change_the_background_color_of_the_input_fields": "變更輸入欄位的背景顏色。",
|
||||
"change_the_background_to_a_color_image_or_animation": "將背景變更為顏色、圖片或動畫。",
|
||||
"change_the_border_color_of_the_card": "變更卡片的邊框顏色。",
|
||||
"change_the_border_color_of_the_input_fields": "變更輸入欄位的邊框顏色。",
|
||||
"change_the_border_radius_of_the_card_and_the_inputs": "變更卡片和輸入的邊框半徑。",
|
||||
"change_the_brand_color_of_the_survey": "變更問卷的品牌顏色。",
|
||||
"change_the_placement_of_this_survey": "變更此問卷的位置。",
|
||||
"change_the_question_color_of_the_survey": "變更問卷的問題顏色。",
|
||||
"changes_saved": "已儲存變更。",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "更改問卷類型會影響其共享方式。如果受訪者已擁有當前類型的存取連結,則在切換後可能會失去存取權限。",
|
||||
"checkbox_label": "核取方塊標籤",
|
||||
@@ -1370,6 +1373,7 @@
|
||||
"hide_progress_bar": "隱藏進度列",
|
||||
"hide_question_settings": "隱藏問題設定",
|
||||
"hostname": "主機名稱",
|
||||
"how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "您希望 '{'surveyTypeDerived'}' 問卷中的卡片有多酷炫",
|
||||
"if_you_need_more_please": "如果您需要更多,請",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "每次觸發時都顯示,直到提交回應為止。",
|
||||
"ignore_global_waiting_time": "忽略冷卻期",
|
||||
@@ -1380,9 +1384,7 @@
|
||||
"initial_value": "初始值",
|
||||
"inner_text": "內部文字",
|
||||
"input_border_color": "輸入邊框顏色",
|
||||
"input_border_color_description": "描繪文字輸入框和文字區域的邊框。",
|
||||
"input_color": "輸入顏色",
|
||||
"input_color_description": "填滿文字輸入框的內部。",
|
||||
"insert_link": "插入 連結",
|
||||
"invalid_targeting": "目標設定無效:請檢查您的受眾篩選器",
|
||||
"invalid_video_url_warning": "請輸入有效的 YouTube、Vimeo 或 Loom 網址。我們目前不支援其他影片託管提供者。",
|
||||
@@ -1466,6 +1468,7 @@
|
||||
"protect_survey_with_pin_description": "只有擁有 PIN 碼的使用者才能存取問卷。",
|
||||
"publish": "發布",
|
||||
"question": "問題",
|
||||
"question_color": "問題顏色",
|
||||
"question_deleted": "問題已刪除。",
|
||||
"question_duplicated": "問題已複製。",
|
||||
"question_id_updated": "問題 ID 已更新",
|
||||
@@ -1527,7 +1530,6 @@
|
||||
"response_limits_redirections_and_more": "回應限制、重新導向等。",
|
||||
"response_options": "回應選項",
|
||||
"roundness": "圓角",
|
||||
"roundness_description": "調整卡片邊角的圓弧度。",
|
||||
"row_used_in_logic_error": "此 row 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"rows": "列",
|
||||
"save_and_close": "儲存並關閉",
|
||||
@@ -1569,6 +1571,7 @@
|
||||
"styling_set_to_theme_styles": "樣式設定為主題樣式",
|
||||
"subheading": "副標題",
|
||||
"subtract": "減 -",
|
||||
"suggest_colors": "建議顏色",
|
||||
"survey_completed_heading": "問卷已完成",
|
||||
"survey_completed_subheading": "此免費且開源的問卷已關閉",
|
||||
"survey_display_settings": "問卷顯示設定",
|
||||
@@ -2052,71 +2055,9 @@
|
||||
"look": {
|
||||
"add_background_color": "新增背景顏色",
|
||||
"add_background_color_description": "為標誌容器新增背景顏色。",
|
||||
"advanced_styling_field_border_radius": "邊框圓角",
|
||||
"advanced_styling_field_button_bg": "按鈕背景",
|
||||
"advanced_styling_field_button_bg_description": "填滿「下一步」/「送出」按鈕。",
|
||||
"advanced_styling_field_button_border_radius_description": "調整按鈕的圓角。",
|
||||
"advanced_styling_field_button_font_size_description": "調整按鈕標籤文字的大小。",
|
||||
"advanced_styling_field_button_font_weight_description": "讓按鈕文字變細或變粗。",
|
||||
"advanced_styling_field_button_height_description": "調整按鈕的高度。",
|
||||
"advanced_styling_field_button_padding_x_description": "在左右兩側增加間距。",
|
||||
"advanced_styling_field_button_padding_y_description": "在上下兩側增加間距。",
|
||||
"advanced_styling_field_button_text": "按鈕文字",
|
||||
"advanced_styling_field_button_text_description": "設定按鈕內標籤的顏色。",
|
||||
"advanced_styling_field_description_color": "說明文字顏色",
|
||||
"advanced_styling_field_description_color_description": "設定每個標題下方文字的顏色。",
|
||||
"advanced_styling_field_description_size": "說明文字大小",
|
||||
"advanced_styling_field_description_size_description": "調整說明文字的大小。",
|
||||
"advanced_styling_field_description_weight": "說明文字粗細",
|
||||
"advanced_styling_field_description_weight_description": "讓說明文字變細或變粗。",
|
||||
"advanced_styling_field_font_size": "字體大小",
|
||||
"advanced_styling_field_font_weight": "字體粗細",
|
||||
"advanced_styling_field_headline_color": "標題顏色",
|
||||
"advanced_styling_field_headline_color_description": "設定主要問題文字的顏色。",
|
||||
"advanced_styling_field_headline_size": "標題字體大小",
|
||||
"advanced_styling_field_headline_size_description": "調整標題文字的大小。",
|
||||
"advanced_styling_field_headline_weight": "標題字體粗細",
|
||||
"advanced_styling_field_headline_weight_description": "讓標題文字變細或變粗。",
|
||||
"advanced_styling_field_height": "高度",
|
||||
"advanced_styling_field_indicator_bg": "指示器背景",
|
||||
"advanced_styling_field_indicator_bg_description": "設定進度條已填滿部分的顏色。",
|
||||
"advanced_styling_field_input_border_radius_description": "調整輸入框的圓角。",
|
||||
"advanced_styling_field_input_font_size_description": "調整輸入框內輸入文字的大小。",
|
||||
"advanced_styling_field_input_height_description": "調整輸入欄位的高度。",
|
||||
"advanced_styling_field_input_padding_x_description": "在左右兩側增加間距。",
|
||||
"advanced_styling_field_input_padding_y_description": "在上方和下方增加間距。",
|
||||
"advanced_styling_field_input_placeholder_opacity_description": "讓提示文字變得更淡。",
|
||||
"advanced_styling_field_input_shadow_description": "在輸入框周圍加上陰影。",
|
||||
"advanced_styling_field_input_text": "輸入文字",
|
||||
"advanced_styling_field_input_text_description": "設定輸入文字的顏色。",
|
||||
"advanced_styling_field_option_bg": "背景",
|
||||
"advanced_styling_field_option_bg_description": "填滿選項項目背景。",
|
||||
"advanced_styling_field_option_border_radius_description": "讓選項的邊角變圓。",
|
||||
"advanced_styling_field_option_font_size_description": "調整選項標籤文字的大小。",
|
||||
"advanced_styling_field_option_label": "標籤顏色",
|
||||
"advanced_styling_field_option_label_description": "設定選項標籤文字的顏色。",
|
||||
"advanced_styling_field_option_padding_x_description": "在左側和右側增加間距。",
|
||||
"advanced_styling_field_option_padding_y_description": "在上方和下方增加間距。",
|
||||
"advanced_styling_field_padding_x": "左右內距",
|
||||
"advanced_styling_field_padding_y": "上下內距",
|
||||
"advanced_styling_field_placeholder_opacity": "預設文字透明度",
|
||||
"advanced_styling_field_shadow": "陰影",
|
||||
"advanced_styling_field_track_bg": "軌道背景",
|
||||
"advanced_styling_field_track_bg_description": "設定進度條未填滿部分的顏色。",
|
||||
"advanced_styling_field_track_height": "軌道高度",
|
||||
"advanced_styling_field_track_height_description": "調整進度條的厚度。",
|
||||
"advanced_styling_field_upper_label_color": "標題標籤顏色",
|
||||
"advanced_styling_field_upper_label_color_description": "設定輸入框上方小標籤的顏色。",
|
||||
"advanced_styling_field_upper_label_size": "標題標籤字體大小",
|
||||
"advanced_styling_field_upper_label_size_description": "調整輸入框上方小標籤的大小。",
|
||||
"advanced_styling_field_upper_label_weight": "標題標籤字體粗細",
|
||||
"advanced_styling_field_upper_label_weight_description": "讓標籤字體變細或變粗。",
|
||||
"advanced_styling_section_buttons": "按鈕",
|
||||
"advanced_styling_section_headlines": "標題與說明",
|
||||
"advanced_styling_section_inputs": "輸入欄位",
|
||||
"advanced_styling_section_options": "選項(單選/複選)",
|
||||
"app_survey_placement": "應用程式問卷位置",
|
||||
"app_survey_placement_settings_description": "變更問卷在您的網頁應用程式或網站中顯示的位置。",
|
||||
"centered_modal_overlay_color": "置中彈窗覆蓋顏色",
|
||||
"email_customization": "電子郵件自訂化",
|
||||
"email_customization_description": "變更 Formbricks 代表您發送的電子郵件外觀與風格。",
|
||||
"enable_custom_styling": "啟用自訂樣式",
|
||||
@@ -2127,9 +2068,6 @@
|
||||
"formbricks_branding_hidden": "Formbricks 品牌標示已隱藏。",
|
||||
"formbricks_branding_settings_description": "我們很感謝您的支持,但若您選擇關閉我們也能理解。",
|
||||
"formbricks_branding_shown": "Formbricks 品牌標示已顯示。",
|
||||
"generate_theme_btn": "產生",
|
||||
"generate_theme_confirmation": "你想根據品牌色產生一組相符的主題色嗎?這將會覆蓋你目前的顏色設定。",
|
||||
"generate_theme_header": "要產生主題色嗎?",
|
||||
"logo_removed_successfully": "標誌已成功移除",
|
||||
"logo_settings_description": "上傳您的公司標誌,以用於問卷和連結預覽的品牌展示。",
|
||||
"logo_updated_successfully": "標誌已成功更新",
|
||||
@@ -2144,7 +2082,6 @@
|
||||
"show_formbricks_branding_in": "在 {type} 問卷中顯示 Formbricks 品牌標示",
|
||||
"show_powered_by_formbricks": "顯示「Powered by Formbricks」標記",
|
||||
"styling_updated_successfully": "樣式已成功更新",
|
||||
"suggest_colors": "建議顏色",
|
||||
"theme": "主題",
|
||||
"theme_settings_description": "為所有調查建立樣式主題。您可以為每個調查啟用自訂樣式。"
|
||||
},
|
||||
@@ -2899,8 +2836,6 @@
|
||||
"picture_selection_description": "要求回應者選擇一張或多張圖片",
|
||||
"preview_survey_ending_card_description": "請繼續您的新手上路程序。",
|
||||
"preview_survey_ending_card_headline": "您完成了!",
|
||||
"preview_survey_input_headline": "還有其他想分享的嗎?",
|
||||
"preview_survey_input_placeholder": "留下備註⋯⋯",
|
||||
"preview_survey_name": "新問卷",
|
||||
"preview_survey_question_1_headline": "您對 '{'projectName'}' 的評分如何?",
|
||||
"preview_survey_question_1_lower_label": "不好",
|
||||
@@ -2910,7 +2845,6 @@
|
||||
"preview_survey_question_2_choice_1_label": "是,請保持通知我。",
|
||||
"preview_survey_question_2_choice_2_label": "不用了,謝謝!",
|
||||
"preview_survey_question_2_headline": "想要緊跟最新動態嗎?",
|
||||
"preview_survey_question_2_subheader": "這是一個範例說明。",
|
||||
"preview_survey_welcome_card_headline": "歡迎!",
|
||||
"prioritize_features_description": "找出您的使用者最需要和最不需要的功能。",
|
||||
"prioritize_features_name": "優先排序功能",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { responses } from "@/modules/api/v2/lib/response";
|
||||
@@ -16,6 +15,7 @@ import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[respo
|
||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
import { formatValidationErrorsForApi, validateResponseData } from "../lib/validation";
|
||||
import { ZResponseIdSchema, ZResponseUpdateSchema } from "./types/responses";
|
||||
|
||||
export const GET = async (request: Request, props: { params: Promise<{ responseId: string }> }) =>
|
||||
@@ -198,7 +198,6 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
|
||||
questionsResponse.data.blocks,
|
||||
body.data,
|
||||
body.language ?? "en",
|
||||
body.finished,
|
||||
questionsResponse.data.questions
|
||||
);
|
||||
|
||||
@@ -207,7 +206,7 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: formatValidationErrorsForV2Api(validationErrors),
|
||||
details: formatValidationErrorsForApi(validationErrors),
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
|
||||
+11
-32
@@ -5,10 +5,10 @@ import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TValidationErrorMap } from "@formbricks/types/surveys/validation-rules";
|
||||
import {
|
||||
formatValidationErrorsForApi,
|
||||
formatValidationErrorsForV1Api,
|
||||
formatValidationErrorsForV2Api,
|
||||
validateResponseData,
|
||||
} from "@/modules/api/lib/validation";
|
||||
} from "./validation";
|
||||
|
||||
const mockTransformQuestionsToBlocks = vi.fn();
|
||||
const mockGetElementsFromBlocks = vi.fn();
|
||||
@@ -95,7 +95,7 @@ describe("validateResponseData", () => {
|
||||
mockGetElementsFromBlocks.mockReturnValue(mockElements);
|
||||
mockValidateBlockResponses.mockReturnValue({});
|
||||
|
||||
validateResponseData([], mockResponseData, "en", true, mockQuestions);
|
||||
validateResponseData([], mockResponseData, "en", mockQuestions);
|
||||
|
||||
expect(mockTransformQuestionsToBlocks).toHaveBeenCalledWith(mockQuestions, []);
|
||||
expect(mockGetElementsFromBlocks).toHaveBeenCalledWith(transformedBlocks);
|
||||
@@ -105,15 +105,15 @@ describe("validateResponseData", () => {
|
||||
mockGetElementsFromBlocks.mockReturnValue(mockElements);
|
||||
mockValidateBlockResponses.mockReturnValue({});
|
||||
|
||||
validateResponseData(mockBlocks, mockResponseData, "en", true, mockQuestions);
|
||||
validateResponseData(mockBlocks, mockResponseData, "en", mockQuestions);
|
||||
|
||||
expect(mockTransformQuestionsToBlocks).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should return null when both blocks and questions are empty", () => {
|
||||
expect(validateResponseData([], mockResponseData, "en", true, [])).toBeNull();
|
||||
expect(validateResponseData(null, mockResponseData, "en", true, [])).toBeNull();
|
||||
expect(validateResponseData(undefined, mockResponseData, "en", true, null)).toBeNull();
|
||||
expect(validateResponseData([], mockResponseData, "en", [])).toBeNull();
|
||||
expect(validateResponseData(null, mockResponseData, "en", [])).toBeNull();
|
||||
expect(validateResponseData(undefined, mockResponseData, "en", null)).toBeNull();
|
||||
});
|
||||
|
||||
test("should use default language code", () => {
|
||||
@@ -124,36 +124,15 @@ describe("validateResponseData", () => {
|
||||
|
||||
expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, mockResponseData, "en");
|
||||
});
|
||||
|
||||
test("should validate only present fields when finished is false", () => {
|
||||
const partialResponseData: TResponseData = { element1: "test" };
|
||||
const partialElements = [mockElements[0]];
|
||||
mockGetElementsFromBlocks.mockReturnValue(mockElements);
|
||||
mockValidateBlockResponses.mockReturnValue({});
|
||||
|
||||
validateResponseData(mockBlocks, partialResponseData, "en", false);
|
||||
|
||||
expect(mockValidateBlockResponses).toHaveBeenCalledWith(partialElements, partialResponseData, "en");
|
||||
});
|
||||
|
||||
test("should validate all fields when finished is true", () => {
|
||||
const partialResponseData: TResponseData = { element1: "test" };
|
||||
mockGetElementsFromBlocks.mockReturnValue(mockElements);
|
||||
mockValidateBlockResponses.mockReturnValue({});
|
||||
|
||||
validateResponseData(mockBlocks, partialResponseData, "en", true);
|
||||
|
||||
expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, partialResponseData, "en");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatValidationErrorsForV2Api", () => {
|
||||
describe("formatValidationErrorsForApi", () => {
|
||||
test("should convert error map to V2 API format", () => {
|
||||
const errorMap: TValidationErrorMap = {
|
||||
element1: [{ ruleId: "minLength", ruleType: "minLength", message: "Min length required" }],
|
||||
};
|
||||
|
||||
const result = formatValidationErrorsForV2Api(errorMap);
|
||||
const result = formatValidationErrorsForApi(errorMap);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
@@ -172,7 +151,7 @@ describe("formatValidationErrorsForV2Api", () => {
|
||||
],
|
||||
};
|
||||
|
||||
const result = formatValidationErrorsForV2Api(errorMap);
|
||||
const result = formatValidationErrorsForApi(errorMap);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].field).toBe("response.data.element1");
|
||||
@@ -185,7 +164,7 @@ describe("formatValidationErrorsForV2Api", () => {
|
||||
element2: [{ ruleId: "maxLength", ruleType: "maxLength", message: "Max length" }],
|
||||
};
|
||||
|
||||
const result = formatValidationErrorsForV2Api(errorMap);
|
||||
const result = formatValidationErrorsForApi(errorMap);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].field).toBe("response.data.element1");
|
||||
+7
-16
@@ -10,20 +10,17 @@ import { ApiErrorDetails } from "@/modules/api/v2/types/api-error";
|
||||
|
||||
/**
|
||||
* Validates response data against survey validation rules
|
||||
* Handles partial responses (in-progress) by only validating present fields when finished is false
|
||||
*
|
||||
* @param blocks - Survey blocks containing elements with validation rules (preferred)
|
||||
* @param questions - Survey questions (legacy format, used as fallback if blocks are empty)
|
||||
* @param responseData - Response data to validate (keyed by element ID)
|
||||
* @param languageCode - Language code for error messages (defaults to "en")
|
||||
* @param finished - Whether the response is finished (defaults to true for management APIs)
|
||||
* @param questions - Survey questions (legacy format, used as fallback if blocks are empty)
|
||||
* @returns Validation error map keyed by element ID, or null if validation passes
|
||||
*/
|
||||
export const validateResponseData = (
|
||||
blocks: TSurveyBlock[] | undefined | null,
|
||||
responseData: TResponseData,
|
||||
languageCode: string = "en",
|
||||
finished: boolean = true,
|
||||
questions?: TSurveyQuestion[] | undefined | null
|
||||
): TValidationErrorMap | null => {
|
||||
// Use blocks if available, otherwise transform questions to blocks
|
||||
@@ -40,28 +37,22 @@ export const validateResponseData = (
|
||||
}
|
||||
|
||||
// Extract elements from blocks
|
||||
const allElements = getElementsFromBlocks(blocksToUse);
|
||||
const elements = getElementsFromBlocks(blocksToUse);
|
||||
|
||||
// If response is not finished, only validate elements that are present in the response data
|
||||
// This prevents "required" errors for fields the user hasn't reached yet
|
||||
const elementsToValidate = finished
|
||||
? allElements
|
||||
: allElements.filter((element) => Object.keys(responseData).includes(element.id));
|
||||
|
||||
// Validate selected elements
|
||||
const errorMap = validateBlockResponses(elementsToValidate, responseData, languageCode);
|
||||
// Validate all elements
|
||||
const errorMap = validateBlockResponses(elements, responseData, languageCode);
|
||||
|
||||
// Return null if no errors (validation passed), otherwise return error map
|
||||
return Object.keys(errorMap).length === 0 ? null : errorMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts validation error map to V2 API error response format
|
||||
* Converts validation error map to API error response format (V2)
|
||||
*
|
||||
* @param errorMap - Validation error map from validateResponseData
|
||||
* @returns V2 API error response details
|
||||
* @returns API error response details
|
||||
*/
|
||||
export const formatValidationErrorsForV2Api = (errorMap: TValidationErrorMap) => {
|
||||
export const formatValidationErrorsForApi = (errorMap: TValidationErrorMap) => {
|
||||
const details: ApiErrorDetails = [];
|
||||
|
||||
for (const [elementId, errors] of Object.entries(errorMap)) {
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Response } from "@prisma/client";
|
||||
import { NextRequest } from "next/server";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { responses } from "@/modules/api/v2/lib/response";
|
||||
@@ -14,6 +13,7 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
import { createResponseWithQuotaEvaluation, getResponses } from "./lib/response";
|
||||
import { formatValidationErrorsForApi, validateResponseData } from "./lib/validation";
|
||||
|
||||
export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
@@ -134,7 +134,6 @@ export const POST = async (request: Request) =>
|
||||
surveyQuestions.data.blocks,
|
||||
body.data,
|
||||
body.language ?? "en",
|
||||
body.finished,
|
||||
surveyQuestions.data.questions
|
||||
);
|
||||
|
||||
@@ -143,7 +142,7 @@ export const POST = async (request: Request) =>
|
||||
request,
|
||||
{
|
||||
type: "bad_request",
|
||||
details: formatValidationErrorsForV2Api(validationErrors),
|
||||
details: formatValidationErrorsForApi(validationErrors),
|
||||
},
|
||||
auditLog
|
||||
);
|
||||
|
||||
@@ -109,13 +109,7 @@ export function SegmentSettings({
|
||||
const handleDeleteSegment = async () => {
|
||||
try {
|
||||
setIsDeletingSegment(true);
|
||||
const result = await deleteSegmentAction({ segmentId: segment.id });
|
||||
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
setIsDeletingSegment(false);
|
||||
return;
|
||||
}
|
||||
await deleteSegmentAction({ segmentId: segment.id });
|
||||
|
||||
setIsDeletingSegment(false);
|
||||
toast.success(t("environments.segments.segment_deleted_successfully"));
|
||||
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import {
|
||||
cloneSegmentAction,
|
||||
createSegmentAction,
|
||||
@@ -136,11 +135,7 @@ export function TargetingCard({
|
||||
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
|
||||
try {
|
||||
if (!segment) throw new Error(t("environments.segments.invalid_segment"));
|
||||
const result = await updateSegmentAction({ segmentId: segment.id, environmentId, data });
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await updateSegmentAction({ segmentId: segment.id, environmentId, data });
|
||||
toast.success(t("environments.segments.segment_saved_successfully"));
|
||||
|
||||
setIsSegmentEditorOpen(false);
|
||||
|
||||
@@ -154,12 +154,7 @@ export function EditLanguage({
|
||||
|
||||
const performLanguageDeletion = async (languageId: string) => {
|
||||
try {
|
||||
const result = await deleteLanguageAction({ languageId, projectId: project.id });
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
setConfirmationModal((prev) => ({ ...prev, isOpen: false }));
|
||||
return;
|
||||
}
|
||||
await deleteLanguageAction({ languageId, projectId: project.id });
|
||||
setLanguages((prev) => prev.filter((lang) => lang.id !== languageId));
|
||||
toast.success(t("environments.workspace.languages.language_deleted_successfully"));
|
||||
// Close the modal after deletion
|
||||
@@ -192,7 +187,7 @@ export function EditLanguage({
|
||||
|
||||
const handleSaveChanges = async () => {
|
||||
if (!validateLanguages(languages, t)) return;
|
||||
const results = await Promise.all(
|
||||
await Promise.all(
|
||||
languages.map((lang) => {
|
||||
return lang.id === "new"
|
||||
? createLanguageAction({
|
||||
@@ -206,11 +201,6 @@ export function EditLanguage({
|
||||
});
|
||||
})
|
||||
);
|
||||
const errorResult = results.find((result) => result?.serverError);
|
||||
if (errorResult) {
|
||||
toast.error(getFormattedErrorMessage(errorResult));
|
||||
return;
|
||||
}
|
||||
toast.success(t("environments.workspace.languages.languages_updated_successfully"));
|
||||
router.refresh();
|
||||
setIsEditing(false);
|
||||
@@ -249,7 +239,7 @@ export function EditLanguage({
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm italic text-slate-500">
|
||||
<p className="text-sm text-slate-500 italic">
|
||||
{t("environments.workspace.languages.no_language_found")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useTransition } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TI18nString } from "@formbricks/types/i18n";
|
||||
import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
@@ -74,8 +74,6 @@ export function LocalizedEditor({
|
||||
[id, isInvalid, localSurvey.languages, value]
|
||||
);
|
||||
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Editor
|
||||
@@ -111,45 +109,44 @@ export function LocalizedEditor({
|
||||
sanitizedContent = v.replaceAll(/<a[^>]*>(.*?)<\/a>/gi, "$1");
|
||||
}
|
||||
|
||||
// Check if the elements still exists before updating
|
||||
const currentElement = elements[elementIdx];
|
||||
|
||||
startTransition(() => {
|
||||
// if this is a card, we wanna check if the card exists in the localSurvey
|
||||
if (isCard) {
|
||||
const isWelcomeCard = elementIdx === -1;
|
||||
const isEndingCard = elementIdx >= elements.length;
|
||||
// if this is a card, we wanna check if the card exists in the localSurvey
|
||||
if (isCard) {
|
||||
const isWelcomeCard = elementIdx === -1;
|
||||
const isEndingCard = elementIdx >= elements.length;
|
||||
|
||||
// For ending cards, check if the field exists before updating
|
||||
if (isEndingCard) {
|
||||
const ending = localSurvey.endings.find((ending) => ending.id === elementId);
|
||||
// If the field doesn't exist on the ending card, don't create it
|
||||
if (!ending || ending[id] === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For welcome cards, check if it exists
|
||||
if (isWelcomeCard && !localSurvey.welcomeCard) {
|
||||
// For ending cards, check if the field exists before updating
|
||||
if (isEndingCard) {
|
||||
const ending = localSurvey.endings.find((ending) => ending.id === elementId);
|
||||
// If the field doesn't exist on the ending card, don't create it
|
||||
if (!ending || ending[id] === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement({ [id]: translatedContent });
|
||||
// For welcome cards, check if it exists
|
||||
if (isWelcomeCard && !localSurvey.welcomeCard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the field exists on the element (not just if it's not undefined)
|
||||
if (currentElement && id in currentElement && currentElement[id] !== undefined) {
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement(elementIdx, { [id]: translatedContent });
|
||||
}
|
||||
});
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement({ [id]: translatedContent });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the field exists on the element (not just if it's not undefined)
|
||||
if (currentElement && id in currentElement && currentElement[id] !== undefined) {
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement(elementIdx, { [id]: translatedContent });
|
||||
}
|
||||
}}
|
||||
localSurvey={localSurvey}
|
||||
elementId={elementId}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { deleteTeamAction } from "@/modules/ee/teams/team-list/actions";
|
||||
import { TTeam } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -28,12 +27,6 @@ export const DeleteTeam = ({ teamId, onDelete, isOwnerOrManager }: DeleteTeamPro
|
||||
setIsDeleting(true);
|
||||
|
||||
const deleteTeamActionResponse = await deleteTeamAction({ teamId });
|
||||
if (deleteTeamActionResponse?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(deleteTeamActionResponse));
|
||||
setIsDeleteDialogOpen(false);
|
||||
setIsDeleting(false);
|
||||
return;
|
||||
}
|
||||
if (deleteTeamActionResponse?.data) {
|
||||
toast.success(t("environments.settings.teams.team_deleted_successfully"));
|
||||
onDelete?.();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId, ZStorageUrl } from "@formbricks/types/common";
|
||||
import { ZId, ZUrl } from "@formbricks/types/common";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
@@ -11,7 +11,7 @@ import { updateOrganizationFaviconUrl } from "@/modules/ee/whitelabel/favicon-cu
|
||||
|
||||
const ZUpdateOrganizationFaviconUrlAction = z.object({
|
||||
organizationId: ZId,
|
||||
faviconUrl: ZStorageUrl,
|
||||
faviconUrl: ZUrl,
|
||||
});
|
||||
|
||||
export const updateOrganizationFaviconUrlAction = authenticatedActionClient
|
||||
|
||||
@@ -2,7 +2,7 @@ import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||
import { ZId, ZStorageUrl } from "@formbricks/types/common";
|
||||
import { ZId, ZUrl } from "@formbricks/types/common";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TOrganizationWhitelabel } from "@formbricks/types/organizations";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
@@ -11,7 +11,7 @@ export const updateOrganizationFaviconUrl = async (
|
||||
organizationId: string,
|
||||
faviconUrl: string | null
|
||||
): Promise<boolean> => {
|
||||
validateInputs([organizationId, ZId], [faviconUrl, ZStorageUrl.nullable()]);
|
||||
validateInputs([organizationId, ZId], [faviconUrl, ZUrl.nullable()]);
|
||||
|
||||
try {
|
||||
const organization = await prisma.organization.findUnique({
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("updateProjectBranding", () => {
|
||||
},
|
||||
placement: "bottomRight" as const,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [{ id: "test-env-id" }],
|
||||
languages: [],
|
||||
logo: null,
|
||||
|
||||
@@ -24,7 +24,6 @@ import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { isLight, mixColor } from "@/lib/utils/colors";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { RatingSmiley } from "@/modules/analysis/components/RatingSmiley";
|
||||
import { resolveStorageUrl } from "@/modules/storage/utils";
|
||||
import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils";
|
||||
|
||||
interface PreviewEmailTemplateProps {
|
||||
@@ -184,7 +183,7 @@ export async function PreviewEmailTemplate({
|
||||
{ctaElement.buttonExternal && ctaElement.ctaButtonLabel && ctaElement.buttonUrl && (
|
||||
<Container className="mx-0 mt-4 flex max-w-none items-center justify-end">
|
||||
<EmailButton
|
||||
className="text-question-color flex items-center rounded-md border-0 bg-transparent px-3 py-3 text-base font-medium leading-4 no-underline shadow-none"
|
||||
className="text-question-color flex items-center rounded-md border-0 bg-transparent px-3 py-3 text-base leading-4 font-medium no-underline shadow-none"
|
||||
href={ctaElement.buttonUrl}>
|
||||
<Text className="inline">
|
||||
{getLocalizedValue(ctaElement.ctaButtonLabel, defaultLanguageCode)}{" "}
|
||||
@@ -307,17 +306,17 @@ export async function PreviewEmailTemplate({
|
||||
{firstQuestion.choices.map((choice) =>
|
||||
firstQuestion.allowMulti ? (
|
||||
<Img
|
||||
className="rounded-custom mb-3 mr-3 inline-block h-[150px] w-[250px]"
|
||||
className="rounded-custom mr-3 mb-3 inline-block h-[150px] w-[250px]"
|
||||
key={choice.id}
|
||||
src={resolveStorageUrl(choice.imageUrl)}
|
||||
src={choice.imageUrl}
|
||||
/>
|
||||
) : (
|
||||
<Link
|
||||
className="rounded-custom mb-3 mr-3 inline-block h-[150px] w-[250px]"
|
||||
className="rounded-custom mr-3 mb-3 inline-block h-[150px] w-[250px]"
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${choice.id}`}
|
||||
key={choice.id}
|
||||
target="_blank">
|
||||
<Img className="rounded-custom h-full w-full" src={resolveStorageUrl(choice.imageUrl)} />
|
||||
<Img className="rounded-custom h-full w-full" src={choice.imageUrl} />
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
@@ -361,11 +360,11 @@ export async function PreviewEmailTemplate({
|
||||
<Container className="mx-0">
|
||||
<Section className="w-full table-auto">
|
||||
<Row>
|
||||
<Column className="w-40 break-words px-4 py-2" />
|
||||
<Column className="w-40 px-4 py-2 break-words" />
|
||||
{firstQuestion.columns.map((column) => {
|
||||
return (
|
||||
<Column
|
||||
className="text-question-color max-w-40 break-words px-4 py-2 text-center"
|
||||
className="text-question-color max-w-40 px-4 py-2 text-center break-words"
|
||||
key={column.id}>
|
||||
{getLocalizedValue(column.label, "default")}
|
||||
</Column>
|
||||
@@ -377,7 +376,7 @@ export async function PreviewEmailTemplate({
|
||||
<Row
|
||||
className={`${rowIndex % 2 === 0 ? "bg-input-color" : ""} rounded-custom`}
|
||||
key={row.id}>
|
||||
<Column className="w-40 break-words px-4 py-2">
|
||||
<Column className="w-40 px-4 py-2 break-words">
|
||||
{getLocalizedValue(row.label, "default")}
|
||||
</Column>
|
||||
{firstQuestion.columns.map((column) => {
|
||||
|
||||
@@ -17,7 +17,6 @@ import { logger } from "@formbricks/logger";
|
||||
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import type { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserEmail, TUserLocale } from "@formbricks/types/user";
|
||||
import {
|
||||
@@ -42,7 +41,6 @@ import { createEmailChangeToken, createInviteToken, createToken, createTokenForL
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { resolveStorageUrl } from "@/modules/storage/utils";
|
||||
|
||||
export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT);
|
||||
|
||||
@@ -243,22 +241,6 @@ export const sendResponseFinishedEmail = async (
|
||||
// Pre-process the element response mapping before passing to email
|
||||
const elements = getElementResponseMapping(survey, response);
|
||||
|
||||
// Resolve relative storage URLs to absolute URLs for email rendering
|
||||
const elementsWithResolvedUrls = elements.map((element) => {
|
||||
if (
|
||||
(element.type === TSurveyElementTypeEnum.PictureSelection ||
|
||||
element.type === TSurveyElementTypeEnum.FileUpload) &&
|
||||
Array.isArray(element.response)
|
||||
) {
|
||||
return {
|
||||
...element,
|
||||
response: element.response.map((url) => resolveStorageUrl(url)),
|
||||
};
|
||||
}
|
||||
|
||||
return element;
|
||||
});
|
||||
|
||||
const html = await renderResponseFinishedEmail({
|
||||
survey,
|
||||
responseCount,
|
||||
@@ -266,7 +248,7 @@ export const sendResponseFinishedEmail = async (
|
||||
WEBAPP_URL,
|
||||
environmentId,
|
||||
organization,
|
||||
elements: elementsWithResolvedUrls,
|
||||
elements,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
@@ -294,12 +276,10 @@ export const sendEmbedSurveyPreviewEmail = async (
|
||||
logoUrl?: string
|
||||
): Promise<boolean> => {
|
||||
const t = await getTranslate(locale);
|
||||
// Resolve relative storage URLs to absolute URLs for email rendering
|
||||
const resolvedLogoUrl = logoUrl ? resolveStorageUrl(logoUrl) : undefined;
|
||||
const html = await renderEmbedSurveyPreviewEmail({
|
||||
html: innerHtml,
|
||||
environmentId,
|
||||
logoUrl: resolvedLogoUrl,
|
||||
logoUrl,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
@@ -317,11 +297,9 @@ export const sendEmailCustomizationPreviewEmail = async (
|
||||
logoUrl?: string
|
||||
): Promise<boolean> => {
|
||||
const t = await getTranslate(locale);
|
||||
// Resolve relative storage URLs to absolute URLs for email rendering
|
||||
const resolvedLogoUrl = logoUrl ? resolveStorageUrl(logoUrl) : undefined;
|
||||
const emailHtmlBody = await renderEmailCustomizationPreviewEmail({
|
||||
userName,
|
||||
logoUrl: resolvedLogoUrl,
|
||||
logoUrl,
|
||||
t,
|
||||
...legalProps,
|
||||
});
|
||||
@@ -338,8 +316,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
|
||||
const email = data.email;
|
||||
const surveyName = data.surveyName;
|
||||
const singleUseId = data.suId;
|
||||
// Resolve relative storage URLs to absolute URLs for email rendering
|
||||
const logoUrl = data.logoUrl ? resolveStorageUrl(data.logoUrl) : "";
|
||||
const logoUrl = data.logoUrl || "";
|
||||
const token = createTokenForLinkSurvey(surveyId, email);
|
||||
const t = await getTranslate(data.locale);
|
||||
const getSurveyLink = (): string => {
|
||||
|
||||
@@ -241,7 +241,7 @@ describe("utils.ts", () => {
|
||||
config: {},
|
||||
placement: "bottomRight" as const,
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
styling: {},
|
||||
logo: null,
|
||||
environments: [
|
||||
@@ -389,7 +389,7 @@ describe("utils.ts", () => {
|
||||
config: {},
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
styling: {},
|
||||
logo: null,
|
||||
environments: [
|
||||
@@ -502,7 +502,7 @@ describe("utils.ts", () => {
|
||||
config: {},
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
styling: {},
|
||||
logo: null,
|
||||
environments: [],
|
||||
@@ -588,7 +588,7 @@ describe("utils.ts", () => {
|
||||
config: {},
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
styling: {},
|
||||
logo: null,
|
||||
environments: [],
|
||||
@@ -627,7 +627,7 @@ describe("utils.ts", () => {
|
||||
config: {},
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: true,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
styling: {},
|
||||
logo: null,
|
||||
environments: [],
|
||||
|
||||
@@ -150,7 +150,7 @@ export const getEnvironmentWithRelations = reactCache(async (environmentId: stri
|
||||
config: true,
|
||||
placement: true,
|
||||
clickOutsideClose: true,
|
||||
overlay: true,
|
||||
darkOverlay: true,
|
||||
styling: true,
|
||||
logo: true,
|
||||
customHeadScripts: true,
|
||||
@@ -220,7 +220,7 @@ export const getEnvironmentWithRelations = reactCache(async (environmentId: stri
|
||||
config: data.project.config,
|
||||
placement: data.project.placement,
|
||||
clickOutsideClose: data.project.clickOutsideClose,
|
||||
overlay: data.project.overlay,
|
||||
darkOverlay: data.project.darkOverlay,
|
||||
styling: data.project.styling,
|
||||
logo: data.project.logo,
|
||||
customHeadScripts: data.project.customHeadScripts,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { OrganizationRole } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId, ZUuid } from "@formbricks/types/common";
|
||||
import { AuthenticationError, OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
|
||||
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
|
||||
@@ -24,7 +23,7 @@ import {
|
||||
getMembershipsByUserId,
|
||||
getOrganizationOwnerCount,
|
||||
} from "@/modules/organization/settings/teams/lib/membership";
|
||||
import { deleteInvite, getInvite, inviteUser, refreshInviteExpiration, resendInvite } from "./lib/invite";
|
||||
import { deleteInvite, getInvite, inviteUser, resendInvite } from "./lib/invite";
|
||||
|
||||
const ZDeleteInviteAction = z.object({
|
||||
inviteId: ZUuid,
|
||||
@@ -58,57 +57,30 @@ const ZCreateInviteTokenAction = z.object({
|
||||
inviteId: ZUuid,
|
||||
});
|
||||
|
||||
export const createInviteTokenAction = authenticatedActionClient.schema(ZCreateInviteTokenAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"invite",
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZCreateInviteTokenAction>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromInviteId(parsedInput.inviteId);
|
||||
export const createInviteTokenAction = authenticatedActionClient
|
||||
.schema(ZCreateInviteTokenAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromInviteId(parsedInput.inviteId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Get old expiresAt for audit logging before update
|
||||
const oldInvite = await prisma.invite.findUnique({
|
||||
where: { id: parsedInput.inviteId },
|
||||
select: { email: true, expiresAt: true },
|
||||
});
|
||||
|
||||
if (!oldInvite) {
|
||||
throw new ValidationError("Invite not found");
|
||||
}
|
||||
|
||||
// Refresh the invitation expiration
|
||||
const updatedInvite = await refreshInviteExpiration(parsedInput.inviteId);
|
||||
|
||||
// Set audit context
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.inviteId = parsedInput.inviteId;
|
||||
ctx.auditLoggingCtx.oldObject = { expiresAt: oldInvite.expiresAt };
|
||||
ctx.auditLoggingCtx.newObject = { expiresAt: updatedInvite.expiresAt };
|
||||
|
||||
const inviteToken = createInviteToken(parsedInput.inviteId, updatedInvite.email, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
|
||||
return { inviteToken: encodeURIComponent(inviteToken) };
|
||||
const invite = await getInvite(parsedInput.inviteId);
|
||||
if (!invite) {
|
||||
throw new ValidationError("Invite not found");
|
||||
}
|
||||
)
|
||||
);
|
||||
const inviteToken = createInviteToken(parsedInput.inviteId, invite.email, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
|
||||
return { inviteToken: encodeURIComponent(inviteToken) };
|
||||
});
|
||||
|
||||
const ZDeleteMembershipAction = z.object({
|
||||
userId: ZId,
|
||||
@@ -219,7 +191,6 @@ export const resendInviteAction = authenticatedActionClient.schema(ZResendInvite
|
||||
invite?.creator?.name ?? "",
|
||||
updatedInvite.name ?? ""
|
||||
);
|
||||
|
||||
return updatedInvite;
|
||||
}
|
||||
)
|
||||
|
||||
+2
-17
@@ -42,27 +42,14 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
if (!member && invite) {
|
||||
// This is an invite
|
||||
|
||||
const result = await deleteInviteAction({ inviteId: invite?.id, organizationId: organization.id });
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
setIsDeleting(false);
|
||||
return;
|
||||
}
|
||||
await deleteInviteAction({ inviteId: invite?.id, organizationId: organization.id });
|
||||
toast.success(t("environments.settings.general.invite_deleted_successfully"));
|
||||
}
|
||||
|
||||
if (member && !invite) {
|
||||
// This is a member
|
||||
|
||||
const result = await deleteMembershipAction({
|
||||
userId: member.userId,
|
||||
organizationId: organization.id,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
setIsDeleting(false);
|
||||
return;
|
||||
}
|
||||
await deleteMembershipAction({ userId: member.userId, organizationId: organization.id });
|
||||
toast.success(t("environments.settings.general.member_deleted_successfully"));
|
||||
}
|
||||
|
||||
@@ -93,7 +80,6 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
if (createInviteTokenResponse?.data) {
|
||||
setShareInviteToken(createInviteTokenResponse.data.inviteToken);
|
||||
setShowShareInviteModal(true);
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createInviteTokenResponse);
|
||||
toast.error(errorMessage);
|
||||
@@ -113,7 +99,6 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
});
|
||||
if (resendInviteResponse?.data) {
|
||||
toast.success(t("environments.settings.general.invitation_sent_once_more"));
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(resendInviteResponse);
|
||||
toast.error(errorMessage);
|
||||
|
||||
+2
-2
@@ -47,8 +47,8 @@ export const MembersInfo = ({
|
||||
<Badge type="gray" text="Expired" size="tiny" data-testid="expired-badge" />
|
||||
) : (
|
||||
<TooltipRenderer
|
||||
tooltipContent={`${t("environments.settings.general.invite_expires_on", {
|
||||
date: getFormattedDateTimeString(member.expiresAt),
|
||||
tooltipContent={`${t("environments.settings.general.invited_on", {
|
||||
date: getFormattedDateTimeString(member.createdAt),
|
||||
})}`}>
|
||||
<Badge type="warning" text="Pending" size="tiny" />
|
||||
</TooltipRenderer>
|
||||
|
||||
+1
-6
@@ -71,12 +71,7 @@ export const OrganizationActions = ({
|
||||
const handleLeaveOrganization = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await leaveOrganizationAction({ organizationId: organization.id });
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await leaveOrganizationAction({ organizationId: organization.id });
|
||||
toast.success(t("environments.settings.general.member_deleted_successfully"));
|
||||
router.refresh();
|
||||
setLoading(false);
|
||||
|
||||
@@ -9,14 +9,7 @@ import {
|
||||
} from "@formbricks/types/errors";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { TInvitee } from "../types/invites";
|
||||
import {
|
||||
deleteInvite,
|
||||
getInvite,
|
||||
getInvitesByOrganizationId,
|
||||
inviteUser,
|
||||
refreshInviteExpiration,
|
||||
resendInvite,
|
||||
} from "./invite";
|
||||
import { deleteInvite, getInvite, getInvitesByOrganizationId, inviteUser, resendInvite } from "./invite";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
@@ -53,129 +46,32 @@ const mockInvite: Invite = {
|
||||
teamIds: [],
|
||||
};
|
||||
|
||||
describe("refreshInviteExpiration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("updates expiresAt to approximately 7 days from now", async () => {
|
||||
const now = Date.now();
|
||||
const expectedExpiresAt = new Date(now + 1000 * 60 * 60 * 24 * 7);
|
||||
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue({
|
||||
...mockInvite,
|
||||
expiresAt: expectedExpiresAt,
|
||||
});
|
||||
|
||||
const result = await refreshInviteExpiration("invite-1");
|
||||
|
||||
expect(prisma.invite.update).toHaveBeenCalledWith({
|
||||
where: { id: "invite-1" },
|
||||
data: {
|
||||
expiresAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.expiresAt.getTime()).toBeGreaterThanOrEqual(now + 1000 * 60 * 60 * 24 * 7 - 1000);
|
||||
expect(result.expiresAt.getTime()).toBeLessThanOrEqual(now + 1000 * 60 * 60 * 24 * 7 + 1000);
|
||||
});
|
||||
|
||||
test("throws ResourceNotFoundError if invite not found (P2025)", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
||||
code: "P2025",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(prismaError);
|
||||
await expect(refreshInviteExpiration("invite-1")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("throws DatabaseError on other prisma errors", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("db", {
|
||||
code: "P2002",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(prismaError);
|
||||
await expect(refreshInviteExpiration("invite-1")).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("throws error if non-prisma error", async () => {
|
||||
const error = new Error("db");
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(error);
|
||||
await expect(refreshInviteExpiration("invite-1")).rejects.toThrow("db");
|
||||
});
|
||||
|
||||
test("returns full invite object with all fields", async () => {
|
||||
const updatedInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
};
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue(updatedInvite);
|
||||
|
||||
const result = await refreshInviteExpiration("invite-1");
|
||||
|
||||
expect(result).toEqual(updatedInvite);
|
||||
expect(result.id).toBe("invite-1");
|
||||
expect(result.email).toBe("test@example.com");
|
||||
expect(result.name).toBe("Test User");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resendInvite", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("returns email and name after updating expiration", async () => {
|
||||
const updatedInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
};
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue(updatedInvite);
|
||||
|
||||
test("returns email and name if invite exists", async () => {
|
||||
vi.mocked(prisma.invite.findUnique).mockResolvedValue({ ...mockInvite, creator: {} });
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue({ ...mockInvite, organizationId: "org-1" });
|
||||
const result = await resendInvite("invite-1");
|
||||
|
||||
expect(result).toEqual({ email: mockInvite.email, name: mockInvite.name });
|
||||
expect(prisma.invite.update).toHaveBeenCalledWith({
|
||||
where: { id: "invite-1" },
|
||||
data: {
|
||||
expiresAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("calls refreshInviteExpiration helper", async () => {
|
||||
const updatedInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
};
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue(updatedInvite);
|
||||
|
||||
await resendInvite("invite-1");
|
||||
|
||||
expect(prisma.invite.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("throws ResourceNotFoundError if invite not found", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
||||
code: "P2025",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(prismaError);
|
||||
vi.mocked(prisma.invite.findUnique).mockResolvedValue(null);
|
||||
await expect(resendInvite("invite-1")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("throws DatabaseError on other prisma errors", async () => {
|
||||
test("throws DatabaseError on prisma error", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("db", {
|
||||
code: "P2002",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(prismaError);
|
||||
vi.mocked(prisma.invite.findUnique).mockRejectedValue(prismaError);
|
||||
await expect(resendInvite("invite-1")).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("throws error if non-prisma error", async () => {
|
||||
test("throws error if prisma error", async () => {
|
||||
const error = new Error("db");
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(error);
|
||||
vi.mocked(prisma.invite.findUnique).mockRejectedValue(error);
|
||||
await expect(resendInvite("invite-1")).rejects.toThrow("db");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,21 +13,44 @@ import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { type InviteWithCreator, type TInvite, type TInvitee } from "../types/invites";
|
||||
|
||||
export const refreshInviteExpiration = async (inviteId: string): Promise<Invite> => {
|
||||
export const resendInvite = async (inviteId: string): Promise<Pick<Invite, "email" | "name">> => {
|
||||
try {
|
||||
const updatedInvite = await prisma.invite.update({
|
||||
where: { id: inviteId },
|
||||
data: {
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 7 days
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
creator: true,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedInvite;
|
||||
if (!invite) {
|
||||
throw new ResourceNotFoundError("Invite", inviteId);
|
||||
}
|
||||
|
||||
const updatedInvite = await prisma.invite.update({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
data: {
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
organizationId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
email: updatedInvite.email,
|
||||
name: updatedInvite.name,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === "P2025") {
|
||||
throw new ResourceNotFoundError("Invite", inviteId);
|
||||
}
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
@@ -35,16 +58,6 @@ export const refreshInviteExpiration = async (inviteId: string): Promise<Invite>
|
||||
}
|
||||
};
|
||||
|
||||
export const resendInvite = async (inviteId: string): Promise<Pick<Invite, "email" | "name">> => {
|
||||
// Refresh expiration and return the updated invite (single query)
|
||||
const updatedInvite = await refreshInviteExpiration(inviteId);
|
||||
|
||||
return {
|
||||
email: updatedInvite.email,
|
||||
name: updatedInvite.name,
|
||||
};
|
||||
};
|
||||
|
||||
export const getInvitesByOrganizationId = reactCache(
|
||||
async (organizationId: string, page?: number): Promise<TInvite[]> => {
|
||||
validateInputs([organizationId, z.string()], [page, z.number().optional()]);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { FormProvider, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TActionClass, TActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import {
|
||||
deleteActionClassAction,
|
||||
updateActionClassAction,
|
||||
@@ -93,14 +92,10 @@ export const ActionSettingsTab = ({
|
||||
validatePermissions(isReadOnly, t);
|
||||
const updatedAction = buildActionObject(data, actionClass.environmentId, t);
|
||||
|
||||
const result = await updateActionClassAction({
|
||||
await updateActionClassAction({
|
||||
actionClassId: actionClass.id,
|
||||
updatedAction: updatedAction,
|
||||
});
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
setOpen(false);
|
||||
router.refresh();
|
||||
toast.success(t("environments.actions.action_updated_successfully"));
|
||||
@@ -114,11 +109,7 @@ export const ActionSettingsTab = ({
|
||||
const handleDeleteAction = async () => {
|
||||
try {
|
||||
setIsDeletingAction(true);
|
||||
const result = await deleteActionClassAction({ actionClassId: actionClass.id });
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await deleteActionClassAction({ actionClassId: actionClass.id });
|
||||
router.refresh();
|
||||
toast.success(t("environments.actions.action_deleted_successfully"));
|
||||
setOpen(false);
|
||||
|
||||
@@ -23,7 +23,7 @@ const baseProject = {
|
||||
config: { channel: null, industry: null },
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: false,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
environments: [
|
||||
{
|
||||
id: "cmi2sra0j000004l73fvh7lhe",
|
||||
|
||||
@@ -24,7 +24,7 @@ const selectProject = {
|
||||
config: true,
|
||||
placement: true,
|
||||
clickOutsideClose: true,
|
||||
overlay: true,
|
||||
darkOverlay: true,
|
||||
environments: true,
|
||||
styling: true,
|
||||
logo: true,
|
||||
|
||||
@@ -15,7 +15,6 @@ import { FormControl, FormField, FormItem, FormLabel, FormProvider } from "@/mod
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { getPlacementStyle } from "@/modules/ui/components/preview-survey/lib/utils";
|
||||
import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group";
|
||||
import { StylingTabs } from "@/modules/ui/components/styling-tabs";
|
||||
|
||||
interface EditPlacementProps {
|
||||
project: Project;
|
||||
@@ -25,7 +24,7 @@ interface EditPlacementProps {
|
||||
|
||||
const ZProjectPlacementInput = z.object({
|
||||
placement: z.enum(["bottomRight", "topRight", "topLeft", "bottomLeft", "center"]),
|
||||
overlay: z.enum(["none", "light", "dark"]),
|
||||
darkOverlay: z.boolean(),
|
||||
clickOutsideClose: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -41,35 +40,28 @@ export const EditPlacementForm = ({ project, isReadOnly }: EditPlacementProps) =
|
||||
{ name: t("common.bottom_left"), value: "bottomLeft", disabled: false },
|
||||
{ name: t("common.centered_modal"), value: "center", disabled: false },
|
||||
];
|
||||
|
||||
const form = useForm<EditPlacementFormValues>({
|
||||
defaultValues: {
|
||||
placement: project.placement,
|
||||
overlay: project.overlay ?? "none",
|
||||
darkOverlay: project.darkOverlay ?? false,
|
||||
clickOutsideClose: project.clickOutsideClose ?? false,
|
||||
},
|
||||
resolver: zodResolver(ZProjectPlacementInput),
|
||||
});
|
||||
|
||||
const currentPlacement = form.watch("placement");
|
||||
const overlay = form.watch("overlay");
|
||||
const darkOverlay = form.watch("darkOverlay");
|
||||
const clickOutsideClose = form.watch("clickOutsideClose");
|
||||
const isSubmitting = form.formState.isSubmitting;
|
||||
|
||||
const hasOverlay = overlay !== "none";
|
||||
|
||||
const getOverlayStyle = () => {
|
||||
if (overlay === "dark") return "bg-slate-700/80";
|
||||
if (overlay === "light") return "bg-slate-400/50";
|
||||
return "bg-slate-200";
|
||||
};
|
||||
const overlayStyle = currentPlacement === "center" && darkOverlay ? "bg-slate-700/80" : "bg-slate-200";
|
||||
|
||||
const onSubmit: SubmitHandler<EditPlacementFormValues> = async (data) => {
|
||||
const updatedProjectResponse = await updateProjectAction({
|
||||
projectId: project.id,
|
||||
data: {
|
||||
placement: data.placement,
|
||||
overlay: data.overlay,
|
||||
darkOverlay: data.darkOverlay,
|
||||
clickOutsideClose: data.clickOutsideClose,
|
||||
},
|
||||
});
|
||||
@@ -121,9 +113,9 @@ export const EditPlacementForm = ({ project, isReadOnly }: EditPlacementProps) =
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
hasOverlay && !clickOutsideClose ? "cursor-not-allowed" : "",
|
||||
clickOutsideClose ? "" : "cursor-not-allowed",
|
||||
"relative ml-8 h-40 w-full rounded",
|
||||
getOverlayStyle()
|
||||
overlayStyle
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -133,69 +125,85 @@ export const EditPlacementForm = ({ project, isReadOnly }: EditPlacementProps) =
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="overlay"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<StylingTabs
|
||||
id="overlay"
|
||||
options={[
|
||||
{ value: "none", label: t("common.no_overlay") },
|
||||
{ value: "light", label: t("common.light_overlay") },
|
||||
{ value: "dark", label: t("common.dark_overlay") },
|
||||
]}
|
||||
defaultSelected={field.value}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
label={t("common.overlay_color")}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasOverlay && (
|
||||
<div className="mt-6 space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clickOutsideClose"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="font-semibold">
|
||||
{t("common.allow_users_to_exit_by_clicking_outside_the_survey")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
disabled={isReadOnly}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value === "allow");
|
||||
}}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="disallow" value="disallow" checked={!field.value} />
|
||||
<Label
|
||||
htmlFor="disallow"
|
||||
className={`text-slate-900 ${isReadOnly ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}>
|
||||
{t("common.disallow")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="allow" value="allow" checked={field.value} />
|
||||
<Label
|
||||
htmlFor="allow"
|
||||
className={`text-slate-900 ${isReadOnly ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}>
|
||||
{t("common.allow")}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{currentPlacement === "center" && (
|
||||
<>
|
||||
<div className="mt-6 space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="darkOverlay"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="font-semibold">
|
||||
{t("environments.workspace.look.centered_modal_overlay_color")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value === "darkOverlay");
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="lightOverlay" value="lightOverlay" checked={!field.value} />
|
||||
<Label
|
||||
htmlFor="lightOverlay"
|
||||
className={`text-slate-900 ${isReadOnly ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}>
|
||||
{t("common.light_overlay")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="darkOverlay" value="darkOverlay" checked={field.value} />
|
||||
<Label
|
||||
htmlFor="darkOverlay"
|
||||
className={`text-slate-900 ${isReadOnly ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}>
|
||||
{t("common.dark_overlay")}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="clickOutsideClose"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="font-semibold">
|
||||
{t("common.allow_users_to_exit_by_clicking_outside_the_survey")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
disabled={isReadOnly}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value === "allow");
|
||||
}}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="disallow" value="disallow" checked={!field.value} />
|
||||
<Label
|
||||
htmlFor="disallow"
|
||||
className={`text-slate-900 ${isReadOnly ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}>
|
||||
{t("common.disallow")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="allow" value="allow" checked={field.value} />
|
||||
<Label
|
||||
htmlFor="allow"
|
||||
className={`text-slate-900 ${isReadOnly ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}>
|
||||
{t("common.allow")}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button className="mt-4 w-fit" size="sm" loading={isSubmitting} disabled={isReadOnly}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Project } from "@prisma/client";
|
||||
import { RotateCcwIcon, SparklesIcon } from "lucide-react";
|
||||
import { RotateCcwIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
import { SubmitHandler, UseFormReturn, useForm } from "react-hook-form";
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { TProjectStyling, ZProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { previewSurvey } from "@/app/lib/templates";
|
||||
import { STYLE_DEFAULTS, getSuggestedColors } from "@/lib/styling/constants";
|
||||
import { defaultStyling } from "@/lib/styling/constants";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { updateProjectAction } from "@/modules/projects/settings/actions";
|
||||
import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
|
||||
@@ -20,7 +20,6 @@ import { AlertDialog } from "@/modules/ui/components/alert-dialog";
|
||||
import { BackgroundStylingCard } from "@/modules/ui/components/background-styling-card";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { CardStylingSettings } from "@/modules/ui/components/card-styling-settings";
|
||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
@@ -54,55 +53,35 @@ export const ThemeStyling = ({
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const savedStyling = project.styling as Partial<TProjectStyling> | null;
|
||||
|
||||
// Strip null/undefined values so they don't override STYLE_DEFAULTS.
|
||||
// Saved styling from before advanced fields existed will have nullish entries.
|
||||
const cleanSaved = savedStyling
|
||||
? Object.fromEntries(Object.entries(savedStyling).filter(([, v]) => v != null))
|
||||
: {};
|
||||
|
||||
const form = useForm<TProjectStyling>({
|
||||
defaultValues: { ...STYLE_DEFAULTS, ...cleanSaved },
|
||||
defaultValues: { ...defaultStyling, ...project.styling },
|
||||
resolver: zodResolver(ZProjectStyling),
|
||||
});
|
||||
|
||||
const [previewSurveyType, setPreviewSurveyType] = useState<TSurveyType>("link");
|
||||
const [confirmResetStylingModalOpen, setConfirmResetStylingModalOpen] = useState(false);
|
||||
const [confirmSuggestColorsOpen, setConfirmSuggestColorsOpen] = useState(false);
|
||||
|
||||
const [formStylingOpen, setFormStylingOpen] = useState(false);
|
||||
const [cardStylingOpen, setCardStylingOpen] = useState(false);
|
||||
const [backgroundStylingOpen, setBackgroundStylingOpen] = useState(false);
|
||||
|
||||
const onReset = useCallback(async () => {
|
||||
const updatedProjectResponse = await updateProjectAction({
|
||||
projectId: project.id,
|
||||
data: {
|
||||
styling: { ...STYLE_DEFAULTS },
|
||||
styling: { ...defaultStyling },
|
||||
},
|
||||
});
|
||||
|
||||
if (updatedProjectResponse?.data) {
|
||||
form.reset({ ...STYLE_DEFAULTS });
|
||||
form.reset({ ...defaultStyling });
|
||||
toast.success(t("environments.workspace.look.styling_updated_successfully"));
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(updatedProjectResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
}, [form, project.id, router, t]);
|
||||
|
||||
const handleSuggestColors = () => {
|
||||
const brandColor = form.getValues().brandColor?.light ?? STYLE_DEFAULTS.brandColor?.light;
|
||||
const suggested = getSuggestedColors(brandColor);
|
||||
|
||||
for (const [key, value] of Object.entries(suggested)) {
|
||||
form.setValue(key as keyof TProjectStyling, value, { shouldDirty: true });
|
||||
}
|
||||
|
||||
toast.success(t("environments.workspace.look.styling_updated_successfully"));
|
||||
setConfirmSuggestColorsOpen(false);
|
||||
};
|
||||
}, [form, project.id, router]);
|
||||
|
||||
const onSubmit: SubmitHandler<TProjectStyling> = async (data) => {
|
||||
const updatedProjectResponse = await updateProjectAction({
|
||||
@@ -165,38 +144,7 @@ export const ThemeStyling = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-slate-50 p-4">
|
||||
<div className="grid grid-cols-2 items-end gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="brandColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-1">
|
||||
<FormLabel>{t("environments.surveys.edit.brand_color")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.edit.brand_color_description")}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
color={field.value ?? STYLE_DEFAULTS.brandColor?.light}
|
||||
onChange={(color) => field.onChange(color)}
|
||||
containerClass="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="h-10 w-full justify-center gap-2"
|
||||
onClick={() => setConfirmSuggestColorsOpen(true)}>
|
||||
<SparklesIcon className="mr-2 h-4 w-4" />
|
||||
{t("environments.workspace.look.suggest_colors")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 rounded-lg bg-slate-50 p-4">
|
||||
<FormStylingSettings
|
||||
open={formStylingOpen}
|
||||
setOpen={setFormStylingOpen}
|
||||
@@ -244,12 +192,12 @@ export const ThemeStyling = ({
|
||||
{/* Survey Preview */}
|
||||
|
||||
<div className="relative w-1/2 rounded-lg bg-slate-100 pt-4">
|
||||
<div className="sticky top-4 mb-4 max-h-[calc(100vh-2rem)]">
|
||||
<div className="sticky top-4 mb-4 h-[600px]">
|
||||
<ThemeStylingPreviewSurvey
|
||||
survey={previewSurvey(project.name, t)}
|
||||
project={{
|
||||
...project,
|
||||
styling: form.watch("allowStyleOverwrite") ? form.watch() : STYLE_DEFAULTS,
|
||||
styling: form.watch(),
|
||||
}}
|
||||
previewType={previewSurveyType}
|
||||
setPreviewType={setPreviewSurveyType}
|
||||
@@ -258,18 +206,6 @@ export const ThemeStyling = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm reset styling modal */}
|
||||
<AlertDialog
|
||||
open={confirmSuggestColorsOpen}
|
||||
setOpen={setConfirmSuggestColorsOpen}
|
||||
headerText={t("environments.workspace.look.generate_theme_header")}
|
||||
mainText={t("environments.workspace.look.generate_theme_confirmation")}
|
||||
confirmBtnLabel={t("environments.workspace.look.generate_theme_btn")}
|
||||
declineBtnLabel={t("common.cancel")}
|
||||
onConfirm={handleSuggestColors}
|
||||
onDecline={() => setConfirmSuggestColorsOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Confirm reset styling modal */}
|
||||
<AlertDialog
|
||||
open={confirmResetStylingModalOpen}
|
||||
|
||||
@@ -21,7 +21,7 @@ const baseProject: Project = {
|
||||
config: { channel: null, industry: null } as any,
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: false,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
logo: null,
|
||||
brandColor: null,
|
||||
highlightBorderColor: null,
|
||||
|
||||
@@ -72,13 +72,13 @@ describe("fileUpload", () => {
|
||||
test("should handle successful file upload with presigned fields", async () => {
|
||||
const file = createMockFile("test.jpg", "image/jpeg", 1000);
|
||||
|
||||
// Mock successful API response - now returns relative path
|
||||
// Mock successful API response
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
signedUrl: "https://s3.example.com/upload",
|
||||
fileUrl: "/storage/test-env/public/file.jpg",
|
||||
fileUrl: "https://s3.example.com/file.jpg",
|
||||
presignedFields: {
|
||||
key: "value",
|
||||
},
|
||||
@@ -98,18 +98,18 @@ describe("fileUpload", () => {
|
||||
|
||||
const result = await fileUploadModule.handleFileUpload(file, "test-env");
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.url).toBe("/storage/test-env/public/file.jpg");
|
||||
expect(result.url).toBe("https://s3.example.com/file.jpg");
|
||||
});
|
||||
|
||||
test("should handle upload error with presigned fields", async () => {
|
||||
const file = createMockFile("test.jpg", "image/jpeg", 1000);
|
||||
// Mock successful API response - now returns relative path
|
||||
// Mock successful API response
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
signedUrl: "https://s3.example.com/upload",
|
||||
fileUrl: "/storage/test-env/public/file.jpg",
|
||||
fileUrl: "https://s3.example.com/file.jpg",
|
||||
presignedFields: {
|
||||
key: "value",
|
||||
},
|
||||
@@ -134,13 +134,13 @@ describe("fileUpload", () => {
|
||||
test("should handle upload error", async () => {
|
||||
const file = createMockFile("test.jpg", "image/jpeg", 1000);
|
||||
|
||||
// Mock successful API response - now returns relative path
|
||||
// Mock successful API response
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
signedUrl: "https://s3.example.com/upload",
|
||||
fileUrl: "/storage/test-env/public/file.jpg",
|
||||
fileUrl: "https://s3.example.com/file.jpg",
|
||||
presignedFields: {
|
||||
key: "value",
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import { TAccessType } from "@formbricks/types/storage";
|
||||
import {
|
||||
deleteFile,
|
||||
deleteFilesByEnvironmentId,
|
||||
getFileStreamForDownload,
|
||||
getSignedUrlForDownload,
|
||||
getSignedUrlForUpload,
|
||||
} from "./service";
|
||||
|
||||
@@ -14,6 +14,14 @@ vi.mock("crypto", () => ({
|
||||
randomUUID: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
WEBAPP_URL: "https://webapp.example.com",
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/getPublicUrl", () => ({
|
||||
getPublicDomain: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
@@ -30,22 +38,22 @@ vi.mock("@formbricks/storage", () => ({
|
||||
},
|
||||
deleteFile: vi.fn(),
|
||||
deleteFilesByPrefix: vi.fn(),
|
||||
getFileStream: vi.fn(),
|
||||
getSignedDownloadUrl: vi.fn(),
|
||||
getSignedUploadUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
// Import mocked dependencies
|
||||
const { logger } = await import("@formbricks/logger");
|
||||
const storageModule = await import("@formbricks/storage");
|
||||
const { getPublicDomain } = await import("@/lib/getPublicUrl");
|
||||
const {
|
||||
deleteFile: deleteFileFromS3,
|
||||
deleteFilesByPrefix,
|
||||
getSignedDownloadUrl,
|
||||
getSignedUploadUrl,
|
||||
getFileStream,
|
||||
} = storageModule;
|
||||
} = await import("@formbricks/storage");
|
||||
|
||||
type MockedSignedUploadReturn = Awaited<ReturnType<typeof getSignedUploadUrl>>;
|
||||
type MockedFileStreamReturn = Awaited<ReturnType<typeof getFileStream>>;
|
||||
type MockedSignedDownloadReturn = Awaited<ReturnType<typeof getSignedDownloadUrl>>;
|
||||
type MockedDeleteFileReturn = Awaited<ReturnType<typeof deleteFile>>;
|
||||
type MockedDeleteFilesByPrefixReturn = Awaited<ReturnType<typeof deleteFilesByPrefix>>;
|
||||
|
||||
@@ -55,6 +63,7 @@ describe("storage service", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(randomUUID).mockReturnValue(mockUUID);
|
||||
vi.mocked(getPublicDomain).mockReturnValue("https://public.example.com");
|
||||
});
|
||||
|
||||
describe("getSignedUrlForUpload", () => {
|
||||
@@ -81,7 +90,7 @@ describe("storage service", () => {
|
||||
expect(result.data).toEqual({
|
||||
signedUrl: "https://s3.example.com/upload",
|
||||
presignedFields: { key: "value" },
|
||||
fileUrl: `/storage/env-123/public/test-image--fid--${mockUUID}.jpg`,
|
||||
fileUrl: `https://public.example.com/storage/env-123/public/test-image--fid--${mockUUID}.jpg`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,7 +102,7 @@ describe("storage service", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("should return relative URL for private files", async () => {
|
||||
test("should use WEBAPP_URL for private files", async () => {
|
||||
const mockSignedUrlResponse = {
|
||||
ok: true,
|
||||
data: {
|
||||
@@ -113,7 +122,9 @@ describe("storage service", () => {
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data.fileUrl).toBe(`/storage/env-123/private/test-doc--fid--${mockUUID}.pdf`);
|
||||
expect(result.data.fileUrl).toBe(
|
||||
`https://webapp.example.com/storage/env-123/private/test-doc--fid--${mockUUID}.pdf`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -138,7 +149,9 @@ describe("storage service", () => {
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
// The filename should be URL-encoded to prevent # from being treated as a URL fragment
|
||||
expect(result.data.fileUrl).toBe(`/storage/env-123/public/testfile--fid--${mockUUID}.txt`);
|
||||
expect(result.data.fileUrl).toBe(
|
||||
`https://public.example.com/storage/env-123/public/testfile--fid--${mockUUID}.txt`
|
||||
);
|
||||
}
|
||||
|
||||
expect(getSignedUploadUrl).toHaveBeenCalledWith(
|
||||
@@ -263,6 +276,86 @@ describe("storage service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSignedUrlForDownload", () => {
|
||||
test("should generate signed URL for download", async () => {
|
||||
const mockSignedUrlResponse = {
|
||||
ok: true,
|
||||
data: "https://s3.example.com/download?signature=abc123",
|
||||
} as MockedSignedDownloadReturn;
|
||||
|
||||
vi.mocked(getSignedDownloadUrl).mockResolvedValue(mockSignedUrlResponse);
|
||||
|
||||
const result = await getSignedUrlForDownload("test-file.jpg", "env-123", "public" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data).toBe("https://s3.example.com/download?signature=abc123");
|
||||
}
|
||||
expect(getSignedDownloadUrl).toHaveBeenCalledWith("env-123/public/test-file.jpg");
|
||||
});
|
||||
|
||||
test("should decode URI-encoded filename", async () => {
|
||||
const mockSignedUrlResponse = {
|
||||
ok: true,
|
||||
data: "https://s3.example.com/download?signature=abc123",
|
||||
} as MockedSignedDownloadReturn;
|
||||
|
||||
vi.mocked(getSignedDownloadUrl).mockResolvedValue(mockSignedUrlResponse);
|
||||
|
||||
const encodedFileName = encodeURIComponent("file with spaces.jpg");
|
||||
await getSignedUrlForDownload(encodedFileName, "env-123", "private" as TAccessType);
|
||||
|
||||
expect(getSignedDownloadUrl).toHaveBeenCalledWith("env-123/private/file with spaces.jpg");
|
||||
});
|
||||
|
||||
test("should return error when getSignedDownloadUrl fails", async () => {
|
||||
const mockErrorResponse = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: StorageErrorCode.S3ClientError,
|
||||
},
|
||||
} as MockedSignedDownloadReturn;
|
||||
|
||||
vi.mocked(getSignedDownloadUrl).mockResolvedValue(mockErrorResponse);
|
||||
|
||||
const result = await getSignedUrlForDownload("missing-file.jpg", "env-123", "public" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe(StorageErrorCode.S3ClientError);
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle unexpected errors and return unknown error", async () => {
|
||||
vi.mocked(getSignedDownloadUrl).mockRejectedValue(new Error("Network error"));
|
||||
|
||||
const result = await getSignedUrlForDownload("test-file.jpg", "env-123", "public" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe(StorageErrorCode.Unknown);
|
||||
}
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ error: expect.any(Error) },
|
||||
"Error getting signed url for download"
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle files with special characters", async () => {
|
||||
const mockSignedUrlResponse = {
|
||||
ok: true,
|
||||
data: "https://s3.example.com/download?signature=abc123",
|
||||
} as MockedSignedDownloadReturn;
|
||||
|
||||
vi.mocked(getSignedDownloadUrl).mockResolvedValue(mockSignedUrlResponse);
|
||||
|
||||
const specialFileName = "file%20with%20%26%20symbols.jpg";
|
||||
await getSignedUrlForDownload(specialFileName, "env-123", "public" as TAccessType);
|
||||
|
||||
expect(getSignedDownloadUrl).toHaveBeenCalledWith("env-123/public/file with & symbols.jpg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteFile", () => {
|
||||
test("should call deleteFileFromS3 with correct file key", async () => {
|
||||
const mockSuccessResult = {
|
||||
@@ -340,147 +433,4 @@ describe("storage service", () => {
|
||||
expect(deleteFilesByPrefix).toHaveBeenCalledWith("env-123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFileStreamForDownload", () => {
|
||||
test("should return file stream for public file", async () => {
|
||||
const mockStream = new ReadableStream();
|
||||
const mockStreamResult = {
|
||||
ok: true,
|
||||
data: {
|
||||
body: mockStream,
|
||||
contentType: "image/jpeg",
|
||||
contentLength: 12345,
|
||||
},
|
||||
} as MockedFileStreamReturn;
|
||||
|
||||
vi.mocked(getFileStream).mockResolvedValue(mockStreamResult);
|
||||
|
||||
const result = await getFileStreamForDownload("test-image.jpg", "env-123", "public" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data.body).toBe(mockStream);
|
||||
expect(result.data.contentType).toBe("image/jpeg");
|
||||
expect(result.data.contentLength).toBe(12345);
|
||||
}
|
||||
expect(getFileStream).toHaveBeenCalledWith("env-123/public/test-image.jpg");
|
||||
});
|
||||
|
||||
test("should return file stream for private file", async () => {
|
||||
const mockStream = new ReadableStream();
|
||||
const mockStreamResult = {
|
||||
ok: true,
|
||||
data: {
|
||||
body: mockStream,
|
||||
contentType: "application/pdf",
|
||||
contentLength: 54321,
|
||||
},
|
||||
} as MockedFileStreamReturn;
|
||||
|
||||
vi.mocked(getFileStream).mockResolvedValue(mockStreamResult);
|
||||
|
||||
const result = await getFileStreamForDownload("document.pdf", "env-456", "private" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.data.contentType).toBe("application/pdf");
|
||||
}
|
||||
expect(getFileStream).toHaveBeenCalledWith("env-456/private/document.pdf");
|
||||
});
|
||||
|
||||
test("should decode URL-encoded filename", async () => {
|
||||
const mockStream = new ReadableStream();
|
||||
const mockStreamResult = {
|
||||
ok: true,
|
||||
data: {
|
||||
body: mockStream,
|
||||
contentType: "image/png",
|
||||
contentLength: 1000,
|
||||
},
|
||||
} as MockedFileStreamReturn;
|
||||
|
||||
vi.mocked(getFileStream).mockResolvedValue(mockStreamResult);
|
||||
|
||||
// URL-encoded filename with spaces: "my file.png" -> "my%20file.png"
|
||||
const result = await getFileStreamForDownload("my%20file.png", "env-123", "public" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
// Should decode %20 to space before passing to getFileStream
|
||||
expect(getFileStream).toHaveBeenCalledWith("env-123/public/my file.png");
|
||||
});
|
||||
|
||||
test("should return error when getFileStream fails with FileNotFoundError", async () => {
|
||||
const mockErrorResult = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: StorageErrorCode.FileNotFoundError,
|
||||
},
|
||||
} as MockedFileStreamReturn;
|
||||
|
||||
vi.mocked(getFileStream).mockResolvedValue(mockErrorResult);
|
||||
|
||||
const result = await getFileStreamForDownload("missing-file.jpg", "env-123", "public" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe(StorageErrorCode.FileNotFoundError);
|
||||
}
|
||||
});
|
||||
|
||||
test("should return error when getFileStream fails with S3ClientError", async () => {
|
||||
const mockErrorResult = {
|
||||
ok: false,
|
||||
error: {
|
||||
code: StorageErrorCode.S3ClientError,
|
||||
},
|
||||
} as MockedFileStreamReturn;
|
||||
|
||||
vi.mocked(getFileStream).mockResolvedValue(mockErrorResult);
|
||||
|
||||
const result = await getFileStreamForDownload("some-file.jpg", "env-123", "public" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe(StorageErrorCode.S3ClientError);
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle unexpected errors and return unknown error", async () => {
|
||||
vi.mocked(getFileStream).mockRejectedValue(new Error("Unexpected S3 error"));
|
||||
|
||||
const result = await getFileStreamForDownload("test-file.jpg", "env-123", "public" as TAccessType);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error.code).toBe(StorageErrorCode.Unknown);
|
||||
}
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ error: expect.any(Error) },
|
||||
"Error getting file stream for download"
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle filename with fid pattern", async () => {
|
||||
const mockStream = new ReadableStream();
|
||||
const mockStreamResult = {
|
||||
ok: true,
|
||||
data: {
|
||||
body: mockStream,
|
||||
contentType: "image/jpeg",
|
||||
contentLength: 5000,
|
||||
},
|
||||
} as MockedFileStreamReturn;
|
||||
|
||||
vi.mocked(getFileStream).mockResolvedValue(mockStreamResult);
|
||||
|
||||
const result = await getFileStreamForDownload(
|
||||
"photo--fid--abc123-def456.jpg",
|
||||
"env-123",
|
||||
"public" as TAccessType
|
||||
);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(getFileStream).toHaveBeenCalledWith("env-123/public/photo--fid--abc123-def456.jpg");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import {
|
||||
type FileStreamResult,
|
||||
type StorageError,
|
||||
StorageErrorCode,
|
||||
deleteFile as deleteFileFromS3,
|
||||
deleteFilesByPrefix,
|
||||
getFileStream,
|
||||
getSignedDownloadUrl,
|
||||
getSignedUploadUrl,
|
||||
} from "@formbricks/storage";
|
||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||
import { TAccessType } from "@formbricks/types/storage";
|
||||
import { WEBAPP_URL } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { sanitizeFileName } from "./utils";
|
||||
|
||||
export const getSignedUrlForUpload = async (
|
||||
@@ -50,11 +51,15 @@ export const getSignedUrlForUpload = async (
|
||||
return signedUrlResult;
|
||||
}
|
||||
|
||||
// Return relative path - can be resolved to absolute URL at runtime when needed
|
||||
// Use PUBLIC_URL for public files, WEBAPP_URL for private files
|
||||
const baseUrl = accessType === "public" ? getPublicDomain() : WEBAPP_URL;
|
||||
|
||||
return ok({
|
||||
signedUrl: signedUrlResult.data.signedUrl,
|
||||
presignedFields: signedUrlResult.data.presignedFields,
|
||||
fileUrl: `/storage/${environmentId}/${accessType}/${encodeURIComponent(updatedFileName)}`,
|
||||
fileUrl: new URL(
|
||||
`${baseUrl}/storage/${environmentId}/${accessType}/${encodeURIComponent(updatedFileName)}`
|
||||
).href,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Error getting signed url for upload");
|
||||
@@ -65,28 +70,24 @@ export const getSignedUrlForUpload = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a file stream for downloading/streaming files directly
|
||||
* Use this instead of signed URL redirect for Next.js Image component compatibility
|
||||
*/
|
||||
export const getFileStreamForDownload = async (
|
||||
export const getSignedUrlForDownload = async (
|
||||
fileName: string,
|
||||
environmentId: string,
|
||||
accessType: TAccessType
|
||||
): Promise<Result<FileStreamResult, StorageError>> => {
|
||||
): Promise<Result<string, StorageError>> => {
|
||||
try {
|
||||
const fileNameDecoded = decodeURIComponent(fileName);
|
||||
const fileKey = `${environmentId}/${accessType}/${fileNameDecoded}`;
|
||||
|
||||
const streamResult = await getFileStream(fileKey);
|
||||
const signedUrlResult = await getSignedDownloadUrl(fileKey);
|
||||
|
||||
if (!streamResult.ok) {
|
||||
return streamResult;
|
||||
if (!signedUrlResult.ok) {
|
||||
return signedUrlResult;
|
||||
}
|
||||
|
||||
return streamResult;
|
||||
return signedUrlResult;
|
||||
} catch (error) {
|
||||
logger.error({ error }, "Error getting file stream for download");
|
||||
logger.error({ error }, "Error getting signed url for download");
|
||||
|
||||
return err({
|
||||
code: StorageErrorCode.Unknown,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Client-safe URL helper utilities for storage files.
|
||||
* These functions can be used in both server and client components.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts the original file name from a storage URL.
|
||||
* Handles both relative paths (/storage/...) and absolute URLs.
|
||||
* @param fileURL The storage URL to parse
|
||||
* @returns The original file name, or empty string if parsing fails
|
||||
*/
|
||||
export const getOriginalFileNameFromUrl = (fileURL: string): string => {
|
||||
try {
|
||||
const lastSegment = fileURL.startsWith("/storage/")
|
||||
? (fileURL.split("/").pop() ?? "")
|
||||
: (new URL(fileURL).pathname.split("/").pop() ?? "");
|
||||
const fileNameFromURL = lastSegment.split(/[?#]/)[0];
|
||||
|
||||
const [namePart, fidPart] = fileNameFromURL.split("--fid--");
|
||||
if (!fidPart) return namePart ? decodeURIComponent(namePart) : "";
|
||||
|
||||
const dotIdx = fileNameFromURL.lastIndexOf(".");
|
||||
const hasExt = dotIdx > fileNameFromURL.indexOf("--fid--");
|
||||
const ext = hasExt ? fileNameFromURL.slice(dotIdx + 1) : "";
|
||||
|
||||
return decodeURIComponent(ext ? `${namePart}.${ext}` : namePart);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
isAllowedFileExtension,
|
||||
isValidFileTypeForExtension,
|
||||
isValidImageFile,
|
||||
resolveStorageUrl,
|
||||
sanitizeFileName,
|
||||
validateFileUploads,
|
||||
validateSingleFile,
|
||||
@@ -146,8 +145,7 @@ describe("storage utils", () => {
|
||||
const { getOriginalFileNameFromUrl } =
|
||||
await vi.importActual<typeof import("@/modules/storage/utils")>("@/modules/storage/utils");
|
||||
const path = "/storage/env/public/Document%20Name.pdf";
|
||||
// Function extracts filename, not full path
|
||||
expect(getOriginalFileNameFromUrl(path)).toBe("Document Name.pdf");
|
||||
expect(getOriginalFileNameFromUrl(path)).toBe("/storage/env/public/Document Name.pdf");
|
||||
});
|
||||
|
||||
test("returns empty string on invalid URL input", async () => {
|
||||
@@ -398,38 +396,4 @@ describe("storage utils", () => {
|
||||
expect(isValidImageFile("https://example.com/image.JPG")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveStorageUrl", () => {
|
||||
test("should return empty string for null or undefined input", () => {
|
||||
expect(resolveStorageUrl(null)).toBe("");
|
||||
expect(resolveStorageUrl(undefined)).toBe("");
|
||||
expect(resolveStorageUrl("")).toBe("");
|
||||
});
|
||||
|
||||
test("should return absolute URL unchanged (backward compatibility)", () => {
|
||||
const httpsUrl = "https://example.com/storage/env-123/public/image.jpg";
|
||||
const httpUrl = "http://example.com/storage/env-123/public/image.jpg";
|
||||
|
||||
expect(resolveStorageUrl(httpsUrl)).toBe(httpsUrl);
|
||||
expect(resolveStorageUrl(httpUrl)).toBe(httpUrl);
|
||||
});
|
||||
|
||||
test("should resolve relative /storage/ path to absolute URL", async () => {
|
||||
// Use actual implementation with mocked dependencies
|
||||
const { resolveStorageUrl: actualResolveStorageUrl } =
|
||||
await vi.importActual<typeof import("@/modules/storage/utils")>("@/modules/storage/utils");
|
||||
|
||||
const relativePath = "/storage/env-123/public/image.jpg";
|
||||
const result = actualResolveStorageUrl(relativePath);
|
||||
|
||||
// Should prepend the base URL (from mocked WEBAPP_URL or getPublicDomain)
|
||||
expect(result).toContain("/storage/env-123/public/image.jpg");
|
||||
expect(result.startsWith("http")).toBe(true);
|
||||
});
|
||||
|
||||
test("should return non-storage paths unchanged", () => {
|
||||
expect(resolveStorageUrl("/some/other/path")).toBe("/some/other/path");
|
||||
expect(resolveStorageUrl("relative/path.jpg")).toBe("relative/path.jpg");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
import "server-only";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { StorageError, StorageErrorCode } from "@formbricks/storage";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TAllowedFileExtension, ZAllowedFileExtension, mimeTypes } from "@formbricks/types/storage";
|
||||
import { TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { WEBAPP_URL } from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getOriginalFileNameFromUrl } from "./url-helpers";
|
||||
|
||||
// Re-export for backward compatibility with server-side code
|
||||
export { getOriginalFileNameFromUrl } from "./url-helpers";
|
||||
export const getOriginalFileNameFromUrl = (fileURL: string) => {
|
||||
try {
|
||||
const lastSegment = fileURL.startsWith("/storage/")
|
||||
? fileURL
|
||||
: (new URL(fileURL).pathname.split("/").pop() ?? "");
|
||||
const fileNameFromURL = lastSegment.split(/[?#]/)[0];
|
||||
|
||||
const [namePart, fidPart] = fileNameFromURL.split("--fid--");
|
||||
if (!fidPart) return namePart ? decodeURIComponent(namePart) : "";
|
||||
|
||||
const dotIdx = fileNameFromURL.lastIndexOf(".");
|
||||
const hasExt = dotIdx > fileNameFromURL.indexOf("--fid--");
|
||||
const ext = hasExt ? fileNameFromURL.slice(dotIdx + 1) : "";
|
||||
|
||||
return decodeURIComponent(ext ? `${namePart}.${ext}` : namePart);
|
||||
} catch (error) {
|
||||
logger.error({ error, fileURL }, "Error parsing file URL");
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitize a provided file name to a safe subset.
|
||||
@@ -148,31 +163,3 @@ export const getErrorResponseFromStorageError = (
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a storage URL to an absolute URL.
|
||||
* - If already absolute, returns as-is (backward compatibility for old data)
|
||||
* - If relative (/storage/...), prepends the appropriate base URL
|
||||
* @param url The storage URL (relative or absolute)
|
||||
* @param accessType The access type to determine which base URL to use (defaults to "public")
|
||||
* @returns The resolved absolute URL, or empty string if url is falsy
|
||||
*/
|
||||
export const resolveStorageUrl = (
|
||||
url: string | undefined | null,
|
||||
accessType: "public" | "private" = "public"
|
||||
): string => {
|
||||
if (!url) return "";
|
||||
|
||||
// Already absolute URL - return as-is (backward compatibility for old data)
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Relative path - resolve with base URL
|
||||
if (url.startsWith("/storage/")) {
|
||||
const baseUrl = accessType === "public" ? getPublicDomain() : WEBAPP_URL;
|
||||
return `${baseUrl}${url}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
@@ -391,7 +391,7 @@ export const ElementFormInput = ({
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<div className="mb-2 mt-3 flex items-center justify-between">
|
||||
<div className="mt-3 mb-2 flex items-center justify-between">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
{id === "headline" && currentElement && updateElement && (
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -521,7 +521,7 @@ export const ElementFormInput = ({
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<div className="mb-2 mt-3 flex items-center justify-between">
|
||||
<div className="mt-3 mb-2 flex items-center justify-between">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
</div>
|
||||
)}
|
||||
@@ -568,7 +568,7 @@ export const ElementFormInput = ({
|
||||
<div className="h-10 w-full"></div>
|
||||
<div
|
||||
ref={highlightContainerRef}
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent ${
|
||||
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
}`}
|
||||
dir="auto"
|
||||
|
||||
@@ -265,7 +265,7 @@ export const BlockCard = ({
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="opacity-0 hover:cursor-move group-hover:opacity-100"
|
||||
className="opacity-0 group-hover:opacity-100 hover:cursor-move"
|
||||
aria-label="Drag to reorder block">
|
||||
<GripIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -67,7 +67,7 @@ export const EditWelcomeCard = ({
|
||||
<div
|
||||
className={cn(
|
||||
open ? "bg-slate-50" : "",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-t border-b border-l group-aria-expanded:rounded-bl-none",
|
||||
isInvalid ? "bg-red-400" : "bg-white group-hover:bg-slate-50"
|
||||
)}>
|
||||
<Hand className="h-4 w-4" />
|
||||
@@ -101,11 +101,7 @@ export const EditWelcomeCard = ({
|
||||
checked={localSurvey?.welcomeCard?.enabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const newEnabledState = !localSurvey.welcomeCard?.enabled;
|
||||
updateSurvey({ enabled: newEnabledState });
|
||||
if (newEnabledState && !open) {
|
||||
setActiveElementId("start");
|
||||
}
|
||||
updateSurvey({ enabled: !localSurvey.welcomeCard?.enabled });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
} from "@/modules/survey/editor/lib/utils";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
||||
import { isEndingCardValid, isWelcomeCardValid, validateElement } from "../lib/validation";
|
||||
import { isEndingCardValid, isWelcomeCardValid, validateSurveyElementsInBatch } from "../lib/validation";
|
||||
|
||||
interface ElementsViewProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -211,6 +211,35 @@ export const ElementsView = ({
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!invalidElements) return;
|
||||
let updatedInvalidElements: string[] = [...invalidElements];
|
||||
|
||||
// Check welcome card
|
||||
if (localSurvey.welcomeCard.enabled && !isWelcomeCardValid(localSurvey.welcomeCard, surveyLanguages)) {
|
||||
if (!updatedInvalidElements.includes("start")) {
|
||||
updatedInvalidElements = [...updatedInvalidElements, "start"];
|
||||
}
|
||||
} else {
|
||||
updatedInvalidElements = updatedInvalidElements.filter((elementId) => elementId !== "start");
|
||||
}
|
||||
|
||||
// Check thank you card
|
||||
localSurvey.endings.forEach((ending) => {
|
||||
if (!isEndingCardValid(ending, surveyLanguages)) {
|
||||
if (!updatedInvalidElements.includes(ending.id)) {
|
||||
updatedInvalidElements = [...updatedInvalidElements, ending.id];
|
||||
}
|
||||
} else {
|
||||
updatedInvalidElements = updatedInvalidElements.filter((elementId) => elementId !== ending.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(updatedInvalidElements) !== JSON.stringify(invalidElements)) {
|
||||
setInvalidElements(updatedInvalidElements);
|
||||
}
|
||||
}, [localSurvey.welcomeCard, localSurvey.endings, surveyLanguages, invalidElements, setInvalidElements]);
|
||||
|
||||
const updateElement = (elementIdx: number, updatedAttributes: any) => {
|
||||
// Get element ID from current elements array (for validation)
|
||||
const element = elements[elementIdx];
|
||||
@@ -221,6 +250,7 @@ export const ElementsView = ({
|
||||
|
||||
// Track side effects that need to happen after state update
|
||||
let newActiveElementId: string | null = null;
|
||||
let invalidElementsUpdate: string[] | null = null;
|
||||
|
||||
// Use functional update to ensure we work with the latest state
|
||||
setLocalSurvey((prevSurvey) => {
|
||||
@@ -266,6 +296,13 @@ export const ElementsView = ({
|
||||
const initialElementId = elementId;
|
||||
updatedSurvey = handleElementLogicChange(updatedSurvey, initialElementId, elementLevelAttributes.id);
|
||||
|
||||
// Track side effects to apply after state update
|
||||
if (invalidElements?.includes(initialElementId)) {
|
||||
invalidElementsUpdate = invalidElements.map((id) =>
|
||||
id === initialElementId ? elementLevelAttributes.id : id
|
||||
);
|
||||
}
|
||||
|
||||
// Track new active element ID
|
||||
newActiveElementId = elementLevelAttributes.id;
|
||||
|
||||
@@ -307,6 +344,9 @@ export const ElementsView = ({
|
||||
});
|
||||
|
||||
// Apply side effects after state update is queued
|
||||
if (invalidElementsUpdate) {
|
||||
setInvalidElements(invalidElementsUpdate);
|
||||
}
|
||||
if (newActiveElementId) {
|
||||
setActiveElementId(newActiveElementId);
|
||||
}
|
||||
@@ -724,67 +764,23 @@ export const ElementsView = ({
|
||||
setLocalSurvey(result.data);
|
||||
};
|
||||
|
||||
// Validate survey when changes are made to languages or elements
|
||||
// using set for O(1) lookup
|
||||
useEffect(
|
||||
() => {
|
||||
if (!invalidElements) return;
|
||||
//useEffect to validate survey when changes are made to languages
|
||||
useEffect(() => {
|
||||
if (!invalidElements) return;
|
||||
let updatedInvalidElements: string[] = invalidElements;
|
||||
// Validate each element
|
||||
elements.forEach((element) => {
|
||||
updatedInvalidElements = validateSurveyElementsInBatch(
|
||||
element,
|
||||
updatedInvalidElements,
|
||||
surveyLanguages
|
||||
);
|
||||
});
|
||||
|
||||
const currentInvalidSet = new Set(invalidElements);
|
||||
let hasChanges = false;
|
||||
|
||||
// Validate each element
|
||||
elements.forEach((element) => {
|
||||
const isValid = validateElement(element, surveyLanguages);
|
||||
if (isValid) {
|
||||
if (currentInvalidSet.has(element.id)) {
|
||||
currentInvalidSet.delete(element.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
} else if (!currentInvalidSet.has(element.id)) {
|
||||
currentInvalidSet.add(element.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check welcome card
|
||||
if (localSurvey.welcomeCard.enabled && !isWelcomeCardValid(localSurvey.welcomeCard, surveyLanguages)) {
|
||||
if (!currentInvalidSet.has("start")) {
|
||||
currentInvalidSet.add("start");
|
||||
hasChanges = true;
|
||||
}
|
||||
} else if (currentInvalidSet.has("start")) {
|
||||
currentInvalidSet.delete("start");
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
// Check thank you card
|
||||
localSurvey.endings.forEach((ending) => {
|
||||
if (!isEndingCardValid(ending, surveyLanguages)) {
|
||||
if (!currentInvalidSet.has(ending.id)) {
|
||||
currentInvalidSet.add(ending.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
} else if (currentInvalidSet.has(ending.id)) {
|
||||
currentInvalidSet.delete(ending.id);
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasChanges) {
|
||||
setInvalidElements(Array.from(currentInvalidSet));
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
elements,
|
||||
surveyLanguages,
|
||||
invalidElements,
|
||||
setInvalidElements,
|
||||
localSurvey.welcomeCard,
|
||||
localSurvey.endings,
|
||||
]
|
||||
);
|
||||
if (JSON.stringify(updatedInvalidElements) !== JSON.stringify(invalidElements)) {
|
||||
setInvalidElements(updatedInvalidElements);
|
||||
}
|
||||
}, [elements, surveyLanguages, invalidElements, setInvalidElements]);
|
||||
|
||||
useEffect(() => {
|
||||
const elementWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
|
||||
@@ -795,7 +791,7 @@ export const ElementsView = ({
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeElementId, setActiveElementId, localSurvey, selectedLanguageCode]);
|
||||
}, [activeElementId, setActiveElementId]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
|
||||
@@ -69,7 +69,6 @@ export const EndScreenForm = ({
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
autoFocus={!endingCard.headline?.default || endingCard.headline.default.trim() === ""}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
<div>
|
||||
{endingCard.subheader !== undefined && (
|
||||
@@ -88,7 +87,6 @@ export const EndScreenForm = ({
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
autoFocus={!endingCard.subheader?.default || endingCard.subheader.default.trim() === ""}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,7 +139,7 @@ export const EndScreenForm = ({
|
||||
</Label>
|
||||
</div>
|
||||
{showEndingCardCTA && (
|
||||
<div className="mt-4 space-y-4 rounded-md border bg-slate-100 p-4 pt-2">
|
||||
<div className="mt-4 space-y-4 rounded-md border border-1 bg-slate-100 p-4 pt-2">
|
||||
<div className="space-y-2">
|
||||
<ElementFormInput
|
||||
id="buttonLabel"
|
||||
@@ -185,7 +183,7 @@ export const EndScreenForm = ({
|
||||
<div className="group relative">
|
||||
{/* The highlight container is absolutely positioned behind the input */}
|
||||
<div
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent`}
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent`}
|
||||
dir="auto"
|
||||
key={highlightedJSX.toString()}>
|
||||
{highlightedJSX}
|
||||
|
||||
@@ -172,7 +172,7 @@ export const FileUploadElementForm = ({
|
||||
|
||||
updateElement(elementIdx, { maxSizeInMB: Number.parseInt(e.target.value, 10) });
|
||||
}}
|
||||
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
|
||||
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
|
||||
/>
|
||||
MB
|
||||
</p>
|
||||
|
||||
@@ -2,20 +2,18 @@
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { CheckIcon, SparklesIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
import {
|
||||
ColorField,
|
||||
DimensionInput,
|
||||
NumberField,
|
||||
StylingSection,
|
||||
TextField,
|
||||
} from "@/modules/ui/components/styling-fields";
|
||||
import { COLOR_DEFAULTS } from "@/lib/styling/constants";
|
||||
import { mixColor } from "@/lib/utils/colors";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
||||
|
||||
type FormStylingSettingsProps = {
|
||||
open: boolean;
|
||||
@@ -33,12 +31,45 @@ export const FormStylingSettings = ({
|
||||
form,
|
||||
}: FormStylingSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const brandColor = form.watch("brandColor.light") || COLOR_DEFAULTS.brandColor;
|
||||
const background = form.watch("background");
|
||||
const highlightBorderColor = form.watch("highlightBorderColor");
|
||||
|
||||
const setQuestionColor = (color: string) => form.setValue("questionColor.light", color);
|
||||
const setInputColor = (color: string) => form.setValue("inputColor.light", color);
|
||||
const setInputBorderColor = (color: string) => form.setValue("inputBorderColor.light", color);
|
||||
const setCardBackgroundColor = (color: string) => form.setValue("cardBackgroundColor.light", color);
|
||||
const setCardBorderColor = (color: string) => form.setValue("cardBorderColor.light", color);
|
||||
|
||||
const setBackgroundColor = (color: string) => {
|
||||
form.setValue("background", {
|
||||
bg: color,
|
||||
bgType: "color",
|
||||
});
|
||||
};
|
||||
const setHighlightBorderColor = (color: string) => {
|
||||
form.setValue("highlightBorderColor", { light: mixColor(color, "#ffffff", 0.25) });
|
||||
};
|
||||
|
||||
const suggestColors = () => {
|
||||
// mix the brand color with different weights of white and set the result as the other colors
|
||||
setQuestionColor(mixColor(brandColor, "#000000", 0.35));
|
||||
setInputColor(mixColor(brandColor, "#ffffff", 0.92));
|
||||
setInputBorderColor(mixColor(brandColor, "#ffffff", 0.6));
|
||||
|
||||
setCardBackgroundColor(mixColor(brandColor, "#ffffff", 0.97));
|
||||
setCardBorderColor(mixColor(brandColor, "#ffffff", 0.8));
|
||||
|
||||
if (!background || background?.bgType === "color") {
|
||||
setBackgroundColor(mixColor(brandColor, "#ffffff", 0.855));
|
||||
}
|
||||
|
||||
if (highlightBorderColor) {
|
||||
setHighlightBorderColor(brandColor);
|
||||
}
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
const [headlinesOpen, setHeadlinesOpen] = useState(false);
|
||||
const [inputsOpen, setInputsOpen] = useState(false);
|
||||
const [buttonsOpen, setButtonsOpen] = useState(false);
|
||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
@@ -79,291 +110,110 @@ export const FormStylingSettings = ({
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
{/* Headlines & Descriptions */}
|
||||
<StylingSection
|
||||
title={t("environments.workspace.look.advanced_styling_section_headlines")}
|
||||
open={headlinesOpen}
|
||||
setOpen={setHeadlinesOpen}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ColorField
|
||||
form={form}
|
||||
name="elementHeadlineColor.light"
|
||||
label={t("environments.workspace.look.advanced_styling_field_headline_color")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_headline_color_description"
|
||||
)}
|
||||
/>
|
||||
<ColorField
|
||||
form={form}
|
||||
name="elementDescriptionColor.light"
|
||||
label={t("environments.workspace.look.advanced_styling_field_description_color")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_description_color_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="elementHeadlineFontSize"
|
||||
label={t("environments.workspace.look.advanced_styling_field_headline_size")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_headline_size_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="elementDescriptionFontSize"
|
||||
label={t("environments.workspace.look.advanced_styling_field_description_size")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_description_size_description"
|
||||
)}
|
||||
/>
|
||||
<NumberField
|
||||
form={form}
|
||||
name="elementHeadlineFontWeight"
|
||||
label={t("environments.workspace.look.advanced_styling_field_headline_weight")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_headline_weight_description"
|
||||
)}
|
||||
/>
|
||||
<NumberField
|
||||
form={form}
|
||||
name="elementDescriptionFontWeight"
|
||||
label={t("environments.workspace.look.advanced_styling_field_description_weight")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_description_weight_description"
|
||||
)}
|
||||
/>
|
||||
<ColorField
|
||||
form={form}
|
||||
name="elementUpperLabelColor.light"
|
||||
label={t("environments.workspace.look.advanced_styling_field_upper_label_color")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_upper_label_color_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="elementUpperLabelFontSize"
|
||||
label={t("environments.workspace.look.advanced_styling_field_upper_label_size")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_upper_label_size_description"
|
||||
)}
|
||||
/>
|
||||
<NumberField
|
||||
form={form}
|
||||
name="elementUpperLabelFontWeight"
|
||||
label={t("environments.workspace.look.advanced_styling_field_upper_label_weight")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_upper_label_weight_description"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</StylingSection>
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="brandColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>{t("environments.surveys.edit.brand_color")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.edit.change_the_brand_color_of_the_survey")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
|
||||
{/* Inputs */}
|
||||
<StylingSection
|
||||
title={t("environments.workspace.look.advanced_styling_section_inputs")}
|
||||
open={inputsOpen}
|
||||
setOpen={setInputsOpen}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ColorField
|
||||
form={form}
|
||||
name="inputColor.light"
|
||||
label={t("environments.surveys.edit.input_color")}
|
||||
description={t("environments.surveys.edit.input_color_description")}
|
||||
/>
|
||||
<ColorField
|
||||
form={form}
|
||||
name="inputBorderColor.light"
|
||||
label={t("environments.surveys.edit.input_border_color")}
|
||||
description={t("environments.surveys.edit.input_border_color_description")}
|
||||
/>
|
||||
<ColorField
|
||||
form={form}
|
||||
name="inputTextColor.light"
|
||||
label={t("environments.workspace.look.advanced_styling_field_input_text")}
|
||||
description={t("environments.workspace.look.advanced_styling_field_input_text_description")}
|
||||
/>
|
||||
<div className="hidden" /> {/* Spacer if needed, or remove for auto flow */}
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="inputBorderRadius"
|
||||
label={t("environments.workspace.look.advanced_styling_field_border_radius")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_input_border_radius_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="inputHeight"
|
||||
label={t("environments.workspace.look.advanced_styling_field_height")}
|
||||
description={t("environments.workspace.look.advanced_styling_field_input_height_description")}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="inputFontSize"
|
||||
label={t("environments.workspace.look.advanced_styling_field_font_size")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_input_font_size_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="inputPaddingX"
|
||||
label={t("environments.workspace.look.advanced_styling_field_padding_x")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_input_padding_x_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="inputPaddingY"
|
||||
label={t("environments.workspace.look.advanced_styling_field_padding_y")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_input_padding_y_description"
|
||||
)}
|
||||
/>
|
||||
<NumberField
|
||||
form={form}
|
||||
name="inputPlaceholderOpacity"
|
||||
label={t("environments.workspace.look.advanced_styling_field_placeholder_opacity")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_input_placeholder_opacity_description"
|
||||
)}
|
||||
step={0.1}
|
||||
max={1}
|
||||
/>
|
||||
<TextField
|
||||
form={form}
|
||||
name="inputShadow"
|
||||
label={t("environments.workspace.look.advanced_styling_field_shadow")}
|
||||
description={t("environments.workspace.look.advanced_styling_field_input_shadow_description")}
|
||||
/>
|
||||
</div>
|
||||
</StylingSection>
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
color={field.value || COLOR_DEFAULTS.brandColor}
|
||||
onChange={(color) => field.onChange(color)}
|
||||
containerClass="max-w-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Buttons */}
|
||||
<StylingSection
|
||||
title={t("environments.workspace.look.advanced_styling_section_buttons")}
|
||||
open={buttonsOpen}
|
||||
setOpen={setButtonsOpen}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ColorField
|
||||
form={form}
|
||||
name="buttonBgColor.light"
|
||||
label={t("environments.workspace.look.advanced_styling_field_button_bg")}
|
||||
description={t("environments.workspace.look.advanced_styling_field_button_bg_description")}
|
||||
/>
|
||||
<ColorField
|
||||
form={form}
|
||||
name="buttonTextColor.light"
|
||||
label={t("environments.workspace.look.advanced_styling_field_button_text")}
|
||||
description={t("environments.workspace.look.advanced_styling_field_button_text_description")}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="buttonBorderRadius"
|
||||
label={t("environments.workspace.look.advanced_styling_field_border_radius")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_button_border_radius_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="buttonHeight"
|
||||
label={t("environments.workspace.look.advanced_styling_field_height")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_button_height_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="buttonFontSize"
|
||||
label={t("environments.workspace.look.advanced_styling_field_font_size")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_button_font_size_description"
|
||||
)}
|
||||
/>
|
||||
<NumberField
|
||||
form={form}
|
||||
name="buttonFontWeight"
|
||||
label={t("environments.workspace.look.advanced_styling_field_font_weight")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_button_font_weight_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="buttonPaddingX"
|
||||
label={t("environments.workspace.look.advanced_styling_field_padding_x")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_button_padding_x_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="buttonPaddingY"
|
||||
label={t("environments.workspace.look.advanced_styling_field_padding_y")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_button_padding_y_description"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</StylingSection>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
onClick={() => suggestColors()}>
|
||||
{t("environments.surveys.edit.suggest_colors")}
|
||||
<SparklesIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<StylingSection
|
||||
title={t("environments.workspace.look.advanced_styling_section_options")}
|
||||
open={optionsOpen}
|
||||
setOpen={setOptionsOpen}>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<ColorField
|
||||
form={form}
|
||||
name="optionBgColor.light"
|
||||
label={t("environments.workspace.look.advanced_styling_field_option_bg")}
|
||||
description={t("environments.workspace.look.advanced_styling_field_option_bg_description")}
|
||||
/>
|
||||
<ColorField
|
||||
form={form}
|
||||
name="optionLabelColor.light"
|
||||
label={t("environments.workspace.look.advanced_styling_field_option_label")}
|
||||
description={t("environments.workspace.look.advanced_styling_field_option_label_description")}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="optionBorderRadius"
|
||||
label={t("environments.workspace.look.advanced_styling_field_border_radius")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_option_border_radius_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="optionPaddingX"
|
||||
label={t("environments.workspace.look.advanced_styling_field_padding_x")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_option_padding_x_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="optionPaddingY"
|
||||
label={t("environments.workspace.look.advanced_styling_field_padding_y")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_option_padding_y_description"
|
||||
)}
|
||||
/>
|
||||
<DimensionInput
|
||||
form={form}
|
||||
name="optionFontSize"
|
||||
label={t("environments.workspace.look.advanced_styling_field_font_size")}
|
||||
description={t(
|
||||
"environments.workspace.look.advanced_styling_field_option_font_size_description"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</StylingSection>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="questionColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>{t("environments.surveys.edit.question_color")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.edit.change_the_question_color_of_the_survey")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
color={field.value || COLOR_DEFAULTS.questionColor}
|
||||
onChange={(color) => field.onChange(color)}
|
||||
containerClass="max-w-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="inputColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>{t("environments.surveys.edit.input_color")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.edit.change_the_background_color_of_the_input_fields")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
color={field.value || COLOR_DEFAULTS.inputColor}
|
||||
onChange={(color: string) => field.onChange(color)}
|
||||
containerClass="max-w-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="inputBorderColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<div>
|
||||
<FormLabel>{t("environments.surveys.edit.input_border_color")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.edit.change_the_border_color_of_the_input_fields")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
color={field.value || COLOR_DEFAULTS.inputBorderColor}
|
||||
onChange={(color: string) => field.onChange(color)}
|
||||
containerClass="max-w-xs"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -10,11 +10,7 @@ import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import {
|
||||
TMultipleChoiceOptionDisplayType,
|
||||
TSurveyElementTypeEnum,
|
||||
TSurveyMultipleChoiceElement,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { TMultipleChoiceOptionDisplayType, TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
|
||||
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
|
||||
@@ -27,7 +27,6 @@ interface PictureSelectionFormProps {
|
||||
isInvalid: boolean;
|
||||
locale: TUserLocale;
|
||||
isStorageConfigured: boolean;
|
||||
isExternalUrlsAllowed?: boolean;
|
||||
}
|
||||
|
||||
export const PictureSelectionForm = ({
|
||||
@@ -40,7 +39,6 @@ export const PictureSelectionForm = ({
|
||||
isInvalid,
|
||||
locale,
|
||||
isStorageConfigured = true,
|
||||
isExternalUrlsAllowed,
|
||||
}: PictureSelectionFormProps): JSX.Element => {
|
||||
const environmentId = localSurvey.environmentId;
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -90,7 +88,6 @@ export const PictureSelectionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
@@ -109,7 +106,6 @@ export const PictureSelectionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOverlay, TPlacement } from "@formbricks/types/common";
|
||||
import { TPlacement } from "@formbricks/types/common";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { getPlacementStyle } from "@/modules/ui/components/preview-survey/lib/utils";
|
||||
import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group";
|
||||
import { StylingTabs } from "@/modules/ui/components/styling-tabs";
|
||||
|
||||
interface TPlacementProps {
|
||||
currentPlacement: TPlacement;
|
||||
setCurrentPlacement: (placement: TPlacement) => void;
|
||||
setOverlay: (overlay: TOverlay) => void;
|
||||
overlay: TOverlay;
|
||||
setOverlay: (overlay: string) => void;
|
||||
overlay: string;
|
||||
setClickOutsideClose: (clickOutside: boolean) => void;
|
||||
clickOutsideClose: boolean;
|
||||
}
|
||||
@@ -33,15 +32,8 @@ export const Placement = ({
|
||||
{ name: t("common.bottom_left"), value: "bottomLeft", disabled: false },
|
||||
{ name: t("common.centered_modal"), value: "center", disabled: false },
|
||||
];
|
||||
|
||||
const hasOverlay = overlay !== "none";
|
||||
|
||||
const getOverlayStyle = () => {
|
||||
if (overlay === "dark") return "bg-slate-700/80";
|
||||
if (overlay === "light") return "bg-slate-400/50";
|
||||
return "bg-slate-200";
|
||||
};
|
||||
|
||||
const overlayStyle =
|
||||
currentPlacement === "center" && overlay === "dark" ? "bg-slate-700/80" : "bg-slate-200";
|
||||
return (
|
||||
<>
|
||||
<div className="flex">
|
||||
@@ -58,9 +50,9 @@ export const Placement = ({
|
||||
<div
|
||||
data-testid="placement-preview"
|
||||
className={cn(
|
||||
hasOverlay && !clickOutsideClose ? "cursor-not-allowed" : "",
|
||||
clickOutsideClose ? "" : "cursor-not-allowed",
|
||||
"relative ml-8 h-40 w-full rounded",
|
||||
getOverlayStyle()
|
||||
overlayStyle
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -69,46 +61,53 @@ export const Placement = ({
|
||||
)}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 space-y-2">
|
||||
<StylingTabs
|
||||
id="overlay"
|
||||
options={[
|
||||
{ value: "none", label: t("common.no_overlay") },
|
||||
{ value: "light", label: t("common.light_overlay") },
|
||||
{ value: "dark", label: t("common.dark_overlay") },
|
||||
]}
|
||||
defaultSelected={overlay}
|
||||
onChange={(value) => setOverlay(value)}
|
||||
label={t("common.overlay_color")}
|
||||
activeTabClassName="bg-slate-200"
|
||||
inactiveTabClassName="bg-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasOverlay && (
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label className="font-semibold">
|
||||
{t("common.allow_users_to_exit_by_clicking_outside_the_survey")}
|
||||
</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => setClickOutsideClose(value === "allow")}
|
||||
value={clickOutsideClose ? "allow" : "disallow"}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="disallow" value="disallow" />
|
||||
<Label htmlFor="disallow" className="text-slate-900">
|
||||
{t("common.disallow")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="allow" value="allow" />
|
||||
<Label htmlFor="allow" className="text-slate-900">
|
||||
{t("common.allow")}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{currentPlacement === "center" && (
|
||||
<>
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label className="font-semibold">
|
||||
{t("environments.surveys.edit.centered_modal_overlay_color")}
|
||||
</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(overlay) => setOverlay(overlay)}
|
||||
value={overlay}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="lightOverlay" value="light" />
|
||||
<Label htmlFor="lightOverlay" className="text-slate-900">
|
||||
{t("common.light_overlay")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="darkOverlay" value="dark" />
|
||||
<Label htmlFor="darkOverlay" className="text-slate-900">
|
||||
{t("common.dark_overlay")}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label className="font-semibold">
|
||||
{t("common.allow_users_to_exit_by_clicking_outside_the_survey")}
|
||||
</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => setClickOutsideClose(value === "allow")}
|
||||
value={clickOutsideClose ? "allow" : "disallow"}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="disallow" value="disallow" />
|
||||
<Label htmlFor="disallow" className="text-slate-900">
|
||||
{t("common.disallow")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="allow" value="allow" />
|
||||
<Label htmlFor="allow" className="text-slate-900">
|
||||
{t("common.allow")}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ export const RedirectUrlForm = ({ localSurvey, endingCard, updateSurvey }: Redir
|
||||
<div className="group relative">
|
||||
{/* The highlight container is absolutely positioned behind the input */}
|
||||
<div
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent`}
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent`}
|
||||
dir="auto"
|
||||
key={highlightedJSX.toString()}>
|
||||
{highlightedJSX}
|
||||
|
||||
@@ -45,13 +45,9 @@ export const SavedActionsTab = ({
|
||||
<Input
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
setFilteredActionClasses(
|
||||
availableActions.filter(
|
||||
(actionClass) =>
|
||||
actionClass.name.toLowerCase().includes(searchTerm) ||
|
||||
actionClass.description?.toLowerCase().includes(searchTerm) ||
|
||||
actionClass.key?.toLowerCase().includes(searchTerm)
|
||||
availableActions.filter((actionClass) =>
|
||||
actionClass.name.toLowerCase().includes(e.target.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Project } from "@prisma/client";
|
||||
import { RotateCcwIcon, SparklesIcon } from "lucide-react";
|
||||
import { RotateCcwIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { UseFormReturn, useForm } from "react-hook-form";
|
||||
@@ -9,14 +9,13 @@ import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TProjectStyling } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { STYLE_DEFAULTS, getSuggestedColors } from "@/lib/styling/constants";
|
||||
import { defaultStyling } from "@/lib/styling/constants";
|
||||
import { FormStylingSettings } from "@/modules/survey/editor/components/form-styling-settings";
|
||||
import { LogoSettingsCard } from "@/modules/survey/editor/components/logo-settings-card";
|
||||
import { AlertDialog } from "@/modules/ui/components/alert-dialog";
|
||||
import { BackgroundStylingCard } from "@/modules/ui/components/background-styling-card";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { CardStylingSettings } from "@/modules/ui/components/card-styling-settings";
|
||||
import { ColorPicker } from "@/modules/ui/components/color-picker";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
@@ -58,22 +57,8 @@ export const StylingView = ({
|
||||
}: StylingViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const savedProjectStyling = project.styling as Partial<TProjectStyling> | null;
|
||||
|
||||
// Strip null/undefined values so they don't override STYLE_DEFAULTS.
|
||||
const cleanProject = savedProjectStyling
|
||||
? Object.fromEntries(Object.entries(savedProjectStyling).filter(([, v]) => v != null))
|
||||
: {};
|
||||
const cleanSurvey = localSurvey.styling
|
||||
? Object.fromEntries(Object.entries(localSurvey.styling).filter(([, v]) => v != null))
|
||||
: {};
|
||||
|
||||
const form = useForm<TSurveyStyling>({
|
||||
defaultValues: {
|
||||
...STYLE_DEFAULTS,
|
||||
...cleanProject,
|
||||
...cleanSurvey,
|
||||
},
|
||||
defaultValues: { ...defaultStyling, ...project.styling, ...localSurvey.styling },
|
||||
});
|
||||
|
||||
const overwriteThemeStyling = form.watch("overwriteThemeStyling");
|
||||
@@ -84,19 +69,6 @@ export const StylingView = ({
|
||||
const [cardStylingOpen, setCardStylingOpen] = useState(false);
|
||||
const [stylingOpen, setStylingOpen] = useState(false);
|
||||
const [confirmResetStylingModalOpen, setConfirmResetStylingModalOpen] = useState(false);
|
||||
const [confirmSuggestColorsOpen, setConfirmSuggestColorsOpen] = useState(false);
|
||||
|
||||
const handleSuggestColors = () => {
|
||||
const currentBrandColor = form.getValues().brandColor?.light ?? STYLE_DEFAULTS.brandColor?.light;
|
||||
const suggested = getSuggestedColors(currentBrandColor);
|
||||
|
||||
for (const [key, value] of Object.entries(suggested)) {
|
||||
form.setValue(key as keyof TSurveyStyling, value, { shouldDirty: true });
|
||||
}
|
||||
|
||||
toast.success(t("environments.workspace.look.styling_updated_successfully"));
|
||||
setConfirmSuggestColorsOpen(false);
|
||||
};
|
||||
|
||||
const onResetThemeStyling = () => {
|
||||
const { allowStyleOverwrite, ...baseStyling } = project.styling ?? {};
|
||||
@@ -217,38 +189,6 @@ export const StylingView = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{overwriteThemeStyling && (
|
||||
<div className="grid grid-cols-2 items-end gap-4 rounded-lg border border-slate-300 bg-white p-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="brandColor.light"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-1">
|
||||
<FormLabel>{t("environments.surveys.edit.brand_color")}</FormLabel>
|
||||
<FormDescription>
|
||||
{t("environments.surveys.edit.brand_color_description")}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<ColorPicker
|
||||
color={field.value ?? STYLE_DEFAULTS.brandColor?.light}
|
||||
onChange={(color) => field.onChange(color)}
|
||||
containerClass="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="h-10 w-full justify-center gap-2"
|
||||
onClick={() => setConfirmSuggestColorsOpen(true)}>
|
||||
<SparklesIcon className="mr-2 h-4 w-4" />
|
||||
{t("environments.workspace.look.suggest_colors")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormStylingSettings
|
||||
open={formStylingOpen}
|
||||
setOpen={setFormStylingOpen}
|
||||
@@ -323,17 +263,6 @@ export const StylingView = ({
|
||||
onDecline={() => setConfirmResetStylingModalOpen(false)}
|
||||
onConfirm={onResetThemeStyling}
|
||||
/>
|
||||
|
||||
<AlertDialog
|
||||
open={confirmSuggestColorsOpen}
|
||||
setOpen={setConfirmSuggestColorsOpen}
|
||||
headerText={t("environments.workspace.look.generate_theme_header")}
|
||||
mainText={t("environments.workspace.look.generate_theme_confirmation")}
|
||||
confirmBtnLabel={t("environments.workspace.look.generate_theme_btn")}
|
||||
declineBtnLabel={t("common.cancel")}
|
||||
onConfirm={handleSuggestColors}
|
||||
onDecline={() => setConfirmSuggestColorsOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
@@ -86,7 +86,6 @@ export const SurveyEditor = ({
|
||||
const [activeElementId, setActiveElementId] = useState<string | null>(null);
|
||||
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>(() => structuredClone(survey));
|
||||
const [invalidElements, setInvalidElements] = useState<string[] | null>(null);
|
||||
|
||||
const [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
|
||||
const surveyEditorRef = useRef(null);
|
||||
const [localProject, setLocalProject] = useState<Project>(project);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOverlay, TPlacement } from "@formbricks/types/common";
|
||||
import { TPlacement } from "@formbricks/types/common";
|
||||
import { TSurvey, TSurveyProjectOverwrites } from "@formbricks/types/surveys/types";
|
||||
import { Placement } from "@/modules/survey/editor/components/placement";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
@@ -27,7 +27,7 @@ export const SurveyPlacementCard = ({
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { projectOverwrites } = localSurvey ?? {};
|
||||
const { placement, clickOutsideClose, overlay } = projectOverwrites ?? {};
|
||||
const { placement, clickOutsideClose, darkOverlay } = projectOverwrites ?? {};
|
||||
|
||||
const setProjectOverwrites = (projectOverwrites: TSurveyProjectOverwrites | null) => {
|
||||
setLocalSurvey({ ...localSurvey, projectOverwrites: projectOverwrites });
|
||||
@@ -41,7 +41,7 @@ export const SurveyPlacementCard = ({
|
||||
setProjectOverwrites({
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: false,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -56,11 +56,13 @@ export const SurveyPlacementCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOverlay = (overlayValue: TOverlay) => {
|
||||
const handleOverlay = (overlayType: string) => {
|
||||
const darkOverlay = overlayType === "dark";
|
||||
|
||||
if (setProjectOverwrites) {
|
||||
setProjectOverwrites({
|
||||
...projectOverwrites,
|
||||
overlay: overlayValue,
|
||||
darkOverlay,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -89,7 +91,7 @@ export const SurveyPlacementCard = ({
|
||||
asChild
|
||||
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<div className="flex items-center pr-5 pl-2">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||
@@ -130,7 +132,7 @@ export const SurveyPlacementCard = ({
|
||||
currentPlacement={placement}
|
||||
setCurrentPlacement={handlePlacementChange}
|
||||
setOverlay={handleOverlay}
|
||||
overlay={overlay ?? "none"}
|
||||
overlay={darkOverlay ? "dark" : "light"}
|
||||
setClickOutsideClose={handleClickOutsideClose}
|
||||
clickOutsideClose={!!clickOutsideClose}
|
||||
/>
|
||||
|
||||
@@ -188,8 +188,6 @@ export const FollowUpModal = ({
|
||||
subject: defaultValues?.subject ?? t("environments.surveys.edit.follow_ups_modal_action_subject"),
|
||||
body: defaultValues?.body ?? getSurveyFollowUpActionDefaultBody(t),
|
||||
attachResponseData: defaultValues?.attachResponseData ?? false,
|
||||
includeVariables: defaultValues?.includeVariables ?? false,
|
||||
includeHiddenFields: defaultValues?.includeHiddenFields ?? false,
|
||||
},
|
||||
resolver: zodResolver(ZCreateSurveyFollowUpFormSchema),
|
||||
mode: "onChange",
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
renderFollowUpEmail,
|
||||
} from "@formbricks/email";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL, TERMS_URL } from "@/lib/constants";
|
||||
import { getElementResponseMapping } from "@/lib/responses";
|
||||
import { parseRecallInfo } from "@/lib/utils/recall";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { sendEmail } from "@/modules/email";
|
||||
import { resolveStorageUrl } from "@/modules/storage/utils";
|
||||
|
||||
export const sendFollowUpEmail = async ({
|
||||
followUp,
|
||||
@@ -59,27 +57,12 @@ export const sendFollowUpEmail = async ({
|
||||
});
|
||||
|
||||
// Process response data
|
||||
// Resolve relative storage URLs to absolute URLs for email rendering
|
||||
const responseData: ProcessedResponseElement[] = attachResponseData
|
||||
? getElementResponseMapping(survey, response).map((e) => {
|
||||
// Resolve URLs for picture selection and file upload responses
|
||||
if (
|
||||
(e.type === TSurveyElementTypeEnum.PictureSelection ||
|
||||
e.type === TSurveyElementTypeEnum.FileUpload) &&
|
||||
Array.isArray(e.response)
|
||||
) {
|
||||
return {
|
||||
element: e.element,
|
||||
response: e.response.map((url) => resolveStorageUrl(url)),
|
||||
type: e.type,
|
||||
};
|
||||
}
|
||||
return {
|
||||
element: e.element,
|
||||
response: e.response,
|
||||
type: e.type,
|
||||
};
|
||||
})
|
||||
? getElementResponseMapping(survey, response).map((e) => ({
|
||||
element: e.element,
|
||||
response: e.response,
|
||||
type: e.type,
|
||||
}))
|
||||
: [];
|
||||
|
||||
// Process variables
|
||||
|
||||
@@ -47,7 +47,7 @@ const mockProjectPrisma = {
|
||||
linkSurveyBranding: false,
|
||||
placement: "bottomRight",
|
||||
clickOutsideClose: false,
|
||||
overlay: "none",
|
||||
darkOverlay: false,
|
||||
segment: null,
|
||||
surveyClosedMessage: null,
|
||||
singleUseId: null,
|
||||
|
||||
@@ -69,11 +69,7 @@ export const SurveyDropDownMenu = ({
|
||||
const handleDeleteSurvey = async (surveyId: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await deleteSurveyAction({ surveyId });
|
||||
if (result?.serverError) {
|
||||
toast.error(getFormattedErrorMessage(result));
|
||||
return;
|
||||
}
|
||||
await deleteSurveyAction({ surveyId });
|
||||
deleteSurvey(surveyId);
|
||||
toast.success(t("environments.surveys.survey_deleted_successfully"));
|
||||
} catch (error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user